diff --git a/docs/api.md b/docs/api.md index 963561fe..a69ae6c4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -48,6 +48,7 @@ Fast access to image metadata without decoding any compressed image data. * `height`: Number of pixels high * `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L522) * `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK +* `density`: Number of pixels per inch (DPI), if present * `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/changelog.md b/docs/changelog.md index fa521173..8393616c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,10 @@ [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) +* Expose density metadata; set density of images from vector input. + [#338](https://github.com/lovell/sharp/issues/338) + [@lookfirst](https://github.com/lookfirst) + ### v0.13 - "*mind*" #### v0.13.1 - 27th February 2016 diff --git a/index.js b/index.js index d959e1f3..4a906b4e 100644 --- a/index.js +++ b/index.js @@ -46,7 +46,7 @@ var Sharp = function(input, options) { streamIn: false, sequentialRead: false, limitInputPixels: maximum.pixels, - density: '72', + density: 72, rawWidth: 0, rawHeight: 0, rawChannels: 0, @@ -172,7 +172,7 @@ Sharp.prototype._inputOptions = function(options) { // Density if (isDefined(options.density)) { if (isInteger(options.density) && inRange(options.density, 1, 2400)) { - this.options.density = options.density.toString(); + this.options.density = options.density; } else { throw new Error('Invalid density (1 to 2400) ' + options.density); } diff --git a/src/common.cc b/src/common.cc index 92aaaefb..46872aa8 100644 --- a/src/common.cc +++ b/src/common.cc @@ -176,6 +176,30 @@ namespace sharp { SetExifOrientation(image, 0); } + /* + Does this image have a non-default density? + */ + bool HasDensity(VImage image) { + return image.xres() > 1.0; + } + + /* + Get pixels/mm resolution as pixels/inch density. + */ + int GetDensity(VImage image) { + return static_cast(round(image.xres() * 25.4)); + } + + /* + Set pixels/mm resolution based on a pixels/inch density. + */ + void SetDensity(VImage image, const int density) { + const double pixelsPerMm = static_cast(density) / 25.4; + image.set("Xres", pixelsPerMm); + image.set("Yres", pixelsPerMm); + image.set(VIPS_META_RESOLUTION_UNIT, "in"); + } + /* Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows */ diff --git a/src/common.h b/src/common.h index a1fcfdb4..be7a2e78 100644 --- a/src/common.h +++ b/src/common.h @@ -77,6 +77,21 @@ namespace sharp { */ void RemoveExifOrientation(VImage image); + /* + Does this image have a non-default density? + */ + bool HasDensity(VImage image); + + /* + Get pixels/mm resolution as pixels/inch density. + */ + int GetDensity(VImage image); + + /* + Set pixels/mm resolution based on a pixels/inch density. + */ + void SetDensity(VImage image, const int density); + /* Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows */ diff --git a/src/metadata.cc b/src/metadata.cc index 196b6f54..f451fedc 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -38,6 +38,8 @@ using sharp::DetermineImageType; using sharp::HasProfile; using sharp::HasAlpha; using sharp::ExifOrientation; +using sharp::HasDensity; +using sharp::GetDensity; using sharp::FreeCallback; using sharp::counterQueue; @@ -52,6 +54,7 @@ struct MetadataBaton { int height; std::string space; int channels; + int density; bool hasProfile; bool hasAlpha; int orientation; @@ -63,6 +66,7 @@ struct MetadataBaton { MetadataBaton(): bufferInLength(0), + density(0), orientation(0), exifLength(0), iccLength(0) {} @@ -120,6 +124,9 @@ class MetadataWorker : public AsyncWorker { baton->height = image.height(); baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image.interpretation()); baton->channels = image.bands(); + if (HasDensity(image)) { + baton->density = GetDensity(image); + } baton->hasProfile = HasProfile(image); // Derived attributes baton->hasAlpha = HasAlpha(image); @@ -161,6 +168,9 @@ class MetadataWorker : public AsyncWorker { Set(info, New("height").ToLocalChecked(), New(baton->height)); Set(info, New("space").ToLocalChecked(), New(baton->space).ToLocalChecked()); Set(info, New("channels").ToLocalChecked(), New(baton->channels)); + if (baton->density > 0) { + Set(info, New("density").ToLocalChecked(), New(baton->density)); + } Set(info, New("hasProfile").ToLocalChecked(), New(baton->hasProfile)); Set(info, New("hasAlpha").ToLocalChecked(), New(baton->hasAlpha)); if (baton->orientation > 0) { diff --git a/src/operations.cc b/src/operations.cc index 7cbedbab..be3c97a9 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -169,4 +169,5 @@ namespace sharp { ); } } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index 7c9d8899..059d8188 100644 --- a/src/operations.h +++ b/src/operations.h @@ -32,6 +32,7 @@ namespace sharp { * Sharpen flat and jagged areas. Use radius of -1 for fast sharpen. */ VImage Sharpen(VImage image, int const radius, double const flat, double const jagged); + } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index 561eff7d..df5770e6 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -58,6 +58,7 @@ using sharp::HasAlpha; using sharp::ExifOrientation; using sharp::SetExifOrientation; using sharp::RemoveExifOrientation; +using sharp::SetDensity; using sharp::IsJpeg; using sharp::IsPng; using sharp::IsWebp; @@ -118,9 +119,12 @@ class PipelineWorker : public AsyncWorker { try { VOption *option = VImage::option()->set("access", baton->accessMethod); if (inputImageType == ImageType::MAGICK) { - option->set("density", baton->density.data()); + option->set("density", std::to_string(baton->density).data()); } image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr, option); + if (inputImageType == ImageType::MAGICK) { + SetDensity(image, baton->density); + } } catch (...) { (baton->err).append("Input buffer has corrupt header"); inputImageType = ImageType::UNKNOWN; @@ -136,9 +140,12 @@ class PipelineWorker : public AsyncWorker { try { VOption *option = VImage::option()->set("access", baton->accessMethod); if (inputImageType == ImageType::MAGICK) { - option->set("density", baton->density.data()); + option->set("density", std::to_string(baton->density).data()); } image = VImage::new_from_file(baton->fileIn.data(), option); + if (inputImageType == ImageType::MAGICK) { + SetDensity(image, baton->density); + } } catch (...) { (baton->err).append("Input file has corrupt header"); inputImageType = ImageType::UNKNOWN; @@ -921,7 +928,7 @@ NAN_METHOD(pipeline) { // Limit input images to a given number of pixels, where pixels = width * height baton->limitInputPixels = attrAs(options, "limitInputPixels"); // Density/DPI at which to load vector images via libmagick - baton->density = attrAsStr(options, "density"); + baton->density = attrAs(options, "density"); // Raw pixel input baton->rawWidth = attrAs(options, "rawWidth"); baton->rawHeight = attrAs(options, "rawHeight"); diff --git a/src/pipeline.h b/src/pipeline.h index 19c33fab..a5778a29 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -19,7 +19,7 @@ struct PipelineBaton { size_t bufferInLength; std::string iccProfilePath; int limitInputPixels; - std::string density; + int density; int rawWidth; int rawHeight; int rawChannels; @@ -79,7 +79,7 @@ struct PipelineBaton { PipelineBaton(): bufferInLength(0), limitInputPixels(0), - density(""), + density(72), rawWidth(0), rawHeight(0), rawChannels(0), diff --git a/test/unit/io.js b/test/unit/io.js index 4ae2de36..a776dbf5 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -662,7 +662,14 @@ describe('Input/output', function() { assert.strictEqual('png', info.format); assert.strictEqual(40, info.width); assert.strictEqual(40, info.height); - fixtures.assertSimilar(fixtures.expected('svg72.png'), data, done); + fixtures.assertSimilar(fixtures.expected('svg72.png'), data, function(err) { + if (err) throw err; + sharp(data).metadata(function(err, info) { + if (err) throw err; + assert.strictEqual(72, info.density); + done(); + }); + }); } }); }); @@ -679,7 +686,14 @@ describe('Input/output', function() { assert.strictEqual('png', info.format); assert.strictEqual(40, info.width); assert.strictEqual(40, info.height); - fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, done); + fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, function(err) { + if (err) throw err; + sharp(data).metadata(function(err, info) { + if (err) throw err; + assert.strictEqual(1200, info.density); + done(); + }); + }); } }); }); diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 26e48fca..05f9503a 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -18,6 +18,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -35,6 +36,7 @@ describe('Image metadata', function() { assert.strictEqual(600, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual(72, metadata.density); assert.strictEqual(true, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(8, metadata.orientation); @@ -64,6 +66,7 @@ describe('Image metadata', function() { assert.strictEqual(3248, metadata.height); assert.strictEqual('b-w', metadata.space); assert.strictEqual(1, metadata.channels); + assert.strictEqual(300, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -82,6 +85,7 @@ describe('Image metadata', function() { assert.strictEqual(2074, metadata.height); assert.strictEqual('b-w', metadata.space); assert.strictEqual(1, metadata.channels); + assert.strictEqual(300, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -99,6 +103,7 @@ describe('Image metadata', function() { assert.strictEqual(1536, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(4, metadata.channels); + assert.strictEqual(72, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(true, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -117,6 +122,7 @@ describe('Image metadata', function() { assert.strictEqual(772, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -135,6 +141,7 @@ describe('Image metadata', function() { assert.strictEqual(800, metadata.width); assert.strictEqual(533, metadata.height); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -153,6 +160,7 @@ describe('Image metadata', function() { assert.strictEqual(2220, metadata.width); assert.strictEqual(2967, metadata.height); assert.strictEqual(4, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual('rgb', metadata.space); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(true, metadata.hasAlpha); @@ -171,6 +179,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -198,6 +207,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -219,6 +229,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -238,6 +249,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation);