Compare commits

...

7 Commits

Author SHA1 Message Date
Charles Kerr
983322a397 test: fix new colorSpace spec 2026-03-23 12:07:32 -05:00
Charles Kerr
968b531890 docs: linter: make change description <= 120 chars 2026-03-23 10:46:37 -05:00
Charles Kerr
19c2920774 docs: linter: remove semicolons from js code samples 2026-03-23 10:45:50 -05:00
Felix Rieseberg
f502ecd743 docs: add breaking-changes entry and update getBitmap docs
- Add breaking-changes.md entry for toBitmap() color space
  normalization under Planned Breaking API Changes (41.0)
- Update getBitmap() docs to include colorSpace option and
  breaking-changes-header since it delegates to toBitmap()
2026-03-23 09:47:35 -05:00
Felix Rieseberg
c2016ffdf9 fix: correct include ordering in electron_api_native_image.cc
Sort ui/gfx/ includes alphabetically: codec/jpeg_codec.h,
codec/png_codec.h, then color_space.h.
2026-03-23 09:47:35 -05:00
Felix Rieseberg
cf05b2f521 fix: implement feedback 2026-03-23 09:47:35 -05:00
Felix Rieseberg
0348df5881 fix: normalize color space for consistent pixel values in nativeImage
toBitmap() now normalizes the color space to sRGB by default, ensuring
that visually identical images produce consistent pixel values regardless
of their original color profile (e.g., Display P3 on macOS).

An optional colorSpace parameter is added to toBitmap() to control the
target color space: 'srgb' (default), 'display-p3', or 'source' (no
conversion).

Closes #46949
2026-03-23 09:47:34 -05:00
7 changed files with 92 additions and 3 deletions

View File

@@ -265,8 +265,20 @@ Returns `Buffer` - A [Buffer][buffer] that contains the image's `JPEG` encoded d
#### `image.toBitmap([options])`
<!--
```YAML history
changes:
- pr-url: https://github.com/electron/electron/pull/48178
description: "Normalized `NativeImage.toBitmap()` pixel data to sRGB by default."
breaking-changes-header: behavior-changed-nativeimagetobitmap-now-normalizes-color-space
```
-->
* `options` Object (optional)
* `scaleFactor` Number (optional) - Defaults to 1.0.
* `colorSpace` [ColorSpace](structures/color-space.md) (optional) - The target color space
for the output pixel data. Defaults to sRGB. Pass the image's original color space to
preserve the previous behavior, or another color space to get pixel values in that space.
Returns `Buffer` - A [Buffer][buffer] that contains a copy of the image's raw bitmap pixel
data.
@@ -289,8 +301,20 @@ Returns `string` - The [Data URL][data-url] of the image.
#### `image.getBitmap([options])` _Deprecated_
<!--
```YAML history
changes:
- pr-url: https://github.com/electron/electron/pull/48178
description: "Normalized `NativeImage.toBitmap()` pixel data to sRGB by default."
breaking-changes-header: behavior-changed-nativeimagetobitmap-now-normalizes-color-space
```
-->
* `options` Object (optional)
* `scaleFactor` Number (optional) - Defaults to 1.0.
* `colorSpace` [ColorSpace](structures/color-space.md) (optional) - The target color space
for the output pixel data. Defaults to sRGB. Pass the image's original color space to
preserve the previous behavior, or another color space to get pixel values in that space.
Legacy alias for `image.toBitmap()`.

View File

@@ -106,6 +106,28 @@ from upstream Chromium.
## Planned Breaking API Changes (41.0)
### Behavior Changed: `NativeImage.toBitmap()` now normalizes color space
`NativeImage.toBitmap()` (and its deprecated alias `NativeImage.getBitmap()`) now normalizes pixel data to sRGB by default. Previously, raw pixel data was returned without color space conversion, which meant pixel values from images with different embedded color profiles (e.g., Display P3 on macOS) could differ for the same visual color.
To preserve the previous behavior, pass the image's original color space in the `colorSpace`
option. You can also pass `colorSpace` to convert to any other specific color space:
```js
const image = nativeImage.createFromPath('photo.png')
// New default: normalized to sRGB
const srgbBitmap = image.toBitmap()
// Convert to Display P3
const p3Bitmap = image.toBitmap({
colorSpace: {
primaries: 'p3',
transfer: 'srgb',
matrix: 'rgb',
range: 'full'
}
})
```
### Behavior Changed: PDFs no longer create a separate WebContents
Previously, PDF resources created a separate guest [WebContents](https://www.electronjs.org/docs/latest/api/web-contents) for rendering. Now, PDFs are rendered within the same WebContents instead. If you have code to detect PDF resources, use the [frame tree](https://www.electronjs.org/docs/latest/api/web-frame-main) instead of WebContents.

View File

@@ -1,6 +1,7 @@
# THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT BY HAND
auto_filenames = {
api_docs = [
"docs/api/.view.md.swp",
"docs/api/app.md",
"docs/api/auto-updater.md",
"docs/api/base-window.md",

View File

@@ -36,6 +36,7 @@
#include "shell/common/skia_util.h"
#include "shell/common/thread_restrictions.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "ui/base/layout.h"
@@ -43,6 +44,7 @@
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
@@ -254,10 +256,17 @@ v8::Local<v8::Value> NativeImage::ToPNG(gin::Arguments* args) {
v8::Local<v8::Value> NativeImage::ToBitmap(gin::Arguments* args) {
v8::Isolate* const isolate = args->isolate();
const float scale = GetScaleFactorFromOptions(args);
const auto src = image_.AsImageSkia().GetRepresentation(scale).GetBitmap();
float scale = 1.0f;
gfx::ColorSpace color_space = gfx::ColorSpace::CreateSRGB();
gin_helper::Dictionary options;
if (args->GetNext(&options)) {
options.Get("scaleFactor", &scale);
options.Get("colorSpace", &color_space);
}
const auto dst_info = SkImageInfo::MakeN32Premul(src.dimensions());
const auto src = image_.AsImageSkia().GetRepresentation(scale).GetBitmap();
const auto dst_info = SkImageInfo::MakeN32Premul(
src.dimensions(), color_space.ToSkColorSpace());
const size_t dst_n_bytes = dst_info.computeMinByteSize();
auto dst_buf = v8::ArrayBuffer::New(isolate, dst_n_bytes);

View File

@@ -33,6 +33,12 @@ describe('nativeImage module', () => {
height: 3,
width: 3
};
const imageColorSpaceSRGB = {
path: path.join(fixturesPath, 'assets', 'colorspace-srgb.png')
};
const imageColorSpaceP3 = {
path: path.join(fixturesPath, 'assets', 'colorspace-p3.png')
};
const dataUrlImages = [
image1x1,
@@ -422,6 +428,33 @@ describe('nativeImage module', () => {
const crop = image.crop({ width: 25, height: 64, x: 0, y: 0 });
expect(crop.toBitmap().length).to.equal(25 * 64 * 4);
});
it('toBitmap() normalizes color space for consistent pixel values', () => {
const srgbImage = nativeImage.createFromPath(imageColorSpaceSRGB.path);
const p3Image = nativeImage.createFromPath(imageColorSpaceP3.path);
// Both default to sRGB normalization
const srgbBuf = srgbImage.toBitmap();
const p3Buf = p3Image.toBitmap();
expect(srgbBuf.length).to.equal(p3Buf.length);
// Pixel values should be nearly identical after sRGB normalization
for (let i = 0; i < srgbBuf.length; i++) {
expect(Math.abs(srgbBuf[i] - p3Buf[i])).to.be.at.most(3);
}
});
it('toBitmap() accepts a colorSpace option', () => {
const image = nativeImage.createFromPath(imageColorSpaceP3.path);
const srgbBuf = image.toBitmap({ colorSpace: { primaries: 'bt709', transfer: 'srgb', matrix: 'rgb', range: 'full' } });
const p3Buf = image.toBitmap({ colorSpace: { primaries: 'p3', transfer: 'srgb', matrix: 'rgb', range: 'full' } });
// Both should produce valid buffers of the same size (same pixel dimensions)
expect(srgbBuf.length).to.be.greaterThan(0);
expect(p3Buf.length).to.equal(srgbBuf.length);
});
});
describe('getAspectRatio()', () => {

BIN
spec/fixtures/assets/colorspace-p3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

BIN
spec/fixtures/assets/colorspace-srgb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B