From 6d18c6fdc6a1a7dbdaeb2776cda9b6f44b6d2e56 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sun, 1 Feb 2026 21:20:16 +0000 Subject: [PATCH] Add Media Type to metadata response #4492 --- docs/src/content/docs/api-input.md | 1 + docs/src/content/docs/changelog/v0.35.0.md | 3 ++ lib/input.js | 1 + src/metadata.cc | 47 ++++++++++++++++++++++ src/metadata.h | 1 + test/unit/avif.js | 8 ++++ test/unit/metadata.js | 36 ++++++++++++++--- test/unit/png.js | 1 + 8 files changed, 93 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/api-input.md b/docs/src/content/docs/api-input.md index eac396b6..618c66b6 100644 --- a/docs/src/content/docs/api-input.md +++ b/docs/src/content/docs/api-input.md @@ -18,6 +18,7 @@ Dimensions in the response will respect the `page` and `pages` properties of the A `Promise` is returned when `callback` is not provided. - `format`: Name of decoder used to parse image e.g. `jpeg`, `png`, `webp`, `gif`, `svg`, `heif`, `tiff` +- `mediaType`: Media Type (MIME Type) e.g. `image/jpeg`, `image/png`, `image/svg+xml`, `image/avif` - `size`: Total size of image in bytes, for Stream and Buffer input only - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below) - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below) diff --git a/docs/src/content/docs/changelog/v0.35.0.md b/docs/src/content/docs/changelog/v0.35.0.md index f047aab4..262e1ab5 100644 --- a/docs/src/content/docs/changelog/v0.35.0.md +++ b/docs/src/content/docs/changelog/v0.35.0.md @@ -43,4 +43,7 @@ slug: changelog/v0.35.0 * Ensure HEIF primary item is used as default page/frame. [#4487](https://github.com/lovell/sharp/issues/4487) +* Add image Media Type (MIME Type) to metadata response. + [#4492](https://github.com/lovell/sharp/issues/4492) + * Add WebP `exact` option for control over transparent pixel colour values. diff --git a/lib/input.js b/lib/input.js index 5347d14b..695f6d39 100644 --- a/lib/input.js +++ b/lib/input.js @@ -567,6 +567,7 @@ function _isStreamInput () { * A `Promise` is returned when `callback` is not provided. * * - `format`: Name of decoder used to parse image e.g. `jpeg`, `png`, `webp`, `gif`, `svg`, `heif`, `tiff` + * - `mediaType`: Media Type (MIME Type) e.g. `image/jpeg`, `image/png`, `image/svg+xml`, `image/avif` * - `size`: Total size of image in bytes, for Stream and Buffer input only * - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below) * - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below) diff --git a/src/metadata.cc b/src/metadata.cc index bb8e9787..769e306f 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -151,6 +151,50 @@ class MetadataWorker : public Napi::AsyncWorker { } // PNG comments vips_image_map(image.get_image(), readPNGComment, &baton->comments); + // Media type + std::string mediaType; + switch (imageType) { + case sharp::ImageType::JPEG: + case sharp::ImageType::PNG: + case sharp::ImageType::WEBP: + case sharp::ImageType::JP2: + case sharp::ImageType::TIFF: + case sharp::ImageType::GIF: + case sharp::ImageType::FITS: + case sharp::ImageType::JXL: + baton->mediaType = "image/" + baton->format; + break; + case sharp::ImageType::SVG: + baton->mediaType = "image/svg+xml"; + break; + case sharp::ImageType::HEIF: + if (baton->compression == "av1") { + baton->mediaType = "image/avif"; + } else if (baton->compression == "hevc") { + baton->mediaType = "image/heic"; + } + break; + case sharp::ImageType::PDF: + baton->mediaType = "application/pdf"; + break; + case sharp::ImageType::OPENSLIDE: + baton->mediaType = "image/tiff"; + break; + case sharp::ImageType::PPM: + baton->mediaType = "image/x-portable-pixmap"; + break; + case sharp::ImageType::EXR: + baton->mediaType = "image/x-exr"; + break; + case sharp::ImageType::RAD: + baton->mediaType = "image/vnd.radiance"; + break; + case sharp::ImageType::UHDR: + baton->mediaType = "image/jpeg"; + break; + default: + break; + } } // Clean up @@ -172,6 +216,9 @@ class MetadataWorker : public Napi::AsyncWorker { if (baton->err.empty()) { Napi::Object info = Napi::Object::New(env); info.Set("format", baton->format); + if (!baton->mediaType.empty()) { + info.Set("mediaType", baton->mediaType); + } if (baton->input->bufferLength > 0) { info.Set("size", baton->input->bufferLength); } diff --git a/src/metadata.h b/src/metadata.h index 7d309bf1..d1781dc5 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -19,6 +19,7 @@ struct MetadataBaton { sharp::InputDescriptor *input; // Output std::string format; + std::string mediaType; int width; int height; std::string space; diff --git a/test/unit/avif.js b/test/unit/avif.js index 851d6852..4455944f 100644 --- a/test/unit/avif.js +++ b/test/unit/avif.js @@ -36,6 +36,7 @@ describe('AVIF', () => { density: 72, depth: 'uchar', format: 'jpeg', + mediaType: 'image/jpeg', hasAlpha: false, hasProfile: false, // 32 / (2048 / 858) = 13.40625 @@ -64,6 +65,7 @@ describe('AVIF', () => { compression: 'av1', depth: 'uchar', format: 'heif', + mediaType: 'image/avif', hasAlpha: false, hasProfile: false, height: 26, @@ -93,6 +95,7 @@ describe('AVIF', () => { compression: 'av1', depth: 'uchar', format: 'heif', + mediaType: 'image/avif', hasAlpha: false, hasProfile: false, height: 24, @@ -119,6 +122,7 @@ describe('AVIF', () => { compression: 'av1', depth: 'uchar', format: 'heif', + mediaType: 'image/avif', hasAlpha: false, hasProfile: false, height: 13, @@ -148,6 +152,7 @@ describe('AVIF', () => { compression: 'av1', depth: 'uchar', format: 'heif', + mediaType: 'image/avif', hasAlpha: true, hasProfile: false, height: 300, @@ -178,6 +183,7 @@ describe('AVIF', () => { compression: 'av1', depth: 'uchar', format: 'heif', + mediaType: 'image/avif', hasAlpha: false, hasProfile: false, height: 26, @@ -236,6 +242,7 @@ describe('AVIF', () => { void exif; assert.deepStrictEqual(metadata, { format: 'heif', + mediaType: 'image/avif', width: 4096, height: 800, space: 'srgb', @@ -259,6 +266,7 @@ describe('AVIF', () => { const { size, ...pngMetadata } = await sharp(data).metadata(); assert.deepStrictEqual(pngMetadata, { format: 'png', + mediaType: 'image/png', width: 4096, height: 800, space: 'srgb', diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 300380ed..99f46c43 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -19,6 +19,7 @@ describe('Image metadata', () => { sharp(fixtures.inputJpg).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2725, metadata.width); assert.strictEqual(2225, metadata.height); @@ -41,6 +42,7 @@ describe('Image metadata', () => { sharp(fixtures.inputJpgWithExif).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(450, metadata.width); assert.strictEqual(600, metadata.height); @@ -66,6 +68,7 @@ describe('Image metadata', () => { const profile = icc.parse(metadata.icc); assert.strictEqual('object', typeof profile); assert.strictEqual('Generic RGB Profile', profile.description); + assert.strictEqual('image/jpeg', metadata.mediaType); done(); }); }); @@ -92,6 +95,7 @@ describe('Image metadata', () => { sharp(fixtures.inputTiff).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('tiff', metadata.format); + assert.strictEqual('image/tiff', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2464, metadata.width); assert.strictEqual(3248, metadata.height); @@ -111,6 +115,7 @@ describe('Image metadata', () => { assert.strictEqual('undefined', typeof metadata.xmp); assert.strictEqual('undefined', typeof metadata.xmpAsString); assert.strictEqual('inch', metadata.resolutionUnit); + assert.strictEqual('image/tiff', metadata.mediaType); done(); }); }); @@ -119,6 +124,7 @@ describe('Image metadata', () => { sharp(fixtures.inputTiffMultipage).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('tiff', metadata.format); + assert.strictEqual('image/tiff', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2464, metadata.width); assert.strictEqual(3248, metadata.height); @@ -142,6 +148,7 @@ describe('Image metadata', () => { sharp(fixtures.inputPng).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('png', metadata.format); + assert.strictEqual('image/png', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2809, metadata.width); assert.strictEqual(2074, metadata.height); @@ -166,6 +173,7 @@ describe('Image metadata', () => { sharp(fixtures.inputPngTestJoinChannel).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('png', metadata.format); + assert.strictEqual('image/png', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(320, metadata.width); assert.strictEqual(240, metadata.height); @@ -191,6 +199,7 @@ describe('Image metadata', () => { sharp(fixtures.inputPngWithTransparency).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('png', metadata.format); + assert.strictEqual('image/png', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2048, metadata.width); assert.strictEqual(1536, metadata.height); @@ -225,6 +234,7 @@ describe('Image metadata', () => { height: 32, isPalette: false, isProgressive: false, + mediaType: 'image/png', space: 'b-w', width: 32, autoOrient: { @@ -250,6 +260,7 @@ describe('Image metadata', () => { height: 32, isPalette: false, isProgressive: false, + mediaType: 'image/png', space: 'grey16', width: 32, autoOrient: { @@ -263,6 +274,7 @@ describe('Image metadata', () => { sharp(fixtures.inputWebP).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('webp', metadata.format); + assert.strictEqual('image/webp', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(1024, metadata.width); assert.strictEqual(772, metadata.height); @@ -285,11 +297,12 @@ describe('Image metadata', () => { sharp(fixtures.inputWebPAnimated) .metadata() .then(({ - format, width, height, space, channels, depth, + format, mediaType, width, height, space, channels, depth, isProgressive, pages, loop, delay, hasProfile, hasAlpha }) => { assert.strictEqual(format, 'webp'); + assert.strictEqual(mediaType, 'image/webp'); assert.strictEqual(width, 80); assert.strictEqual(height, 80); assert.strictEqual(space, 'srgb'); @@ -308,11 +321,12 @@ describe('Image metadata', () => { sharp(fixtures.inputWebPAnimated, { pages: -1 }) .metadata() .then(({ - format, width, height, space, channels, depth, + format, mediaType, width, height, space, channels, depth, isProgressive, pages, pageHeight, loop, delay, hasProfile, hasAlpha }) => { assert.strictEqual(format, 'webp'); + assert.strictEqual(mediaType, 'image/webp'); assert.strictEqual(width, 80); assert.strictEqual(height, 720); assert.strictEqual(space, 'srgb'); @@ -332,11 +346,12 @@ describe('Image metadata', () => { sharp(fixtures.inputWebPAnimatedLoop3) .metadata() .then(({ - format, width, height, space, channels, depth, + format, mediaType, width, height, space, channels, depth, isProgressive, pages, loop, delay, hasProfile, hasAlpha }) => { assert.strictEqual(format, 'webp'); + assert.strictEqual(mediaType, 'image/webp'); assert.strictEqual(width, 370); assert.strictEqual(height, 285); assert.strictEqual(space, 'srgb'); @@ -355,6 +370,7 @@ describe('Image metadata', () => { sharp(fixtures.inputGif).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('gif', metadata.format); + assert.strictEqual('image/gif', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(800, metadata.width); assert.strictEqual(533, metadata.height); @@ -375,6 +391,7 @@ describe('Image metadata', () => { sharp(fixtures.inputGifGreyPlusAlpha).metadata((err, metadata) => { if (err) throw err; assert.strictEqual('gif', metadata.format); + assert.strictEqual('image/gif', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2, metadata.width); assert.strictEqual(1, metadata.height); @@ -395,11 +412,12 @@ describe('Image metadata', () => { sharp(fixtures.inputGifAnimated) .metadata() .then(({ - format, width, height, space, channels, depth, + format, mediaType, width, height, space, channels, depth, isProgressive, pages, loop, delay, background, hasProfile, hasAlpha }) => { assert.strictEqual(format, 'gif'); + assert.strictEqual(mediaType, 'image/gif'); assert.strictEqual(width, 80); assert.strictEqual(height, 80); assert.strictEqual(space, 'srgb'); @@ -419,11 +437,12 @@ describe('Image metadata', () => { sharp(fixtures.inputGifAnimatedLoop3) .metadata() .then(({ - format, width, height, space, channels, depth, + format, mediaType, width, height, space, channels, depth, isProgressive, pages, loop, delay, hasProfile, hasAlpha }) => { assert.strictEqual(format, 'gif'); + assert.strictEqual(mediaType, 'image/gif'); assert.strictEqual(width, 370); assert.strictEqual(height, 285); assert.strictEqual(space, 'srgb'); @@ -462,6 +481,7 @@ describe('Image metadata', () => { it('File in, Promise out', (_t, done) => { sharp(fixtures.inputJpg).metadata().then((metadata) => { assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2725, metadata.width); assert.strictEqual(2225, metadata.height); @@ -508,6 +528,7 @@ describe('Image metadata', () => { const pipeline = sharp(); pipeline.metadata().then((metadata) => { assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual(829183, metadata.size); assert.strictEqual(2725, metadata.width); assert.strictEqual(2225, metadata.height); @@ -559,6 +580,7 @@ describe('Image metadata', () => { const pipeline = sharp().metadata((err, metadata) => { if (err) throw err; assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual(829183, metadata.size); assert.strictEqual(2725, metadata.width); assert.strictEqual(2225, metadata.height); @@ -583,6 +605,7 @@ describe('Image metadata', () => { image.metadata((err, metadata) => { if (err) throw err; assert.strictEqual('jpeg', metadata.format); + assert.strictEqual('image/jpeg', metadata.mediaType); assert.strictEqual('undefined', typeof metadata.size); assert.strictEqual(2725, metadata.width); assert.strictEqual(2225, metadata.height); @@ -917,6 +940,7 @@ describe('Image metadata', () => { .metadata() .then(metadata => { assert.strictEqual(metadata.format, 'tiff'); + assert.strictEqual(metadata.mediaType, 'image/tiff'); assert.strictEqual(metadata.width, 317); assert.strictEqual(metadata.height, 211); assert.strictEqual(metadata.space, 'rgb16'); @@ -931,6 +955,7 @@ describe('Image metadata', () => { const metadata = await sharp(fixtures.inputAvif).metadata(); assert.deepStrictEqual(metadata, { format: 'heif', + mediaType: 'image/avif', width: 2048, height: 858, space: 'srgb', @@ -1014,6 +1039,7 @@ describe('Image metadata', () => { const metadata = await sharp(fixtures.inputJpgLossless).metadata(); assert.deepStrictEqual(metadata, { format: 'jpeg', + mediaType: 'image/jpeg', width: 227, height: 149, space: 'srgb', diff --git a/test/unit/png.js b/test/unit/png.js index df2dff0b..d9704c3c 100644 --- a/test/unit/png.js +++ b/test/unit/png.js @@ -145,6 +145,7 @@ describe('PNG', () => { width: 68 }, format: 'png', + mediaType: 'image/png', width: 68, height: 68, space: 'srgb',