Skip to content

Commit abe530b

Browse files
Implement native image export to raster formats using ImageMagick as the canvas (#1325)
1 parent bdeeb06 commit abe530b

File tree

132 files changed

+8433
-283
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+8433
-283
lines changed

build.gradle.kts

+43-15
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@ plugins {
1717
kotlin("multiplatform") apply false
1818
kotlin("js") apply false
1919
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
20-
id ("org.openjfx.javafxplugin") version "0.1.0" apply false
20+
id("org.openjfx.javafxplugin") version "0.1.0" apply false
2121
}
2222

2323

24-
fun ExtraPropertiesExtension.getOrNull(name: String): Any? = if (has(name)) { get(name) } else { null }
24+
fun ExtraPropertiesExtension.getOrNull(name: String): Any? = if (has(name)) {
25+
get(name)
26+
} else {
27+
null
28+
}
2529

2630

2731
val os: OperatingSystem = OperatingSystem.current()
2832
val letsPlotTaskGroup by extra { "lets-plot" }
2933

3034
allprojects {
3135
group = "org.jetbrains.lets-plot"
32-
version = "4.6.3-SNAPSHOT" // see also: python-package/lets_plot/_version.py
33-
// version = "0.0.0-SNAPSHOT" // for local publishing only
36+
// version = "4.6.3-SNAPSHOT" // see also: python-package/lets_plot/_version.py
37+
version = "0.0.0-SNAPSHOT" // for local publishing only
3438

3539
// Generate JVM 1.8 bytecode
3640
tasks.withType<KotlinJvmCompile>().configureEach {
@@ -131,9 +135,9 @@ if (project.hasProperty("build_release")) {
131135
// define local Maven Repository path:
132136
val localMavenRepository by extra { "$rootDir/.maven-publish-dev-repo" }
133137
// define Sonatype nexus repository manager settings:
134-
val sonatypeUsername = extra.getOrNull("sonatype.username")?: ""
135-
val sonatypePassword = extra.getOrNull("sonatype.password")?: ""
136-
val sonatypeProfileID = extra.getOrNull("sonatype.profileID")?: ""
138+
val sonatypeUsername = extra.getOrNull("sonatype.username") ?: ""
139+
val sonatypePassword = extra.getOrNull("sonatype.password") ?: ""
140+
val sonatypeProfileID = extra.getOrNull("sonatype.profileID") ?: ""
137141

138142
nexusPublishing {
139143
repositories {
@@ -149,11 +153,30 @@ nexusPublishing {
149153

150154
// Publish some sub-projects as Kotlin Multi-project libraries.
151155
val publishLetsPlotCoreModulesToMavenLocalRepository by tasks.registering {
152-
group=letsPlotTaskGroup
156+
group = letsPlotTaskGroup
153157
}
154158

155159
val publishLetsPlotCoreModulesToMavenRepository by tasks.registering {
156-
group=letsPlotTaskGroup
160+
group = letsPlotTaskGroup
161+
}
162+
163+
if ((extra.get("enable_magick_canvas") as? String ?: "false").toBoolean()) {
164+
extra.set("imagemagick_lib_path", rootDir.path + "/platf-imagick/ImageMagick/install")
165+
166+
val initImageMagick by tasks.registering {
167+
group = letsPlotTaskGroup
168+
doLast {
169+
exec {
170+
this.workingDir = File(rootDir.path + "/platf-imagick")
171+
commandLine(
172+
"python",
173+
"init_imagemagick.py"
174+
)
175+
}
176+
}
177+
}
178+
179+
logger.info("Run './gradlew initImageMagick' to initialize ImageMagick.")
157180
}
158181

159182
// Generating JavaDoc task for each publication task.
@@ -163,7 +186,7 @@ val publishLetsPlotCoreModulesToMavenRepository by tasks.registering {
163186
// - https://github.com/gradle-nexus/publish-plugin/issues/208
164187
// - https://github.com/gradle/gradle/issues/26091
165188
//
166-
fun getJarJavaDocsTask(distributeName:String): TaskProvider<Jar> {
189+
fun getJarJavaDocsTask(distributeName: String): TaskProvider<Jar> {
167190
return tasks.register<Jar>("${distributeName}JarJavaDoc") {
168191
archiveClassifier.set("javadoc")
169192
from("$rootDir/README.md")
@@ -175,12 +198,16 @@ fun getJarJavaDocsTask(distributeName:String): TaskProvider<Jar> {
175198
subprojects {
176199
val pythonExtensionModules = listOf(
177200
"commons",
201+
"canvas",
178202
"datamodel",
179203
"plot-base",
180204
"plot-builder",
181205
"plot-stem",
182-
"platf-native",
183-
"demo-and-test-shared"
206+
"plot-raster",
207+
208+
"demo-and-test-shared",
209+
"demo-common-svg",
210+
"demo-svg-native",
184211
)
185212
val projectArchitecture = rootProject.extra.getOrNull("architecture")
186213

@@ -244,7 +271,7 @@ val jvmCoreModulesForPublish = listOf(
244271
)
245272

246273
subprojects {
247-
if(name in jvmCoreModulesForPublish) {
274+
if (name in jvmCoreModulesForPublish) {
248275
apply(plugin = "org.jetbrains.kotlin.jvm")
249276
apply(plugin = "maven-publish")
250277

@@ -272,7 +299,7 @@ subprojects {
272299

273300
// Configure Maven publication for Lets-Plot Core modules.
274301
subprojects {
275-
if(name in multiPlatformCoreModulesForPublish + jvmCoreModulesForPublish) {
302+
if (name in multiPlatformCoreModulesForPublish + jvmCoreModulesForPublish) {
276303
apply(plugin = "maven-publish")
277304
apply(plugin = "signing")
278305
// Do not publish 'native' targets:
@@ -283,7 +310,8 @@ subprojects {
283310
"jvm",
284311
"js",
285312
"kotlinMultiplatform",
286-
"metadata")
313+
"metadata"
314+
)
287315

288316
configure<PublishingExtension> {
289317
publications {

build_release.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
1313
pip install pyyaml
1414
15-
Script requires YAML-formatted settings file with paths to host Python installations.
15+
Script requires YAML-formatted settings file with paths to host Python installations
16+
as the first command line argument.
17+
1618
Settings file must have the next format (EXAMPLE):
1719
1820
python37:
@@ -25,14 +27,18 @@
2527
2628
You can place it anywhere you want, but do not push it to the project repository.
2729
30+
The second command line argument must contain a path to the ImageMagick library root
31+
directory.
32+
2833
Run script in terminal by its name and do not forget to pass a path to the settings
2934
file as argument (EXAMPLE):
3035
31-
./build_release.py path/to/settings_file.yml
36+
./build_release.py path/to/settings_file.yml /home/letsplotter/ImageMagick-7.1.1
3237
3338
"""
3439

3540

41+
import os
3642
import platform
3743
import sys
3844
import subprocess
@@ -54,9 +60,6 @@ def print_error_and_exit(error_message):
5460

5561
def read_settings_file():
5662
# Reads settings file name from commandline arguments.
57-
if len(sys.argv) != 2:
58-
print_error_and_exit(f"Wrong number of arguments. {len(sys.argv)}\n"
59-
f"Pass the settings filename.")
6063
try:
6164
py_settings_file = open(sys.argv[1])
6265
py_settings = yaml.load(py_settings_file, Loader=yaml.SafeLoader)
@@ -66,6 +69,18 @@ def read_settings_file():
6669
else:
6770
return py_settings
6871

72+
def read_imagemagick_path():
73+
# Reads path to ImageMagick library from commandline arguments.
74+
try:
75+
imagemagick_path = sys.argv[2]
76+
except Exception as exception:
77+
print_error_and_exit("Cannot read path to ImageMagick library root directory!\n"
78+
f"{exception}")
79+
if os.path.isdir(imagemagick_path):
80+
return imagemagick_path
81+
else:
82+
print_error_and_exit("ImageMagick path doesn't exist or it is not a directory!\n"
83+
f"{imagemagick_path}")
6984

7085
def run_command(command):
7186
# Runs shell-command and handles its exit code.
@@ -100,6 +115,11 @@ def get_python_architecture(python_bin_path):
100115
f"Check your settings file or Python installation.")
101116

102117

118+
# Check command line arguments:
119+
if len(sys.argv) != 3:
120+
print_error_and_exit(f"Wrong number of arguments. {len(sys.argv)}\n"
121+
f"Pass the settings filename and path to ImageMagick.")
122+
103123
# Read Python settings file from script argument.
104124
# Paths to Python binaries and include directories will be got from here:
105125
python_settings = read_settings_file()
@@ -135,15 +155,16 @@ def get_python_architecture(python_bin_path):
135155

136156
# For each of supported architectures run manylinux build for all Python binaries,
137157
# defined in the settings file.
138-
for architecture in ["arm64", "x86_64"]:
158+
for architecture in ["x86_64", "arm64"]:
139159
for python_paths in python_settings.values():
140160
# Collect all predefined parameters:
141161
build_parameters = [
142162
"-Pbuild_release=true",
143163
"-Ppython.bin_path=%s" % (python_paths["bin_path"]),
144164
"-Ppython.include_path=%s" % (python_paths["include_path"]),
145165
f"-Penable_python_package=true",
146-
"-Parchitecture=%s" % architecture
166+
"-Parchitecture=%s" % architecture,
167+
"-Pimagemagick_lib_path=%s" % read_imagemagick_path()
147168
]
148169

149170
# Get current Python version in format 'cp3XX':
@@ -173,7 +194,8 @@ def get_python_architecture(python_bin_path):
173194
"-Ppython.bin_path=%s" % (python_paths["bin_path"]),
174195
"-Ppython.include_path=%s" % (python_paths["include_path"]),
175196
f"-Penable_python_package=true",
176-
"-Parchitecture=%s" % (get_python_architecture(python_paths["bin_path"]))
197+
"-Parchitecture=%s" % (get_python_architecture(python_paths["bin_path"])),
198+
"-Pimagemagick_lib_path=%s" % read_imagemagick_path()
177199
]
178200

179201
# Run Python package build:

canvas/src/commonMain/kotlin/org/jetbrains/letsPlot/core/canvas/Context2d.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface Context2d {
3636
fun moveTo(x: Double, y: Double)
3737
fun lineTo(x: Double, y: Double)
3838
fun arc(x: Double, y: Double, radius: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean = false)
39+
fun ellipse(x: Double, y: Double, radiusX: Double, radiusY: Double, rotation: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean)
3940
fun save()
4041
fun restore()
4142
fun setFillStyle(color: Color?)
@@ -60,7 +61,8 @@ interface Context2d {
6061
fun setTransform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double)
6162
fun setLineDash(lineDash: DoubleArray)
6263
fun setLineDashOffset(lineDashOffset: Double)
63-
fun measureText(str: String): Double
64+
fun measureTextWidth(str: String): Double
65+
fun measureText(str: String): TextMetrics
6466

6567
// https://github.com/d3/d3/blob/9364923ee2b35ec2eb80ffc4bdac12a7930097fc/src/svg/line.js#L236
6668
fun drawBezierCurve(points: List<Vec<*>>) {

canvas/src/commonMain/kotlin/org/jetbrains/letsPlot/core/canvas/Delegates.kt

+59-42
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,75 @@
55

66
package org.jetbrains.letsPlot.core.canvas
77

8-
import org.jetbrains.letsPlot.commons.intern.async.Async
9-
import org.jetbrains.letsPlot.commons.intern.async.Asyncs
108
import org.jetbrains.letsPlot.commons.event.MouseEvent
119
import org.jetbrains.letsPlot.commons.event.MouseEventSpec
1210
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
1311
import org.jetbrains.letsPlot.commons.geometry.Vector
12+
import org.jetbrains.letsPlot.commons.intern.async.Async
13+
import org.jetbrains.letsPlot.commons.intern.async.Asyncs
1414
import org.jetbrains.letsPlot.commons.intern.observable.event.EventHandler
1515
import org.jetbrains.letsPlot.commons.registration.Registration
1616
import org.jetbrains.letsPlot.commons.values.Color
1717
import org.jetbrains.letsPlot.core.canvas.AnimationProvider.AnimationEventHandler
1818
import org.jetbrains.letsPlot.core.canvas.AnimationProvider.AnimationTimer
1919

20-
class Context2dDelegate : Context2d {
21-
override fun clearRect(rect: DoubleRectangle) { }
22-
override fun drawImage(snapshot: Canvas.Snapshot) { }
23-
override fun drawImage(snapshot: Canvas.Snapshot, x: Double, y: Double) { }
24-
override fun drawImage(snapshot: Canvas.Snapshot, x: Double, y: Double, dw: Double, dh: Double) { }
25-
override fun drawImage(snapshot: Canvas.Snapshot, sx: Double, sy: Double, sw: Double, sh: Double, dx: Double, dy: Double, dw: Double, dh: Double) { }
26-
override fun beginPath() { }
27-
override fun closePath() { }
28-
override fun stroke() { }
29-
override fun fill() { }
30-
override fun fillEvenOdd() { }
31-
override fun fillRect(x: Double, y: Double, w: Double, h: Double) { }
32-
override fun moveTo(x: Double, y: Double) { }
33-
override fun lineTo(x: Double, y: Double) { }
34-
override fun arc(x: Double, y: Double, radius: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean) { }
35-
override fun save() { }
36-
override fun restore() { }
37-
override fun setFillStyle(color: Color?) { }
38-
override fun setStrokeStyle(color: Color?) { }
39-
override fun setGlobalAlpha(alpha: Double) { }
40-
override fun setFont(f: Font) { }
41-
override fun setLineWidth(lineWidth: Double) { }
42-
override fun strokeRect(x: Double, y: Double, w: Double, h: Double) { }
43-
override fun strokeText(text: String, x: Double, y: Double) { }
44-
override fun fillText(text: String, x: Double, y: Double) { }
45-
override fun scale(x: Double, y: Double) { }
46-
override fun scale(xy: Double) { }
47-
override fun rotate(angle: Double) { }
48-
override fun translate(x: Double, y: Double) { }
49-
override fun transform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double) { }
50-
override fun bezierCurveTo(cp1x: Double, cp1y: Double, cp2x: Double, cp2y: Double, x: Double, y: Double) { }
51-
override fun setLineJoin(lineJoin: LineJoin) { }
52-
override fun setLineCap(lineCap: LineCap) { }
53-
override fun setStrokeMiterLimit(miterLimit: Double) { }
54-
override fun setTextBaseline(baseline: TextBaseline) { }
55-
override fun setTextAlign(align: TextAlign) { }
56-
override fun setTransform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double) { }
57-
override fun setLineDash(lineDash: DoubleArray) { }
58-
override fun setLineDashOffset(lineDashOffset: Double) { }
59-
override fun measureText(str: String): Double { return 0.0}
20+
class Context2dDelegate(
21+
private val logEnabled: Boolean = false
22+
) : Context2d {
23+
private fun log(msg: String) {
24+
if (logEnabled) {
25+
println(msg)
26+
}
27+
}
28+
29+
override fun clearRect(rect: DoubleRectangle) { log("clearRect: $rect") }
30+
override fun drawImage(snapshot: Canvas.Snapshot) { log("drawImage: $snapshot") }
31+
override fun drawImage(snapshot: Canvas.Snapshot, x: Double, y: Double) { log("drawImage: $snapshot, x=$x, y=$y") }
32+
override fun drawImage(snapshot: Canvas.Snapshot, x: Double, y: Double, dw: Double, dh: Double) { log("drawImage: $snapshot, x=$x, y=$y, dw=$dw, dh=$dh") }
33+
override fun drawImage(snapshot: Canvas.Snapshot, sx: Double, sy: Double, sw: Double, sh: Double, dx: Double, dy: Double, dw: Double, dh: Double) { log("drawImage: $snapshot, sx=$sx, sy=$sy, sw=$sw, sh=$sh, dx=$dx, dy=$dy, dw=$dw, dh=$dh") }
34+
override fun beginPath() { log("beginPath") }
35+
override fun closePath() { log("closePath") }
36+
override fun stroke() { log("stroke") }
37+
override fun fill() { log("fill") }
38+
override fun fillEvenOdd() { log("fillEvenOdd") }
39+
override fun fillRect(x: Double, y: Double, w: Double, h: Double) { log("fillRect: x=$x, y=$y, w=$w, h=$h") }
40+
override fun moveTo(x: Double, y: Double) { log("moveTo: x=$x, y=$y") }
41+
override fun lineTo(x: Double, y: Double) { log("lineTo: x=$x, y=$y") }
42+
override fun arc(x: Double, y: Double, radius: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean) { log("arc: x=$x, y=$y, radius=$radius, startAngle=$startAngle, endAngle=$endAngle, anticlockwise=$anticlockwise") }
43+
override fun ellipse(x: Double, y: Double, radiusX: Double, radiusY: Double, rotation: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean) { log("ellipse: x=$x, y=$y, radiusX=$radiusX, radiusY=$radiusY, rotation=$rotation, startAngle=$startAngle, endAngle=$endAngle, anticlockwise=$anticlockwise") }
44+
override fun save() { log("save") }
45+
override fun restore() { log("restore") }
46+
override fun setFillStyle(color: Color?) { log("setFillStyle: $color") }
47+
override fun setStrokeStyle(color: Color?) { log("setStrokeStyle: $color") }
48+
override fun setGlobalAlpha(alpha: Double) { log("setGlobalAlpha: $alpha") }
49+
override fun setFont(f: Font) { log("setFont: $f") }
50+
override fun setLineWidth(lineWidth: Double) { log("setLineWidth: $lineWidth") }
51+
override fun strokeRect(x: Double, y: Double, w: Double, h: Double) { log("strokeRect: x=$x, y=$y, w=$w, h=$h") }
52+
override fun strokeText(text: String, x: Double, y: Double) { log("strokeText: $text, x=$x, y=$y") }
53+
override fun fillText(text: String, x: Double, y: Double) { log("fillText: $text, x=$x, y=$y") }
54+
override fun scale(x: Double, y: Double) { log("scale: x=$x, y=$y") }
55+
override fun scale(xy: Double) { log("scale: xy=$xy") }
56+
override fun rotate(angle: Double) { log("rotate: angle=$angle") }
57+
override fun translate(x: Double, y: Double) { log("translate: x=$x, y=$y") }
58+
override fun transform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double) { log("transform: m11=$m11, m12=$m12, m21=$m21, m22=$m22, dx=$dx, dy=$dy") }
59+
override fun bezierCurveTo(cp1x: Double, cp1y: Double, cp2x: Double, cp2y: Double, x: Double, y: Double) { log("bezierCurveTo: cp1x=$cp1x, cp1y=$cp1y, cp2x=$cp2x, cp2y=$cp2y, x=$x, y=$y") }
60+
override fun setLineJoin(lineJoin: LineJoin) { log("setLineJoin: $lineJoin") }
61+
override fun setLineCap(lineCap: LineCap) { log("setLineCap: $lineCap") }
62+
override fun setStrokeMiterLimit(miterLimit: Double) { log("setStrokeMiterLimit: $miterLimit") }
63+
override fun setTextBaseline(baseline: TextBaseline) { log("setTextBaseline: $baseline") }
64+
override fun setTextAlign(align: TextAlign) { log("setTextAlign: $align") }
65+
override fun setTransform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double) { log("setTransform: m11=$m11, m12=$m12, m21=$m21, m22=$m22, dx=$dx, dy=$dy") }
66+
override fun setLineDash(lineDash: DoubleArray) { log("setLineDash: $lineDash") }
67+
override fun setLineDashOffset(lineDashOffset: Double) { log("setLineDashOffset: $lineDashOffset") }
68+
override fun measureTextWidth(str: String): Double {
69+
log("measureTextWidth: '$str'")
70+
return str.length * 8.0
71+
}
72+
73+
override fun measureText(str: String): TextMetrics {
74+
log("measureText: '$str'")
75+
return TextMetrics(0.0, 0.0, DoubleRectangle.LTRB(0, 0, str.length * 8.0, 14.0))
76+
}
6077
}
6178

6279
class CanvasDelegate(

canvas/src/commonMain/kotlin/org/jetbrains/letsPlot/core/canvas/ScaledContext2d.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ internal class ScaledContext2d(
7878
ctx.arc(scaled(x), scaled(y), scaled(radius), startAngle, endAngle, anticlockwise)
7979
}
8080

81+
override fun ellipse(x: Double, y: Double, radiusX: Double, radiusY: Double, rotation: Double, startAngle: Double, endAngle: Double, anticlockwise: Boolean) {
82+
ctx.ellipse(scaled(x), scaled(y), scaled(radiusX), scaled(radiusY), rotation, startAngle, endAngle, anticlockwise)
83+
}
84+
8185
override fun save() = ctx.save()
8286
override fun restore() = ctx.restore()
8387
override fun setFillStyle(color: Color?) = ctx.setFillStyle(color)
@@ -118,7 +122,8 @@ internal class ScaledContext2d(
118122
override fun fillEvenOdd() = ctx.fillEvenOdd()
119123
override fun setLineDash(lineDash: DoubleArray) = ctx.setLineDash(scaled(lineDash))
120124
override fun setLineDashOffset(lineDashOffset: Double) = ctx.setLineDashOffset(lineDashOffset)
121-
override fun measureText(str: String): Double = descaled(ctx.measureText(str))
125+
override fun measureTextWidth(str: String): Double = descaled(ctx.measureTextWidth(str))
126+
override fun measureText(str: String): TextMetrics = ctx.measureText(str)
122127

123128
override fun clearRect(rect: DoubleRectangle) {
124129
ctx.clearRect(

0 commit comments

Comments
 (0)