Improve multi-frame image resizing (#2789)

* Ports vips_thumbnail logic to sharp 
* Deprecates the pageHeight output option for WebP/GIF
This commit is contained in:
Kleis Auke Wolthuizen
2021-12-10 21:32:04 +01:00
committed by GitHub
parent 659cdabd8e
commit 513fb40f40
29 changed files with 619 additions and 334 deletions

View File

@@ -27,7 +27,7 @@ describe('AVIF', () => {
format: 'jpeg',
hasAlpha: false,
hasProfile: false,
height: 13,
height: 14,
isProgressive: false,
space: 'srgb',
width: 32
@@ -50,7 +50,6 @@ describe('AVIF', () => {
hasProfile: false,
height: 26,
isProgressive: false,
pageHeight: 26,
pagePrimary: 0,
pages: 1,
space: 'srgb',
@@ -71,9 +70,8 @@ describe('AVIF', () => {
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 12,
height: 14,
isProgressive: false,
pageHeight: 12,
pagePrimary: 0,
pages: 1,
space: 'srgb',
@@ -97,7 +95,6 @@ describe('AVIF', () => {
hasProfile: false,
height: 300,
isProgressive: false,
pageHeight: 300,
pagePrimary: 0,
pages: 1,
space: 'srgb',

View File

@@ -6,16 +6,30 @@ const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Extend', function () {
it('extend all sides equally via a single value', function (done) {
sharp(fixtures.inputJpg)
.resize(120)
.extend(10)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(118, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.jpg'), data, done);
});
describe('extend all sides equally via a single value', function () {
it('JPEG', function (done) {
sharp(fixtures.inputJpg)
.resize(120)
.extend(10)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(118, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.jpg'), data, done);
});
});
it('Animated WebP', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(120)
.extend(10)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(140 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.webp'), data, done);
});
});
});
it('extend all sides equally with RGB', function (done) {

View File

@@ -39,10 +39,35 @@ describe('Partial image extraction', function () {
});
});
describe('Animated WebP', function () {
it('Before resize', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.extract({ left: 0, top: 30, width: 80, height: 20 })
.resize(320, 80)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
it('After resize', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 320)
.extract({ left: 0, top: 120, width: 320, height: 80 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
});
it('TIFF', function (done) {
sharp(fixtures.inputTiff)
.extract({ left: 34, top: 63, width: 341, height: 529 })
.jpeg()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(341, info.width);

View File

@@ -80,12 +80,6 @@ describe('GIF input', () => {
assert.strictEqual(true, reduced.length < original.length);
});
it('invalid pageHeight throws', () => {
assert.throws(() => {
sharp().gif({ pageHeight: 0 });
});
});
it('invalid loop throws', () => {
assert.throws(() => {
sharp().gif({ loop: -1 });
@@ -97,7 +91,7 @@ describe('GIF input', () => {
it('invalid delay throws', () => {
assert.throws(() => {
sharp().gif({ delay: [-1] });
sharp().gif({ delay: -1 });
});
assert.throws(() => {
sharp().gif({ delay: [65536] });

View File

@@ -194,6 +194,29 @@ describe('Image metadata', function () {
it('Animated WebP', () =>
sharp(fixtures.inputWebPAnimated)
.metadata()
.then(({
format, width, height, space, channels, depth,
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(width, 80);
assert.strictEqual(height, 80);
assert.strictEqual(space, 'srgb');
assert.strictEqual(channels, 4);
assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 9);
assert.strictEqual(loop, 0);
assert.deepStrictEqual(delay, [120, 120, 90, 120, 120, 90, 120, 90, 30]);
assert.strictEqual(hasProfile, false);
assert.strictEqual(hasAlpha, true);
})
);
it('Animated WebP with all pages', () =>
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.metadata()
.then(({
format, width, height, space, channels, depth,
@@ -202,7 +225,7 @@ describe('Image metadata', function () {
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(width, 80);
assert.strictEqual(height, 80);
assert.strictEqual(height, 720);
assert.strictEqual(space, 'srgb');
assert.strictEqual(channels, 4);
assert.strictEqual(depth, 'uchar');
@@ -221,8 +244,8 @@ describe('Image metadata', function () {
.metadata()
.then(({
format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay,
hasProfile, hasAlpha
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'webp');
assert.strictEqual(width, 370);
@@ -232,7 +255,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 10);
assert.strictEqual(pageHeight, 285);
assert.strictEqual(loop, 3);
assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
assert.strictEqual(hasProfile, false);
@@ -285,8 +307,8 @@ describe('Image metadata', function () {
.metadata()
.then(({
format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay,
background, hasProfile, hasAlpha
isProgressive, pages, loop, delay, background,
hasProfile, hasAlpha
}) => {
assert.strictEqual(format, 'gif');
assert.strictEqual(width, 80);
@@ -296,7 +318,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 30);
assert.strictEqual(pageHeight, 80);
assert.strictEqual(loop, 0);
assert.deepStrictEqual(delay, Array(30).fill(30));
assert.deepStrictEqual(background, { r: 0, g: 0, b: 0 });
@@ -310,8 +331,8 @@ describe('Image metadata', function () {
.metadata()
.then(({
format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay,
hasProfile, hasAlpha
isProgressive, pages, loop, delay, hasProfile,
hasAlpha
}) => {
assert.strictEqual(format, 'gif');
assert.strictEqual(width, 370);
@@ -321,7 +342,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 10);
assert.strictEqual(pageHeight, 285);
assert.strictEqual(loop, 2);
assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
assert.strictEqual(hasProfile, false);
@@ -522,7 +542,7 @@ describe('Image metadata', function () {
assert.strictEqual('Relative', profile.intent);
assert.strictEqual('Printer', profile.deviceClass);
});
fixtures.assertSimilar(output, fixtures.path('expected/icc-cmyk.jpg'), { threshold: 0 }, done);
fixtures.assertSimilar(output, fixtures.expected('icc-cmyk.jpg'), { threshold: 0 }, done);
});
});
@@ -533,7 +553,7 @@ describe('Image metadata', function () {
.withMetadata({ icc: fixtures.path('hilutite.icm') })
.toFile(output, function (err, info) {
if (err) throw err;
fixtures.assertMaxColourDistance(output, fixtures.path('expected/hilutite.jpg'), 9);
fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9);
done();
});
});
@@ -737,7 +757,6 @@ describe('Image metadata', function () {
depth: 'uchar',
isProgressive: false,
pages: 1,
pageHeight: 858,
pagePrimary: 0,
compression: 'av1',
hasProfile: false,

View File

@@ -148,6 +148,42 @@ describe('Resize fit=contain', function () {
});
});
describe('Animated WebP', function () {
it('Width only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 240, {
fit: 'contain',
background: { r: 255, g: 0, b: 0 }
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240 * 9, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-animated-width.webp'), data, done);
});
});
it('Height only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(240, 320, {
fit: 'contain',
background: { r: 255, g: 0, b: 0 }
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(240, info.width);
assert.strictEqual(320 * 9, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-animated-height.webp'), data, done);
});
});
});
it('Invalid position values should fail', function () {
[-1, 8.1, 9, 1000000, false, 'vallejo'].forEach(function (position) {
assert.throws(function () {

View File

@@ -269,6 +269,30 @@ describe('Resize fit=cover', function () {
});
});
describe('Animated WebP', function () {
it('Width only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(80, 320, { fit: sharp.fit.cover })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-width.webp'), data, done);
});
});
it('Height only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 80, { fit: sharp.fit.cover })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
});
describe('Entropy-based strategy', function () {
it('JPEG', function (done) {
sharp(fixtures.inputJpg)

View File

@@ -74,6 +74,27 @@ describe('SVG input', function () {
});
});
it('Convert SVG to PNG utilizing scale-on-load', function (done) {
const size = 1024;
sharp(fixtures.inputSvgSmallViewBox)
.resize(size)
.toFormat('png')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(size, info.width);
assert.strictEqual(size, info.height);
fixtures.assertSimilar(fixtures.expected('circle.png'), data, function (err) {
if (err) throw err;
sharp(data).metadata(function (err, info) {
if (err) throw err;
assert.strictEqual(72, info.density);
done();
});
});
});
});
it('Convert SVG to PNG at 14.4DPI', function (done) {
sharp(fixtures.inputSvg, { density: 14.4 })
.toFormat('png')

View File

@@ -133,12 +133,6 @@ describe('WebP', function () {
});
});
it('invalid pageHeight throws', () => {
assert.throws(() => {
sharp().webp({ pageHeight: 0 });
});
});
it('invalid loop throws', () => {
assert.throws(() => {
sharp().webp({ loop: -1 });
@@ -151,7 +145,7 @@ describe('WebP', function () {
it('invalid delay throws', () => {
assert.throws(() => {
sharp().webp({ delay: [-1] });
sharp().webp({ delay: -1 });
});
assert.throws(() => {
@@ -159,16 +153,13 @@ describe('WebP', function () {
});
});
it('should double the number of frames with default delay', async () => {
const original = await sharp(fixtures.inputWebPAnimated, { pages: -1 }).metadata();
it('should repeat a single delay for all frames', async () => {
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
.webp({ pageHeight: original.pageHeight / 2 })
.webp({ delay: 100 })
.toBuffer()
.then(data => sharp(data, { pages: -1 }).metadata());
assert.strictEqual(updated.pages, original.pages * 2);
assert.strictEqual(updated.pageHeight, original.pageHeight / 2);
assert.deepStrictEqual(updated.delay, [...original.delay, ...Array(9).fill(120)]);
assert.deepStrictEqual(updated.delay, Array(updated.pages).fill(100));
});
it('should limit animation loop', async () => {
@@ -216,22 +207,14 @@ describe('WebP', function () {
});
});
it('should remove animation properties when loading single page', async () => {
const data = await sharp(fixtures.inputGifAnimatedLoop3)
it('should resize animated image to page height', async () => {
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize({ height: 570 })
.webp({ effort: 0 })
.toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
assert.deepStrictEqual(metadata, {
format: 'webp',
width: 740,
height: 570,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
hasProfile: false,
hasAlpha: false
});
.toBuffer()
.then(data => sharp(data, { pages: -1 }).metadata());
assert.strictEqual(updated.height, 570 * 9);
assert.strictEqual(updated.pageHeight, 570);
});
});