mirror of
https://github.com/lovell/sharp.git
synced 2025-12-19 07:15:08 +01:00
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:
committed by
GitHub
parent
659cdabd8e
commit
513fb40f40
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user