Skip to content

Commit d365188

Browse files
authored
Merge branch 'brendan-duncan:main' into main
2 parents 163ed60 + d495150 commit d365188

18 files changed

Lines changed: 652 additions & 10 deletions

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ pubspec.lock
55
.packages
66
.DS_Store
77
_out/
8-
build
8+
build
9+
*.dart.js
10+
*.map
11+
*.deps
12+
*.mjs
13+
*.support.js

build_web.bat

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dart compile js web\web_test.dart -o web/web_test.dart.js
2+
dart compile wasm web\web_test.dart

lib/src/draw/composite_image.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,25 @@ void _directComposite(
8787
Image? mask,
8888
Channel maskChannel) {
8989
Pixel? p;
90+
final dw = dst.width;
91+
final dh = dst.height;
9092
if (mask != null) {
9193
for (var y = 0; y < dstH; ++y) {
9294
for (var x = 0; x < dstW; ++x) {
95+
final dx = dstX + x;
96+
final dy = dstY + y;
97+
if (dx >= dw || dy >= dh) {
98+
continue;
99+
}
100+
93101
final sx = xCache[x];
94102
final sy = yCache[y];
95103
p = src.getPixel(sx, sy, p);
96104
final m = mask.getPixel(sx, sy).getChannelNormalized(maskChannel);
97105
if (m == 1) {
98-
dst.setPixel(dstX + x, dstY + y, p);
106+
dst.setPixel(dx, dy, p);
99107
} else {
100-
final dp = dst.getPixel(dstX + x, dstY + y);
108+
final dp = dst.getPixel(dx, dy);
101109
dp
102110
..r = mix(dp.r, p.r, m)
103111
..g = mix(dp.g, p.g, m)
@@ -109,8 +117,13 @@ void _directComposite(
109117
} else {
110118
for (var y = 0; y < dstH; ++y) {
111119
for (var x = 0; x < dstW; ++x) {
120+
final dx = dstX + x;
121+
final dy = dstY + y;
122+
if (dx >= dw || dy >= dh) {
123+
continue;
124+
}
112125
p = src.getPixel(xCache[x], yCache[y], p);
113-
dst.setPixel(dstX + x, dstY + y, p);
126+
dst.setPixel(dx, dy, p);
114127
}
115128
}
116129
}

lib/src/filter/histogram_equalization.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Image histogramEqualization(Image src,
6868
H[l.round()]++;
6969
validPixelCounts++;
7070
}
71+
if (validPixelCounts == 0) continue;
7172

7273
final double numPixelPerBin = validPixelCounts / numOutputBin;
7374
final List<num> Hmap = List<num>.generate(
@@ -162,6 +163,7 @@ Image histogramStretch(Image src,
162163
H[l.round()]++;
163164
validPixelCounts++;
164165
}
166+
if (validPixelCounts == 0) continue;
165167

166168
// Find the high & low percentile
167169
int lowPercentileBin = 0;
@@ -192,7 +194,7 @@ Image histogramStretch(Image src,
192194
(l - lowPercentileBin) * outputDynamicRange / inputDynamicRange +
193195
outputRangeMin;
194196
Hmap[l] =
195-
max(outputRangeMin, min(newIntensityLv.round(), outputDynamicRange));
197+
max(outputRangeMin, min(newIntensityLv.round(), outputRangeMax));
196198
}
197199

198200
// produce output
@@ -211,7 +213,13 @@ void _applyHistogramTransform(Image frame, List<num> Hmap,
211213
continue;
212214
}
213215
if (mode == HistogramEqualizeMode.grayscale) {
214-
final newl = Hmap[p.luminance.round()];
216+
final oriLuminance = p.luminance.clamp(0, maxChannelValue);
217+
final baseIndex = min(oriLuminance.floor(), Hmap.length - 1);
218+
final frac = oriLuminance - baseIndex;
219+
220+
final newl = (Hmap[baseIndex] * (1 - frac) +
221+
Hmap[min(baseIndex + 1, Hmap.length - 1)] * frac)
222+
.round();
215223

216224
final msk = mask?.getPixel(p.x, p.y).getChannelNormalized(maskChannel);
217225
if (msk == null) {
@@ -228,7 +236,13 @@ void _applyHistogramTransform(Image frame, List<num> Hmap,
228236
} else {
229237
// color mode
230238
final hsl = rgbToHsl(p.r, p.g, p.b);
231-
final newl = Hmap[(hsl[2] * maxChannelValue).round()];
239+
final oriLuminance = hsl[2].clamp(0, 1) * maxChannelValue;
240+
final baseIndex = min(oriLuminance.floor(), Hmap.length - 1);
241+
final frac = oriLuminance - baseIndex;
242+
final newl = (Hmap[baseIndex] * (1 - frac) +
243+
Hmap[min(baseIndex + 1, Hmap.length - 1)] * frac)
244+
.round();
245+
232246
final List<int> newRGB = [0, 0, 0];
233247
hslToRgb(hsl[0], hsl[1], newl / maxChannelValue, newRGB);
234248

lib/src/formats/formats.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'image_format.dart';
1818
import 'jpeg/jpeg_util.dart';
1919
import 'jpeg_decoder.dart';
2020
import 'jpeg_encoder.dart';
21+
import 'png/png_info.dart';
2122
import 'png_decoder.dart';
2223
import 'png_encoder.dart';
2324
import 'pnm_decoder.dart';
@@ -356,8 +357,9 @@ Future<Image?> decodePngFile(String path) async {
356357
Uint8List encodePng(Image image,
357358
{bool singleFrame = false,
358359
int level = 6,
359-
PngFilter filter = PngFilter.paeth}) =>
360-
PngEncoder(filter: filter, level: level)
360+
PngFilter filter = PngFilter.paeth,
361+
PngCicpData? cicpData}) =>
362+
PngEncoder(filter: filter, level: level, cicpData: cicpData)
361363
.encode(image, singleFrame: singleFrame);
362364

363365
/// Encode an [image] to a PNG file at the given [path].

lib/src/formats/png/png_info.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,70 @@ class PngColorType {
2525

2626
enum PngFilterType { none, sub, up, average, paeth }
2727

28+
/// Colour information for the image from a cICP chunk, also known as
29+
/// "Coding-independent code points" (CICP).
30+
///
31+
/// The values are defined by ITU-T H.273. Presence of a cICP chunk signals
32+
/// that the image pixel data uses the specified colour space (typically
33+
/// Display P3 or BT.2020) instead of the default sRGB assumed by older
34+
/// decoders.
35+
///
36+
/// See <https://www.w3.org/TR/png-3/#cICP-chunk>.
37+
class PngCicpData {
38+
/// Colour primaries, as defined in ITU-T H.273 Table 2.
39+
///
40+
/// Common values:
41+
/// - 1 = BT.709 / sRGB
42+
/// - 9 = BT.2020
43+
/// - 12 = Display P3
44+
final int colourPrimaries;
45+
46+
/// Transfer characteristics, as defined in ITU-T H.273 Table 3.
47+
///
48+
/// Common values:
49+
/// - 1 = BT.709 (≈ sRGB)
50+
/// - 13 = sRGB / Display P3
51+
/// - 16 = ST 2084 (PQ, HDR)
52+
final int transferCharacteristics;
53+
54+
/// Matrix coefficients, as defined in ITU-T H.273 Table 4.
55+
///
56+
/// For still images this MUST be 0 (identity / RGB).
57+
final int matrixCoefficients;
58+
59+
/// Video full range flag.
60+
///
61+
/// 0 = narrow / limited range (16–235).
62+
/// 1 = full range (0–255).
63+
final int videoFullRangeFlag;
64+
65+
const PngCicpData({
66+
required this.colourPrimaries,
67+
required this.transferCharacteristics,
68+
required this.matrixCoefficients,
69+
required this.videoFullRangeFlag,
70+
});
71+
72+
@override
73+
int get hashCode => Object.hash(colourPrimaries, transferCharacteristics,
74+
matrixCoefficients, videoFullRangeFlag);
75+
76+
@override
77+
bool operator ==(Object other) =>
78+
other is PngCicpData &&
79+
other.colourPrimaries == colourPrimaries &&
80+
other.transferCharacteristics == transferCharacteristics &&
81+
other.matrixCoefficients == matrixCoefficients &&
82+
other.videoFullRangeFlag == videoFullRangeFlag;
83+
84+
@override
85+
String toString() => 'PngCicpData('
86+
'primaries=$colourPrimaries, '
87+
'transfer=$transferCharacteristics, '
88+
'matrix=$matrixCoefficients, '
89+
'fullRange=$videoFullRangeFlag)';
90+
}
91+
2892
/// The intended physical pixel size of the image.
2993
/// See <https://www.w3.org/TR/png-3/#11pHYs>.
3094
class PngPhysicalPixelDimensions {
@@ -90,6 +154,7 @@ class PngInfo implements DecodeInfo {
90154
Uint8List? iccpData;
91155
Map<String, String> textData = {};
92156
PngPhysicalPixelDimensions? pixelDimensions;
157+
PngCicpData? cicpData;
93158

94159
// APNG extensions
95160
@override

lib/src/formats/png_decoder.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,22 @@ class PngDecoder extends Decoder {
260260
_info.iccpData = profile.toUint8List();
261261
_input.skip(4); // CRC
262262
break;
263+
case 'cICP':
264+
// Coding-independent code points (PNG spec 1.3 / ITU-T H.273).
265+
// The chunk is exactly 4 bytes: colour primaries, transfer
266+
// characteristics, matrix coefficients, video full range flag.
267+
if (chunkSize == 4) {
268+
_info.cicpData = PngCicpData(
269+
colourPrimaries: _input.readByte(),
270+
transferCharacteristics: _input.readByte(),
271+
matrixCoefficients: _input.readByte(),
272+
videoFullRangeFlag: _input.readByte(),
273+
);
274+
} else {
275+
_input.skip(chunkSize);
276+
}
277+
_input.skip(4); // CRC
278+
break;
263279
default:
264280
//print('Skipping $chunkType');
265281
_input.skip(chunkSize);

lib/src/formats/png_encoder.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ enum PngFilter { none, sub, up, average, paeth }
1919
class PngEncoder extends Encoder {
2020
Quantizer? _globalQuantizer;
2121

22-
PngEncoder({this.filter = PngFilter.paeth, this.level, this.pixelDimensions});
22+
PngEncoder(
23+
{this.filter = PngFilter.paeth,
24+
this.level,
25+
this.pixelDimensions,
26+
this.cicpData});
2327

2428
int _numChannels(Image image) => image.hasPalette ? 1 : image.numChannels;
2529

@@ -43,6 +47,10 @@ class PngEncoder extends Encoder {
4347
_writeICCPChunk(output, image.iccProfile!);
4448
}
4549

50+
if (cicpData != null) {
51+
_writeCicpChunk(output!, cicpData!);
52+
}
53+
4654
if (image.hasPalette) {
4755
if (_globalQuantizer != null) {
4856
_writePalette(_globalQuantizer!.palette);
@@ -244,6 +252,15 @@ class PngEncoder extends Encoder {
244252
}
245253
}
246254

255+
void _writeCicpChunk(OutputBuffer out, PngCicpData cicp) {
256+
final chunk = OutputBuffer(bigEndian: true)
257+
..writeByte(cicp.colourPrimaries)
258+
..writeByte(cicp.transferCharacteristics)
259+
..writeByte(cicp.matrixCoefficients)
260+
..writeByte(cicp.videoFullRangeFlag);
261+
_writeChunk(out, 'cICP', chunk.getBytes());
262+
}
263+
247264
void _writeICCPChunk(OutputBuffer? out, IccProfile iccp) {
248265
final chunk = OutputBuffer(bigEndian: true)
249266

@@ -419,4 +436,5 @@ class PngEncoder extends Encoder {
419436
OutputBuffer? output;
420437
Map<String, String>? textData;
421438
PngPhysicalPixelDimensions? pixelDimensions;
439+
PngCicpData? cicpData;
422440
}

lib/src/formats/webp_encoder.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ class WebPEncoder extends Encoder {
133133
for (var ci = candidates.length - 1; ci >= 0; ci--) {
134134
final c = candidates[ci];
135135
final dist = j - c;
136+
137+
// VP8L spec limits max distance to 1048576.
138+
// The first 120 values are reserved, so the actual
139+
// maximum distance is 1048576 - 120 offset = 1048456
140+
// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#522_lz77_backward_reference
141+
if (dist > 1048456) break;
142+
136143
// Extend the match forward.
137144
var len = 1;
138145
while (len < maxMatchLen &&

test/_data/png/rgba_iccp_cicp.png

149 Bytes
Loading

0 commit comments

Comments
 (0)