Add ignoreIcc input option to ignore embedded ICC profile

This commit is contained in:
Lovell Fuller 2023-02-12 17:51:24 +00:00
parent a2988c9edc
commit 42d2f07e44
8 changed files with 54 additions and 5 deletions

View File

@ -18,6 +18,8 @@ Requires libvips v8.14.0
* Prefer integer (un)premultiply for faster resizing of RGBA images.
* Add `ignoreIcc` input option to ignore embedded ICC profile.
* Allow use of GPS (IFD3) EXIF metadata.
[#2767](https://github.com/lovell/sharp/issues/2767)

View File

@ -126,6 +126,7 @@ const debuglog = util.debuglog('sharp');
* @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF).
* @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically.
* @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
* @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored.
* @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages.
* @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based.
* @param {number} [options.subifd=-1] - subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image.

2
lib/index.d.ts vendored
View File

@ -814,6 +814,8 @@ declare namespace sharp {
sequentialRead?: boolean | undefined;
/** Number representing the DPI for vector images in the range 1 to 100000. (optional, default 72) */
density?: number | undefined;
/** Should the embedded ICC profile, if any, be ignored. */
ignoreIcc?: boolean | undefined;
/** Number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages */
pages?: number | undefined;
/** Page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default 0) */

View File

@ -21,9 +21,9 @@ const align = {
* @private
*/
function _inputOptionsFromObject (obj) {
const { raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj;
return [raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined)
? { raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd }
const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj;
return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined)
? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd }
: undefined;
}
@ -35,6 +35,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
const inputDescriptor = {
failOn: 'warning',
limitInputPixels: Math.pow(0x3FFF, 2),
ignoreIcc: false,
unlimited: false,
sequentialRead: true
};
@ -97,6 +98,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw is.invalidParameterError('density', 'number between 1 and 100000', inputOptions.density);
}
}
// Ignore embeddded ICC profile
if (is.defined(inputOptions.ignoreIcc)) {
if (is.bool(inputOptions.ignoreIcc)) {
inputDescriptor.ignoreIcc = inputOptions.ignoreIcc;
} else {
throw is.invalidParameterError('ignoreIcc', 'boolean', inputOptions.ignoreIcc);
}
}
// limitInputPixels
if (is.defined(inputOptions.limitInputPixels)) {
if (is.bool(inputOptions.limitInputPixels)) {

View File

@ -103,6 +103,10 @@ namespace sharp {
if (HasAttr(input, "density")) {
descriptor->density = AttrAsDouble(input, "density");
}
// Should we ignore any embedded ICC profile
if (HasAttr(input, "ignoreIcc")) {
descriptor->ignoreIcc = AttrAsBool(input, "ignoreIcc");
}
// Raw pixel input
if (HasAttr(input, "rawChannels")) {
descriptor->rawDepth = AttrAsEnum<VipsBandFormat>(input, "rawDepth", VIPS_TYPE_BAND_FORMAT);

View File

@ -55,6 +55,7 @@ namespace sharp {
size_t bufferLength;
bool isBuffer;
double density;
bool ignoreIcc;
VipsBandFormat rawDepth;
int rawChannels;
int rawWidth;
@ -93,6 +94,7 @@ namespace sharp {
bufferLength(0),
isBuffer(FALSE),
density(72.0),
ignoreIcc(FALSE),
rawDepth(VIPS_FORMAT_UCHAR),
rawChannels(0),
rawWidth(0),

View File

@ -320,7 +320,8 @@ class PipelineWorker : public Napi::AsyncWorker {
if (
sharp::HasProfile(image) &&
image.interpretation() != VIPS_INTERPRETATION_LABS &&
image.interpretation() != VIPS_INTERPRETATION_GREY16
image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
!baton->input->ignoreIcc
) {
// Convert to sRGB/P3 using embedded profile
try {
@ -329,7 +330,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
->set("intent", VIPS_INTENT_PERCEPTUAL));
} catch(...) {
// Ignore failure of embedded profile
sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid embedded profile", nullptr);
}
} else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
image = image.icc_transform(processingProfile, VImage::option()

View File

@ -652,6 +652,7 @@ describe('Input/output', function () {
it('toFormat=JPEG takes precedence over WebP extension', function (done) {
const outputWebP = fixtures.path('output.webp');
sharp(fixtures.inputPng)
.resize(8)
.jpeg()
.toFile(outputWebP, function (err, info) {
if (err) throw err;
@ -662,6 +663,7 @@ describe('Input/output', function () {
it('toFormat=WebP takes precedence over JPEG extension', function (done) {
sharp(fixtures.inputPng)
.resize(8)
.webp()
.toFile(outputJpg, function (err, info) {
if (err) throw err;
@ -697,6 +699,27 @@ describe('Input/output', function () {
});
});
it('can ignore ICC profile', async () => {
const [r1, g1, b1] = await sharp(fixtures.inputJpgWithPortraitExif5, { ignoreIcc: true })
.extract({ width: 1, height: 1, top: 16, left: 16 })
.raw()
.toBuffer();
const [r2, g2, b2] = await sharp(fixtures.inputJpgWithPortraitExif5, { ignoreIcc: false })
.extract({ width: 1, height: 1, top: 16, left: 16 })
.raw()
.toBuffer();
assert.deepStrictEqual({ r1, g1, b1, r2, g2, b2 }, {
r1: 60,
r2: 77,
g1: 54,
g2: 69,
b1: 20,
b2: 25
});
});
describe('Switch off safety limits for certain formats', () => {
it('Valid', () => {
assert.doesNotThrow(() => {
@ -816,6 +839,11 @@ describe('Input/output', function () {
sharp({ density: 'zoinks' });
}, /Expected number between 1 and 100000 for density but received zoinks of type string/);
});
it('Invalid ignoreIcc: string', function () {
assert.throws(function () {
sharp({ ignoreIcc: 'zoinks' });
}, /Expected boolean for ignoreIcc but received zoinks of type string/);
});
it('Setting animated property updates pages property', function () {
assert.strictEqual(sharp({ animated: false }).options.input.pages, 1);
assert.strictEqual(sharp({ animated: true }).options.input.pages, -1);