mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add experimental support for JPEG-XL, requires libvips with libjxl
The prebuilt binaries do not include support for this format.
This commit is contained in:
parent
f92e33fbff
commit
a7fa7014ef
@ -536,6 +536,38 @@ Returns **Sharp** 
|
||||
|
||||
* **since**: 0.23.0
|
||||
|
||||
## jxl
|
||||
|
||||
Use these JPEG-XL (JXL) options for output image.
|
||||
|
||||
This feature is experimental, please do not use in production systems.
|
||||
|
||||
Requires libvips compiled with support for libjxl.
|
||||
The prebuilt binaries do not include this - see
|
||||
[installing a custom libvips][14].
|
||||
|
||||
Image metadata (EXIF, XMP) is unsupported.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `options` **[Object][6]?** output options
|
||||
|
||||
* `options.distance` **[number][12]** maximum encoding error, between 0 (highest quality) and 15 (lowest quality) (optional, default `1.0`)
|
||||
* `options.quality` **[number][12]?** calculate `distance` based on JPEG-like quality, between 1 and 100, overrides distance if specified
|
||||
* `options.decodingTier` **[number][12]** target decode speed tier, between 0 (highest quality) and 4 (lowest quality) (optional, default `0`)
|
||||
* `options.lossless` **[boolean][10]** use lossless compression (optional, default `false`)
|
||||
* `options.effort` **[number][12]** CPU effort, between 3 (fastest) and 9 (slowest) (optional, default `7`)
|
||||
|
||||
<!---->
|
||||
|
||||
* Throws **[Error][4]** Invalid options
|
||||
|
||||
Returns **Sharp** 
|
||||
|
||||
**Meta**
|
||||
|
||||
* **since**: 0.31.3
|
||||
|
||||
## raw
|
||||
|
||||
Force output to be raw, uncompressed pixel data.
|
||||
|
@ -6,6 +6,9 @@ Requires libvips v8.13.3
|
||||
|
||||
### v0.31.3 - TBD
|
||||
|
||||
* Add experimental support for JPEG-XL images. Requires libvips compiled with libjxl.
|
||||
[#2731](https://github.com/lovell/sharp/issues/2731)
|
||||
|
||||
* Expose `interFrameMaxError` and `interPaletteMaxError` GIF optimisation properties.
|
||||
[#3401](https://github.com/lovell/sharp/issues/3401)
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -308,6 +308,10 @@ const Sharp = function (input, options) {
|
||||
heifCompression: 'av1',
|
||||
heifEffort: 4,
|
||||
heifChromaSubsampling: '4:4:4',
|
||||
jxlDistance: 1,
|
||||
jxlDecodingTier: 0,
|
||||
jxlEffort: 7,
|
||||
jxlLossless: false,
|
||||
rawDepth: 'uchar',
|
||||
tileSize: 256,
|
||||
tileOverlap: 0,
|
||||
|
@ -22,7 +22,8 @@ const formats = new Map([
|
||||
['jp2', 'jp2'],
|
||||
['jpx', 'jp2'],
|
||||
['j2k', 'jp2'],
|
||||
['j2c', 'jp2']
|
||||
['j2c', 'jp2'],
|
||||
['jxl', 'jxl']
|
||||
]);
|
||||
|
||||
const jp2Regex = /\.jp[2x]|j2[kc]$/i;
|
||||
@ -938,6 +939,71 @@ function heif (options) {
|
||||
return this._updateFormatOut('heif', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use these JPEG-XL (JXL) options for output image.
|
||||
*
|
||||
* This feature is experimental, please do not use in production systems.
|
||||
*
|
||||
* Requires libvips compiled with support for libjxl.
|
||||
* The prebuilt binaries do not include this - see
|
||||
* {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
|
||||
*
|
||||
* Image metadata (EXIF, XMP) is unsupported.
|
||||
*
|
||||
* @since 0.31.3
|
||||
*
|
||||
* @param {Object} [options] - output options
|
||||
* @param {number} [options.distance=1.0] - maximum encoding error, between 0 (highest quality) and 15 (lowest quality)
|
||||
* @param {number} [options.quality] - calculate `distance` based on JPEG-like quality, between 1 and 100, overrides distance if specified
|
||||
* @param {number} [options.decodingTier=0] - target decode speed tier, between 0 (highest quality) and 4 (lowest quality)
|
||||
* @param {boolean} [options.lossless=false] - use lossless compression
|
||||
* @param {number} [options.effort=7] - CPU effort, between 3 (fastest) and 9 (slowest)
|
||||
* @returns {Sharp}
|
||||
* @throws {Error} Invalid options
|
||||
*/
|
||||
function jxl (options) {
|
||||
if (is.object(options)) {
|
||||
if (is.defined(options.quality)) {
|
||||
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
|
||||
// https://github.com/libjxl/libjxl/blob/0aeea7f180bafd6893c1db8072dcb67d2aa5b03d/tools/cjxl_main.cc#L640-L644
|
||||
this.options.jxlDistance = options.quality >= 30
|
||||
? 0.1 + (100 - options.quality) * 0.09
|
||||
: 53 / 3000 * options.quality * options.quality - 23 / 20 * options.quality + 25;
|
||||
} else {
|
||||
throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
|
||||
}
|
||||
} else if (is.defined(options.distance)) {
|
||||
if (is.number(options.distance) && is.inRange(options.distance, 0, 15)) {
|
||||
this.options.jxlDistance = options.distance;
|
||||
} else {
|
||||
throw is.invalidParameterError('distance', 'number between 0.0 and 15.0', options.distance);
|
||||
}
|
||||
}
|
||||
if (is.defined(options.decodingTier)) {
|
||||
if (is.integer(options.decodingTier) && is.inRange(options.decodingTier, 0, 4)) {
|
||||
this.options.jxlDecodingTier = options.decodingTier;
|
||||
} else {
|
||||
throw is.invalidParameterError('decodingTier', 'integer between 0 and 4', options.decodingTier);
|
||||
}
|
||||
}
|
||||
if (is.defined(options.lossless)) {
|
||||
if (is.bool(options.lossless)) {
|
||||
this.options.jxlLossless = options.lossless;
|
||||
} else {
|
||||
throw is.invalidParameterError('lossless', 'boolean', options.lossless);
|
||||
}
|
||||
}
|
||||
if (is.defined(options.effort)) {
|
||||
if (is.integer(options.effort) && is.inRange(options.effort, 3, 9)) {
|
||||
this.options.jxlEffort = options.effort;
|
||||
} else {
|
||||
throw is.invalidParameterError('effort', 'integer between 3 and 9', options.effort);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._updateFormatOut('jxl', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force output to be raw, uncompressed pixel data.
|
||||
* Pixel ordering is left-to-right, top-to-bottom, without padding.
|
||||
@ -1308,6 +1374,7 @@ module.exports = function (Sharp) {
|
||||
tiff,
|
||||
avif,
|
||||
heif,
|
||||
jxl,
|
||||
gif,
|
||||
raw,
|
||||
tile,
|
||||
|
@ -207,6 +207,9 @@ namespace sharp {
|
||||
bool IsAvif(std::string const &str) {
|
||||
return EndsWith(str, ".avif") || EndsWith(str, ".AVIF");
|
||||
}
|
||||
bool IsJxl(std::string const &str) {
|
||||
return EndsWith(str, ".jxl") || EndsWith(str, ".JXL");
|
||||
}
|
||||
bool IsDz(std::string const &str) {
|
||||
return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
|
||||
}
|
||||
@ -237,6 +240,7 @@ namespace sharp {
|
||||
case ImageType::PPM: id = "ppm"; break;
|
||||
case ImageType::FITS: id = "fits"; break;
|
||||
case ImageType::EXR: id = "exr"; break;
|
||||
case ImageType::JXL: id = "jxl"; break;
|
||||
case ImageType::VIPS: id = "vips"; break;
|
||||
case ImageType::RAW: id = "raw"; break;
|
||||
case ImageType::UNKNOWN: id = "unknown"; break;
|
||||
@ -281,6 +285,8 @@ namespace sharp {
|
||||
{ "VipsForeignLoadPpmFile", ImageType::PPM },
|
||||
{ "VipsForeignLoadFitsFile", ImageType::FITS },
|
||||
{ "VipsForeignLoadOpenexr", ImageType::EXR },
|
||||
{ "VipsForeignLoadJxlFile", ImageType::JXL },
|
||||
{ "VipsForeignLoadJxlBuffer", ImageType::JXL },
|
||||
{ "VipsForeignLoadVips", ImageType::VIPS },
|
||||
{ "VipsForeignLoadVipsFile", ImageType::VIPS },
|
||||
{ "VipsForeignLoadRaw", ImageType::RAW }
|
||||
|
@ -152,6 +152,7 @@ namespace sharp {
|
||||
PPM,
|
||||
FITS,
|
||||
EXR,
|
||||
JXL,
|
||||
VIPS,
|
||||
RAW,
|
||||
UNKNOWN,
|
||||
@ -182,6 +183,7 @@ namespace sharp {
|
||||
bool IsHeic(std::string const &str);
|
||||
bool IsHeif(std::string const &str);
|
||||
bool IsAvif(std::string const &str);
|
||||
bool IsJxl(std::string const &str);
|
||||
bool IsDz(std::string const &str);
|
||||
bool IsDzZip(std::string const &str);
|
||||
bool IsV(std::string const &str);
|
||||
|
@ -939,6 +939,21 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
area->free_fn = nullptr;
|
||||
vips_area_unref(area);
|
||||
baton->formatOut = "dz";
|
||||
} else if (baton->formatOut == "jxl" ||
|
||||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
|
||||
// Write JXL to buffer
|
||||
image = sharp::RemoveAnimationProperties(image);
|
||||
VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
|
||||
->set("strip", !baton->withMetadata)
|
||||
->set("distance", baton->jxlDistance)
|
||||
->set("tier", baton->jxlDecodingTier)
|
||||
->set("effort", baton->jxlEffort)
|
||||
->set("lossless", baton->jxlLossless)));
|
||||
baton->bufferOut = static_cast<char*>(area->data);
|
||||
baton->bufferOutLength = area->length;
|
||||
area->free_fn = nullptr;
|
||||
vips_area_unref(area);
|
||||
baton->formatOut = "jxl";
|
||||
} else if (baton->formatOut == "raw" ||
|
||||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
|
||||
// Write raw, uncompressed image data to buffer
|
||||
@ -977,6 +992,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
bool const isTiff = sharp::IsTiff(baton->fileOut);
|
||||
bool const isJp2 = sharp::IsJp2(baton->fileOut);
|
||||
bool const isHeif = sharp::IsHeif(baton->fileOut);
|
||||
bool const isJxl = sharp::IsJxl(baton->fileOut);
|
||||
bool const isDz = sharp::IsDz(baton->fileOut);
|
||||
bool const isDzZip = sharp::IsDzZip(baton->fileOut);
|
||||
bool const isV = sharp::IsV(baton->fileOut);
|
||||
@ -1094,6 +1110,17 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
|
||||
->set("lossless", baton->heifLossless));
|
||||
baton->formatOut = "heif";
|
||||
} else if (baton->formatOut == "jxl" || (mightMatchInput && isJxl) ||
|
||||
(willMatchInput && inputImageType == sharp::ImageType::JXL)) {
|
||||
// Write JXL to file
|
||||
image = sharp::RemoveAnimationProperties(image);
|
||||
image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
||||
->set("strip", !baton->withMetadata)
|
||||
->set("distance", baton->jxlDistance)
|
||||
->set("tier", baton->jxlDecodingTier)
|
||||
->set("effort", baton->jxlEffort)
|
||||
->set("lossless", baton->jxlLossless));
|
||||
baton->formatOut = "jxl";
|
||||
} else if (baton->formatOut == "dz" || isDz || isDzZip) {
|
||||
// Write DZ to file
|
||||
if (isDzZip) {
|
||||
@ -1579,6 +1606,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
||||
options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION);
|
||||
baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
|
||||
baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
|
||||
baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
|
||||
baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
|
||||
baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
|
||||
baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
|
||||
baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
|
||||
// Animated output properties
|
||||
if (sharp::HasAttr(options, "loop")) {
|
||||
|
@ -182,6 +182,10 @@ struct PipelineBaton {
|
||||
int heifEffort;
|
||||
std::string heifChromaSubsampling;
|
||||
bool heifLossless;
|
||||
double jxlDistance;
|
||||
int jxlDecodingTier;
|
||||
int jxlEffort;
|
||||
bool jxlLossless;
|
||||
VipsBandFormat rawDepth;
|
||||
std::string err;
|
||||
bool withMetadata;
|
||||
@ -335,6 +339,10 @@ struct PipelineBaton {
|
||||
heifEffort(4),
|
||||
heifChromaSubsampling("4:4:4"),
|
||||
heifLossless(false),
|
||||
jxlDistance(1.0),
|
||||
jxlDecodingTier(0),
|
||||
jxlEffort(7),
|
||||
jxlLossless(false),
|
||||
rawDepth(VIPS_FORMAT_UCHAR),
|
||||
withMetadata(false),
|
||||
withMetadataOrientation(-1),
|
||||
|
@ -115,7 +115,7 @@ Napi::Value format(const Napi::CallbackInfo& info) {
|
||||
Napi::Object format = Napi::Object::New(env);
|
||||
for (std::string const f : {
|
||||
"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz",
|
||||
"ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k"
|
||||
"ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl"
|
||||
}) {
|
||||
// Input
|
||||
const VipsObjectClass *oc = vips_class_find("VipsOperation", (f + "load").c_str());
|
||||
|
97
test/unit/jxl.js
Normal file
97
test/unit/jxl.js
Normal file
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const sharp = require('../../');
|
||||
|
||||
describe('JXL', () => {
|
||||
it('called without options does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl();
|
||||
});
|
||||
});
|
||||
it('valid distance does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl({ distance: 2.3 });
|
||||
});
|
||||
});
|
||||
it('invalid distance should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ distance: 15.1 });
|
||||
});
|
||||
});
|
||||
it('non-numeric distance should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ distance: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid quality > 30 does not throw an error', () => {
|
||||
const s = sharp();
|
||||
assert.doesNotThrow(() => {
|
||||
s.jxl({ quality: 80 });
|
||||
});
|
||||
assert.strictEqual(s.options.jxlDistance, 1.9);
|
||||
});
|
||||
it('valid quality < 30 does not throw an error', () => {
|
||||
const s = sharp();
|
||||
assert.doesNotThrow(() => {
|
||||
s.jxl({ quality: 20 });
|
||||
});
|
||||
assert.strictEqual(s.options.jxlDistance, 9.066666666666666);
|
||||
});
|
||||
it('valid quality does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl({ quality: 80 });
|
||||
});
|
||||
});
|
||||
it('invalid quality should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ quality: 101 });
|
||||
});
|
||||
});
|
||||
it('non-numeric quality should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ quality: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid decodingTier does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl({ decodingTier: 2 });
|
||||
});
|
||||
});
|
||||
it('invalid decodingTier should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ decodingTier: 5 });
|
||||
});
|
||||
});
|
||||
it('non-numeric decodingTier should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ decodingTier: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid lossless does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl({ lossless: true });
|
||||
});
|
||||
});
|
||||
it('non-boolean lossless should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ lossless: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid effort does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().jxl({ effort: 6 });
|
||||
});
|
||||
});
|
||||
it('out of range effort should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ effort: 10 });
|
||||
});
|
||||
});
|
||||
it('invalid effort should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().jxl({ effort: 'fail' });
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user