From b737d4601ed2f00a506ae062cd634763d17eeb43 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 4 Jul 2019 13:20:24 +0100 Subject: [PATCH] Add experimental support for HEIF images #1105 Requires a custom, globally-installed libvips compiled with libheif --- docs/api-input.md | 3 +- docs/api-output.md | 23 ++++++++++++++ docs/changelog.md | 3 ++ lib/constructor.js | 3 ++ lib/input.js | 3 +- lib/output.js | 48 ++++++++++++++++++++++++++++ src/common.cc | 15 +++++++++ src/common.h | 4 +++ src/metadata.cc | 6 ++++ src/metadata.h | 2 ++ src/pipeline.cc | 35 +++++++++++++++++++- src/pipeline.h | 6 ++++ src/utilities.cc | 2 +- test/unit/heif.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 test/unit/heif.js diff --git a/docs/api-input.md b/docs/api-input.md index ad87df76..b8124f65 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -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 diff --git a/docs/api-output.md b/docs/api-output.md index 9c068d7b..e6a5fd58 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -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** diff --git a/docs/changelog.md b/docs/changelog.md index ee10540a..df8c0122 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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) diff --git a/lib/constructor.js b/lib/constructor.js index 3924d050..e51debe2 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -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, diff --git a/lib/input.js b/lib/input.js index a94ff029..ecdd24d2 100644 --- a/lib/input.js +++ b/lib/input.js @@ -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 diff --git a/lib/output.js b/lib/output.js index 985cc95b..f49d392f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -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, diff --git a/src/common.cc b/src/common.cc index 36799619..044ce124 100644 --- a/src/common.cc +++ b/src/common.cc @@ -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; } diff --git a/src/common.h b/src/common.h index ed04ea02..3a1b2273 100644 --- a/src/common.h +++ b/src/common.h @@ -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); diff --git a/src/metadata.cc b/src/metadata.cc index 128375a4..c3e9ab08 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -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(baton->pageHeight)); } + if (baton->pagePrimary > -1) { + Set(info, New("pagePrimary").ToLocalChecked(), New(baton->pagePrimary)); + } Set(info, New("hasProfile").ToLocalChecked(), New(baton->hasProfile)); Set(info, New("hasAlpha").ToLocalChecked(), New(baton->hasAlpha)); if (baton->orientation > 0) { diff --git a/src/metadata.h b/src/metadata.h index e1369969..d025a65c 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -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), diff --git a/src/pipeline.cc b/src/pipeline.cc index 0fda3b8e..0b8d3522 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -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(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(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( vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR, AttrAsStr(options, "tiffPredictor").data())); - + baton->heifQuality = AttrTo(options, "heifQuality"); + baton->heifLossless = AttrTo(options, "heifLossless"); + #ifdef VIPS_TYPE_FOREIGN_HEIF_COMPRESSION + baton->heifCompression = static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION, + AttrAsStr(options, "heifCompression").data())); + #endif // Tile output baton->tileSize = AttrTo(options, "tileSize"); baton->tileOverlap = AttrTo(options, "tileOverlap"); diff --git a/src/pipeline.h b/src/pipeline.h index 10ae3508..364efc6c 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -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), diff --git a/src/utilities.cc b/src/utilities.cc index 572ffa19..e86654b3 100644 --- a/src/utilities.cc +++ b/src/utilities.cc @@ -151,7 +151,7 @@ NAN_METHOD(format) { // Which load/save operations are available for each compressed format? Local format = New(); 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 hasInputFile = diff --git a/test/unit/heif.js b/test/unit/heif.js new file mode 100644 index 00000000..60e6ab2b --- /dev/null +++ b/test/unit/heif.js @@ -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 }); + }); + }); + }); +});