Skip to content

Commit aaeded2

Browse files
committed
Add withGainMap to process HDR JPEGs with embedded gain map #4314
1 parent f6cdd36 commit aaeded2

File tree

16 files changed

+197
-2
lines changed

16 files changed

+197
-2
lines changed

docs/src/content/docs/api-input.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ A `Promise` is returned when `callback` is not provided.
5050
- `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present
5151
- `formatMagick`: String containing format for images loaded via *magick
5252
- `comments`: Array of keyword/text pairs representing PNG text blocks, if present.
53+
- `gainMap.image`: HDR gain map, if present, as compressed JPEG image.
5354

5455

5556

docs/src/content/docs/api-output.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,27 @@ const outputWithP3 = await sharp(input)
250250
```
251251

252252

253+
## withGainMap
254+
> withGainMap() ⇒ <code>Sharp</code>
255+
256+
If the input contains gain map metadata, use it to convert the main image to HDR (High Dynamic Range) before further processing.
257+
The input gain map is discarded.
258+
259+
If the output is JPEG, generate and attach a new ISO 21496-1 gain map.
260+
JPEG output options other than `quality` are ignored.
261+
262+
This feature is experimental and the API may change.
263+
264+
265+
**Since**: 0.35.0
266+
**Example**
267+
```js
268+
const outputWithGainMap = await sharp(inputWithGainMap)
269+
.withGainMap()
270+
.toBuffer();
271+
```
272+
273+
253274
## keepXmp
254275
> keepXmp() ⇒ <code>Sharp</code>
255276

docs/src/content/docs/changelog/v0.35.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ slug: changelog/v0.35.0
66
* Upgrade to libvips v8.18.0 for upstream bug fixes.
77

88
* Drop support for Node.js 18, now requires Node.js >= 20.9.0.
9+
10+
* Add `withGainMap` to process HDR JPEG images with embedded gain maps.
11+
[#4314](https://github.com/lovell/sharp/issues/4314)

lib/constructor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ const Sharp = function (input, options) {
313313
withExif: {},
314314
withExifMerge: true,
315315
withXmp: '',
316+
withGainMap: false,
316317
resolveWithObject: false,
317318
loop: -1,
318319
delay: [],

lib/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,8 @@ declare namespace sharp {
12771277
formatMagick?: string | undefined;
12781278
/** Array of keyword/text pairs representing PNG text blocks, if present. */
12791279
comments?: CommentsMetadata[] | undefined;
1280+
/** HDR gain map, if present */
1281+
gainMap?: GainMapMetadata | undefined;
12801282
}
12811283

12821284
interface LevelMetadata {
@@ -1289,6 +1291,11 @@ declare namespace sharp {
12891291
text: string;
12901292
}
12911293

1294+
interface GainMapMetadata {
1295+
/** JPEG image */
1296+
image: Buffer;
1297+
}
1298+
12921299
interface Stats {
12931300
/** Array of channel statistics for each channel in the image. */
12941301
channels: ChannelStats[];

lib/input.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ function _isStreamInput () {
607607
* - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present
608608
* - `formatMagick`: String containing format for images loaded via *magick
609609
* - `comments`: Array of keyword/text pairs representing PNG text blocks, if present.
610+
* - `gainMap.image`: HDR gain map, if present, as compressed JPEG image.
610611
*
611612
* @example
612613
* const metadata = await sharp(input).metadata();

lib/output.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,30 @@ function withIccProfile (icc, options) {
319319
return this;
320320
}
321321

322+
/**
323+
* If the input contains gain map metadata, use it to convert the main image to HDR (High Dynamic Range) before further processing.
324+
* The input gain map is discarded.
325+
*
326+
* If the output is JPEG, generate and attach a new ISO 21496-1 gain map.
327+
* JPEG output options other than `quality` are ignored.
328+
*
329+
* This feature is experimental and the API may change.
330+
*
331+
* @since 0.35.0
332+
*
333+
* @example
334+
* const outputWithGainMap = await sharp(inputWithGainMap)
335+
* .withGainMap()
336+
* .toBuffer();
337+
*
338+
* @returns {Sharp}
339+
*/
340+
function withGainMap() {
341+
this.options.withGainMap = true;
342+
this.options.colourspace = 'scrgb';
343+
return this;
344+
}
345+
322346
/**
323347
* Keep XMP metadata from the input image in the output image.
324348
*
@@ -1640,6 +1664,7 @@ module.exports = (Sharp) => {
16401664
withExifMerge,
16411665
keepIccProfile,
16421666
withIccProfile,
1667+
withGainMap,
16431668
keepXmp,
16441669
withXmp,
16451670
keepMetadata,

src/common.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ namespace sharp {
289289
case ImageType::JXL: id = "jxl"; break;
290290
case ImageType::RAD: id = "rad"; break;
291291
case ImageType::DCRAW: id = "dcraw"; break;
292+
case ImageType::UHDR: id = "uhdr"; break;
292293
case ImageType::VIPS: id = "vips"; break;
293294
case ImageType::RAW: id = "raw"; break;
294295
case ImageType::UNKNOWN: id = "unknown"; break;
@@ -339,6 +340,9 @@ namespace sharp {
339340
{ "VipsForeignLoadRadBuffer", ImageType::RAD },
340341
{ "VipsForeignLoadDcRawFile", ImageType::DCRAW },
341342
{ "VipsForeignLoadDcRawBuffer", ImageType::DCRAW },
343+
{ "VipsForeignLoadUhdr", ImageType::UHDR },
344+
{ "VipsForeignLoadUhdrFile", ImageType::UHDR },
345+
{ "VipsForeignLoadUhdrBuffer", ImageType::UHDR },
342346
{ "VipsForeignLoadVips", ImageType::VIPS },
343347
{ "VipsForeignLoadVipsFile", ImageType::VIPS },
344348
{ "VipsForeignLoadRaw", ImageType::RAW }
@@ -356,6 +360,9 @@ namespace sharp {
356360
imageType = it->second;
357361
}
358362
}
363+
if (imageType == ImageType::UHDR) {
364+
imageType = ImageType::JPEG;
365+
}
359366
return imageType;
360367
}
361368

@@ -375,6 +382,9 @@ namespace sharp {
375382
imageType = ImageType::MISSING;
376383
}
377384
}
385+
if (imageType == ImageType::UHDR) {
386+
imageType = ImageType::JPEG;
387+
}
378388
return imageType;
379389
}
380390

@@ -1127,4 +1137,20 @@ namespace sharp {
11271137
}
11281138
return image;
11291139
}
1140+
1141+
/*
1142+
Does this image have a gain map?
1143+
*/
1144+
bool HasGainMap(VImage image) {
1145+
return image.get_typeof("gainmap-data") == VIPS_TYPE_BLOB;
1146+
}
1147+
1148+
/*
1149+
Removes gain map, if any.
1150+
*/
1151+
VImage RemoveGainMap(VImage image) {
1152+
VImage copy = image.copy();
1153+
copy.remove("gainmap-data");
1154+
return copy;
1155+
}
11301156
} // namespace sharp

src/common.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ namespace sharp {
173173
JXL,
174174
RAD,
175175
DCRAW,
176+
UHDR,
176177
VIPS,
177178
RAW,
178179
UNKNOWN,
@@ -397,6 +398,16 @@ namespace sharp {
397398
*/
398399
VImage StaySequential(VImage image, bool condition = true);
399400

401+
/*
402+
Does this image have a gain map?
403+
*/
404+
bool HasGainMap(VImage image);
405+
406+
/*
407+
Removes gain map, if any.
408+
*/
409+
VImage RemoveGainMap(VImage image);
410+
400411
} // namespace sharp
401412

402413
#endif // SRC_COMMON_H_

src/metadata.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ class MetadataWorker : public Napi::AsyncWorker {
141141
memcpy(baton->tifftagPhotoshop, tifftagPhotoshop, tifftagPhotoshopLength);
142142
baton->tifftagPhotoshopLength = tifftagPhotoshopLength;
143143
}
144+
// Gain Map
145+
if (image.get_typeof("gainmap-data") == VIPS_TYPE_BLOB) {
146+
size_t gainMapLength;
147+
void const *gainMap = image.get_blob("gainmap-data", &gainMapLength);
148+
baton->gainMap = static_cast<char *>(g_malloc(gainMapLength));
149+
memcpy(baton->gainMap, gainMap, gainMapLength);
150+
baton->gainMapLength = gainMapLength;
151+
}
144152
// PNG comments
145153
vips_image_map(image.get_image(), readPNGComment, &baton->comments);
146154
}
@@ -276,6 +284,12 @@ class MetadataWorker : public Napi::AsyncWorker {
276284
Napi::Buffer<char>::NewOrCopy(env, baton->tifftagPhotoshop,
277285
baton->tifftagPhotoshopLength, sharp::FreeCallback));
278286
}
287+
if (baton->gainMapLength > 0) {
288+
Napi::Object gainMap = Napi::Object::New(env);
289+
info.Set("gainMap", gainMap);
290+
gainMap.Set("image",
291+
Napi::Buffer<char>::NewOrCopy(env, baton->gainMap, baton->gainMapLength, sharp::FreeCallback));
292+
}
279293
if (baton->comments.size() > 0) {
280294
int i = 0;
281295
Napi::Array comments = Napi::Array::New(env, baton->comments.size());

0 commit comments

Comments
 (0)