Compare commits

...

22 Commits

Author SHA1 Message Date
Lovell Fuller
0144358afb Release v0.20.8 2018-09-05 08:44:01 +01:00
Lovell Fuller
136097efe7 Downgrade nyc for continued Node 4 support 2018-09-04 17:07:10 +01:00
Lovell Fuller
374c6959d7 Changelog and credit for #1358 #1362 2018-09-04 16:39:24 +01:00
Axel Eirola
7d48a5ccf4 Allow floating point density input (#1362)
Metadata output will still remain integer
2018-09-01 08:58:30 +01:00
ajhool
bf3254cb16 Install: avoid race conditions when creating directories (#1358) 2018-08-29 09:20:26 +01:00
Lovell Fuller
5bed3a7d52 Release v0.20.7 2018-08-21 11:50:14 +01:00
Lovell Fuller
ece111280b Use copy+unlink if rename fails during install #1345 2018-08-20 15:14:31 +01:00
Lovell Fuller
a15a9b956b Release v0.20.6 2018-08-20 11:40:10 +01:00
Lovell Fuller
42860c2f83 Changelog, credit and doc refresh for #1342 2018-08-19 10:43:25 +01:00
Alun Davies
b5b95e5ae1 Expose depth option for tile-based output (#1342) 2018-08-18 15:09:53 +01:00
Lovell Fuller
d705cffdd6 Ensure extractChannel works with 16-bit images #1330 2018-08-12 20:22:39 +01:00
Rodrigo Alviani
23a4bc103e Docs: correct quality option in overlayWith example (#1325) 2018-08-08 08:42:18 +01:00
Lovell Fuller
c14434f9e7 Add removeAlpha op, removes alpha channel if any #1248 2018-08-07 20:32:11 +01:00
Lovell Fuller
25bd2cea3e Add experimental entropy field to stats response 2018-08-06 15:41:27 +01:00
Lovell Fuller
532de4ecab Cache libvips binaries to reduce re-install time #1301 2018-08-05 10:31:41 +01:00
Lovell Fuller
bfdd27eeef Doc refresh and dependency bumps 2018-08-05 09:42:09 +01:00
Lovell Fuller
bd9f238ab4 Improve install time error messages for FreeBSD #1310 2018-08-04 22:27:32 +01:00
Lovell Fuller
75556bb57c Ensure vendor platform mismatch throws error #1303 2018-08-04 21:34:11 +01:00
thegareth
2de062a34a Docs: update the "make a transparent image" example (#1316)
Alpha for colour is between 0-1, not 0-255.
2018-08-02 09:42:25 +01:00
Lovell Fuller
4589b15dea Changelog and credit for #1285 #1290 2018-07-10 16:12:16 +01:00
Sylvain Dumont
8b75ce6786 Allow full WebP alphaQuality range of 0-100 (#1290) 2018-07-10 15:58:17 +01:00
Espen Hovlandsdal
7bbc5176a1 Expose mozjpeg quant_table flag (#1285) 2018-07-10 15:56:05 +01:00
32 changed files with 701 additions and 118 deletions

View File

@@ -1,5 +1,21 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. --> <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## removeAlpha
Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel.
### Examples
```javascript
sharp('rgba.png')
.removeAlpha()
.toFile('rgb.png', function(err, info) {
// rgb.png is a 3 channel image without an alpha channel
});
```
Returns **Sharp**
## extractChannel ## extractChannel
Extract a single channel from a multi-channel image. Extract a single channel from a multi-channel image.

View File

@@ -18,7 +18,7 @@ If the overlay image contains an alpha channel then composition with premultipli
- `options.left` **[Number][4]?** the pixel offset from the left edge. - `options.left` **[Number][4]?** the pixel offset from the left edge.
- `options.tile` **[Boolean][5]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`) - `options.tile` **[Boolean][5]** set to true to repeat the overlay image across the entire image with the given `gravity`. (optional, default `false`)
- `options.cutout` **[Boolean][5]** set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another. (optional, default `false`) - `options.cutout` **[Boolean][5]** set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another. (optional, default `false`)
- `options.density` **[Number][4]** integral number representing the DPI for vector overlay image. (optional, default `72`) - `options.density` **[Number][4]** number representing the DPI for vector overlay image. (optional, default `72`)
- `options.raw` **[Object][3]?** describes overlay when using raw pixel data. - `options.raw` **[Object][3]?** describes overlay when using raw pixel data.
- `options.raw.width` **[Number][4]?** - `options.raw.width` **[Number][4]?**
- `options.raw.height` **[Number][4]?** - `options.raw.height` **[Number][4]?**
@@ -40,8 +40,7 @@ sharp('input.png')
.overlayWith('overlay.png', { gravity: sharp.gravity.southeast } ) .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } )
.sharpen() .sharpen()
.withMetadata() .withMetadata()
.quality(90) .webp( { quality: 90 } )
.webp()
.toBuffer() .toBuffer()
.then(function(outputBuffer) { .then(function(outputBuffer) {
// outputBuffer contains upside down, 300px wide, alpha channel flattened // outputBuffer contains upside down, 300px wide, alpha channel flattened

View File

@@ -12,7 +12,7 @@
- `options.failOnError` **[Boolean][4]** by default apply a "best effort" - `options.failOnError` **[Boolean][4]** by default apply a "best effort"
to decode images, even if the data is corrupt or invalid. Set this flag to true to decode images, even if the data is corrupt or invalid. Set this flag to true
if you'd rather halt processing and raise an error when loading invalid images. (optional, default `false`) if you'd rather halt processing and raise an error when loading invalid images. (optional, default `false`)
- `options.density` **[Number][5]** integral number representing the DPI for vector images. (optional, default `72`) - `options.density` **[Number][5]** number representing the DPI for vector images. (optional, default `72`)
- `options.page` **[Number][5]** page number to extract for multi-page input (GIF, TIFF) (optional, default `0`) - `options.page` **[Number][5]** page number to extract for multi-page input (GIF, TIFF) (optional, default `0`)
- `options.raw` **[Object][3]?** describes raw pixel input image data. See `raw()` for pixel ordering. - `options.raw` **[Object][3]?** describes raw pixel input image data. See `raw()` for pixel ordering.
- `options.raw.width` **[Number][5]?** - `options.raw.width` **[Number][5]?**
@@ -55,7 +55,7 @@ sharp({
width: 300, width: 300,
height: 200, height: 200,
channels: 4, channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 128 } background: { r: 255, g: 0, b: 0, alpha: 0.5 }
} }
}) })
.png() .png()

View File

@@ -79,6 +79,7 @@ A Promise is returned when `callback` is not provided.
- `maxX` (x-coordinate of one of the pixel where the maximum lies) - `maxX` (x-coordinate of one of the pixel where the maximum lies)
- `maxY` (y-coordinate of one of the pixel where the maximum lies) - `maxY` (y-coordinate of one of the pixel where the maximum lies)
- `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel - `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel
- `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental)
### Parameters ### Parameters

View File

@@ -121,6 +121,8 @@ Use these JPEG options for output image.
- `options.optimizeScans` **[Boolean][6]** alternative spelling of optimiseScans (optional, default `false`) - `options.optimizeScans` **[Boolean][6]** alternative spelling of optimiseScans (optional, default `false`)
- `options.optimiseCoding` **[Boolean][6]** optimise Huffman coding tables (optional, default `true`) - `options.optimiseCoding` **[Boolean][6]** optimise Huffman coding tables (optional, default `true`)
- `options.optimizeCoding` **[Boolean][6]** alternative spelling of optimiseCoding (optional, default `true`) - `options.optimizeCoding` **[Boolean][6]** alternative spelling of optimiseCoding (optional, default `true`)
- `options.quantisationTable` **[Number][8]** quantization table to use, integer 0-8, requires mozjpeg (optional, default `0`)
- `options.quantizationTable` **[Number][8]** alternative spelling of quantisationTable (optional, default `0`)
- `options.force` **[Boolean][6]** force JPEG output, otherwise attempt to use input format (optional, default `true`) - `options.force` **[Boolean][6]** force JPEG output, otherwise attempt to use input format (optional, default `true`)
### Examples ### Examples
@@ -276,6 +278,7 @@ Warning: multiple sharp instances concurrently producing tile output can expose
- `tile.size` **[Number][8]** tile size in pixels, a value between 1 and 8192. (optional, default `256`) - `tile.size` **[Number][8]** tile size in pixels, a value between 1 and 8192. (optional, default `256`)
- `tile.overlap` **[Number][8]** tile overlap in pixels, a value between 0 and 8192. (optional, default `0`) - `tile.overlap` **[Number][8]** tile overlap in pixels, a value between 0 and 8192. (optional, default `0`)
- `tile.angle` **[Number][8]** tile angle of rotation, must be a multiple of 90. (optional, default `0`) - `tile.angle` **[Number][8]** tile angle of rotation, must be a multiple of 90. (optional, default `0`)
- `tile.depth` **[String][1]?** how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
- `tile.container` **[String][1]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`) - `tile.container` **[String][1]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`)
- `tile.layout` **[String][1]** filesystem layout, possible values are `dz`, `zoomify` or `google`. (optional, default `'dz'`) - `tile.layout` **[String][1]** filesystem layout, possible values are `dz`, `zoomify` or `google`. (optional, default `'dz'`)

View File

@@ -4,6 +4,52 @@
Requires libvips v8.6.1. Requires libvips v8.6.1.
#### v0.20.8 - 5<sup>th</sup> September 2018
* Avoid race conditions when creating directories during installation.
[#1358](https://github.com/lovell/sharp/pull/1358)
[@ajhool](https://github.com/ajhool)
* Accept floating point values for input density parameter.
[#1362](https://github.com/lovell/sharp/pull/1362)
[@aeirola](https://github.com/aeirola)
#### v0.20.7 - 21<sup>st</sup> August 2018
* Use copy+unlink if rename operation fails during installation.
[#1345](https://github.com/lovell/sharp/issues/1345)
#### v0.20.6 - 20<sup>th</sup> August 2018
* Add removeAlpha operation to remove alpha channel, if any.
[#1248](https://github.com/lovell/sharp/issues/1248)
* Expose mozjpeg quant_table flag.
[#1285](https://github.com/lovell/sharp/pull/1285)
[@rexxars](https://github.com/rexxars)
* Allow full WebP alphaQuality range of 0-100.
[#1290](https://github.com/lovell/sharp/pull/1290)
[@sylvaindumont](https://github.com/sylvaindumont)
* Cache libvips binaries to reduce re-install time.
[#1301](https://github.com/lovell/sharp/issues/1301)
* Ensure vendor platform mismatch throws error at install time.
[#1303](https://github.com/lovell/sharp/issues/1303)
* Improve install time error messages for FreeBSD users.
[#1310](https://github.com/lovell/sharp/issues/1310)
* Ensure extractChannel works with 16-bit images.
[#1330](https://github.com/lovell/sharp/issues/1330)
* Expose depth option for tile-based output.
[#1342](https://github.com/lovell/sharp/pull/1342)
[@alundavies](https://github.com/alundavies)
* Add experimental entropy field to stats response.
#### v0.20.5 - 27<sup>th</sup> June 2018 #### v0.20.5 - 27<sup>th</sup> June 2018
* Expose libjpeg optimize_coding flag. * Expose libjpeg optimize_coding flag.

View File

@@ -113,6 +113,11 @@ the help and code contributions of the following people:
* [Thomas Parisot](https://github.com/oncletom) * [Thomas Parisot](https://github.com/oncletom)
* [Nathan Graves](https://github.com/woolite64) * [Nathan Graves](https://github.com/woolite64)
* [Tom Lokhorst](https://github.com/tomlokhorst) * [Tom Lokhorst](https://github.com/tomlokhorst)
* [Espen Hovlandsdal](https://github.com/rexxars)
* [Sylvain Dumont](https://github.com/sylvaindumont)
* [Alun Davies](https://github.com/alundavies)
* [Aidan Hoolachan](https://github.com/ajhool)
* [Axel Eirola](https://github.com/aeirola)
Thank you! Thank you!

View File

@@ -4,6 +4,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const copyFileSync = require('fs-copy-file-sync'); const copyFileSync = require('fs-copy-file-sync');
const libvips = require('../lib/libvips');
const npmLog = require('npmlog'); const npmLog = require('npmlog');
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -11,8 +12,8 @@ if (process.platform === 'win32') {
const buildReleaseDir = path.join(buildDir, 'Release'); const buildReleaseDir = path.join(buildDir, 'Release');
npmLog.info('sharp', `Creating ${buildReleaseDir}`); npmLog.info('sharp', `Creating ${buildReleaseDir}`);
try { try {
fs.mkdirSync(buildDir); libvips.mkdirSync(buildDir);
fs.mkdirSync(buildReleaseDir); libvips.mkdirSync(buildReleaseDir);
} catch (err) {} } catch (err) {}
const vendorLibDir = path.join(__dirname, '..', 'vendor', 'lib'); const vendorLibDir = path.join(__dirname, '..', 'vendor', 'lib');
npmLog.info('sharp', `Copying DLLs from ${vendorLibDir} to ${buildReleaseDir}`); npmLog.info('sharp', `Copying DLLs from ${vendorLibDir} to ${buildReleaseDir}`);

View File

@@ -9,6 +9,7 @@ const npmLog = require('npmlog');
const semver = require('semver'); const semver = require('semver');
const simpleGet = require('simple-get'); const simpleGet = require('simple-get');
const tar = require('tar'); const tar = require('tar');
const copyFileSync = require('fs-copy-file-sync');
const agent = require('../lib/agent'); const agent = require('../lib/agent');
const libvips = require('../lib/libvips'); const libvips = require('../lib/libvips');
@@ -17,6 +18,20 @@ const platform = require('../lib/platform');
const minimumLibvipsVersion = libvips.minimumLibvipsVersion; const minimumLibvipsVersion = libvips.minimumLibvipsVersion;
const distBaseUrl = process.env.SHARP_DIST_BASE_URL || `https://github.com/lovell/sharp-libvips/releases/download/v${minimumLibvipsVersion}/`; const distBaseUrl = process.env.SHARP_DIST_BASE_URL || `https://github.com/lovell/sharp-libvips/releases/download/v${minimumLibvipsVersion}/`;
const extractTarball = function (tarPath) {
const vendorPath = path.join(__dirname, '..', 'vendor');
libvips.mkdirSync(vendorPath);
tar
.extract({
file: tarPath,
cwd: vendorPath,
strict: true
})
.catch(function (err) {
throw err;
});
};
try { try {
const useGlobalLibvips = libvips.useGlobalLibvips(); const useGlobalLibvips = libvips.useGlobalLibvips();
if (useGlobalLibvips) { if (useGlobalLibvips) {
@@ -29,11 +44,15 @@ try {
} else { } else {
// Is this arch/platform supported? // Is this arch/platform supported?
const arch = process.env.npm_config_arch || process.arch; const arch = process.env.npm_config_arch || process.arch;
if (platform() === 'win32-ia32') { const platformAndArch = platform();
if (platformAndArch === 'win32-ia32') {
throw new Error('Windows x86 (32-bit) node.exe is not supported'); throw new Error('Windows x86 (32-bit) node.exe is not supported');
} }
if (arch === 'ia32') { if (arch === 'ia32') {
throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}\n`); throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
}
if (platformAndArch === 'freebsd-x64') {
throw new Error(`FreeBSD systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
if (detectLibc.isNonGlibcLinux) { if (detectLibc.isNonGlibcLinux) {
throw new Error(`Use with ${detectLibc.family} libc requires manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`Use with ${detectLibc.family} libc requires manual installation of libvips >= ${minimumLibvipsVersion}`);
@@ -42,38 +61,37 @@ try {
throw new Error(`Use with glibc version ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`); throw new Error(`Use with glibc version ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
} }
// Download to per-process temporary file // Download to per-process temporary file
const tarFilename = ['libvips', minimumLibvipsVersion, platform()].join('-') + '.tar.gz'; const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.gz';
const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`); const tarPathCache = path.join(libvips.cachePath(), tarFilename);
const tmpFile = fs.createWriteStream(tarPathTemp); if (fs.existsSync(tarPathCache)) {
const url = distBaseUrl + tarFilename; npmLog.info('sharp', `Using cached ${tarPathCache}`);
npmLog.info('sharp', `Downloading ${url}`); extractTarball(tarPathCache);
simpleGet({ url: url, agent: agent() }, function (err, response) { } else {
if (err) { const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`);
throw err; const tmpFile = fs.createWriteStream(tarPathTemp);
} const url = distBaseUrl + tarFilename;
if (response.statusCode !== 200) { npmLog.info('sharp', `Downloading ${url}`);
throw new Error(`Status ${response.statusCode}`); simpleGet({ url: url, agent: agent() }, function (err, response) {
} if (err) {
response.pipe(tmpFile);
});
tmpFile.on('close', function () {
const vendorPath = path.join(__dirname, '..', 'vendor');
fs.mkdirSync(vendorPath);
tar
.extract({
file: tarPathTemp,
cwd: vendorPath,
strict: true
})
.then(function () {
try {
fs.unlinkSync(tarPathTemp);
} catch (err) {}
})
.catch(function (err) {
throw err; throw err;
}); }
}); if (response.statusCode !== 200) {
throw new Error(`Status ${response.statusCode}`);
}
response.pipe(tmpFile);
});
tmpFile.on('close', function () {
try {
// Attempt to rename
fs.renameSync(tarPathTemp, tarPathCache);
} catch (err) {
// Fall back to copy and unlink
copyFileSync(tarPathTemp, tarPathCache);
fs.unlinkSync(tarPathTemp);
}
extractTarball(tarPathCache);
});
}
} }
} catch (err) { } catch (err) {
npmLog.error('sharp', err.message); npmLog.error('sharp', err.message);

View File

@@ -12,6 +12,23 @@ const bool = {
eor: 'eor' eor: 'eor'
}; };
/**
* Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel.
*
* @example
* sharp('rgba.png')
* .removeAlpha()
* .toFile('rgb.png', function(err, info) {
* // rgb.png is a 3 channel image without an alpha channel
* });
*
* @returns {Sharp}
*/
function removeAlpha () {
this.options.removeAlpha = true;
return this;
}
/** /**
* Extract a single channel from a multi-channel image. * Extract a single channel from a multi-channel image.
* *
@@ -102,6 +119,7 @@ function bandbool (boolOp) {
module.exports = function (Sharp) { module.exports = function (Sharp) {
// Public instance functions // Public instance functions
[ [
removeAlpha,
extractChannel, extractChannel,
joinChannel, joinChannel,
bandbool bandbool

View File

@@ -19,8 +19,7 @@ const is = require('./is');
* .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } ) * .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } )
* .sharpen() * .sharpen()
* .withMetadata() * .withMetadata()
* .quality(90) * .webp( { quality: 90 } )
* .webp()
* .toBuffer() * .toBuffer()
* .then(function(outputBuffer) { * .then(function(outputBuffer) {
* // outputBuffer contains upside down, 300px wide, alpha channel flattened * // outputBuffer contains upside down, 300px wide, alpha channel flattened
@@ -35,7 +34,7 @@ const is = require('./is');
* @param {Number} [options.left] - the pixel offset from the left edge. * @param {Number} [options.left] - the pixel offset from the left edge.
* @param {Boolean} [options.tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`. * @param {Boolean} [options.tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`.
* @param {Boolean} [options.cutout=false] - set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another. * @param {Boolean} [options.cutout=false] - set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another.
* @param {Number} [options.density=72] - integral number representing the DPI for vector overlay image. * @param {Number} [options.density=72] - number representing the DPI for vector overlay image.
* @param {Object} [options.raw] - describes overlay when using raw pixel data. * @param {Object} [options.raw] - describes overlay when using raw pixel data.
* @param {Number} [options.raw.width] * @param {Number} [options.raw.width]
* @param {Number} [options.raw.height] * @param {Number} [options.raw.height]

View File

@@ -81,7 +81,7 @@ const debuglog = util.debuglog('sharp');
* width: 300, * width: 300,
* height: 200, * height: 200,
* channels: 4, * channels: 4,
* background: { r: 255, g: 0, b: 0, alpha: 128 } * background: { r: 255, g: 0, b: 0, alpha: 0.5 }
* } * }
* }) * })
* .png() * .png()
@@ -96,7 +96,7 @@ const debuglog = util.debuglog('sharp');
* @param {Boolean} [options.failOnError=false] - by default apply a "best effort" * @param {Boolean} [options.failOnError=false] - by default apply a "best effort"
* to decode images, even if the data is corrupt or invalid. Set this flag to true * to decode images, even if the data is corrupt or invalid. Set this flag to true
* if you'd rather halt processing and raise an error when loading invalid images. * if you'd rather halt processing and raise an error when loading invalid images.
* @param {Number} [options.density=72] - integral number representing the DPI for vector images. * @param {Number} [options.density=72] - number representing the DPI for vector images.
* @param {Number} [options.page=0] - page number to extract for multi-page input (GIF, TIFF) * @param {Number} [options.page=0] - page number to extract for multi-page input (GIF, TIFF)
* @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
* @param {Number} [options.raw.width] * @param {Number} [options.raw.width]
@@ -171,6 +171,7 @@ const Sharp = function (input, options) {
booleanFileIn: '', booleanFileIn: '',
joinChannelIn: [], joinChannelIn: [],
extractChannel: -1, extractChannel: -1,
removeAlpha: false,
colourspace: 'srgb', colourspace: 'srgb',
// overlay // overlay
overlayGravity: 0, overlayGravity: 0,
@@ -193,6 +194,7 @@ const Sharp = function (input, options) {
jpegOvershootDeringing: false, jpegOvershootDeringing: false,
jpegOptimiseScans: false, jpegOptimiseScans: false,
jpegOptimiseCoding: true, jpegOptimiseCoding: true,
jpegQuantisationTable: 0,
pngProgressive: false, pngProgressive: false,
pngCompressionLevel: 9, pngCompressionLevel: 9,
pngAdaptiveFiltering: false, pngAdaptiveFiltering: false,

View File

@@ -36,7 +36,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
} }
// Density // Density
if (is.defined(inputOptions.density)) { if (is.defined(inputOptions.density)) {
if (is.integer(inputOptions.density) && is.inRange(inputOptions.density, 1, 2400)) { if (is.inRange(inputOptions.density, 1, 2400)) {
inputDescriptor.density = inputOptions.density; inputDescriptor.density = inputOptions.density;
} else { } else {
throw new Error('Invalid density (1 to 2400) ' + inputOptions.density); throw new Error('Invalid density (1 to 2400) ' + inputOptions.density);
@@ -264,6 +264,7 @@ function metadata (callback) {
* - `maxX` (x-coordinate of one of the pixel where the maximum lies) * - `maxX` (x-coordinate of one of the pixel where the maximum lies)
* - `maxY` (y-coordinate of one of the pixel where the maximum lies) * - `maxY` (y-coordinate of one of the pixel where the maximum lies)
* - `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel * - `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel
* - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental)
* *
* @example * @example
* const image = sharp(inputJpg); * const image = sharp(inputJpg);

View File

@@ -1,17 +1,38 @@
'use strict'; 'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path'); const path = require('path');
const spawnSync = require('child_process').spawnSync; const spawnSync = require('child_process').spawnSync;
const semver = require('semver'); const semver = require('semver');
const platform = require('./platform'); const platform = require('./platform');
const minimumLibvipsVersion = process.env.npm_package_config_libvips || require('../package.json').config.libvips; const env = process.env;
const minimumLibvipsVersion = env.npm_package_config_libvips || require('../package.json').config.libvips;
const spawnSyncOptions = { const spawnSyncOptions = {
encoding: 'utf8', encoding: 'utf8',
shell: true shell: true
}; };
const mkdirSync = function (dirPath) {
try {
fs.mkdirSync(dirPath);
} catch (err) {
if (err.code !== 'EEXIST') {
throw err;
}
}
};
const cachePath = function () {
const npmCachePath = env.npm_config_cache || (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm'));
mkdirSync(npmCachePath);
const libvipsCachePath = path.join(npmCachePath, '_libvips');
mkdirSync(libvipsCachePath);
return libvipsCachePath;
};
const globalLibvipsVersion = function () { const globalLibvipsVersion = function () {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout || ''; const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout || '';
@@ -23,21 +44,24 @@ const globalLibvipsVersion = function () {
const hasVendoredLibvips = function () { const hasVendoredLibvips = function () {
const currentPlatformId = platform(); const currentPlatformId = platform();
let vendorPlatformId;
try { try {
const vendorPlatformId = require(path.join(__dirname, '..', 'vendor', 'platform.json')); vendorPlatformId = require(path.join(__dirname, '..', 'vendor', 'platform.json'));
} catch (err) {}
if (vendorPlatformId) {
if (currentPlatformId === vendorPlatformId) { if (currentPlatformId === vendorPlatformId) {
return true; return true;
} else { } else {
throw new Error(`'${vendorPlatformId}' binaries cannot be used on the '${currentPlatformId}' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`); throw new Error(`'${vendorPlatformId}' binaries cannot be used on the '${currentPlatformId}' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'.`);
} }
} catch (err) {} }
return false; return false;
}; };
const pkgConfigPath = function () { const pkgConfigPath = function () {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
const brewPkgConfigPath = spawnSync('which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR', spawnSyncOptions).stdout || ''; const brewPkgConfigPath = spawnSync('which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR', spawnSyncOptions).stdout || '';
return [brewPkgConfigPath.trim(), process.env.PKG_CONFIG_PATH, '/usr/local/lib/pkgconfig', '/usr/lib/pkgconfig'] return [brewPkgConfigPath.trim(), env.PKG_CONFIG_PATH, '/usr/local/lib/pkgconfig', '/usr/lib/pkgconfig']
.filter(function (p) { return !!p; }) .filter(function (p) { return !!p; })
.join(':'); .join(':');
} else { } else {
@@ -46,7 +70,7 @@ const pkgConfigPath = function () {
}; };
const useGlobalLibvips = function () { const useGlobalLibvips = function () {
if (Boolean(process.env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) { if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) {
return false; return false;
} }
@@ -56,8 +80,10 @@ const useGlobalLibvips = function () {
module.exports = { module.exports = {
minimumLibvipsVersion: minimumLibvipsVersion, minimumLibvipsVersion: minimumLibvipsVersion,
cachePath: cachePath,
globalLibvipsVersion: globalLibvipsVersion, globalLibvipsVersion: globalLibvipsVersion,
hasVendoredLibvips: hasVendoredLibvips, hasVendoredLibvips: hasVendoredLibvips,
pkgConfigPath: pkgConfigPath, pkgConfigPath: pkgConfigPath,
useGlobalLibvips: useGlobalLibvips useGlobalLibvips: useGlobalLibvips,
mkdirSync: mkdirSync
}; };

View File

@@ -150,6 +150,8 @@ function withMetadata (withMetadata) {
* @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans * @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
* @param {Boolean} [options.optimiseCoding=true] - optimise Huffman coding tables * @param {Boolean} [options.optimiseCoding=true] - optimise Huffman coding tables
* @param {Boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding * @param {Boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding
* @param {Number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires mozjpeg
* @param {Number} [options.quantizationTable=0] - alternative spelling of quantisationTable
* @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format * @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -191,6 +193,14 @@ function jpeg (options) {
if (is.defined(options.optimiseCoding)) { if (is.defined(options.optimiseCoding)) {
this._setBooleanOption('jpegOptimiseCoding', options.optimiseCoding); this._setBooleanOption('jpegOptimiseCoding', options.optimiseCoding);
} }
options.quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable;
if (is.defined(options.quantisationTable)) {
if (is.integer(options.quantisationTable) && is.inRange(options.quantisationTable, 0, 8)) {
this.options.jpegQuantisationTable = options.quantisationTable;
} else {
throw new Error('Invalid quantisation table (integer, 0-8) ' + options.quantisationTable);
}
}
} }
return this._updateFormatOut('jpeg', options); return this._updateFormatOut('jpeg', options);
} }
@@ -261,10 +271,10 @@ function webp (options) {
} }
} }
if (is.object(options) && is.defined(options.alphaQuality)) { if (is.object(options) && is.defined(options.alphaQuality)) {
if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 1, 100)) { if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) {
this.options.webpAlphaQuality = options.alphaQuality; this.options.webpAlphaQuality = options.alphaQuality;
} else { } else {
throw new Error('Invalid webp alpha quality (integer, 1-100) ' + options.alphaQuality); throw new Error('Invalid webp alpha quality (integer, 0-100) ' + options.alphaQuality);
} }
} }
if (is.object(options) && is.defined(options.lossless)) { if (is.object(options) && is.defined(options.lossless)) {
@@ -413,6 +423,7 @@ function toFormat (format, options) {
* @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192. * @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192.
* @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192. * @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192.
* @param {Number} [tile.angle=0] tile angle of rotation, must be a multiple of 90. * @param {Number} [tile.angle=0] tile angle of rotation, must be a multiple of 90.
* @param {String} [tile.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
* @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file). * @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
* @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`. * @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`.
* @returns {Sharp} * @returns {Sharp}
@@ -464,6 +475,15 @@ function tile (tile) {
throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle); throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle);
} }
} }
// Depth of tiles
if (is.defined(tile.depth)) {
if (is.string(tile.depth) && is.inArray(tile.depth, ['onepixel', 'onetile', 'one'])) {
this.options.tileDepth = tile.depth;
} else {
throw new Error("Invalid tile depth '" + tile.depth + "', should be one of 'onepixel', 'onetile' or 'one'");
}
}
} }
// Format // Format
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {

View File

@@ -1,7 +1,7 @@
{ {
"name": "sharp", "name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images", "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images",
"version": "0.20.5", "version": "0.20.8",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp", "homepage": "https://github.com/lovell/sharp",
"contributors": [ "contributors": [
@@ -49,7 +49,12 @@
"Rik Heywood <rik@rik.org>", "Rik Heywood <rik@rik.org>",
"Thomas Parisot <hi@oncletom.io>", "Thomas Parisot <hi@oncletom.io>",
"Nathan Graves <nathanrgraves+github@gmail.com>", "Nathan Graves <nathanrgraves+github@gmail.com>",
"Tom Lokhorst <tom@lokhorst.eu>" "Tom Lokhorst <tom@lokhorst.eu>",
"Espen Hovlandsdal <espen@hovlandsdal.com>",
"Sylvain Dumont <sylvain.dumont35@gmail.com>",
"Alun Davies <alun.owain.davies@googlemail.com>",
"Aidan Hoolachan <ajhoolachan21@gmail.com>",
"Axel Eirola <axel.eirola@iki.fi>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
@@ -83,25 +88,26 @@
"dependencies": { "dependencies": {
"color": "^3.0.0", "color": "^3.0.0",
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"nan": "^2.10.0", "nan": "^2.11.0",
"fs-copy-file-sync": "^1.1.1", "fs-copy-file-sync": "^1.1.1",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"prebuild-install": "^4.0.0", "prebuild-install": "^4.0.0",
"semver": "^5.5.0", "semver": "^5.5.1",
"simple-get": "^2.8.1", "simple-get": "^2.8.1",
"tar": "^4.4.4", "tar": "^4.4.6",
"tunnel-agent": "^0.6.0" "tunnel-agent": "^0.6.0"
}, },
"devDependencies": { "devDependencies": {
"async": "^2.6.1", "async": "^2.6.1",
"cc": "^1.0.2", "cc": "^1.0.2",
"decompress-zip": "^0.3.1", "decompress-zip": "^0.3.1",
"documentation": "^8.0.0", "documentation": "^8.1.2",
"exif-reader": "^1.0.2", "exif-reader": "^1.0.2",
"icc": "^1.0.0", "icc": "^1.0.0",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"nyc": "^12.0.2", "mock-fs": "^4.6.0",
"prebuild": "^7.6.0", "nyc": "^12.0.1",
"prebuild": "^7.6.2",
"prebuild-ci": "^2.2.3", "prebuild-ci": "^2.2.3",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"semistandard": "^12.0.1" "semistandard": "^12.0.1"

View File

@@ -55,7 +55,7 @@ namespace sharp {
descriptor->failOnError = AttrTo<bool>(input, "failOnError"); descriptor->failOnError = AttrTo<bool>(input, "failOnError");
// Density for vector-based input // Density for vector-based input
if (HasAttr(input, "density")) { if (HasAttr(input, "density")) {
descriptor->density = AttrTo<uint32_t>(input, "density"); descriptor->density = AttrTo<double>(input, "density");
} }
// Raw pixel input // Raw pixel input
if (HasAttr(input, "rawChannels")) { if (HasAttr(input, "rawChannels")) {
@@ -228,7 +228,7 @@ namespace sharp {
->set("access", accessMethod) ->set("access", accessMethod)
->set("fail", descriptor->failOnError); ->set("fail", descriptor->failOnError);
if (imageType == ImageType::SVG || imageType == ImageType::PDF) { if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
option->set("dpi", static_cast<double>(descriptor->density)); option->set("dpi", descriptor->density);
} }
if (imageType == ImageType::MAGICK) { if (imageType == ImageType::MAGICK) {
option->set("density", std::to_string(descriptor->density).data()); option->set("density", std::to_string(descriptor->density).data());
@@ -270,7 +270,7 @@ namespace sharp {
->set("access", accessMethod) ->set("access", accessMethod)
->set("fail", descriptor->failOnError); ->set("fail", descriptor->failOnError);
if (imageType == ImageType::SVG || imageType == ImageType::PDF) { if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
option->set("dpi", static_cast<double>(descriptor->density)); option->set("dpi", descriptor->density);
} }
if (imageType == ImageType::MAGICK) { if (imageType == ImageType::MAGICK) {
option->set("density", std::to_string(descriptor->density).data()); option->set("density", std::to_string(descriptor->density).data());
@@ -355,8 +355,8 @@ namespace sharp {
/* /*
Set pixels/mm resolution based on a pixels/inch density. Set pixels/mm resolution based on a pixels/inch density.
*/ */
void SetDensity(VImage image, const int density) { void SetDensity(VImage image, const double density) {
const double pixelsPerMm = static_cast<double>(density) / 25.4; const double pixelsPerMm = density / 25.4;
image.set("Xres", pixelsPerMm); image.set("Xres", pixelsPerMm);
image.set("Yres", pixelsPerMm); image.set("Yres", pixelsPerMm);
image.set(VIPS_META_RESOLUTION_UNIT, "in"); image.set(VIPS_META_RESOLUTION_UNIT, "in");

View File

@@ -49,7 +49,7 @@ namespace sharp {
char *buffer; char *buffer;
bool failOnError; bool failOnError;
size_t bufferLength; size_t bufferLength;
int density; double density;
int rawChannels; int rawChannels;
int rawWidth; int rawWidth;
int rawHeight; int rawHeight;
@@ -63,7 +63,7 @@ namespace sharp {
buffer(nullptr), buffer(nullptr),
failOnError(FALSE), failOnError(FALSE),
bufferLength(0), bufferLength(0),
density(72), density(72.0),
rawChannels(0), rawChannels(0),
rawWidth(0), rawWidth(0),
rawHeight(0), rawHeight(0),
@@ -186,7 +186,7 @@ namespace sharp {
/* /*
Set pixels/mm resolution based on a pixels/inch density. Set pixels/mm resolution based on a pixels/inch density.
*/ */
void SetDensity(VImage image, const int density); void SetDensity(VImage image, const double density);
/* /*
Check the proposed format supports the current dimensions. Check the proposed format supports the current dimensions.

View File

@@ -28,6 +28,16 @@ using vips::VError;
namespace sharp { namespace sharp {
/*
Removes alpha channel, if any.
*/
VImage RemoveAlpha(VImage image) {
if (HasAlpha(image)) {
image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1));
}
return image;
}
/* /*
Composite overlayImage over image at given position Composite overlayImage over image at given position
Assumes alpha channels are already premultiplied and will be unpremultiplied after Assumes alpha channels are already premultiplied and will be unpremultiplied after
@@ -223,10 +233,8 @@ namespace sharp {
VImage Gamma(VImage image, double const exponent) { VImage Gamma(VImage image, double const exponent) {
if (HasAlpha(image)) { if (HasAlpha(image)) {
// Separate alpha channel // Separate alpha channel
VImage imageWithoutAlpha = image.extract_band(0,
VImage::option()->set("n", image.bands() - 1));
VImage alpha = image[image.bands() - 1]; VImage alpha = image[image.bands() - 1];
return imageWithoutAlpha.gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha); return RemoveAlpha(image).gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha);
} else { } else {
return image.gamma(VImage::option()->set("exponent", exponent)); return image.gamma(VImage::option()->set("exponent", exponent));
} }
@@ -374,10 +382,8 @@ namespace sharp {
VImage Linear(VImage image, double const a, double const b) { VImage Linear(VImage image, double const a, double const b) {
if (HasAlpha(image)) { if (HasAlpha(image)) {
// Separate alpha channel // Separate alpha channel
VImage imageWithoutAlpha = image.extract_band(0,
VImage::option()->set("n", image.bands() - 1));
VImage alpha = image[image.bands() - 1]; VImage alpha = image[image.bands() - 1];
return imageWithoutAlpha.linear(a, b).bandjoin(alpha); return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
} else { } else {
return image.linear(a, b); return image.linear(a, b);
} }

View File

@@ -25,6 +25,11 @@ using vips::VImage;
namespace sharp { namespace sharp {
/*
Removes alpha channel, if any.
*/
VImage RemoveAlpha(VImage image);
/* /*
Alpha composite src over dst with given gravity. Alpha composite src over dst with given gravity.
Assumes alpha channels are already premultiplied and will be unpremultiplied after. Assumes alpha channels are already premultiplied and will be unpremultiplied after.

View File

@@ -694,10 +694,19 @@ class PipelineWorker : public Nan::AsyncWorker {
(baton->err).append("Cannot extract channel from image. Too few channels in image."); (baton->err).append("Cannot extract channel from image. Too few channels in image.");
return Error(); return Error();
} }
VipsInterpretation const interpretation = sharp::Is16Bit(image.interpretation())
? VIPS_INTERPRETATION_GREY16
: VIPS_INTERPRETATION_B_W;
image = image image = image
.extract_band(baton->extractChannel) .extract_band(baton->extractChannel)
.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W)); .copy(VImage::option()->set("interpretation", interpretation));
} }
// Remove alpha channel, if any
if (baton->removeAlpha) {
image = sharp::RemoveAlpha(image);
}
// Convert image to sRGB, if not already // Convert image to sRGB, if not already
if (sharp::Is16Bit(image.interpretation())) { if (sharp::Is16Bit(image.interpretation())) {
image = image.cast(VIPS_FORMAT_USHORT); image = image.cast(VIPS_FORMAT_USHORT);
@@ -733,6 +742,7 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("interlace", baton->jpegProgressive) ->set("interlace", baton->jpegProgressive)
->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4") ->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4")
->set("trellis_quant", baton->jpegTrellisQuantisation) ->set("trellis_quant", baton->jpegTrellisQuantisation)
->set("quant_table", baton->jpegQuantisationTable)
->set("overshoot_deringing", baton->jpegOvershootDeringing) ->set("overshoot_deringing", baton->jpegOvershootDeringing)
->set("optimize_scans", baton->jpegOptimiseScans) ->set("optimize_scans", baton->jpegOptimiseScans)
->set("optimize_coding", baton->jpegOptimiseCoding))); ->set("optimize_coding", baton->jpegOptimiseCoding)));
@@ -848,6 +858,7 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("interlace", baton->jpegProgressive) ->set("interlace", baton->jpegProgressive)
->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4") ->set("no_subsample", baton->jpegChromaSubsampling == "4:4:4")
->set("trellis_quant", baton->jpegTrellisQuantisation) ->set("trellis_quant", baton->jpegTrellisQuantisation)
->set("quant_table", baton->jpegQuantisationTable)
->set("overshoot_deringing", baton->jpegOvershootDeringing) ->set("overshoot_deringing", baton->jpegOvershootDeringing)
->set("optimize_scans", baton->jpegOptimiseScans) ->set("optimize_scans", baton->jpegOptimiseScans)
->set("optimize_coding", baton->jpegOptimiseCoding)); ->set("optimize_coding", baton->jpegOptimiseCoding));
@@ -927,6 +938,7 @@ class PipelineWorker : public Nan::AsyncWorker {
{"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"}, {"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
{"no_subsample", baton->jpegChromaSubsampling == "4:4:4" ? "TRUE": "FALSE"}, {"no_subsample", baton->jpegChromaSubsampling == "4:4:4" ? "TRUE": "FALSE"},
{"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"}, {"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
{"quant_table", std::to_string(baton->jpegQuantisationTable)},
{"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"}, {"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
{"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"}, {"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
{"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"} {"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"}
@@ -934,14 +946,22 @@ class PipelineWorker : public Nan::AsyncWorker {
suffix = AssembleSuffixString(extname, options); suffix = AssembleSuffixString(extname, options);
} }
// Write DZ to file // Write DZ to file
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option() vips::VOption *options = VImage::option()
->set("strip", !baton->withMetadata) ->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize) ->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap) ->set("overlap", baton->tileOverlap)
->set("container", baton->tileContainer) ->set("container", baton->tileContainer)
->set("layout", baton->tileLayout) ->set("layout", baton->tileLayout)
->set("suffix", const_cast<char*>(suffix.data())) ->set("suffix", const_cast<char*>(suffix.data()))
->set("angle", CalculateAngleRotation(baton->tileAngle))); ->set("angle", CalculateAngleRotation(baton->tileAngle));
// libvips chooses a default depth based on layout. Instead of replicating that logic here by
// not passing anything - libvips will handle choice
if (baton->tileDepth < VIPS_FOREIGN_DZ_DEPTH_LAST) {
options->set("depth", baton->tileDepth);
}
image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
baton->formatOut = "dz"; baton->formatOut = "dz";
} else if (baton->formatOut == "v" || (mightMatchInput && isV) || } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
(willMatchInput && inputImageType == ImageType::VIPS)) { (willMatchInput && inputImageType == ImageType::VIPS)) {
@@ -1232,6 +1252,7 @@ NAN_METHOD(pipeline) {
baton->extendLeft = AttrTo<int32_t>(options, "extendLeft"); baton->extendLeft = AttrTo<int32_t>(options, "extendLeft");
baton->extendRight = AttrTo<int32_t>(options, "extendRight"); baton->extendRight = AttrTo<int32_t>(options, "extendRight");
baton->extractChannel = AttrTo<int32_t>(options, "extractChannel"); baton->extractChannel = AttrTo<int32_t>(options, "extractChannel");
baton->removeAlpha = AttrTo<bool>(options, "removeAlpha");
if (HasAttr(options, "boolean")) { if (HasAttr(options, "boolean")) {
baton->boolean = CreateInputDescriptor(AttrAs<v8::Object>(options, "boolean"), buffersToPersist); baton->boolean = CreateInputDescriptor(AttrAs<v8::Object>(options, "boolean"), buffersToPersist);
baton->booleanOp = sharp::GetBooleanOperation(AttrAsStr(options, "booleanOp")); baton->booleanOp = sharp::GetBooleanOperation(AttrAsStr(options, "booleanOp"));
@@ -1266,6 +1287,7 @@ NAN_METHOD(pipeline) {
baton->jpegProgressive = AttrTo<bool>(options, "jpegProgressive"); baton->jpegProgressive = AttrTo<bool>(options, "jpegProgressive");
baton->jpegChromaSubsampling = AttrAsStr(options, "jpegChromaSubsampling"); baton->jpegChromaSubsampling = AttrAsStr(options, "jpegChromaSubsampling");
baton->jpegTrellisQuantisation = AttrTo<bool>(options, "jpegTrellisQuantisation"); baton->jpegTrellisQuantisation = AttrTo<bool>(options, "jpegTrellisQuantisation");
baton->jpegQuantisationTable = AttrTo<uint32_t>(options, "jpegQuantisationTable");
baton->jpegOvershootDeringing = AttrTo<bool>(options, "jpegOvershootDeringing"); baton->jpegOvershootDeringing = AttrTo<bool>(options, "jpegOvershootDeringing");
baton->jpegOptimiseScans = AttrTo<bool>(options, "jpegOptimiseScans"); baton->jpegOptimiseScans = AttrTo<bool>(options, "jpegOptimiseScans");
baton->jpegOptimiseCoding = AttrTo<bool>(options, "jpegOptimiseCoding"); baton->jpegOptimiseCoding = AttrTo<bool>(options, "jpegOptimiseCoding");
@@ -1307,6 +1329,17 @@ NAN_METHOD(pipeline) {
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
} }
baton->tileFormat = AttrAsStr(options, "tileFormat"); baton->tileFormat = AttrAsStr(options, "tileFormat");
std::string tileDepth = AttrAsStr(options, "tileDepth");
if (tileDepth == "onetile") {
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONETILE;
} else if (tileDepth == "one") {
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONE;
} else if (tileDepth == "onepixel") {
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONEPIXEL;
} else {
// signal that we do not want to pass any value to dzSave
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_LAST;
}
// Force random access for certain operations // Force random access for certain operations
if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && ( if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && (
baton->trimTolerance != 0 || baton->normalise || baton->trimTolerance != 0 || baton->normalise ||

View File

@@ -102,6 +102,7 @@ struct PipelineBaton {
bool jpegProgressive; bool jpegProgressive;
std::string jpegChromaSubsampling; std::string jpegChromaSubsampling;
bool jpegTrellisQuantisation; bool jpegTrellisQuantisation;
int jpegQuantisationTable;
bool jpegOvershootDeringing; bool jpegOvershootDeringing;
bool jpegOptimiseScans; bool jpegOptimiseScans;
bool jpegOptimiseCoding; bool jpegOptimiseCoding;
@@ -130,6 +131,7 @@ struct PipelineBaton {
VipsOperationBoolean booleanOp; VipsOperationBoolean booleanOp;
VipsOperationBoolean bandBoolOp; VipsOperationBoolean bandBoolOp;
int extractChannel; int extractChannel;
bool removeAlpha;
VipsInterpretation colourspace; VipsInterpretation colourspace;
int tileSize; int tileSize;
int tileOverlap; int tileOverlap;
@@ -137,6 +139,7 @@ struct PipelineBaton {
VipsForeignDzLayout tileLayout; VipsForeignDzLayout tileLayout;
std::string tileFormat; std::string tileFormat;
int tileAngle; int tileAngle;
VipsForeignDzDepth tileDepth;
PipelineBaton(): PipelineBaton():
input(nullptr), input(nullptr),
@@ -188,6 +191,7 @@ struct PipelineBaton {
jpegProgressive(false), jpegProgressive(false),
jpegChromaSubsampling("4:2:0"), jpegChromaSubsampling("4:2:0"),
jpegTrellisQuantisation(false), jpegTrellisQuantisation(false),
jpegQuantisationTable(0),
jpegOvershootDeringing(false), jpegOvershootDeringing(false),
jpegOptimiseScans(false), jpegOptimiseScans(false),
jpegOptimiseCoding(true), jpegOptimiseCoding(true),
@@ -211,12 +215,14 @@ struct PipelineBaton {
booleanOp(VIPS_OPERATION_BOOLEAN_LAST), booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
extractChannel(-1), extractChannel(-1),
removeAlpha(false),
colourspace(VIPS_INTERPRETATION_LAST), colourspace(VIPS_INTERPRETATION_LAST),
tileSize(256), tileSize(256),
tileOverlap(0), tileOverlap(0),
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ), tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ),
tileAngle(0){ tileAngle(0),
tileDepth(VIPS_FOREIGN_DZ_DEPTH_LAST){
background[0] = 0.0; background[0] = 0.0;
background[1] = 0.0; background[1] = 0.0;
background[2] = 0.0; background[2] = 0.0;

View File

@@ -59,7 +59,6 @@ class StatsWorker : public Nan::AsyncWorker {
using sharp::MaximumImageAlpha; using sharp::MaximumImageAlpha;
vips::VImage image; vips::VImage image;
vips::VImage stats;
sharp::ImageType imageType = sharp::ImageType::UNKNOWN; sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try { try {
@@ -69,9 +68,8 @@ class StatsWorker : public Nan::AsyncWorker {
} }
if (imageType != sharp::ImageType::UNKNOWN) { if (imageType != sharp::ImageType::UNKNOWN) {
try { try {
stats = image.stats(); vips::VImage stats = image.stats();
int bands = image.bands(); int const bands = image.bands();
double const max = MaximumImageAlpha(image.interpretation());
for (int b = 1; b <= bands; b++) { for (int b = 1; b <= bands; b++) {
ChannelStats cStats(static_cast<int>(stats.getpoint(STAT_MIN_INDEX, b).front()), ChannelStats cStats(static_cast<int>(stats.getpoint(STAT_MIN_INDEX, b).front()),
static_cast<int>(stats.getpoint(STAT_MAX_INDEX, b).front()), static_cast<int>(stats.getpoint(STAT_MAX_INDEX, b).front()),
@@ -83,11 +81,15 @@ class StatsWorker : public Nan::AsyncWorker {
static_cast<int>(stats.getpoint(STAT_MAXY_INDEX, b).front())); static_cast<int>(stats.getpoint(STAT_MAXY_INDEX, b).front()));
baton->channelStats.push_back(cStats); baton->channelStats.push_back(cStats);
} }
// Image is not opaque when alpha layer is present and contains a non-mamixa value
// alpha layer is there and the last band i.e. alpha has its max value greater than 0) if (sharp::HasAlpha(image)) {
if (sharp::HasAlpha(image) && stats.getpoint(STAT_MIN_INDEX, bands).front() != max) { double const minAlpha = static_cast<double>(stats.getpoint(STAT_MIN_INDEX, bands).front());
baton->isOpaque = false; if (minAlpha != MaximumImageAlpha(image.interpretation())) {
baton->isOpaque = false;
}
} }
// Estimate entropy via histogram of greyscale value frequency
baton->entropy = std::abs(image.colourspace(VIPS_INTERPRETATION_B_W)[0].hist_find().hist_entropy());
} catch (vips::VError const &err) { } catch (vips::VError const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
@@ -130,6 +132,7 @@ class StatsWorker : public Nan::AsyncWorker {
Set(info, New("channels").ToLocalChecked(), channels); Set(info, New("channels").ToLocalChecked(), channels);
Set(info, New("isOpaque").ToLocalChecked(), New<v8::Boolean>(baton->isOpaque)); Set(info, New("isOpaque").ToLocalChecked(), New<v8::Boolean>(baton->isOpaque));
Set(info, New("entropy").ToLocalChecked(), New<v8::Number>(baton->entropy));
argv[1] = info; argv[1] = info;
} }

View File

@@ -51,12 +51,14 @@ struct StatsBaton {
// Output // Output
std::vector<ChannelStats> channelStats; std::vector<ChannelStats> channelStats;
bool isOpaque; bool isOpaque;
double entropy;
std::string err; std::string err;
StatsBaton(): StatsBaton():
input(nullptr), input(nullptr),
isOpaque(true) isOpaque(true),
entropy(0.0)
{} {}
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
test/fixtures/expected/svg14.4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -81,35 +81,45 @@ describe('Alpha transparency', function () {
}); });
}); });
it('Enlargement with non-nearest neighbor interpolation shouldnt cause dark edges', function (done) { it('Enlargement with non-nearest neighbor interpolation shouldnt cause dark edges', function () {
const base = 'alpha-premultiply-enlargement-2048x1536-paper.png'; const base = 'alpha-premultiply-enlargement-2048x1536-paper.png';
const actual = fixtures.path('output.' + base); const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base); const expected = fixtures.expected(base);
sharp(fixtures.inputPngAlphaPremultiplicationSmall) return sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.resize(2048, 1536) .resize(2048, 1536)
.toFile(actual, function (err) { .toFile(actual)
if (err) { .then(function () {
done(err); fixtures.assertMaxColourDistance(actual, expected, 102);
} else {
fixtures.assertMaxColourDistance(actual, expected, 102);
done();
}
}); });
}); });
it('Reduction with non-nearest neighbor interpolation shouldnt cause dark edges', function (done) { it('Reduction with non-nearest neighbor interpolation shouldnt cause dark edges', function () {
const base = 'alpha-premultiply-reduction-1024x768-paper.png'; const base = 'alpha-premultiply-reduction-1024x768-paper.png';
const actual = fixtures.path('output.' + base); const actual = fixtures.path('output.' + base);
const expected = fixtures.expected(base); const expected = fixtures.expected(base);
sharp(fixtures.inputPngAlphaPremultiplicationLarge) return sharp(fixtures.inputPngAlphaPremultiplicationLarge)
.resize(1024, 768) .resize(1024, 768)
.toFile(actual, function (err) { .toFile(actual)
if (err) { .then(function () {
done(err); fixtures.assertMaxColourDistance(actual, expected, 102);
} else {
fixtures.assertMaxColourDistance(actual, expected, 102);
done();
}
}); });
}); });
it('Removes alpha from fixtures with transparency, ignores those without', function () {
return Promise.all([
fixtures.inputPngWithTransparency,
fixtures.inputPngWithTransparency16bit,
fixtures.inputWebPWithTransparency,
fixtures.inputJpg,
fixtures.inputPng,
fixtures.inputWebP
].map(function (input) {
return sharp(input)
.removeAlpha()
.toBuffer({ resolveWithObject: true })
.then(function (result) {
assert.strictEqual(3, result.info.channels);
});
}));
});
}); });

View File

@@ -69,6 +69,17 @@ describe('Image channel extraction', function () {
}); });
}); });
it('Alpha from 16-bit PNG', function (done) {
const output = fixtures.path('output.extract-alpha-16bit.jpg');
sharp(fixtures.inputPngWithTransparency16bit)
.extractChannel(3)
.toFile(output, function (err, info) {
if (err) throw err;
fixtures.assertMaxColourDistance(output, fixtures.expected('extract-alpha-16bit.jpg'));
done();
});
});
it('Invalid channel number', function () { it('Invalid channel number', function () {
assert.throws(function () { assert.throws(function () {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)

View File

@@ -389,6 +389,16 @@ describe('Input/output', function () {
}); });
}); });
describe('Invalid JPEG quantisation table', function () {
[-1, 88.2, 'test'].forEach(function (table) {
it(table.toString(), function () {
assert.throws(function () {
sharp().jpeg({ quantisationTable: table });
});
});
});
});
it('Progressive JPEG image', function (done) { it('Progressive JPEG image', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, 240)
@@ -856,6 +866,37 @@ describe('Input/output', function () {
}); });
}); });
it('Specifying quantisation table provides different JPEG', function (done) {
// First generate with default quantisation table
sharp(fixtures.inputJpg)
.resize(320, 240)
.jpeg({ optimiseCoding: false })
.toBuffer(function (err, withDefaultQuantisationTable, withInfo) {
if (err) throw err;
assert.strictEqual(true, withDefaultQuantisationTable.length > 0);
assert.strictEqual(withDefaultQuantisationTable.length, withInfo.size);
assert.strictEqual('jpeg', withInfo.format);
assert.strictEqual(320, withInfo.width);
assert.strictEqual(240, withInfo.height);
// Then generate with different quantisation table
sharp(fixtures.inputJpg)
.resize(320, 240)
.jpeg({ optimiseCoding: false, quantisationTable: 3 })
.toBuffer(function (err, withQuantTable3, withoutInfo) {
if (err) throw err;
assert.strictEqual(true, withQuantTable3.length > 0);
assert.strictEqual(withQuantTable3.length, withoutInfo.size);
assert.strictEqual('jpeg', withoutInfo.format);
assert.strictEqual(320, withoutInfo.width);
assert.strictEqual(240, withoutInfo.height);
// Verify image is same (as mozjpeg may not be present) size or less
assert.strictEqual(true, withQuantTable3.length <= withDefaultQuantisationTable.length);
done();
});
});
});
it('Convert SVG to PNG at default 72DPI', function (done) { it('Convert SVG to PNG at default 72DPI', function (done) {
sharp(fixtures.inputSvg) sharp(fixtures.inputSvg)
.resize(1024) .resize(1024)
@@ -898,6 +939,21 @@ describe('Input/output', function () {
}); });
}); });
it('Convert SVG to PNG at 14.4DPI', function (done) {
sharp(fixtures.inputSvg, { density: 14.4 })
.toFormat('png')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(20, info.width);
assert.strictEqual(20, info.height);
fixtures.assertSimilar(fixtures.expected('svg14.4.png'), data, function (err) {
if (err) throw err;
done();
});
});
});
it('Convert SVG with embedded images to PNG, respecting dimensions, autoconvert to PNG', function (done) { it('Convert SVG with embedded images to PNG, respecting dimensions, autoconvert to PNG', function (done) {
sharp(fixtures.inputSvgWithEmbeddedImages) sharp(fixtures.inputSvgWithEmbeddedImages)
.toBuffer(function (err, data, info) { .toBuffer(function (err, data, info) {
@@ -1465,11 +1521,6 @@ describe('Input/output', function () {
sharp(null, { density: 'zoinks' }); sharp(null, { density: 'zoinks' });
}); });
}); });
it('Invalid density: float', function () {
assert.throws(function () {
sharp(null, { density: 0.5 });
});
});
it('Ignore unknown attribute', function () { it('Ignore unknown attribute', function () {
sharp(null, { unknown: true }); sharp(null, { unknown: true });
}); });

View File

@@ -1,8 +1,10 @@
'use strict'; 'use strict';
const assert = require('assert'); const assert = require('assert');
const fs = require('fs');
const semver = require('semver'); const semver = require('semver');
const libvips = require('../../lib/libvips'); const libvips = require('../../lib/libvips');
const mockFS = require('mock-fs');
const originalPlatform = process.platform; const originalPlatform = process.platform;
@@ -66,5 +68,41 @@ describe('libvips binaries', function () {
delete process.env.SHARP_IGNORE_GLOBAL_LIBVIPS; delete process.env.SHARP_IGNORE_GLOBAL_LIBVIPS;
}); });
it('cachePath returns a valid path ending with _libvips', function () {
const cachePath = libvips.cachePath();
assert.strictEqual('string', typeof cachePath);
assert.strictEqual('_libvips', cachePath.substr(-8));
assert.strictEqual(true, fs.existsSync(cachePath));
});
});
describe('safe directory creation', function () {
before(function () {
mockFS({
exampleDirA: {
exampleDirB: {
exampleFile: 'Example test file'
}
}
});
});
after(function () { mockFS.restore(); });
it('mkdirSync creates a directory', function () {
const dirPath = 'createdDir';
libvips.mkdirSync(dirPath);
assert.strictEqual(true, fs.existsSync(dirPath));
});
it('mkdirSync does not throw error or overwrite an existing dir', function () {
const dirPath = 'exampleDirA';
const nestedDirPath = 'exampleDirA/exampleDirB';
assert.strictEqual(true, fs.existsSync(dirPath));
libvips.mkdirSync(dirPath);
assert.strictEqual(true, fs.existsSync(dirPath));
assert.strictEqual(true, fs.existsSync(nestedDirPath));
});
}); });
}); });

View File

@@ -24,6 +24,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -82,6 +83,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3409031108021736));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -105,7 +107,9 @@ describe('Image Stats', function () {
it('PNG with transparency', function (done) { it('PNG with transparency', function (done) {
sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) { sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.06778064835816622));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -180,6 +184,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0));
// alpha channel // alpha channel
assert.strictEqual(0, stats.channels[3]['min']); assert.strictEqual(0, stats.channels[3]['min']);
@@ -204,7 +209,9 @@ describe('Image Stats', function () {
it('Tiff', function (done) { it('Tiff', function (done) {
sharp(fixtures.inputTiff).stats(function (err, stats) { sharp(fixtures.inputTiff).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 0.3851250782608986));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -231,6 +238,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.51758075132966));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -289,6 +297,7 @@ describe('Image Stats', function () {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 6.087309412541799));
// red channel // red channel
assert.strictEqual(35, stats.channels[0]['min']); assert.strictEqual(35, stats.channels[0]['min']);
@@ -345,7 +354,9 @@ describe('Image Stats', function () {
it('Grayscale GIF with alpha', function (done) { it('Grayscale GIF with alpha', function (done) {
sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) { sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(false, stats.isOpaque); assert.strictEqual(false, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 1));
// gray channel // gray channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -387,7 +398,9 @@ describe('Image Stats', function () {
const readable = fs.createReadStream(fixtures.inputJpg); const readable = fs.createReadStream(fixtures.inputJpg);
const pipeline = sharp().stats(function (err, stats) { const pipeline = sharp().stats(function (err, stats) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -449,6 +462,7 @@ describe('Image Stats', function () {
return pipeline.stats().then(function (stats) { return pipeline.stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);
@@ -505,6 +519,7 @@ describe('Image Stats', function () {
it('File in, Promise out', function () { it('File in, Promise out', function () {
return sharp(fixtures.inputJpg).stats().then(function (stats) { return sharp(fixtures.inputJpg).stats().then(function (stats) {
assert.strictEqual(true, stats.isOpaque); assert.strictEqual(true, stats.isOpaque);
assert.strictEqual(true, isInAcceptableRange(stats.entropy, 7.319914765248541));
// red channel // red channel
assert.strictEqual(0, stats.channels[0]['min']); assert.strictEqual(0, stats.channels[0]['min']);

View File

@@ -46,6 +46,51 @@ const assertDeepZoomTiles = function (directory, expectedSize, expectedLevels, d
}, done); }, done);
}; };
const assertZoomifyTiles = function (directory, expectedTileSize, expectedLevels, done) {
fs.stat(path.join(directory, 'ImageProperties.xml'), function (err, stat) {
if (err) throw err;
assert.ok(stat.isFile());
assert.ok(stat.size > 0);
let maxTileLevel = -1;
fs.readdirSync(path.join(directory, 'TileGroup0')).forEach(function (tile) {
// Verify tile file name
assert.ok(/^[0-9]+-[0-9]+-[0-9]+\.jpg$/.test(tile));
let level = parseInt(tile.split('-')[0]);
maxTileLevel = Math.max(maxTileLevel, level);
});
assert.strictEqual(maxTileLevel + 1, expectedLevels); // add one to account for zero level tile
done();
});
};
const assertGoogleTiles = function (directory, expectedTileSize, expectedLevels, done) {
const levels = fs.readdirSync(directory);
assert.strictEqual(expectedLevels, levels.length - 1); // subtract one to account for default blank tile
fs.stat(path.join(directory, 'blank.png'), function (err, stat) {
if (err) throw err;
assert.ok(stat.isFile());
assert.ok(stat.size > 0);
// Basic check to confirm lowest and highest level tiles exist
fs.stat(path.join(directory, '0', '0', '0.jpg'), function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.isFile());
assert.strictEqual(true, stat.size > 0);
fs.stat(path.join(directory, (expectedLevels - 1).toString(), '0', '0.jpg'), function (err, stat) {
if (err) throw err;
assert.strictEqual(true, stat.isFile());
assert.strictEqual(true, stat.size > 0);
done();
});
});
});
};
describe('Tile', function () { describe('Tile', function () {
it('Valid size values pass', function () { it('Valid size values pass', function () {
[1, 8192].forEach(function (size) { [1, 8192].forEach(function (size) {
@@ -144,6 +189,26 @@ describe('Tile', function () {
}); });
}); });
it('Valid depths pass', function () {
['onepixel', 'onetile', 'one'].forEach(function (depth) {
assert.doesNotThrow(function (depth) {
sharp().tile({
depth: depth
});
});
});
});
it('Invalid depths fail', function () {
['depth', 1].forEach(function (depth) {
assert.throws(function () {
sharp().tile({
depth: depth
});
});
});
});
it('Prevent larger overlap than default size', function () { it('Prevent larger overlap than default size', function () {
assert.throws(function () { assert.throws(function () {
sharp().tile({ sharp().tile({
@@ -251,6 +316,54 @@ describe('Tile', function () {
}); });
}); });
it('Deep Zoom layout with depth of one', function (done) {
const directory = fixtures.path('output.512_depth_one.dzi_files');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 512,
depth: 'one'
})
.toFile(fixtures.path('output.512_depth_one.dzi'), function (err, info) {
if (err) throw err;
// Verify only one depth generated
assertDeepZoomTiles(directory, 512, 1, done);
});
});
});
it('Deep Zoom layout with depth of onepixel', function (done) {
const directory = fixtures.path('output.512_depth_onepixel.dzi_files');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 512,
depth: 'onepixel'
})
.toFile(fixtures.path('output.512_depth_onepixel.dzi'), function (err, info) {
if (err) throw err;
// Verify only one depth generated
assertDeepZoomTiles(directory, 512, 13, done);
});
});
});
it('Deep Zoom layout with depth of onetile', function (done) {
const directory = fixtures.path('output.256_depth_onetile.dzi_files');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 256,
depth: 'onetile'
})
.toFile(fixtures.path('output.256_depth_onetile.dzi'), function (err, info) {
if (err) throw err;
// Verify only one depth generated
assertDeepZoomTiles(directory, 256, 5, done);
});
});
});
it('Zoomify layout', function (done) { it('Zoomify layout', function (done) {
const directory = fixtures.path('output.zoomify.dzi'); const directory = fixtures.path('output.zoomify.dzi');
rimraf(directory, function () { rimraf(directory, function () {
@@ -275,6 +388,69 @@ describe('Tile', function () {
}); });
}); });
it('Zoomify layout with depth one', function (done) {
const directory = fixtures.path('output.zoomify.depth_one.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 256,
layout: 'zoomify',
depth: 'one'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertZoomifyTiles(directory, 256, 1, done);
});
});
});
it('Zoomify layout with depth onetile', function (done) {
const directory = fixtures.path('output.zoomify.depth_onetile.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 256,
layout: 'zoomify',
depth: 'onetile'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertZoomifyTiles(directory, 256, 5, done);
});
});
});
it('Zoomify layout with depth onepixel', function (done) {
const directory = fixtures.path('output.zoomify.depth_onepixel.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
size: 256,
layout: 'zoomify',
depth: 'onepixel'
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertZoomifyTiles(directory, 256, 13, done);
});
});
});
it('Google layout', function (done) { it('Google layout', function (done) {
const directory = fixtures.path('output.google.dzi'); const directory = fixtures.path('output.google.dzi');
rimraf(directory, function () { rimraf(directory, function () {
@@ -410,6 +586,72 @@ describe('Tile', function () {
}); });
}); });
it('Google layout with depth one', function (done) {
const directory = fixtures.path('output.google_depth_one.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
layout: 'google',
depth: 'one',
size: 256
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertGoogleTiles(directory, 256, 1, done);
});
});
});
it('Google layout with depth onepixel', function (done) {
const directory = fixtures.path('output.google_depth_onepixel.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
layout: 'google',
depth: 'onepixel',
size: 256
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertGoogleTiles(directory, 256, 13, done);
});
});
});
it('Google layout with depth onetile', function (done) {
const directory = fixtures.path('output.google_depth_onetile.dzi');
rimraf(directory, function () {
sharp(fixtures.inputJpg)
.tile({
layout: 'google',
depth: 'onetile',
size: 256
})
.toFile(directory, function (err, info) {
if (err) throw err;
assert.strictEqual('dz', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual('number', typeof info.size);
assertGoogleTiles(directory, 256, 5, done);
});
});
});
it('Write to ZIP container using file extension', function (done) { it('Write to ZIP container using file extension', function (done) {
const container = fixtures.path('output.dz.container.zip'); const container = fixtures.path('output.dz.container.zip');
const extractTo = fixtures.path('output.dz.container'); const extractTo = fixtures.path('output.dz.container');