diff --git a/docs/api-input.md b/docs/api-input.md index 70b4fa75b..c1ff16cea 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -42,6 +42,7 @@ A `Promise` is returned when `callback` is not provided. - `xmp`: Buffer containing raw XMP data, if present - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present - `formatMagick`: String containing format for images loaded via *magick +- `comments`: Array of keyword/text pairs representing PNG text blocks, if present. diff --git a/lib/index.d.ts b/lib/index.d.ts index 6e25529d8..ff33e7cbb 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1108,6 +1108,8 @@ declare namespace sharp { resolutionUnit?: 'inch' | 'cm' | undefined; /** String containing format for images loaded via *magick */ formatMagick?: string | undefined; + /** Array of keyword/text pairs representing PNG text blocks, if present. */ + comments?: CommentsMetadata[] | undefined; } interface LevelMetadata { @@ -1115,6 +1117,11 @@ declare namespace sharp { height: number; } + interface CommentsMetadata { + keyword: string; + text: string; + } + interface Stats { /** Array of channel statistics for each channel in the image. */ channels: ChannelStats[]; diff --git a/lib/input.js b/lib/input.js index 612b03880..ae3eb9430 100644 --- a/lib/input.js +++ b/lib/input.js @@ -450,6 +450,7 @@ function _isStreamInput () { * - `xmp`: Buffer containing raw XMP data, if present * - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present * - `formatMagick`: String containing format for images loaded via *magick + * - `comments` Array of keyword/text pairs representing PNG text blocks, if present. * * @example * const metadata = await sharp(input).metadata(); diff --git a/src/metadata.cc b/src/metadata.cc index 29e41ebb8..9d0ac6f38 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -10,6 +10,8 @@ #include "common.h" #include "metadata.h" +static void* readPNGComment(VipsImage *image, const char *field, GValue *value, void *p); + class MetadataWorker : public Napi::AsyncWorker { public: MetadataWorker(Napi::Function callback, MetadataBaton *baton, Napi::Function debuglog) : @@ -131,6 +133,8 @@ class MetadataWorker : public Napi::AsyncWorker { memcpy(baton->tifftagPhotoshop, tifftagPhotoshop, tifftagPhotoshopLength); baton->tifftagPhotoshopLength = tifftagPhotoshopLength; } + // PNG comments + vips_image_map(image.get_image(), readPNGComment, &baton->comments); } // Clean up @@ -246,6 +250,17 @@ class MetadataWorker : public Napi::AsyncWorker { Napi::Buffer::NewOrCopy(env, baton->tifftagPhotoshop, baton->tifftagPhotoshopLength, sharp::FreeCallback)); } + if (baton->comments.size() > 0) { + int i = 0; + Napi::Array comments = Napi::Array::New(env, baton->comments.size()); + for (auto &c : baton->comments) { + Napi::Object comment = Napi::Object::New(env); + comment.Set("keyword", c.first); + comment.Set("text", c.second); + comments.Set(i++, comment); + } + info.Set("comments", comments); + } Callback().Call(Receiver().Value(), { env.Null(), info }); } else { Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); @@ -285,3 +300,21 @@ Napi::Value metadata(const Napi::CallbackInfo& info) { return info.Env().Undefined(); } + +const char *PNG_COMMENT_START = "png-comment-"; +const int PNG_COMMENT_START_LEN = strlen(PNG_COMMENT_START); + +static void* readPNGComment(VipsImage *image, const char *field, GValue *value, void *p) { + MetadataComments *comments = static_cast(p); + + if (vips_isprefix(PNG_COMMENT_START, field)) { + const char *keyword = strchr(field + PNG_COMMENT_START_LEN, '-'); + const char *str; + if (keyword != NULL && !vips_image_get_string(image, field, &str)) { + keyword++; // Skip the hyphen + comments->push_back(std::make_pair(keyword, str)); + } + } + + return NULL; +} diff --git a/src/metadata.h b/src/metadata.h index 3030ae297..b31af869d 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -9,6 +9,8 @@ #include "./common.h" +typedef std::vector> MetadataComments; + struct MetadataBaton { // Input sharp::InputDescriptor *input; @@ -47,6 +49,7 @@ struct MetadataBaton { size_t xmpLength; char *tifftagPhotoshop; size_t tifftagPhotoshopLength; + MetadataComments comments; std::string err; MetadataBaton(): diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 09de787cf..60cee7a13 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -154,6 +154,31 @@ describe('Image metadata', function () { }); }); + it('PNG with comment', function (done) { + sharp(fixtures.inputPngTestJoinChannel).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual('png', metadata.format); + assert.strictEqual('undefined', typeof metadata.size); + assert.strictEqual(320, metadata.width); + assert.strictEqual(240, metadata.height); + assert.strictEqual('b-w', metadata.space); + assert.strictEqual(1, metadata.channels); + assert.strictEqual('uchar', metadata.depth); + assert.strictEqual(72, metadata.density); + assert.strictEqual('undefined', typeof metadata.chromaSubsampling); + assert.strictEqual(false, metadata.isProgressive); + assert.strictEqual(false, metadata.hasProfile); + assert.strictEqual(false, metadata.hasAlpha); + assert.strictEqual('undefined', typeof metadata.orientation); + assert.strictEqual('undefined', typeof metadata.exif); + assert.strictEqual('undefined', typeof metadata.icc); + assert.strictEqual(1, metadata.comments.length); + assert.strictEqual('Comment', metadata.comments[0].keyword); + assert.strictEqual('Created with GIMP', metadata.comments[0].text); + done(); + }); + }); + it('Transparent PNG', function (done) { sharp(fixtures.inputPngWithTransparency).metadata(function (err, metadata) { if (err) throw err;