mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add experimental support for HEIF images #1105
Requires a custom, globally-installed libvips compiled with libheif
This commit is contained in:
parent
3ff3353550
commit
b737d4601e
@ -34,8 +34,9 @@ A `Promise` is returned when `callback` is not provided.
|
||||
- `density`: Number of pixels per inch (DPI), if present
|
||||
- `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
|
||||
- `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
|
||||
- `pages`: Number of pages/frames contained within the image, with support for TIFF, PDF, animated GIF and animated WebP
|
||||
- `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP
|
||||
- `pageHeight`: Number of pixels high each page in this PDF image will be.
|
||||
- `pagePrimary`: Number of the primary page in a HEIF image
|
||||
- `hasProfile`: Boolean indicating the presence of an embedded ICC profile
|
||||
- `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
|
||||
- `orientation`: Number value of the EXIF Orientation header, if present
|
||||
|
@ -232,6 +232,29 @@ sharp('input.svg')
|
||||
.then(info => { ... });
|
||||
```
|
||||
|
||||
- Throws **[Error][3]** Invalid options
|
||||
|
||||
Returns **Sharp**
|
||||
|
||||
## heif
|
||||
|
||||
Use these HEIF options for output image.
|
||||
|
||||
Support for HEIF (HEIC/AVIF) is experimental.
|
||||
Do not use this in production systems.
|
||||
|
||||
Requires a custom, globally-installed libvips compiled with support for libheif.
|
||||
|
||||
Most versions of libheif support only the patent-encumbered HEVC compression format.
|
||||
|
||||
### Parameters
|
||||
|
||||
- `options` **[Object][5]?** output options
|
||||
- `options.quality` **[Number][8]** quality, integer 1-100 (optional, default `80`)
|
||||
- `options.compression` **[Boolean][6]** compression format: hevc, avc, jpeg, av1 (optional, default `'hevc'`)
|
||||
- `options.lossless` **[Boolean][6]** use lossless compression (optional, default `false`)
|
||||
|
||||
|
||||
- Throws **[Error][3]** Invalid options
|
||||
|
||||
Returns **Sharp**
|
||||
|
@ -8,6 +8,9 @@ Requires libvips v8.8.0.
|
||||
|
||||
* Remove `overlayWith` previously deprecated in v0.22.0.
|
||||
|
||||
* Add experimental support for HEIF images. Requires libvips compiled with libheif.
|
||||
[#1105](https://github.com/lovell/sharp/issues/1105)
|
||||
|
||||
* Add experimental support for Worker Threads.
|
||||
[#1558](https://github.com/lovell/sharp/issues/1558)
|
||||
|
||||
|
@ -207,6 +207,9 @@ const Sharp = function (input, options) {
|
||||
tiffTileWidth: 256,
|
||||
tiffXres: 1.0,
|
||||
tiffYres: 1.0,
|
||||
heifQuality: 80,
|
||||
heifLossless: false,
|
||||
heifCompression: 'hevc',
|
||||
tileSize: 256,
|
||||
tileOverlap: 0,
|
||||
linearA: 1,
|
||||
|
@ -195,8 +195,9 @@ function clone () {
|
||||
* - `density`: Number of pixels per inch (DPI), if present
|
||||
* - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
|
||||
* - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
|
||||
* - `pages`: Number of pages/frames contained within the image, with support for TIFF, PDF, animated GIF and animated WebP
|
||||
* - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP
|
||||
* - `pageHeight`: Number of pixels high each page in this PDF image will be.
|
||||
* - `pagePrimary`: Number of the primary page in a HEIF image
|
||||
* - `hasProfile`: Boolean indicating the presence of an embedded ICC profile
|
||||
* - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
|
||||
* - `orientation`: Number value of the EXIF Orientation header, if present
|
||||
|
@ -429,6 +429,53 @@ function tiff (options) {
|
||||
return this._updateFormatOut('tiff', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use these HEIF options for output image.
|
||||
*
|
||||
* Support for HEIF (HEIC/AVIF) is experimental.
|
||||
* Do not use this in production systems.
|
||||
*
|
||||
* Requires a custom, globally-installed libvips compiled with support for libheif.
|
||||
*
|
||||
* Most versions of libheif support only the patent-encumbered HEVC compression format.
|
||||
*
|
||||
* @param {Object} [options] - output options
|
||||
* @param {Number} [options.quality=80] - quality, integer 1-100
|
||||
* @param {Boolean} [options.compression='hevc'] - compression format: hevc, avc, jpeg, av1
|
||||
* @param {Boolean} [options.lossless=false] - use lossless compression
|
||||
* @returns {Sharp}
|
||||
* @throws {Error} Invalid options
|
||||
*/
|
||||
function heif (options) {
|
||||
if (!this.constructor.format.heif.output.buffer) {
|
||||
throw new Error('The heif operation requires libvips to have been installed with support for libheif');
|
||||
}
|
||||
if (is.object(options)) {
|
||||
if (is.defined(options.quality)) {
|
||||
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
|
||||
this.options.heifQuality = options.quality;
|
||||
} else {
|
||||
throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
|
||||
}
|
||||
}
|
||||
if (is.defined(options.lossless)) {
|
||||
if (is.bool(options.lossless)) {
|
||||
this.options.heifLossless = options.lossless;
|
||||
} else {
|
||||
throw is.invalidParameterError('lossless', 'boolean', options.lossless);
|
||||
}
|
||||
}
|
||||
if (is.defined(options.compression)) {
|
||||
if (is.string(options.compression) && is.inArray(options.compression, ['hevc', 'avc', 'jpeg', 'av1'])) {
|
||||
this.options.heifCompression = options.compression;
|
||||
} else {
|
||||
throw is.invalidParameterError('compression', 'one of: hevc, avc, jpeg, av1', options.compression);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._updateFormatOut('heif', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force output to be raw, uncompressed uint8 pixel data.
|
||||
*
|
||||
@ -720,6 +767,7 @@ module.exports = function (Sharp) {
|
||||
png,
|
||||
webp,
|
||||
tiff,
|
||||
heif,
|
||||
raw,
|
||||
toFormat,
|
||||
tile,
|
||||
|
@ -110,6 +110,15 @@ namespace sharp {
|
||||
bool IsTiff(std::string const &str) {
|
||||
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
|
||||
}
|
||||
bool IsHeic(std::string const &str) {
|
||||
return EndsWith(str, ".heic") || EndsWith(str, ".HEIC");
|
||||
}
|
||||
bool IsHeif(std::string const &str) {
|
||||
return EndsWith(str, ".heif") || EndsWith(str, ".HEIF") || IsHeic(str) || IsAvif(str);
|
||||
}
|
||||
bool IsAvif(std::string const &str) {
|
||||
return EndsWith(str, ".avif") || EndsWith(str, ".AVIF");
|
||||
}
|
||||
bool IsDz(std::string const &str) {
|
||||
return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
|
||||
}
|
||||
@ -132,6 +141,7 @@ namespace sharp {
|
||||
case ImageType::TIFF: id = "tiff"; break;
|
||||
case ImageType::GIF: id = "gif"; break;
|
||||
case ImageType::SVG: id = "svg"; break;
|
||||
case ImageType::HEIF: id = "heif"; break;
|
||||
case ImageType::PDF: id = "pdf"; break;
|
||||
case ImageType::MAGICK: id = "magick"; break;
|
||||
case ImageType::OPENSLIDE: id = "openslide"; break;
|
||||
@ -165,6 +175,8 @@ namespace sharp {
|
||||
imageType = ImageType::GIF;
|
||||
} else if (EndsWith(loader, "SvgBuffer")) {
|
||||
imageType = ImageType::SVG;
|
||||
} else if (EndsWith(loader, "HeifBuffer")) {
|
||||
imageType = ImageType::HEIF;
|
||||
} else if (EndsWith(loader, "PdfBuffer")) {
|
||||
imageType = ImageType::PDF;
|
||||
} else if (EndsWith(loader, "MagickBuffer")) {
|
||||
@ -196,6 +208,8 @@ namespace sharp {
|
||||
imageType = ImageType::GIF;
|
||||
} else if (EndsWith(loader, "SvgFile")) {
|
||||
imageType = ImageType::SVG;
|
||||
} else if (EndsWith(loader, "HeifFile")) {
|
||||
imageType = ImageType::HEIF;
|
||||
} else if (EndsWith(loader, "PdfFile")) {
|
||||
imageType = ImageType::PDF;
|
||||
} else if (EndsWith(loader, "Ppm")) {
|
||||
@ -222,6 +236,7 @@ namespace sharp {
|
||||
return
|
||||
imageType == ImageType::GIF ||
|
||||
imageType == ImageType::TIFF ||
|
||||
imageType == ImageType::HEIF ||
|
||||
imageType == ImageType::PDF;
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,7 @@ namespace sharp {
|
||||
TIFF,
|
||||
GIF,
|
||||
SVG,
|
||||
HEIF,
|
||||
PDF,
|
||||
MAGICK,
|
||||
OPENSLIDE,
|
||||
@ -123,6 +124,9 @@ namespace sharp {
|
||||
bool IsPng(std::string const &str);
|
||||
bool IsWebp(std::string const &str);
|
||||
bool IsTiff(std::string const &str);
|
||||
bool IsHeic(std::string const &str);
|
||||
bool IsHeif(std::string const &str);
|
||||
bool IsAvif(std::string const &str);
|
||||
bool IsDz(std::string const &str);
|
||||
bool IsDzZip(std::string const &str);
|
||||
bool IsV(std::string const &str);
|
||||
|
@ -77,6 +77,9 @@ class MetadataWorker : public Nan::AsyncWorker {
|
||||
if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
|
||||
baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT);
|
||||
}
|
||||
if (image.get_typeof("heif-primary") == G_TYPE_INT) {
|
||||
baton->pagePrimary = image.get_int("heif-primary");
|
||||
}
|
||||
baton->hasProfile = sharp::HasProfile(image);
|
||||
// Derived attributes
|
||||
baton->hasAlpha = sharp::HasAlpha(image);
|
||||
@ -158,6 +161,9 @@ class MetadataWorker : public Nan::AsyncWorker {
|
||||
if (baton->pageHeight > 0) {
|
||||
Set(info, New("pageHeight").ToLocalChecked(), New<v8::Uint32>(baton->pageHeight));
|
||||
}
|
||||
if (baton->pagePrimary > -1) {
|
||||
Set(info, New("pagePrimary").ToLocalChecked(), New<v8::Uint32>(baton->pagePrimary));
|
||||
}
|
||||
Set(info, New("hasProfile").ToLocalChecked(), New<v8::Boolean>(baton->hasProfile));
|
||||
Set(info, New("hasAlpha").ToLocalChecked(), New<v8::Boolean>(baton->hasAlpha));
|
||||
if (baton->orientation > 0) {
|
||||
|
@ -36,6 +36,7 @@ struct MetadataBaton {
|
||||
int paletteBitDepth;
|
||||
int pages;
|
||||
int pageHeight;
|
||||
int pagePrimary;
|
||||
bool hasProfile;
|
||||
bool hasAlpha;
|
||||
int orientation;
|
||||
@ -59,6 +60,7 @@ struct MetadataBaton {
|
||||
paletteBitDepth(0),
|
||||
pages(0),
|
||||
pageHeight(0),
|
||||
pagePrimary(-1),
|
||||
hasProfile(false),
|
||||
hasAlpha(false),
|
||||
orientation(0),
|
||||
|
@ -793,6 +793,18 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
vips_area_unref(area);
|
||||
baton->formatOut = "tiff";
|
||||
baton->channels = std::min(baton->channels, 3);
|
||||
} else if (baton->formatOut == "heif" || (baton->formatOut == "input" && inputImageType == ImageType::HEIF)) {
|
||||
// Write HEIF to buffer
|
||||
VipsArea *area = VIPS_AREA(image.heifsave_buffer(VImage::option()
|
||||
->set("strip", !baton->withMetadata)
|
||||
->set("compression", baton->heifCompression)
|
||||
->set("Q", baton->heifQuality)
|
||||
->set("lossless", baton->heifLossless)));
|
||||
baton->bufferOut = static_cast<char*>(area->data);
|
||||
baton->bufferOutLength = area->length;
|
||||
area->free_fn = nullptr;
|
||||
vips_area_unref(area);
|
||||
baton->formatOut = "heif";
|
||||
} else if (baton->formatOut == "raw" || (baton->formatOut == "input" && inputImageType == ImageType::RAW)) {
|
||||
// Write raw, uncompressed image data to buffer
|
||||
if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) {
|
||||
@ -827,6 +839,7 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
bool const isPng = sharp::IsPng(baton->fileOut);
|
||||
bool const isWebp = sharp::IsWebp(baton->fileOut);
|
||||
bool const isTiff = sharp::IsTiff(baton->fileOut);
|
||||
bool const isHeif = sharp::IsHeif(baton->fileOut);
|
||||
bool const isDz = sharp::IsDz(baton->fileOut);
|
||||
bool const isDzZip = sharp::IsDzZip(baton->fileOut);
|
||||
bool const isV = sharp::IsV(baton->fileOut);
|
||||
@ -893,6 +906,20 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
->set("yres", baton->tiffYres));
|
||||
baton->formatOut = "tiff";
|
||||
baton->channels = std::min(baton->channels, 3);
|
||||
} else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
|
||||
(willMatchInput && inputImageType == ImageType::HEIF)) {
|
||||
// Write HEIF to file
|
||||
#ifdef VIPS_TYPE_FOREIGN_HEIF_COMPRESSION
|
||||
if (sharp::IsAvif(baton->fileOut)) {
|
||||
baton->heifCompression = VIPS_FOREIGN_HEIF_COMPRESSION_AV1;
|
||||
}
|
||||
#endif
|
||||
image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
||||
->set("strip", !baton->withMetadata)
|
||||
->set("Q", baton->heifQuality)
|
||||
->set("compression", baton->heifCompression)
|
||||
->set("lossless", baton->heifLossless));
|
||||
baton->formatOut = "heif";
|
||||
} else if (baton->formatOut == "dz" || isDz || isDzZip) {
|
||||
if (isDzZip) {
|
||||
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
|
||||
@ -1332,7 +1359,13 @@ NAN_METHOD(pipeline) {
|
||||
baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
|
||||
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
|
||||
AttrAsStr(options, "tiffPredictor").data()));
|
||||
|
||||
baton->heifQuality = AttrTo<uint32_t>(options, "heifQuality");
|
||||
baton->heifLossless = AttrTo<bool>(options, "heifLossless");
|
||||
#ifdef VIPS_TYPE_FOREIGN_HEIF_COMPRESSION
|
||||
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
|
||||
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
|
||||
AttrAsStr(options, "heifCompression").data()));
|
||||
#endif
|
||||
// Tile output
|
||||
baton->tileSize = AttrTo<uint32_t>(options, "tileSize");
|
||||
baton->tileOverlap = AttrTo<uint32_t>(options, "tileOverlap");
|
||||
|
@ -148,6 +148,9 @@ struct PipelineBaton {
|
||||
int tiffTileWidth;
|
||||
double tiffXres;
|
||||
double tiffYres;
|
||||
int heifQuality;
|
||||
int heifCompression; // TODO(libvips 8.9.0): VipsForeignHeifCompression
|
||||
bool heifLossless;
|
||||
std::string err;
|
||||
bool withMetadata;
|
||||
int withMetadataOrientation;
|
||||
@ -247,6 +250,9 @@ struct PipelineBaton {
|
||||
tiffTileWidth(256),
|
||||
tiffXres(1.0),
|
||||
tiffYres(1.0),
|
||||
heifQuality(80),
|
||||
heifCompression(1), // TODO(libvips 8.9.0): VIPS_FOREIGN_HEIF_COMPRESSION_HEVC
|
||||
heifLossless(false),
|
||||
withMetadata(false),
|
||||
withMetadataOrientation(-1),
|
||||
convKernelWidth(0),
|
||||
|
@ -151,7 +151,7 @@ NAN_METHOD(format) {
|
||||
// Which load/save operations are available for each compressed format?
|
||||
Local<Object> format = New<Object>();
|
||||
for (std::string f : {
|
||||
"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz", "ppm", "fits", "gif", "svg", "pdf", "v"
|
||||
"jpeg", "png", "webp", "tiff", "magick", "openslide", "dz", "ppm", "fits", "gif", "svg", "heif", "pdf", "v"
|
||||
}) {
|
||||
// Input
|
||||
Local<Boolean> hasInputFile =
|
||||
|
79
test/unit/heif.js
Normal file
79
test/unit/heif.js
Normal file
@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const sharp = require('../../');
|
||||
|
||||
const formatHeifOutputBuffer = sharp.format.heif.output.buffer;
|
||||
|
||||
describe('HEIF (experimental)', () => {
|
||||
describe('Stubbed without support for HEIF', () => {
|
||||
before(() => {
|
||||
sharp.format.heif.output.buffer = false;
|
||||
});
|
||||
after(() => {
|
||||
sharp.format.heif.output.buffer = formatHeifOutputBuffer;
|
||||
});
|
||||
|
||||
it('should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stubbed with support for HEIF', () => {
|
||||
before(() => {
|
||||
sharp.format.heif.output.buffer = true;
|
||||
});
|
||||
after(() => {
|
||||
sharp.format.heif.output.buffer = formatHeifOutputBuffer;
|
||||
});
|
||||
|
||||
it('called without options does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().heif();
|
||||
});
|
||||
});
|
||||
it('valid quality does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().heif({ quality: 50 });
|
||||
});
|
||||
});
|
||||
it('invalid quality should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif({ quality: 101 });
|
||||
});
|
||||
});
|
||||
it('non-numeric quality should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif({ quality: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid lossless does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().heif({ lossless: true });
|
||||
});
|
||||
});
|
||||
it('non-boolean lossless should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif({ lossless: 'fail' });
|
||||
});
|
||||
});
|
||||
it('valid compression does not throw an error', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
sharp().heif({ compression: 'avc' });
|
||||
});
|
||||
});
|
||||
it('unknown compression should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif({ compression: 'fail' });
|
||||
});
|
||||
});
|
||||
it('invalid compression should throw an error', () => {
|
||||
assert.throws(() => {
|
||||
sharp().heif({ compression: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user