Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ namespace sharp {
imageType == ImageType::JP2 ||
imageType == ImageType::TIFF ||
imageType == ImageType::HEIF ||
imageType == ImageType::JXL ||
imageType == ImageType::PDF;
}

Expand Down
2 changes: 0 additions & 2 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,6 @@ class PipelineWorker : public Napi::AsyncWorker {
} else if (baton->formatOut == "jxl" ||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
// Write JXL to buffer
image = sharp::RemoveAnimationProperties(image);
VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
->set("keep", baton->keepMetadata)
->set("distance", baton->jxlDistance)
Expand Down Expand Up @@ -1397,7 +1396,6 @@ class PipelineWorker : public Napi::AsyncWorker {
} else if (baton->formatOut == "jxl" || (mightMatchInput && isJxl) ||
(willMatchInput && inputImageType == sharp::ImageType::JXL)) {
// Write JXL to file
image = sharp::RemoveAnimationProperties(image);
image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("keep", baton->keepMetadata)
->set("distance", baton->jxlDistance)
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ module.exports = {
inputGifGreyPlusAlpha: getPath('grey-plus-alpha.gif'), // http://i.imgur.com/gZ5jlmE.gif
inputGifAnimated: getPath('rotating-squares.gif'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif
inputGifAnimatedLoop3: getPath('animated-loop-3.gif'), // CC-BY-SA-4.0 Petrus3743 https://commons.wikimedia.org/wiki/File:01-Goldener_Schnitt_Formel-Animation.gif
inputJxlAnimated: getPath('rotating-squares.jxl'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I converted rotating-squares.gif using sharp with this change

inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg
inputSvgSmallViewBox: getPath('circle.svg'),
inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg
Expand Down
Binary file added test/fixtures/rotating-squares.jxl
Binary file not shown.
90 changes: 90 additions & 0 deletions test/unit/jxl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const { suite, test } = require('node:test');

const sharp = require('../../');
const fixtures = require('../fixtures');

suite('JXL', () => {
test('called without options does not throw an error', (t) => {
Expand Down Expand Up @@ -114,4 +115,93 @@ suite('JXL', () => {
sharp().jxl({ effort: 'fail' });
});
});
test('invalid loop throws', (t) => {
t.plan(2);
t.assert.throws(() => {
sharp().jxl({ loop: -1 });
});
t.assert.throws(() => {
sharp().jxl({ loop: 65536 });
});
});
test('invalid delay throws', (t) => {
t.plan(2);
t.assert.throws(() => {
sharp().jxl({ delay: -1 });
});
t.assert.throws(() => {
sharp().jxl({ delay: [65536] });
});
});

suite('animated', { skip: !sharp.format.jxl.input.buffer || !sharp.format.jxl.output.buffer }, () => {
test('should read animated image', async (t) => {
t.plan(5);
const { format, pages, pageHeight, loop, delay } = await sharp(fixtures.inputJxlAnimated, { pages: -1 }).metadata();
t.assert.strictEqual(format, 'jxl');
t.assert.strictEqual(pages, 30);
t.assert.strictEqual(pageHeight, 80);
t.assert.strictEqual(loop, 0);
t.assert.deepStrictEqual(delay, Array(30).fill(30));
});
test('should repeat a single delay for all frames', async (t) => {
t.plan(1);
const data = await sharp(fixtures.inputJxlAnimated, { pages: -1 })
.jxl({ delay: 100 })
.toBuffer();
const updated = await sharp(data, { pages: -1 }).metadata();

t.assert.deepStrictEqual(updated.delay, Array(updated.pages).fill(100));
});
test('should limit animation loop', async (t) => {
t.plan(1);
const data = await sharp(fixtures.inputJxlAnimated, { pages: -1 })
.jxl({ loop: 3 })
.toBuffer();
const updated = await sharp(data, { pages: -1 }).metadata();

t.assert.strictEqual(updated.loop, 3);
});
test('should change delay between frames', async (t) => {
t.plan(1);
const original = await sharp(fixtures.inputJxlAnimated, { pages: -1 }).metadata();

const expectedDelay = [...Array(original.pages).fill(40)];
const data = await sharp(fixtures.inputJxlAnimated, { pages: -1 })
.jxl({ delay: expectedDelay })
.toBuffer();
const updated = await sharp(data, { pages: -1 }).metadata();

t.assert.deepStrictEqual(updated.delay, expectedDelay);
});
test('should preserve delay between frames', async (t) => {
t.plan(1);
const data = await sharp(fixtures.inputJxlAnimated, { pages: -1 })
.jxl()
.toBuffer();
const updated = await sharp(data, { pages: -1 }).metadata();

t.assert.deepStrictEqual(updated.delay, Array(30).fill(30));
});
test('should round trip to file', async (t) => {
t.plan(3);
const output = fixtures.path('output.animated.jxl');
await sharp(fixtures.inputJxlAnimated, { pages: -1 }).jxl({ loop: 3 }).toFile(output);
const { pages, loop, delay } = await sharp(output, { pages: -1 }).metadata();
t.assert.strictEqual(pages, 30);
t.assert.strictEqual(loop, 3);
t.assert.deepStrictEqual(delay, Array(30).fill(30));
});
test('should resize animated image to page height', async (t) => {
t.plan(2);
const data = await sharp(fixtures.inputJxlAnimated, { pages: -1 })
.resize({ height: 40 })
.jxl({ effort: 1 })
.toBuffer();
const updated = await sharp(data, { pages: -1 }).metadata();

t.assert.strictEqual(updated.height, 40 * 30);
t.assert.strictEqual(updated.pageHeight, 40);
});
});
});
Loading