Skip to content

Commit 3df668f

Browse files
authored
themeroller-image: Migrate to ImageMagick 7 with cross-spawn
Changes: * Migrate from ImageMagick 6 used via the `gm` npm package to ImageMagick 7 used via `cross-spawn` * Build ImageMagick 7 in a GitHub workflow; we'll be able to drop this when Ubuntu Plucky is released & available in GitHub Actions * Update Node from 18 to 22, including migrating the Dockerfile from `node:18-alpine` to `node:22-alpine` Closes gh-632
1 parent 0530834 commit 3df668f

File tree

8 files changed

+97
-140
lines changed

8 files changed

+97
-140
lines changed

.github/workflows/node.js.yml

+23-4
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,34 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
# Node.js 18 is required by jQuery infra
15-
NODE_VERSION: [18.x, 20.x, 22.x]
14+
# Node.js 22 is required by jQuery infra
15+
NODE_VERSION: [20.x, 22.x]
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
1919

20-
- name: Install xsltproc & ImageMagick
20+
- name: Install xsltproc
2121
run: |
22-
sudo apt-get install xsltproc imagemagick
22+
sudo apt-get install xsltproc
23+
24+
# When Ubuntu Plucky is available in GitHub Actions, switch to it, remove
25+
# the following section and just install the `imagemagick` package normally
26+
# via apt-get.
27+
- name: Download and build ImageMagick 7
28+
run: |
29+
sudo apt-get install -y build-essential pkg-config \
30+
libjpeg-dev libpng-dev libtiff-dev libwebp-dev
31+
32+
# Replace the version below with the desired release
33+
IM_VERSION="7.1.1-44"
34+
wget https://download.imagemagick.org/ImageMagick/download/releases/ImageMagick-${IM_VERSION}.tar.xz
35+
tar -xf ImageMagick-${IM_VERSION}.tar.xz
36+
cd ImageMagick-${IM_VERSION}
37+
38+
./configure
39+
make -j$(nproc)
40+
sudo make install
41+
sudo ldconfig
2342
2443
- name: Use Node.js ${{ matrix.NODE_VERSION }}
2544
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0

.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18
1+
22

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:18-alpine
1+
FROM node:22-alpine
22

33
WORKDIR /app
44
COPY package*.json ./

README.md

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
jQuery UI DownloadBuilder & ThemeRoller backend and frontend application.
22

33
## Requirements
4-
- [node >= 18 and npm](https://nodejs.org/en/download/)
5-
- ImageMagick 6.6.x. ([see below for instructions to compile it from source](#compile-and-install-imagemagick-from-source))
4+
- [node >= 20 and npm](https://nodejs.org/en/download/)
5+
- ImageMagick 7.x. ([See below for instructions how to install it](#install-imagemagick))
66
- grunt-cli: `npm install -g grunt-cli`
77

88
## Getting Started
@@ -95,9 +95,11 @@ $ grunt deploy
9595

9696
## Appendix
9797

98-
### Compile and install ImageMagick from source
98+
### Install ImageMagick
9999

100-
Follow instructions from https://legacy.imagemagick.org/script/install-source.php to install ImageMagic `6.6.9-10`. Then, in the ImageMagick directory, invoke:
100+
You will need ImageMagic `7.x` with PNG support. If your distribution doesn't provide such a version (on macOS it is included in the `imagemagick` Homebrew package), you will need to compile ImageMagick from source.
101+
102+
Follow instructions from https://imagemagick.org/script/install-source.php to install ImageMagic `7.x`. Then, in the ImageMagick directory, invoke:
101103
```
102104
$ ./configure CFLAGS=-O5 CXXFLAGS=-O5 --prefix=/opt --enable-static --with-png --disable-shared
103105
```
@@ -107,7 +109,7 @@ Make sure you have the below in the output.
107109
PNG --with-png=yes yes
108110
```
109111

110-
If "png=yes no", libpng is missing and needs to be installed, `apt-get install libpng-dev` on linux or `brew install libpng` on OS X.
112+
If "png=yes no", `libpng` is missing and needs to be installed, `apt-get install libpng-dev` on linux or `brew install libpng` on macOS.
111113

112114
Continuing...
113115
```
@@ -120,8 +122,8 @@ export DYLD_LIBRARY_PATH="$MAGICK_HOME/lib/"
120122

121123
Make sure you get the right bin when running it.
122124
```
123-
$ which convert
124-
/opt/bin/convert
125+
$ which magick
126+
/opt/bin/magick
125127
```
126128

127-
Hint: add those export statements into your .bash_profile.
129+
Hint: add those export statements into your `.bash_profile`.

eslint.config.mjs

-5
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ export default [
6464
{
6565
files: [ "app/src/**/*.js" ],
6666
languageOptions: {
67-
68-
// No need to keep IE support, so we could bump it to ES2022 as well,
69-
// but we need to switch the minifier to something other than UglifJS
70-
// which is ES5-only.
71-
ecmaVersion: 5,
7267
sourceType: "script",
7368
parserOptions: {
7469
globalReturn: false

lib/themeroller-image.js

+58-74
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
"use strict";
22

3-
var cache, imVersion,
4-
async = require( "async" ),
5-
Cache = require( "./cache" ),
6-
im = require( "gm" ).subClass( { imageMagick: true } ),
7-
semver = require( "semver" ),
8-
dimensionLimit = 3000,
9-
namedColors = require( "./themeroller-colors" );
3+
const path = require( "node:path" );
4+
const async = require( "async" );
5+
const spawn = require( "cross-spawn" );
6+
const Cache = require( "./cache" );
7+
const semver = require( "semver" );
8+
const dimensionLimit = 3000;
9+
const namedColors = require( "./themeroller-colors" );
1010

11-
cache = new Cache( "Image Cache" );
11+
const cache = new Cache( "Image Cache" );
12+
13+
function processImageMagick( args, callback ) {
14+
const proc = spawn( "magick", args );
15+
16+
const stdoutBuffers = [];
17+
const stderrBuffers = [];
18+
19+
proc.stdout.on( "data", data => stdoutBuffers.push( data ) );
20+
proc.stderr.on( "data", data => stderrBuffers.push( data ) );
21+
22+
proc.on( "close", code => {
23+
if ( code !== 0 ) {
24+
return callback( new Error( `magick process exited with code ${ code }: ${ Buffer.concat( stderrBuffers ).toString() }` ) );
25+
}
26+
callback( null, Buffer.concat( stdoutBuffers ) );
27+
} );
28+
29+
proc.on( "error", callback );
30+
}
1231

1332
function expandColor( color ) {
1433
if ( color.length === 3 && /^[0-9a-f]+$/i.test( color ) ) {
@@ -26,44 +45,6 @@ function hashColor( color ) {
2645
return color;
2746
}
2847

29-
// I don't know if there's a better solution, but without the below conversion to Buffer we're not able to use it.
30-
function stream2Buffer( callback ) {
31-
return function( err, stdin, stderr ) {
32-
if ( err ) {
33-
return callback( err );
34-
}
35-
var chunks = [],
36-
dataLen = 0;
37-
err = "";
38-
39-
stdin.on( "data", function( chunk ) {
40-
chunks.push( chunk );
41-
dataLen += chunk.length;
42-
} );
43-
44-
stderr.on( "data", function( chunk ) {
45-
err += chunk;
46-
} );
47-
48-
stdin.on( "end", function() {
49-
var i = 0,
50-
buffer = Buffer.alloc( dataLen );
51-
if ( err.length ) {
52-
return callback( new Error( err ) );
53-
}
54-
chunks.forEach( function( chunk ) {
55-
chunk.copy( buffer, i, 0, chunk.length );
56-
i += chunk.length;
57-
} );
58-
callback( null, buffer );
59-
} );
60-
61-
stdin.on( "error", function( err ) {
62-
callback( err );
63-
} );
64-
};
65-
}
66-
6748
function validateColor( color ) {
6849
color = color.replace( /^#/, "" );
6950
if ( ( color.length === 3 || color.length === 6 ) && /^[0-9a-f]+$/i.test( color ) ) {
@@ -147,27 +128,21 @@ generateIcon = function( params, callback ) {
147128
// Add '#' in the beginning of the colors if needed
148129
color = hashColor( params.color );
149130

150-
// https://www.imagemagick.org/Usage/masking/#shapes
151-
// IM 6.7.9 and below:
152-
// $ convert <icons_mask_filename> -background <color> -alpha shape output.png
153-
// IM > 6.7.9: (see https://github.com/jquery/download.jqueryui.com/issues/132)
154-
// $ convert <icons_mask_filename> -set colorspace RGB -background <color> -alpha shape -set colorspace sRGB output.png
131+
// https://usage.imagemagick.org/masking/#shapes
132+
// See https://github.com/jquery/download.jqueryui.com/issues/132 for why
133+
// `-set colorspace RGB` is needed (twice) in IM >6.7.9. Full command:
134+
// $ magick <icons_mask_filename> -set colorspace RGB -background <color> -alpha shape -set colorspace sRGB output.png
155135

156136
imageQueue.push( function( innerCallback ) {
157137
try {
158-
if ( semver.gt( imVersion, "6.7.9" ) ) {
159-
im( __dirname + "/../template/themeroller/icon/mask.png" )
160-
.out( "-set", "colorspace", "RGB" )
161-
.background( color )
162-
.out( "-alpha", "shape" )
163-
.out( "-set", "colorspace", "sRGB" )
164-
.stream( "png", stream2Buffer( innerCallback ) );
165-
} else {
166-
im( __dirname + "/../template/themeroller/icon/mask.png" )
167-
.background( color )
168-
.out( "-alpha", "shape" )
169-
.stream( "png", stream2Buffer( innerCallback ) );
170-
}
138+
processImageMagick( [
139+
path.join( __dirname, "/../template/themeroller/icon/mask.png" ),
140+
"-set", "colorspace", "RGB",
141+
"-background", color,
142+
"-alpha", "shape",
143+
"-set", "colorspace", "sRGB",
144+
"png:-"
145+
], innerCallback );
171146
} catch ( err ) {
172147
return innerCallback( err );
173148
}
@@ -182,14 +157,20 @@ generateTexture = function( params, callback ) {
182157

183158
filename = params.type.replace( /-/g, "_" ).replace( /$/, ".png" );
184159

185-
// https://www.imagemagick.org/Usage/compose/#dissolve
186-
// $ convert -size <width>x<height> 'xc:<color>' <texture_filename> -compose dissolve -define compose:args=<opacity>,100 -composite output.png
160+
// https://usage.imagemagick.org/compose/#dissolve
161+
// $ magick -size <width>x<height> 'xc:<color>' <texture_filename> -compose dissolve -define compose:args=<opacity>,100 -composite output.png
187162

188163
imageQueue.push( function( innerCallback ) {
189164
try {
190-
im( params.width, params.height, color )
191-
.out( __dirname + "/../template/themeroller/texture/" + filename, "-compose", "dissolve", "-define", "compose:args=" + params.opacity + ",100", "-composite" )
192-
.stream( "png", stream2Buffer( innerCallback ) );
165+
processImageMagick( [
166+
"-size", `${ params.width }x${ params.height }`,
167+
"canvas:" + color,
168+
path.join( __dirname, "/../template/themeroller/texture/", filename ),
169+
"-compose", "dissolve",
170+
"-define", `compose:args=${ params.opacity },100`,
171+
"-composite",
172+
"png:-"
173+
], innerCallback );
193174
} catch ( err ) {
194175
return innerCallback( err );
195176
}
@@ -327,7 +308,7 @@ Image.prototype = {
327308
}
328309
};
329310

330-
// Check the ImageMagick installation using node-gm (in a hacky way).
311+
// Check the ImageMagick installation.
331312
async.series( [
332313
function( callback ) {
333314
var wrappedCallback = function( err ) {
@@ -337,24 +318,27 @@ async.series( [
337318
callback();
338319
};
339320
try {
340-
im()._spawn( [ "convert", "-version" ], true, wrappedCallback );
321+
processImageMagick( [ "-version" ], wrappedCallback );
341322
} catch ( err ) {
342323
return wrappedCallback( err );
343324
}
344325
},
345326
function( callback ) {
346-
im()._spawn( [ "convert", "-version" ], false, stream2Buffer( function( err, buffer ) {
327+
processImageMagick( [ "-version" ], function( err, buffer ) {
328+
if ( err ) {
329+
return callback( err );
330+
}
347331
var output = buffer.toString( "utf8" );
348332
if ( !( /ImageMagick/ ).test( output ) ) {
349333
return callback( new Error( "ImageMagick not installed.\n" + output ) );
350334
}
351-
imVersion = output.split( /\r?\n/ )[ 0 ].replace( /^Version: ImageMagick ([^ ]*).*/, "$1" );
335+
const imVersion = output.split( /\r?\n/ )[ 0 ].replace( /^Version: ImageMagick ([^ ]*).*/, "$1" );
352336
if ( !semver.valid( imVersion ) ) {
353337
return callback( new Error( "Could not identify ImageMagick version.\n" + output ) );
354338
}
355339
imageQueue.resume();
356340
callback();
357-
} ) );
341+
} );
358342
}
359343
], function( err ) {
360344
if ( err ) {

package-lock.json

+2-45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)