Expose stylesheet and highBitdepth SVG input params

This commit is contained in:
Lovell Fuller 2025-06-16 11:11:02 +01:00
parent f92540f134
commit c4b1d80c35
9 changed files with 102 additions and 3 deletions

View File

@ -80,6 +80,9 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
| [options.join.valign] | <code>string</code> | <code>&quot;&#x27;top&#x27;&quot;</code> | vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). |
| [options.tiff] | <code>Object</code> | | Describes TIFF specific options. |
| [options.tiff.subifd] | <code>number</code> | <code>-1</code> | Sub Image File Directory to extract for OME-TIFF, defaults to main image. |
| [options.svg] | <code>Object</code> | | Describes SVG specific options. |
| [options.svg.stylesheet] | <code>string</code> | | Custom CSS for SVG input, applied with a User Origin during the CSS cascade. |
| [options.svg.highBitdepth] | <code>boolean</code> | <code>false</code> | 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] | <code>Object</code> | | Describes PDF specific options. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. |
| [options.pdf.background] | <code>string</code> \| <code>Object</code> | | 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] | <code>Object</code> | | Describes OpenSlide specific options. Requires the use of a globally-installed libvips compiled with support for OpenSlide. |

View File

@ -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.

View File

@ -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.

9
lib/index.d.ts vendored
View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -77,6 +77,8 @@ namespace sharp {
std::vector<double> joinBackground;
VipsAlign joinHalign;
VipsAlign joinValign;
std::string svgStylesheet;
bool svgHighBitdepth;
std::vector<double> 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) {}
};

View File

@ -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 });

View File

@ -139,6 +139,41 @@ describe('SVG input', function () {
assert.strictEqual(info.channels, 4);
});
it('Can apply custom CSS', async () => {
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="4" fill="green" />
</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('<svg xmlns="http://www.w3.org/2000/svg" width="32768" height="1" />')).toBuffer(),