From c4b1d80c350d0282c23ac5b8943594b27359ac07 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 16 Jun 2025 11:11:02 +0100 Subject: [PATCH] Expose stylesheet and highBitdepth SVG input params --- docs/src/content/docs/api-constructor.md | 3 ++ docs/src/content/docs/changelog.md | 2 ++ lib/constructor.js | 3 ++ lib/index.d.ts | 9 ++++++ lib/input.js | 36 ++++++++++++++++++++++-- src/common.cc | 11 ++++++++ src/common.h | 3 ++ test/types/sharp.test-d.ts | 3 ++ test/unit/svg.js | 35 +++++++++++++++++++++++ 9 files changed, 102 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/api-constructor.md b/docs/src/content/docs/api-constructor.md index 7c7e8e9c..139e08f4 100644 --- a/docs/src/content/docs/api-constructor.md +++ b/docs/src/content/docs/api-constructor.md @@ -80,6 +80,9 @@ where the overall height is the `pageHeight` multiplied by the number of `pages` | [options.join.valign] | string | "'top'" | vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). | | [options.tiff] | Object | | Describes TIFF specific options. | | [options.tiff.subifd] | number | -1 | Sub Image File Directory to extract for OME-TIFF, defaults to main image. | +| [options.svg] | Object | | Describes SVG specific options. | +| [options.svg.stylesheet] | string | | Custom CSS for SVG input, applied with a User Origin during the CSS cascade. | +| [options.svg.highBitdepth] | boolean | false | Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. | | [options.pdf] | Object | | Describes PDF specific options. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. | | [options.pdf.background] | string \| Object | | Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. | | [options.openSlide] | Object | | Describes OpenSlide specific options. Requires the use of a globally-installed libvips compiled with support for OpenSlide. | diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md index 125f774f..d39669d7 100644 --- a/docs/src/content/docs/changelog.md +++ b/docs/src/content/docs/changelog.md @@ -14,6 +14,8 @@ Requires libvips v8.17.0 * Deprecate top-level, format-specific constructor parameters, e.g. `subifd` becomes `tiff.subifd`. +* Expose `stylesheet` and `highBitdepth` SVG input parameters. + * Expose `keepDuplicateFrames` GIF output parameter. * Expose JPEG 2000 `oneshot` decoder option. diff --git a/lib/constructor.js b/lib/constructor.js index 5d86c422..daac3b5f 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -190,6 +190,9 @@ const debuglog = util.debuglog('sharp'); * @param {string} [options.join.valign='top'] - vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). * @param {Object} [options.tiff] - Describes TIFF specific options. * @param {number} [options.tiff.subifd=-1] - Sub Image File Directory to extract for OME-TIFF, defaults to main image. + * @param {Object} [options.svg] - Describes SVG specific options. + * @param {string} [options.svg.stylesheet] - Custom CSS for SVG input, applied with a User Origin during the CSS cascade. + * @param {boolean} [options.svg.highBitdepth=false] - Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. * @param {Object} [options.pdf] - Describes PDF specific options. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. * @param {string|Object} [options.pdf.background] - Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @param {Object} [options.openSlide] - Describes OpenSlide specific options. Requires the use of a globally-installed libvips compiled with support for OpenSlide. diff --git a/lib/index.d.ts b/lib/index.d.ts index 1ac6fc24..4b39148e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1005,6 +1005,8 @@ declare namespace sharp { page?: number | undefined; /** TIFF specific input options */ tiff?: TiffInputOptions | undefined; + /** SVG specific input options */ + svg?: SvgInputOptions | undefined; /** PDF specific input options */ pdf?: PdfInputOptions | undefined; /** OpenSlide specific input options */ @@ -1127,6 +1129,13 @@ declare namespace sharp { subifd?: number | undefined; } + interface SvgInputOptions { + /** Custom CSS for SVG input, applied with a User Origin during the CSS cascade. */ + stylesheet?: string | undefined; + /** Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. */ + highBitdepth?: boolean | undefined; + } + interface PdfInputOptions { /** Background colour to use when PDF is partially transparent. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. */ background?: Colour | Color | undefined; diff --git a/lib/input.js b/lib/input.js index d1571555..26aa2f54 100644 --- a/lib/input.js +++ b/lib/input.js @@ -22,14 +22,27 @@ const align = { high: 'high' }; +const inputStreamParameters = [ + // Limits and error handling + 'failOn', 'limitInputPixels', 'unlimited', + // Format-generic + 'animated', 'autoOrient', 'density', 'ignoreIcc', 'page', 'pages', 'sequentialRead', + // Format-specific + 'jp2', 'openSlide', 'pdf', 'raw', 'svg', 'tiff', + // Deprecated + 'failOnError', 'level', 'pdfBackground', 'subifd' +]; + /** * Extract input options, if any, from an object. * @private */ function _inputOptionsFromObject (obj) { - const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot } = obj; - return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot].some(is.defined) - ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot } + const params = inputStreamParameters + .filter(p => is.defined(obj[p])) + .map(p => ([p, obj[p]])); + return params.length + ? Object.fromEntries(params) : undefined; } @@ -260,6 +273,23 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('subifd', 'integer between -1 and 100000', inputOptions.subifd); } } + // SVG specific options + if (is.object(inputOptions.svg)) { + if (is.defined(inputOptions.svg.stylesheet)) { + if (is.string(inputOptions.svg.stylesheet)) { + inputDescriptor.svgStylesheet = inputOptions.svg.stylesheet; + } else { + throw is.invalidParameterError('svg.stylesheet', 'string', inputOptions.svg.stylesheet); + } + } + if (is.defined(inputOptions.svg.highBitdepth)) { + if (is.bool(inputOptions.svg.highBitdepth)) { + inputDescriptor.svgHighBitdepth = inputOptions.svg.highBitdepth; + } else { + throw is.invalidParameterError('svg.highBitdepth', 'boolean', inputOptions.svg.highBitdepth); + } + } + } // PDF specific options if (is.object(inputOptions.pdf) && is.defined(inputOptions.pdf.background)) { inputDescriptor.pdfBackground = this._getBackgroundColourOption(inputOptions.pdf.background); diff --git a/src/common.cc b/src/common.cc index 57ae4aef..32437068 100644 --- a/src/common.cc +++ b/src/common.cc @@ -101,6 +101,13 @@ namespace sharp { if (HasAttr(input, "page")) { descriptor->page = AttrAsUint32(input, "page"); } + // SVG + if (HasAttr(input, "svgStylesheet")) { + descriptor->svgStylesheet = AttrAsStr(input, "svgStylesheet"); + } + if (HasAttr(input, "svgHighBitdepth")) { + descriptor->svgHighBitdepth = AttrAsBool(input, "svgHighBitdepth"); + } // Multi-level input (OpenSlide) if (HasAttr(input, "level")) { descriptor->level = AttrAsUint32(input, "level"); @@ -429,6 +436,10 @@ namespace sharp { option->set("n", descriptor->pages); option->set("page", descriptor->page); } + if (imageType == ImageType::SVG) { + option->set("stylesheet", descriptor->svgStylesheet.data()); + option->set("high_bitdepth", descriptor->svgHighBitdepth); + } if (imageType == ImageType::OPENSLIDE) { option->set("level", descriptor->level); } diff --git a/src/common.h b/src/common.h index b926972b..0b37dbeb 100644 --- a/src/common.h +++ b/src/common.h @@ -77,6 +77,8 @@ namespace sharp { std::vector joinBackground; VipsAlign joinHalign; VipsAlign joinValign; + std::string svgStylesheet; + bool svgHighBitdepth; std::vector pdfBackground; bool jp2Oneshot; @@ -121,6 +123,7 @@ namespace sharp { joinBackground{ 0.0, 0.0, 0.0, 255.0 }, joinHalign(VIPS_ALIGN_LOW), joinValign(VIPS_ALIGN_LOW), + svgHighBitdepth(false), pdfBackground{ 255.0, 255.0, 255.0, 255.0 }, jp2Oneshot(false) {} }; diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts index 8575f1e6..d00cbd29 100644 --- a/test/types/sharp.test-d.ts +++ b/test/types/sharp.test-d.ts @@ -730,6 +730,9 @@ sharp({ openSlide: { level: 0 } }); sharp({ level: 0 }); // Deprecated sharp({ jp2: { oneshot: true } }); sharp({ jp2: { oneshot: false } }); +sharp({ svg: { stylesheet: 'test' }}); +sharp({ svg: { highBitdepth: true }}); +sharp({ svg: { highBitdepth: false }}); sharp({ autoOrient: true }); sharp({ autoOrient: false }); diff --git a/test/unit/svg.js b/test/unit/svg.js index 8f147b0a..6591cf2d 100644 --- a/test/unit/svg.js +++ b/test/unit/svg.js @@ -139,6 +139,41 @@ describe('SVG input', function () { assert.strictEqual(info.channels, 4); }); + it('Can apply custom CSS', async () => { + const svg = ` + + + `; + const stylesheet = 'circle { fill: red }'; + + const [r, g, b, a] = await sharp(Buffer.from(svg), { svg: { stylesheet } }) + .extract({ left: 5, top: 5, width: 1, height: 1 }) + .raw() + .toBuffer(); + + assert.deepEqual([r, g, b, a], [255, 0, 0, 255]); + }); + + it('Invalid stylesheet input option throws', () => + assert.throws( + () => sharp({ svg: { stylesheet: 123 } }), + /Expected string for svg\.stylesheet but received 123 of type number/ + ) + ); + + it('Valid highBitdepth input option does not throw', () => + assert.doesNotThrow( + () => sharp({ svg: { highBitdepth: true } }) + ) + ); + + it('Invalid highBitdepth input option throws', () => + assert.throws( + () => sharp({ svg: { highBitdepth: 123 } }), + /Expected boolean for svg\.highBitdepth but received 123 of type number/ + ) + ); + it('Fails to render SVG larger than 32767x32767', () => assert.rejects( () => sharp(Buffer.from('')).toBuffer(),