Skip to content

Commit 8561f0d

Browse files
committed
Ensure HEIF primary item is used as default page #4487
1 parent 0468c1b commit 8561f0d

File tree

7 files changed

+111
-36
lines changed

7 files changed

+111
-36
lines changed

docs/src/content/docs/changelog/v0.35.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ slug: changelog/v0.35.0
3838
[#4480](https://github.com/lovell/sharp/issues/4480)
3939
[@eddienubes](https://github.com/eddienubes)
4040

41+
* Ensure HEIF primary item is used as default page/frame.
42+
[#4487](https://github.com/lovell/sharp/issues/4487)
43+
4144
* Add WebP `exact` option for control over transparent pixel colour values.

src/common.cc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ namespace sharp {
426426
}
427427
if (ImageTypeSupportsPage(imageType)) {
428428
option->set("n", descriptor->pages);
429-
option->set("page", descriptor->page);
429+
option->set("page", std::max(0, descriptor->page));
430430
}
431431
switch (imageType) {
432432
case ImageType::SVG:
@@ -456,6 +456,22 @@ namespace sharp {
456456
return option;
457457
}
458458

459+
/*
460+
Should HEIF image be re-opened using the primary item?
461+
*/
462+
static bool HeifPrimaryPageReopen(VImage image, InputDescriptor *descriptor, vips::VOption *option) {
463+
if (image.get_typeof(VIPS_META_N_PAGES) == G_TYPE_INT && image.get_typeof("heif-primary") == G_TYPE_INT) {
464+
if (image.get_int(VIPS_META_N_PAGES) > 1 && descriptor->pages == 1 && descriptor->page == -1) {
465+
int const pagePrimary = image.get_int("heif-primary");
466+
if (pagePrimary != 0) {
467+
descriptor->page = pagePrimary;
468+
return true;
469+
}
470+
}
471+
}
472+
return false;
473+
}
474+
459475
/*
460476
Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
461477
*/
@@ -490,6 +506,9 @@ namespace sharp {
490506
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
491507
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
492508
image = SetDensity(image, descriptor->density);
509+
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor, option)) {
510+
option = GetOptionsForImageType(imageType, descriptor);
511+
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
493512
}
494513
} catch (vips::VError const &err) {
495514
throw vips::VError(std::string("Input buffer has corrupt header: ") + err.what());
@@ -577,6 +596,9 @@ namespace sharp {
577596
image = VImage::new_from_file(descriptor->file.data(), option);
578597
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
579598
image = SetDensity(image, descriptor->density);
599+
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor, option)) {
600+
option = GetOptionsForImageType(imageType, descriptor);
601+
image = VImage::new_from_file(descriptor->file.data(), option);
580602
}
581603
} catch (vips::VError const &err) {
582604
throw vips::VError(std::string("Input file has corrupt header: ") + err.what());

src/common.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ namespace sharp {
105105
rawPremultiplied(false),
106106
rawPageHeight(0),
107107
pages(1),
108-
page(0),
108+
page(-1),
109109
createChannels(0),
110110
createWidth(0),
111111
createHeight(0),

src/pipeline.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
8484
if (nPages == -1) {
8585
// Resolve the number of pages if we need to render until the end of the document
8686
nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
87-
? image.get_int(VIPS_META_N_PAGES) - baton->input->page
87+
? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
8888
: 1;
8989
}
9090

test/fixtures/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ module.exports = {
127127
inputSvgSmallViewBox: getPath('circle.svg'),
128128
inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg
129129
inputAvif: getPath('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix
130-
130+
inputAvifWithPitmBox: getPath('pitm.avif'), // https://github.com/lovell/sharp/issues/4487
131131
inputJPGBig: getPath('flowers.jpeg'),
132132

133133
inputPngDotAndLines: getPath('dot-and-lines.png'),

test/fixtures/pitm.avif

65.2 KB
Binary file not shown.

test/unit/avif.js

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ const { describe, it } = require('node:test');
77
const assert = require('node:assert');
88

99
const sharp = require('../../');
10-
const { inputAvif, inputJpg, inputGifAnimated, inputPng } = require('../fixtures');
10+
const {
11+
inputAvif,
12+
inputAvifWithPitmBox,
13+
inputJpg,
14+
inputGifAnimated,
15+
inputPng,
16+
} = require('../fixtures');
1117

1218
describe('AVIF', () => {
1319
it('called without options does not throw an error', () => {
@@ -17,16 +23,13 @@ describe('AVIF', () => {
1723
});
1824

1925
it('can convert AVIF to JPEG', async () => {
20-
const data = await sharp(inputAvif)
21-
.resize(32)
22-
.jpeg()
23-
.toBuffer();
26+
const data = await sharp(inputAvif).resize(32).jpeg().toBuffer();
2427
const { size, ...metadata } = await sharp(data).metadata();
2528
void size;
2629
assert.deepStrictEqual(metadata, {
2730
autoOrient: {
2831
height: 13,
29-
width: 32
32+
width: 32,
3033
},
3134
channels: 3,
3235
chromaSubsampling: '4:2:0',
@@ -41,7 +44,7 @@ describe('AVIF', () => {
4144
isProgressive: false,
4245
isPalette: false,
4346
space: 'srgb',
44-
width: 32
47+
width: 32,
4548
});
4649
});
4750

@@ -55,7 +58,7 @@ describe('AVIF', () => {
5558
assert.deepStrictEqual(metadata, {
5659
autoOrient: {
5760
height: 26,
58-
width: 32
61+
width: 32,
5962
},
6063
channels: 3,
6164
compression: 'av1',
@@ -70,7 +73,7 @@ describe('AVIF', () => {
7073
pagePrimary: 0,
7174
pages: 1,
7275
space: 'srgb',
73-
width: 32
76+
width: 32,
7477
});
7578
});
7679

@@ -84,7 +87,7 @@ describe('AVIF', () => {
8487
assert.deepStrictEqual(metadata, {
8588
autoOrient: {
8689
height: 24,
87-
width: 32
90+
width: 32,
8891
},
8992
channels: 3,
9093
compression: 'av1',
@@ -99,20 +102,18 @@ describe('AVIF', () => {
99102
pagePrimary: 0,
100103
pages: 1,
101104
space: 'srgb',
102-
width: 32
105+
width: 32,
103106
});
104107
});
105108

106109
it('can passthrough AVIF', async () => {
107-
const data = await sharp(inputAvif)
108-
.resize(32)
109-
.toBuffer();
110+
const data = await sharp(inputAvif).resize(32).toBuffer();
110111
const { size, ...metadata } = await sharp(data).metadata();
111112
void size;
112113
assert.deepStrictEqual(metadata, {
113114
autoOrient: {
114115
height: 13,
115-
width: 32
116+
width: 32,
116117
},
117118
channels: 3,
118119
compression: 'av1',
@@ -127,7 +128,7 @@ describe('AVIF', () => {
127128
pagePrimary: 0,
128129
pages: 1,
129130
space: 'srgb',
130-
width: 32
131+
width: 32,
131132
});
132133
});
133134

@@ -141,7 +142,7 @@ describe('AVIF', () => {
141142
assert.deepStrictEqual(metadata, {
142143
autoOrient: {
143144
height: 300,
144-
width: 10
145+
width: 10,
145146
},
146147
channels: 4,
147148
compression: 'av1',
@@ -156,7 +157,7 @@ describe('AVIF', () => {
156157
pagePrimary: 0,
157158
pages: 1,
158159
space: 'srgb',
159-
width: 10
160+
width: 10,
160161
});
161162
});
162163

@@ -171,7 +172,7 @@ describe('AVIF', () => {
171172
assert.deepStrictEqual(metadata, {
172173
autoOrient: {
173174
height: 26,
174-
width: 32
175+
width: 32,
175176
},
176177
channels: 3,
177178
compression: 'av1',
@@ -186,30 +187,37 @@ describe('AVIF', () => {
186187
pagePrimary: 0,
187188
pages: 1,
188189
space: 'srgb',
189-
width: 32
190+
width: 32,
190191
});
191192
});
192193

193194
it('Invalid width - too large', async () =>
194195
assert.rejects(
195-
() => sharp({ create: { width: 16385, height: 16, channels: 3, background: 'red' } }).avif().toBuffer(),
196-
/Processed image is too large for the HEIF format/
197-
)
198-
);
196+
() =>
197+
sharp({
198+
create: { width: 16385, height: 16, channels: 3, background: 'red' },
199+
})
200+
.avif()
201+
.toBuffer(),
202+
/Processed image is too large for the HEIF format/,
203+
));
199204

200205
it('Invalid height - too large', async () =>
201206
assert.rejects(
202-
() => sharp({ create: { width: 16, height: 16385, channels: 3, background: 'red' } }).avif().toBuffer(),
203-
/Processed image is too large for the HEIF format/
204-
)
205-
);
207+
() =>
208+
sharp({
209+
create: { width: 16, height: 16385, channels: 3, background: 'red' },
210+
})
211+
.avif()
212+
.toBuffer(),
213+
/Processed image is too large for the HEIF format/,
214+
));
206215

207216
it('Invalid bitdepth value throws error', () =>
208217
assert.throws(
209218
() => sharp().avif({ bitdepth: 11 }),
210-
/Expected 8, 10 or 12 for bitdepth but received 11 of type number/
211-
)
212-
);
219+
/Expected 8, 10 or 12 for bitdepth but received 11 of type number/,
220+
));
213221

214222
it('Different tune options result in different file sizes', async () => {
215223
const ssim = await sharp(inputJpg)
@@ -221,5 +229,47 @@ describe('AVIF', () => {
221229
.avif({ tune: 'iq', effort: 0 })
222230
.toBuffer();
223231
assert(ssim.length < iq.length);
224-
})
232+
});
233+
234+
it('AVIF with non-zero primary item uses it as default page', async () => {
235+
const { exif, ...metadata } = await sharp(inputAvifWithPitmBox).metadata();
236+
void exif;
237+
assert.deepStrictEqual(metadata, {
238+
format: 'heif',
239+
width: 4096,
240+
height: 800,
241+
space: 'srgb',
242+
channels: 3,
243+
depth: 'uchar',
244+
isProgressive: false,
245+
isPalette: false,
246+
bitsPerSample: 8,
247+
pages: 5,
248+
pagePrimary: 4,
249+
compression: 'av1',
250+
resolutionUnit: 'cm',
251+
hasProfile: false,
252+
hasAlpha: false,
253+
autoOrient: { width: 4096, height: 800 },
254+
});
255+
256+
const data = await sharp(inputAvifWithPitmBox)
257+
.png({ compressionLevel: 0 })
258+
.toBuffer();
259+
const { size, ...pngMetadata } = await sharp(data).metadata();
260+
assert.deepStrictEqual(pngMetadata, {
261+
format: 'png',
262+
width: 4096,
263+
height: 800,
264+
space: 'srgb',
265+
channels: 3,
266+
depth: 'uchar',
267+
isProgressive: false,
268+
isPalette: false,
269+
bitsPerSample: 8,
270+
hasProfile: false,
271+
hasAlpha: false,
272+
autoOrient: { width: 4096, height: 800 },
273+
});
274+
});
225275
});

0 commit comments

Comments
 (0)