emf-converter 4.3.4 → 4.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -214,7 +214,7 @@ Everything below is verified against output painted by Windows itself; `src/gdi-
214
214
 
215
215
  Everything is measured against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the exact per-fixture bounds. Open work items are tracked in [docs/outstanding-work.md](docs/outstanding-work.md).
216
216
 
217
- - **Colour adjustment and image effects**: The `HALFTONE` stretch mode reproduces Windows' halftone engine for 32-bit output: an enlargement smooths isolated pixels and then takes the nearest source pixel, and a reduction averages each pixel's footprint and then sharpens. All seven plain `HALFTONE` fixtures (2x, 3x, 0.5x, 0.65x and 1.37x) match exactly. It is not verified for a stretch that enlarges one axis and reduces the other. `EMR_SETCOLORADJUSTMENT` is applied to the source of `HALFTONE` `StretchBlt` and `StretchDIBits` calls, as in GDI. Windows does not publish its adjustment formulas. The converter uses a model fitted to two Windows captures: contrast and brightness on CIE L*, colourfulness and red-green tint on u'v' chromaticity, with gamma as the source encoding, so a gamma on its own changes nothing. Windows also dithers the adjusted colours over 32 levels per channel, and that pattern is not reproduced. On the fixtures, 0% to 15.1% of pixels differ by more than 24. The log filter uses an unverified curve, and the illuminant is ignored. EMF+ image effects (`SerializableObject`) are applied to the image's pixels before it is drawn. Colour matrix and lookup table effects follow GDI+'s definitions; MS-EMFPLUS does not specify the algorithms of the others (blur, sharpen, brightness/contrast, levels, colour balance, colour curves, hue/saturation/lightness, tint, red-eye), so those use documented approximations and have not been checked against GDI+ output. An effect is applied to the draw's source rectangle only, and a blur with `expandEdge` grows it by the blur radius, as GDI+ does. When the source rectangle is not in pixels, the effect is applied to the whole image without that growth. An effect is skipped when the image cannot be decoded to pixels: PNG and BMP always can be, while JPEG, GIF, TIFF and similar formats need a canvas backend (a browser or `@napi-rs/canvas`).
217
+ - **Colour adjustment and image effects**: The `HALFTONE` stretch mode reproduces Windows' halftone engine for 32-bit output: an enlargement smooths isolated pixels and then takes the nearest source pixel, and a reduction averages each pixel's footprint and then sharpens. All seven plain `HALFTONE` fixtures (2x, 3x, 0.5x, 0.65x and 1.37x) match exactly. It is not verified for a stretch that enlarges one axis and reduces the other. `EMR_SETCOLORADJUSTMENT` is applied to the source of `HALFTONE` `StretchBlt` and `StretchDIBits` calls, as in GDI. Windows does not publish its adjustment formulas. The converter uses a model fitted to two Windows captures: contrast and brightness on CIE L*, colourfulness and red-green tint on u'v' chromaticity, with gamma as the source encoding, so a gamma on its own changes nothing. Windows also dithers the adjusted colours over 32 levels per channel, and that pattern is not reproduced. On the fixtures, 0% to 15.1% of pixels differ by more than 24. The log filter uses an unverified curve, and the illuminant is ignored. EMF+ image effects (`SerializableObject`) are applied to the image's pixels before it is drawn. MS-EMFPLUS does not specify the algorithms, so they were measured against the effected bitmaps GDI+ itself writes into its recordings. Colour matrix, lookup table, brightness/contrast, colour balance, levels and sharpen match GDI+ exactly; blur matches to within one level (GDI+ blurs along rows only, plus a vertical blur of the top row, and so does the converter); colour curves match exactly for exposure, density, midtone and black saturation and to within one level on up to 2.3% of pixels for contrast, highlight, shadow and white saturation, whose control points were measured at intensity ±50 and are scaled for other values; hue/saturation/lightness uses GDI+'s integer HSL, but its hue rounding is only partly reproduced, so lightness and saturation changes differ on under 1% of pixels and a hue rotation is more than two levels off on 0.3% to 32% of pixels (up to 11 levels); tint is within two levels (three on 0.07% of pixels); red-eye correction is an approximation, as GDI+'s pupil detection and textured repaint are not reproduced (up to 17% of pixels differ). GDI+ follows each effect draw with a plain draw of the bitmap it effected, so Windows' playback depends on the effect only where that bitmap is translucent; played back, the image effect fixtures match Windows to within one level, except one edge of a rotated blurred draw (0.22% of pixels). An effect is applied to the draw's source rectangle only, reading one more column and row, as GDI+ does; a blur with `expandEdge` blurs transparency in at the edges but, as in GDI+, draws nothing beyond the source rectangle. When the source rectangle is not in pixels, the effect is applied to the whole image. GDI+ records a colour curve effect under the lookup table's identifier and its own playback draws the image without the effect; the converter does the same. An effect is skipped when the image cannot be decoded to pixels: PNG and BMP always can be, while JPEG, GIF, TIFF and similar formats need a canvas backend (a browser or `@napi-rs/canvas`).
218
218
  - **Pen transforms**: EMF+ pen transforms (uniform, nonuniform and skewed) match GDI+ exactly on the 17 pen-transform fixtures, including dashes, which GDI+ lays out along the path in world space at the untransformed pen width. The fixtures include dashes only under a non-uniform scale. Dashes under a uniform scale follow the same rule but have not been checked. A pen transform that cannot be inverted is ignored. Of the anchor caps, only `ArrowAnchor` is drawn with its real shape. The others are drawn as their base cap.
219
219
  - **Text**: ANSI, multi-string and small-text records now render; the Windows text fixtures retain a few glyph-edge differences and a `PolyTextOut` C1 control-glyph difference (under 0.1% of pixels; bounds in `src/emf-text-records.fixture.test.ts`). ANSI decoding uses the host's `TextDecoder` for common Windows code pages; unsupported encodings (including Johab and OEM on standard runtimes) fall back to Windows-1252. Vertical `ETO_PDY` advances require the `fonts` engine. Without `fonts`, SVG text measurements use deterministic estimates, so precise justification requires supplying the matching fonts.
220
220
  - **Wide pens and paths**: flat-capped GDI pens 7 px and wider can differ by a few pixels at round joins, and dashed wide Bezier curves follow `WidenPath` (which Windows' direct drawing does not quite match); at most 0.2% of pixels on the fixtures. `EMR_WIDENPATH` does not reproduce the extra inner join triangles GDI's own `WidenPath` emits (visible only when the widened outline is itself stroked). EMF+ 1-pixel antialiased lines can differ by one antialiasing sample at their ends, some closed widened outlines by one sample along an edge, and Inset or compound pens on closed figures are approximate.
package/dist/index.js CHANGED
@@ -12319,7 +12319,7 @@ function blendSeparable(rgba, width, box, iu0, wu, iv0, wv, verticalFirst, edge
12319
12319
  }
12320
12320
  var OUTSIDE_TRANSPARENT = -1;
12321
12321
  var OUTSIDE_CLAMP = -2;
12322
- function wrapTap(t, lo, hi, mirror, edge) {
12322
+ function wrapTap(t, lo, hi, mirror2, edge) {
12323
12323
  if (t >= lo && t < hi) {
12324
12324
  return t;
12325
12325
  }
@@ -12330,7 +12330,7 @@ function wrapTap(t, lo, hi, mirror, edge) {
12330
12330
  return OUTSIDE_CLAMP;
12331
12331
  }
12332
12332
  const n = hi - lo;
12333
- if (!mirror) {
12333
+ if (!mirror2) {
12334
12334
  return lo + ((t - lo) % n + n) % n;
12335
12335
  }
12336
12336
  const q = ((t - lo) % (2 * n) + 2 * n) % (2 * n);
@@ -17630,9 +17630,9 @@ function buildPattern(ctx, rect, tw, th, wrap, brush, delta, sampling, colorAt)
17630
17630
  if (typeof ctx.createPattern !== "function") {
17631
17631
  return null;
17632
17632
  }
17633
- const mirror = wrap === "clamp" ? { x: false, y: false } : mirrorFlags(wrap);
17634
- const w = tw * (mirror.x ? 2 : 1);
17635
- const h = th * (mirror.y ? 2 : 1);
17633
+ const mirror2 = wrap === "clamp" ? { x: false, y: false } : mirrorFlags(wrap);
17634
+ const w = tw * (mirror2.x ? 2 : 1);
17635
+ const h = th * (mirror2.y ? 2 : 1);
17636
17636
  const temp = createTempCanvas(w, h);
17637
17637
  if (!temp) {
17638
17638
  return null;
@@ -18054,8 +18054,8 @@ function createHatchPattern(ctx, hatch, origin, user, deviceToCanvas) {
18054
18054
  }
18055
18055
 
18056
18056
  // src/emf-plus-brush-texture.ts
18057
- function wrapTexel(i, n, mirror) {
18058
- if (!mirror) {
18057
+ function wrapTexel(i, n, mirror2) {
18058
+ if (!mirror2) {
18059
18059
  return (i % n + n) % n;
18060
18060
  }
18061
18061
  const p = (i % (2 * n) + 2 * n) % (2 * n);
@@ -19116,11 +19116,11 @@ function boundaryBox2(points) {
19116
19116
  }
19117
19117
  return x1 > x0 && y1 > y0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null;
19118
19118
  }
19119
- function foldIntoTile(v, origin, size, mirror, lag) {
19119
+ function foldIntoTile(v, origin, size, mirror2, lag) {
19120
19120
  const rel = v - origin;
19121
19121
  const k = Math.floor(rel / size);
19122
19122
  const local = rel - k * size;
19123
- if (mirror && (k % 2 + 2) % 2 === 1) {
19123
+ if (mirror2 && (k % 2 + 2) % 2 === 1) {
19124
19124
  return origin + size - lag - local;
19125
19125
  }
19126
19126
  return origin + local;
@@ -23974,6 +23974,7 @@ function readGuid(view, off) {
23974
23974
  ].join("-");
23975
23975
  }
23976
23976
  var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
23977
+ var COLOR_CURVE_PARAMS_SIZE = 12;
23977
23978
  function parseSerializableObject(view, dataOff, dataSize) {
23978
23979
  if (dataSize < 20) {
23979
23980
  return null;
@@ -23998,7 +23999,7 @@ function parseSerializableObject(view, dataOff, dataSize) {
23998
23999
  case "colorBalance":
23999
24000
  return need(12) ? { kind, cyanRed: i32(0, -100, 100), magentaGreen: i32(1, -100, 100), yellowBlue: i32(2, -100, 100) } : null;
24000
24001
  case "colorCurve":
24001
- return need(12) ? {
24002
+ return need(COLOR_CURVE_PARAMS_SIZE) ? {
24002
24003
  kind,
24003
24004
  adjustment: view.getInt32(p, true),
24004
24005
  channel: view.getInt32(p + 4, true),
@@ -24052,66 +24053,101 @@ function parseSerializableObject(view, dataOff, dataSize) {
24052
24053
  return need(8) ? { kind, hue: i32(0, -180, 180), amount: i32(1, -100, 100) } : null;
24053
24054
  }
24054
24055
  }
24055
- function buildLut(f) {
24056
+ var roundHalfDown = (v) => Math.ceil(v - 0.5);
24057
+ function buildLut(f, round = Math.round) {
24056
24058
  const lut = new Uint8Array(256);
24057
24059
  for (let v = 0; v < 256; v++) {
24058
- lut[v] = clamp(Math.round(f(v)), 0, 255);
24060
+ lut[v] = clamp(round(f(v)), 0, 255);
24059
24061
  }
24060
24062
  return lut;
24061
24063
  }
24062
- function contrastCurve(c) {
24063
- const gain = c >= 100 ? 1e6 : c >= 0 ? 100 / (100 - c) : (100 + c) / 100;
24064
- return (v) => (v - 127.5) * gain + 127.5;
24065
- }
24066
- function balanceCurve(t) {
24067
- const f = t / 100;
24068
- return (v) => f >= 0 ? v + (255 - v) * f : v * (1 + f);
24069
- }
24070
- function midtoneCurve(t) {
24071
- const gamma = 2 ** (-t / 100);
24072
- return (v) => 255 * (v / 255) ** gamma;
24064
+ function midtoneGamma(t) {
24065
+ return t >= 0 ? 1 / (1 + t / 50) : 1 - t / 50;
24066
+ }
24067
+ function brightnessContrastLut(brightness, contrast) {
24068
+ const gain = contrast >= 100 ? 1e6 : contrast >= 0 ? 100 / (100 - contrast) : (100 + contrast) / 100;
24069
+ const half = brightness / 2;
24070
+ return buildLut((v) => (v + half - 127.5) * gain + 127.5 + half);
24071
+ }
24072
+ function balanceLut(t) {
24073
+ return buildLut((v) => v * (1 + t / 100));
24074
+ }
24075
+ function splineLut(ys) {
24076
+ const n = ys.length;
24077
+ const h = 255 / (n - 1);
24078
+ const m = new Float64Array(n);
24079
+ const c = new Float64Array(n);
24080
+ const d = new Float64Array(n);
24081
+ for (let i = 1; i < n - 1; i++) {
24082
+ const rhs = 6 * (ys[i + 1] - 2 * ys[i] + ys[i - 1]) / (h * h);
24083
+ const den = 4 - c[i - 1];
24084
+ c[i] = 1 / den;
24085
+ d[i] = (rhs - d[i - 1]) / den;
24086
+ }
24087
+ for (let i = n - 2; i >= 1; i--) {
24088
+ m[i] = d[i] - c[i] * m[i + 1];
24089
+ }
24090
+ return buildLut((v) => {
24091
+ const i = Math.min(n - 2, Math.floor(v / h));
24092
+ const b = (v - i * h) / h;
24093
+ const a = 1 - b;
24094
+ return a * ys[i] + b * ys[i + 1] + ((a * a * a - a) * m[i] + (b * b * b - b) * m[i + 1]) * h * h / 6;
24095
+ });
24073
24096
  }
24097
+ var CURVE_POINTS = 23;
24098
+ var CURVE_X = Array.from({ length: CURVE_POINTS }, (_, k) => k * 255 / (CURVE_POINTS - 1));
24099
+ var CURVE_TABLES = {
24100
+ "contrast+": [0, 3.8, 8, 12.2, 17.4, 23.5, 31.1, 40.2, 53, 70.2, 94.5, 127.5, 160.5, 184.8, 202, 214.8, 223.9, 231.5, 237.6, 242.8, 247, 251.2, 255],
24101
+ "contrast-": [0, 33.1, 57.2, 74.7, 87.2, 96.6, 103.9, 109.8, 115.3, 119.6, 123.7, 127.5, 131.3, 135.4, 139.7, 145.2, 151.1, 158.4, 167.8, 180.3, 197.8, 221.9, 255],
24102
+ "highlight+": [0, 11.6, 23.2, 34.8, 46.4, 58, 69.6, 81.1, 92.7, 104.3, 116, 127.6, 142.3, 160.6, 186.4, 208, 220.6, 229.4, 236.2, 242, 246.6, 251, 255],
24103
+ "highlight-": [0, 11.6, 23.2, 34.8, 46.4, 58, 69.5, 81.1, 92.7, 104.4, 115.9, 127.5, 136.7, 144.9, 151.4, 157.1, 161.8, 167.2, 174.3, 185.3, 200.9, 223.7, 255],
24104
+ "shadow+": [0, 31.3, 54, 70, 82.5, 92.3, 99.5, 104.8, 109.5, 115, 121.3, 127.7, 139.2, 150.6, 162.3, 173.8, 185.5, 197, 208.6, 220.2, 231.8, 243.4, 255],
24105
+ "shadow-": [0, 4, 8.4, 13, 18.7, 25.7, 34.4, 45, 58.4, 80, 105.8, 127.5, 139.2, 150.7, 162.3, 173.9, 185.4, 197, 208.6, 220.2, 231.8, 243.4, 255]
24106
+ };
24074
24107
  function curveAdjustmentLut(adjustment, intensity) {
24075
24108
  const t = intensity;
24109
+ let ys;
24076
24110
  switch (adjustment) {
24077
24111
  case CurveAdjustment.Exposure:
24078
- return buildLut((v) => v * (1 + t / 255));
24112
+ ys = CURVE_X.map((x) => roundHalfDown(x) + t);
24113
+ break;
24079
24114
  case CurveAdjustment.Density:
24080
- return buildLut((v) => v * (1 - t / 255));
24115
+ ys = CURVE_X.map((x) => x + t);
24116
+ break;
24081
24117
  case CurveAdjustment.Contrast:
24082
- return buildLut(contrastCurve(clamp(t, -100, 100)));
24083
24118
  case CurveAdjustment.Highlight:
24084
24119
  case CurveAdjustment.Shadow: {
24085
- const high = adjustment === CurveAdjustment.Highlight;
24086
- const amp = clamp(t, -100, 100) * 0.32;
24087
- return buildLut((v) => {
24088
- const inside = high ? v > 128 : v < 128;
24089
- if (!inside) {
24090
- return v;
24091
- }
24092
- const u = high ? (v - 128) / 127 : v / 128;
24093
- return v + amp * Math.sin(Math.PI * u);
24094
- });
24120
+ const c = clamp(t, -100, 100);
24121
+ const name = ["contrast", "highlight", "shadow"][adjustment - CurveAdjustment.Contrast] + (c >= 0 ? "+" : "-");
24122
+ const table = CURVE_TABLES[name];
24123
+ ys = CURVE_X.map((x, k) => x + Math.abs(c) / 50 * (table[k] - x));
24124
+ break;
24125
+ }
24126
+ case CurveAdjustment.Midtone: {
24127
+ const g = midtoneGamma(clamp(t, -100, 100));
24128
+ ys = CURVE_X.map((x) => 255 * (x / 255) ** g);
24129
+ break;
24095
24130
  }
24096
- case CurveAdjustment.Midtone:
24097
- return buildLut(midtoneCurve(clamp(t, -100, 100)));
24098
24131
  case CurveAdjustment.WhiteSaturation: {
24099
24132
  const w = Math.max(1, clamp(t, 0, 255));
24100
- return buildLut((v) => v * 255 / w);
24133
+ ys = CURVE_X.map((x) => x * 255 / w);
24134
+ break;
24101
24135
  }
24102
24136
  case CurveAdjustment.BlackSaturation: {
24103
24137
  const b = Math.min(254, clamp(t, 0, 255));
24104
- return buildLut((v) => (v - b) * 255 / (255 - b));
24138
+ ys = CURVE_X.map((x) => (x - b) * 255 / (255 - b));
24139
+ break;
24105
24140
  }
24106
24141
  default:
24107
24142
  return null;
24108
24143
  }
24144
+ return splineLut(ys.map((y) => clamp(y, 0, 255)));
24109
24145
  }
24110
24146
  function levelsLut(highlight, midtone, shadow) {
24111
24147
  const black = shadow / 100 * 255;
24112
24148
  const white = Math.max(black + 1, highlight / 100 * 255);
24113
- const gamma = midtoneCurve(midtone);
24114
- return buildLut((v) => gamma(clamp((v - black) / (white - black), 0, 1) * 255));
24149
+ const gamma = midtoneGamma(midtone);
24150
+ return buildLut((v) => 255 * clamp((v - black) / (white - black), 0, 1) ** gamma, roundHalfDown);
24115
24151
  }
24116
24152
  function applyLuts(src, r, g, b, a = null) {
24117
24153
  const out = new Uint8ClampedArray(src);
@@ -24137,26 +24173,6 @@ function applyColorMatrix(src, m) {
24137
24173
  }
24138
24174
  return out;
24139
24175
  }
24140
- function rgbToHsl(r, g, b) {
24141
- const max = Math.max(r, g, b);
24142
- const min = Math.min(r, g, b);
24143
- const l = (max + min) / 2;
24144
- const d = max - min;
24145
- if (d === 0) {
24146
- return [0, 0, l];
24147
- }
24148
- const s = d / (1 - Math.abs(2 * l - 1));
24149
- let h;
24150
- if (max === r) {
24151
- h = (g - b) / d % 6;
24152
- } else if (max === g) {
24153
- h = (b - r) / d + 2;
24154
- } else {
24155
- h = (r - g) / d + 4;
24156
- }
24157
- h *= 60;
24158
- return [h < 0 ? h + 360 : h, s, l];
24159
- }
24160
24176
  function hslToRgb(h, s, l) {
24161
24177
  const c = (1 - Math.abs(2 * l - 1)) * s;
24162
24178
  const hp = (h % 360 + 360) % 360 / 60;
@@ -24165,29 +24181,80 @@ function hslToRgb(h, s, l) {
24165
24181
  const [r, g, b] = hp < 1 ? [c, x, 0] : hp < 2 ? [x, c, 0] : hp < 3 ? [0, c, x] : hp < 4 ? [0, x, c] : hp < 5 ? [x, 0, c] : [c, 0, x];
24166
24182
  return [r + m, g + m, b + m];
24167
24183
  }
24184
+ var HUE_SEXTANT = 43;
24185
+ var HUE_CIRCLE = 6 * HUE_SEXTANT;
24186
+ function gdipHue(r, g, b) {
24187
+ const max = Math.max(r, g, b);
24188
+ const d = max - Math.min(r, g, b);
24189
+ const f = (x) => Math.floor(x * HUE_SEXTANT / d);
24190
+ if (max === r && g >= b) {
24191
+ return f(g - b);
24192
+ }
24193
+ if (max === g) {
24194
+ return b >= r ? 87 + f(b - r) : 86 - f(r - b);
24195
+ }
24196
+ if (max === b && b !== r) {
24197
+ return r >= g ? 172 + f(r - g) : 171 - f(g - r);
24198
+ }
24199
+ const t = f(b - g);
24200
+ return (256 - t - (t >= 40 ? 1 : 0)) % 256;
24201
+ }
24168
24202
  function applyHueSaturationLightness(src, hue, saturation, lightness2) {
24169
24203
  const out = new Uint8ClampedArray(src);
24170
- const push = (v, t) => t >= 0 ? v + (1 - v) * (t / 100) : v * (1 + t / 100);
24204
+ const shift = Math.round(hue * HUE_CIRCLE / 360);
24205
+ const sMul = 1 + saturation / 100;
24171
24206
  for (let i = 0; i < src.length; i += 4) {
24172
- const [h, s, l] = rgbToHsl(src[i] / 255, src[i + 1] / 255, src[i + 2] / 255);
24173
- const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness2));
24174
- out[i] = Math.round(r * 255);
24175
- out[i + 1] = Math.round(g * 255);
24176
- out[i + 2] = Math.round(b * 255);
24207
+ const r = src[i];
24208
+ const g = src[i + 1];
24209
+ const b = src[i + 2];
24210
+ const max = Math.max(r, g, b);
24211
+ const min = Math.min(r, g, b);
24212
+ const l = clamp(Math.round(((max + min >> 1) * 100 + 255 * lightness2) / 100), 0, 255);
24213
+ if (max === min) {
24214
+ out[i] = out[i + 1] = out[i + 2] = l;
24215
+ continue;
24216
+ }
24217
+ const sum = max + min;
24218
+ const s = clamp((max - min) / (sum <= 255 ? sum : 510 - sum) * sMul, 0, 1);
24219
+ const hi = l <= 127 ? l * (1 + s) : l + s * (255 - l);
24220
+ const m1 = l <= 127 ? 2 * l - Math.trunc(hi) : Math.round(2 * l - hi);
24221
+ const m2 = 2 * l - m1;
24222
+ const q = ((gdipHue(r, g, b) + shift) % HUE_CIRCLE + HUE_CIRCLE) % HUE_CIRCLE;
24223
+ const sextant = Math.min(5, Math.floor(q / HUE_SEXTANT));
24224
+ const f = (q - sextant * HUE_SEXTANT) / HUE_SEXTANT;
24225
+ const up = Math.trunc(m1 + (m2 - m1) * f);
24226
+ const down = Math.trunc(m1 + (m2 - m1) * (1 - f));
24227
+ const [nr, ng, nb] = [
24228
+ [m2, up, m1],
24229
+ [down, m2, m1],
24230
+ [m1, m2, up],
24231
+ [m1, down, m2],
24232
+ [up, m1, m2],
24233
+ [m2, m1, down]
24234
+ ][sextant];
24235
+ out[i] = nr;
24236
+ out[i + 1] = ng;
24237
+ out[i + 2] = nb;
24177
24238
  }
24178
24239
  return out;
24179
24240
  }
24180
- var LUMA = [0.299, 0.587, 0.114];
24241
+ var LUMA_709 = [0.2126, 0.7152, 0.0722];
24242
+ var TINT_CHROMA_SCALE = 0.985;
24181
24243
  function applyTint(src, hue, amount) {
24182
24244
  const tint = hslToRgb(hue, 1, 0.5);
24183
- const y = tint[0] * LUMA[0] + tint[1] * LUMA[1] + tint[2] * LUMA[2];
24184
- const k = amount / 100 * 255;
24185
- const add = tint.map((c) => (c - y) * k);
24245
+ const ty = tint[0] * LUMA_709[0] + tint[1] * LUMA_709[1] + tint[2] * LUMA_709[2];
24246
+ const chroma = tint.map((c) => (c - ty) * TINT_CHROMA_SCALE);
24247
+ const a = amount / 100;
24186
24248
  const out = new Uint8ClampedArray(src);
24187
24249
  for (let i = 0; i < src.length; i += 4) {
24188
- out[i] = Math.round(src[i] + add[0]);
24189
- out[i + 1] = Math.round(src[i + 1] + add[1]);
24190
- out[i + 2] = Math.round(src[i + 2] + add[2]);
24250
+ const r = src[i];
24251
+ const g = src[i + 1];
24252
+ const b = src[i + 2];
24253
+ const y = r * LUMA_709[0] + g * LUMA_709[1] + b * LUMA_709[2];
24254
+ const v = Math.max(r, g, b);
24255
+ out[i] = Math.round((1 - a) * r + a * (y + v * chroma[0]));
24256
+ out[i + 1] = Math.round((1 - a) * g + a * (y + v * chroma[1]));
24257
+ out[i + 2] = Math.round((1 - a) * b + a * (y + v * chroma[2]));
24191
24258
  }
24192
24259
  return out;
24193
24260
  }
@@ -24204,7 +24271,7 @@ function applyRedEyeCorrection(src, width, height, areas) {
24204
24271
  const r = out[i];
24205
24272
  const g = out[i + 1];
24206
24273
  const b = out[i + 2];
24207
- if (r > g && r > b) {
24274
+ if (r >= 2 * g && r >= 2 * b && r > 64) {
24208
24275
  out[i] = Math.round((g + b) / 2);
24209
24276
  }
24210
24277
  }
@@ -24212,83 +24279,96 @@ function applyRedEyeCorrection(src, width, height, areas) {
24212
24279
  }
24213
24280
  return out;
24214
24281
  }
24215
- function boxRadiiForSigma(sigma) {
24216
- const target = sigma * sigma;
24217
- let k = 0;
24218
- while ((k + 1) * (k + 2) <= target) {
24219
- k++;
24282
+ function blurKernel(radius) {
24283
+ const taps = Math.ceil(radius);
24284
+ const k = new Float64Array(2 * taps + 1);
24285
+ if (taps === 0) {
24286
+ k[0] = 1;
24287
+ return k;
24220
24288
  }
24221
- const v = (n) => n * (n + 1) / 3;
24222
- let best = 0;
24223
- for (let m = 1; m <= 3; m++) {
24224
- if (Math.abs((3 - m) * v(k) + m * v(k + 1) - target) < Math.abs((3 - best) * v(k) + best * v(k + 1) - target)) {
24225
- best = m;
24226
- }
24227
- }
24228
- return [0, 1, 2].map((i) => i < best ? k + 1 : k);
24229
- }
24230
- function boxPass(src, w, h, r, horizontal) {
24231
- if (r <= 0) {
24232
- return src;
24233
- }
24234
- const out = new Float64Array(src.length);
24235
- const lines = horizontal ? h : w;
24236
- const len = horizontal ? w : h;
24237
- const step = horizontal ? 4 : w * 4;
24238
- const norm = 1 / (2 * r + 1);
24239
- for (let line = 0; line < lines; line++) {
24240
- const base = horizontal ? line * w * 4 : line * 4;
24241
- for (let c = 0; c < 4; c++) {
24242
- const at = (p) => src[base + clamp(p, 0, len - 1) * step + c];
24243
- let sum = 0;
24244
- for (let p = -r; p <= r; p++) {
24245
- sum += at(p);
24246
- }
24247
- for (let p = 0; p < len; p++) {
24248
- out[base + p * step + c] = sum * norm;
24249
- sum += at(p + r + 1) - at(p - r);
24250
- }
24251
- }
24289
+ const sigma = radius / 1.98;
24290
+ let sum = 0;
24291
+ for (let i = -taps; i <= taps; i++) {
24292
+ const w = Math.exp(-(i * i) / (2 * sigma * sigma));
24293
+ k[i + taps] = w;
24294
+ sum += w;
24252
24295
  }
24253
- return out;
24296
+ for (let i = 0; i < k.length; i++) {
24297
+ k[i] /= sum;
24298
+ }
24299
+ return k;
24254
24300
  }
24255
- function blurFloat(src, w, h, radius) {
24256
- let buf = new Float64Array(src.length);
24257
- for (let i = 0; i < src.length; i += 4) {
24258
- const a = src[i + 3] / 255;
24259
- buf[i] = src[i] * a;
24260
- buf[i + 1] = src[i + 1] * a;
24261
- buf[i + 2] = src[i + 2] * a;
24262
- buf[i + 3] = src[i + 3];
24301
+ function mirror(p, n) {
24302
+ if (n === 1) {
24303
+ return 0;
24263
24304
  }
24264
- for (const r of boxRadiiForSigma(radius / 2)) {
24265
- buf = boxPass(buf, w, h, r, true);
24266
- buf = boxPass(buf, w, h, r, false);
24305
+ while (p < 0 || p >= n) {
24306
+ p = p < 0 ? -p : 2 * (n - 1) - p;
24267
24307
  }
24268
- for (let i = 0; i < buf.length; i += 4) {
24269
- const a = buf[i + 3] / 255;
24270
- for (let c = 0; c < 3; c++) {
24271
- buf[i + c] = a > 0 ? buf[i + c] / a : 0;
24308
+ return p;
24309
+ }
24310
+ function gdipBlur(src, w, h, radius, vRow = 0, vZeroAbove = false) {
24311
+ const k = blurKernel(radius);
24312
+ const taps = (k.length - 1) / 2;
24313
+ const out = new Float64Array(w * h * 4);
24314
+ for (let y = 0; y < h; y++) {
24315
+ const row = y * w * 4;
24316
+ for (let x = 0; x < w; x++) {
24317
+ let s0 = 0;
24318
+ let s1 = 0;
24319
+ let s2 = 0;
24320
+ let s3 = 0;
24321
+ for (let j = -taps; j <= taps; j++) {
24322
+ const p = row + mirror(x + j, w) * 4;
24323
+ const kw = k[j + taps];
24324
+ s0 += kw * src[p];
24325
+ s1 += kw * src[p + 1];
24326
+ s2 += kw * src[p + 2];
24327
+ s3 += kw * src[p + 3];
24328
+ }
24329
+ const o = row + x * 4;
24330
+ out[o] = s0;
24331
+ out[o + 1] = s1;
24332
+ out[o + 2] = s2;
24333
+ out[o + 3] = s3;
24334
+ }
24335
+ }
24336
+ if (taps > 0 && vRow >= 0 && vRow < h) {
24337
+ const col = new Float64Array(w * 4);
24338
+ for (let j = -taps; j <= taps; j++) {
24339
+ const yy = vRow + j;
24340
+ if (vZeroAbove && yy < vRow) {
24341
+ continue;
24342
+ }
24343
+ const base = mirror(yy, h) * w * 4;
24344
+ for (let i = 0; i < w * 4; i++) {
24345
+ col[i] += k[j + taps] * out[base + i];
24346
+ }
24272
24347
  }
24348
+ out.set(col, vRow * w * 4);
24273
24349
  }
24274
- return buf;
24350
+ return out;
24275
24351
  }
24276
24352
  function applyBlur(src, w, h, radius) {
24277
- const blurred = blurFloat(src, w, h, radius);
24278
- const out = new Uint8ClampedArray(src.length);
24279
- for (let i = 0; i < src.length; i++) {
24280
- out[i] = Math.round(blurred[i]);
24353
+ return Uint8ClampedArray.from(gdipBlur(src, w, h, radius), Math.round);
24354
+ }
24355
+ var SHARPEN_FALLOFF = Math.log2(96 / 25);
24356
+ function sharpenGain(radius, amount) {
24357
+ const x = amount / 100;
24358
+ if (radius <= 3) {
24359
+ return x * 2 * radius / 3;
24281
24360
  }
24282
- return out;
24361
+ return x * 2 * (3 / radius) ** SHARPEN_FALLOFF;
24283
24362
  }
24284
24363
  function applySharpen(src, w, h, radius, amount) {
24285
- const blurred = blurFloat(src, w, h, radius);
24286
- const k = amount / 100;
24364
+ const blurred = gdipBlur(src, w, h, radius);
24365
+ const gain = sharpenGain(radius, amount);
24287
24366
  const out = new Uint8ClampedArray(src);
24288
24367
  for (let i = 0; i < src.length; i += 4) {
24289
24368
  for (let c = 0; c < 3; c++) {
24290
- out[i + c] = Math.round(src[i + c] + k * (src[i + c] - blurred[i + c]));
24369
+ out[i + c] = Math.round(src[i + c] + gain * (src[i + c] - Math.round(blurred[i + c])));
24291
24370
  }
24371
+ out[i + 3] = Math.max(src[i + 3], out[i], out[i + 1], out[i + 2]);
24292
24372
  }
24293
24373
  return out;
24294
24374
  }
@@ -24302,16 +24382,15 @@ function applyImageEffect(rgba, width, height, effect) {
24302
24382
  case "sharpen":
24303
24383
  return applySharpen(rgba, width, height, effect.radius, effect.amount);
24304
24384
  case "brightnessContrast": {
24305
- const contrast = contrastCurve(effect.contrast);
24306
- const lut = buildLut((v) => contrast(v) + effect.brightness);
24385
+ const lut = brightnessContrastLut(effect.brightness, effect.contrast);
24307
24386
  return applyLuts(rgba, lut, lut, lut);
24308
24387
  }
24309
24388
  case "colorBalance":
24310
24389
  return applyLuts(
24311
24390
  rgba,
24312
- buildLut(balanceCurve(effect.cyanRed)),
24313
- buildLut(balanceCurve(effect.magentaGreen)),
24314
- buildLut(balanceCurve(effect.yellowBlue))
24391
+ balanceLut(effect.cyanRed),
24392
+ balanceLut(effect.magentaGreen),
24393
+ balanceLut(effect.yellowBlue)
24315
24394
  );
24316
24395
  case "colorCurve": {
24317
24396
  const lut = curveAdjustmentLut(effect.adjustment, effect.intensity);
@@ -24338,6 +24417,38 @@ function applyImageEffect(rgba, width, height, effect) {
24338
24417
  return applyTint(rgba, effect.hue, effect.amount);
24339
24418
  }
24340
24419
  }
24420
+ function copyBlock(src, width, sx, sy, w, h, dst, bw, dx, dy) {
24421
+ for (let y = 0; y < h; y++) {
24422
+ const from = ((sy + y) * width + sx) * 4;
24423
+ dst.set(src.subarray(from, from + w * 4), ((dy + y) * bw + dx) * 4);
24424
+ }
24425
+ }
24426
+ function expandedBlur(rgba, width, height, radius, x0, y0, x1, y1) {
24427
+ const r = Math.ceil(radius);
24428
+ const bw = x1 - x0 + 2 * r;
24429
+ const bh = y1 - y0 + 2 * r;
24430
+ const buf = new Uint8ClampedArray(bw * bh * 4);
24431
+ const inside = x0 - r >= 0 && y0 - r >= 0 && x1 + r <= width && y1 + r <= height;
24432
+ if (inside) {
24433
+ copyBlock(rgba, width, x0, y0, x1 - x0, y1 - y0, buf, bw, r, r);
24434
+ } else {
24435
+ const gx0 = Math.max(0, x0 - r);
24436
+ const gy0 = Math.max(0, y0 - r);
24437
+ const gx1 = Math.min(width, x1 + r);
24438
+ const gy1 = Math.min(height, y1 + r);
24439
+ copyBlock(rgba, width, gx0, gy0, gx1 - gx0, gy1 - gy0, buf, bw, gx0 - (x0 - r), gy0 - (y0 - r));
24440
+ }
24441
+ const blurred = gdipBlur(buf, bw, bh, radius, inside ? r : 0, inside);
24442
+ const w = x1 - x0;
24443
+ const out = new Uint8ClampedArray(w * (y1 - y0) * 4);
24444
+ for (let y = 0; y < y1 - y0; y++) {
24445
+ const from = ((y + r) * bw + r) * 4;
24446
+ for (let i = 0; i < w * 4; i++) {
24447
+ out[y * w * 4 + i] = Math.round(blurred[from + i]);
24448
+ }
24449
+ }
24450
+ return out;
24451
+ }
24341
24452
  function applyImageEffectToRect(rgba, width, height, effect, src) {
24342
24453
  if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
24343
24454
  return null;
@@ -24349,38 +24460,43 @@ function applyImageEffectToRect(rgba, width, height, effect, src) {
24349
24460
  if (src) {
24350
24461
  x0 = clamp(Math.floor(src.x), 0, width);
24351
24462
  y0 = clamp(Math.floor(src.y), 0, height);
24352
- x1 = clamp(Math.ceil(src.x + src.w), 0, width);
24353
- y1 = clamp(Math.ceil(src.y + src.h), 0, height);
24354
- if (x1 <= x0 || y1 <= y0) {
24463
+ x1 = clamp(Math.ceil(src.x + src.w) + 1, 0, width);
24464
+ y1 = clamp(Math.ceil(src.y + src.h) + 1, 0, height);
24465
+ if (x1 <= x0 || y1 <= y0 || src.x + src.w <= x0 || src.y + src.h <= y0) {
24355
24466
  return null;
24356
24467
  }
24357
24468
  }
24358
- const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
24359
- const cw = x1 - x0;
24360
- const ch = y1 - y0;
24361
- const w = cw + 2 * pad;
24362
- const h = ch + 2 * pad;
24469
+ const w = x1 - x0;
24470
+ const h = y1 - y0;
24471
+ const shownWidth = src ? Math.min(w, Math.ceil(src.x + src.w) - x0) : w;
24472
+ const shownHeight = src ? Math.min(h, Math.ceil(src.y + src.h) - y0) : h;
24473
+ const result = (out2) => {
24474
+ if (shownWidth === w && shownHeight === h) {
24475
+ return { rgba: out2, x: x0, y: y0, width: w, height: h };
24476
+ }
24477
+ const shown = new Uint8ClampedArray(shownWidth * shownHeight * 4);
24478
+ copyBlock(out2, w, 0, 0, shownWidth, shownHeight, shown, shownWidth, 0, 0);
24479
+ return { rgba: shown, x: x0, y: y0, width: shownWidth, height: shownHeight };
24480
+ };
24481
+ if (src && effect.kind === "blur" && effect.expandEdge) {
24482
+ return result(expandedBlur(rgba, width, height, effect.radius, x0, y0, x1, y1));
24483
+ }
24363
24484
  let region = rgba;
24364
- if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
24485
+ if (w !== width || h !== height) {
24365
24486
  region = new Uint8ClampedArray(w * h * 4);
24366
- for (let y = 0; y < ch; y++) {
24367
- const from = ((y0 + y) * width + x0) * 4;
24368
- region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
24369
- }
24487
+ copyBlock(rgba, width, x0, y0, w, h, region, w, 0, 0);
24370
24488
  }
24371
- const ox = x0 - pad;
24372
- const oy = y0 - pad;
24373
24489
  const local = effect.kind === "redEyeCorrection" ? {
24374
24490
  kind: effect.kind,
24375
24491
  areas: effect.areas.map((a) => ({
24376
- left: a.left - ox,
24377
- top: a.top - oy,
24378
- right: a.right - ox,
24379
- bottom: a.bottom - oy
24492
+ left: a.left - x0,
24493
+ top: a.top - y0,
24494
+ right: a.right - x0,
24495
+ bottom: a.bottom - y0
24380
24496
  }))
24381
24497
  } : effect;
24382
24498
  const out = applyImageEffect(region, w, h, local);
24383
- return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
24499
+ return out ? result(out) : null;
24384
24500
  }
24385
24501
 
24386
24502
  // src/emf-plus-draw-image.ts
@@ -24544,11 +24660,10 @@ function effectedDraw(img, bitmap, effect, draw, source) {
24544
24660
  if (!rect) {
24545
24661
  return { bitmap: out, draw };
24546
24662
  }
24547
- const grow = region.pad;
24548
- const srcX = rect.srcX - grow - region.x;
24549
- const srcY = rect.srcY - grow - region.y;
24550
- const srcW = rect.srcW + 2 * grow;
24551
- const srcH = rect.srcH + 2 * grow;
24663
+ const srcX = rect.srcX - region.x;
24664
+ const srcY = rect.srcY - region.y;
24665
+ const srcW = rect.srcW;
24666
+ const srcH = rect.srcH;
24552
24667
  const shift = [1, 0, 0, 1, region.x, region.y];
24553
24668
  const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24554
24669
  return {
package/dist/index.mjs CHANGED
@@ -12317,7 +12317,7 @@ function blendSeparable(rgba, width, box, iu0, wu, iv0, wv, verticalFirst, edge
12317
12317
  }
12318
12318
  var OUTSIDE_TRANSPARENT = -1;
12319
12319
  var OUTSIDE_CLAMP = -2;
12320
- function wrapTap(t, lo, hi, mirror, edge) {
12320
+ function wrapTap(t, lo, hi, mirror2, edge) {
12321
12321
  if (t >= lo && t < hi) {
12322
12322
  return t;
12323
12323
  }
@@ -12328,7 +12328,7 @@ function wrapTap(t, lo, hi, mirror, edge) {
12328
12328
  return OUTSIDE_CLAMP;
12329
12329
  }
12330
12330
  const n = hi - lo;
12331
- if (!mirror) {
12331
+ if (!mirror2) {
12332
12332
  return lo + ((t - lo) % n + n) % n;
12333
12333
  }
12334
12334
  const q = ((t - lo) % (2 * n) + 2 * n) % (2 * n);
@@ -17628,9 +17628,9 @@ function buildPattern(ctx, rect, tw, th, wrap, brush, delta, sampling, colorAt)
17628
17628
  if (typeof ctx.createPattern !== "function") {
17629
17629
  return null;
17630
17630
  }
17631
- const mirror = wrap === "clamp" ? { x: false, y: false } : mirrorFlags(wrap);
17632
- const w = tw * (mirror.x ? 2 : 1);
17633
- const h = th * (mirror.y ? 2 : 1);
17631
+ const mirror2 = wrap === "clamp" ? { x: false, y: false } : mirrorFlags(wrap);
17632
+ const w = tw * (mirror2.x ? 2 : 1);
17633
+ const h = th * (mirror2.y ? 2 : 1);
17634
17634
  const temp = createTempCanvas(w, h);
17635
17635
  if (!temp) {
17636
17636
  return null;
@@ -18052,8 +18052,8 @@ function createHatchPattern(ctx, hatch, origin, user, deviceToCanvas) {
18052
18052
  }
18053
18053
 
18054
18054
  // src/emf-plus-brush-texture.ts
18055
- function wrapTexel(i, n, mirror) {
18056
- if (!mirror) {
18055
+ function wrapTexel(i, n, mirror2) {
18056
+ if (!mirror2) {
18057
18057
  return (i % n + n) % n;
18058
18058
  }
18059
18059
  const p = (i % (2 * n) + 2 * n) % (2 * n);
@@ -19114,11 +19114,11 @@ function boundaryBox2(points) {
19114
19114
  }
19115
19115
  return x1 > x0 && y1 > y0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null;
19116
19116
  }
19117
- function foldIntoTile(v, origin, size, mirror, lag) {
19117
+ function foldIntoTile(v, origin, size, mirror2, lag) {
19118
19118
  const rel = v - origin;
19119
19119
  const k = Math.floor(rel / size);
19120
19120
  const local = rel - k * size;
19121
- if (mirror && (k % 2 + 2) % 2 === 1) {
19121
+ if (mirror2 && (k % 2 + 2) % 2 === 1) {
19122
19122
  return origin + size - lag - local;
19123
19123
  }
19124
19124
  return origin + local;
@@ -23972,6 +23972,7 @@ function readGuid(view, off) {
23972
23972
  ].join("-");
23973
23973
  }
23974
23974
  var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
23975
+ var COLOR_CURVE_PARAMS_SIZE = 12;
23975
23976
  function parseSerializableObject(view, dataOff, dataSize) {
23976
23977
  if (dataSize < 20) {
23977
23978
  return null;
@@ -23996,7 +23997,7 @@ function parseSerializableObject(view, dataOff, dataSize) {
23996
23997
  case "colorBalance":
23997
23998
  return need(12) ? { kind, cyanRed: i32(0, -100, 100), magentaGreen: i32(1, -100, 100), yellowBlue: i32(2, -100, 100) } : null;
23998
23999
  case "colorCurve":
23999
- return need(12) ? {
24000
+ return need(COLOR_CURVE_PARAMS_SIZE) ? {
24000
24001
  kind,
24001
24002
  adjustment: view.getInt32(p, true),
24002
24003
  channel: view.getInt32(p + 4, true),
@@ -24050,66 +24051,101 @@ function parseSerializableObject(view, dataOff, dataSize) {
24050
24051
  return need(8) ? { kind, hue: i32(0, -180, 180), amount: i32(1, -100, 100) } : null;
24051
24052
  }
24052
24053
  }
24053
- function buildLut(f) {
24054
+ var roundHalfDown = (v) => Math.ceil(v - 0.5);
24055
+ function buildLut(f, round = Math.round) {
24054
24056
  const lut = new Uint8Array(256);
24055
24057
  for (let v = 0; v < 256; v++) {
24056
- lut[v] = clamp(Math.round(f(v)), 0, 255);
24058
+ lut[v] = clamp(round(f(v)), 0, 255);
24057
24059
  }
24058
24060
  return lut;
24059
24061
  }
24060
- function contrastCurve(c) {
24061
- const gain = c >= 100 ? 1e6 : c >= 0 ? 100 / (100 - c) : (100 + c) / 100;
24062
- return (v) => (v - 127.5) * gain + 127.5;
24063
- }
24064
- function balanceCurve(t) {
24065
- const f = t / 100;
24066
- return (v) => f >= 0 ? v + (255 - v) * f : v * (1 + f);
24067
- }
24068
- function midtoneCurve(t) {
24069
- const gamma = 2 ** (-t / 100);
24070
- return (v) => 255 * (v / 255) ** gamma;
24062
+ function midtoneGamma(t) {
24063
+ return t >= 0 ? 1 / (1 + t / 50) : 1 - t / 50;
24064
+ }
24065
+ function brightnessContrastLut(brightness, contrast) {
24066
+ const gain = contrast >= 100 ? 1e6 : contrast >= 0 ? 100 / (100 - contrast) : (100 + contrast) / 100;
24067
+ const half = brightness / 2;
24068
+ return buildLut((v) => (v + half - 127.5) * gain + 127.5 + half);
24069
+ }
24070
+ function balanceLut(t) {
24071
+ return buildLut((v) => v * (1 + t / 100));
24072
+ }
24073
+ function splineLut(ys) {
24074
+ const n = ys.length;
24075
+ const h = 255 / (n - 1);
24076
+ const m = new Float64Array(n);
24077
+ const c = new Float64Array(n);
24078
+ const d = new Float64Array(n);
24079
+ for (let i = 1; i < n - 1; i++) {
24080
+ const rhs = 6 * (ys[i + 1] - 2 * ys[i] + ys[i - 1]) / (h * h);
24081
+ const den = 4 - c[i - 1];
24082
+ c[i] = 1 / den;
24083
+ d[i] = (rhs - d[i - 1]) / den;
24084
+ }
24085
+ for (let i = n - 2; i >= 1; i--) {
24086
+ m[i] = d[i] - c[i] * m[i + 1];
24087
+ }
24088
+ return buildLut((v) => {
24089
+ const i = Math.min(n - 2, Math.floor(v / h));
24090
+ const b = (v - i * h) / h;
24091
+ const a = 1 - b;
24092
+ return a * ys[i] + b * ys[i + 1] + ((a * a * a - a) * m[i] + (b * b * b - b) * m[i + 1]) * h * h / 6;
24093
+ });
24071
24094
  }
24095
+ var CURVE_POINTS = 23;
24096
+ var CURVE_X = Array.from({ length: CURVE_POINTS }, (_, k) => k * 255 / (CURVE_POINTS - 1));
24097
+ var CURVE_TABLES = {
24098
+ "contrast+": [0, 3.8, 8, 12.2, 17.4, 23.5, 31.1, 40.2, 53, 70.2, 94.5, 127.5, 160.5, 184.8, 202, 214.8, 223.9, 231.5, 237.6, 242.8, 247, 251.2, 255],
24099
+ "contrast-": [0, 33.1, 57.2, 74.7, 87.2, 96.6, 103.9, 109.8, 115.3, 119.6, 123.7, 127.5, 131.3, 135.4, 139.7, 145.2, 151.1, 158.4, 167.8, 180.3, 197.8, 221.9, 255],
24100
+ "highlight+": [0, 11.6, 23.2, 34.8, 46.4, 58, 69.6, 81.1, 92.7, 104.3, 116, 127.6, 142.3, 160.6, 186.4, 208, 220.6, 229.4, 236.2, 242, 246.6, 251, 255],
24101
+ "highlight-": [0, 11.6, 23.2, 34.8, 46.4, 58, 69.5, 81.1, 92.7, 104.4, 115.9, 127.5, 136.7, 144.9, 151.4, 157.1, 161.8, 167.2, 174.3, 185.3, 200.9, 223.7, 255],
24102
+ "shadow+": [0, 31.3, 54, 70, 82.5, 92.3, 99.5, 104.8, 109.5, 115, 121.3, 127.7, 139.2, 150.6, 162.3, 173.8, 185.5, 197, 208.6, 220.2, 231.8, 243.4, 255],
24103
+ "shadow-": [0, 4, 8.4, 13, 18.7, 25.7, 34.4, 45, 58.4, 80, 105.8, 127.5, 139.2, 150.7, 162.3, 173.9, 185.4, 197, 208.6, 220.2, 231.8, 243.4, 255]
24104
+ };
24072
24105
  function curveAdjustmentLut(adjustment, intensity) {
24073
24106
  const t = intensity;
24107
+ let ys;
24074
24108
  switch (adjustment) {
24075
24109
  case CurveAdjustment.Exposure:
24076
- return buildLut((v) => v * (1 + t / 255));
24110
+ ys = CURVE_X.map((x) => roundHalfDown(x) + t);
24111
+ break;
24077
24112
  case CurveAdjustment.Density:
24078
- return buildLut((v) => v * (1 - t / 255));
24113
+ ys = CURVE_X.map((x) => x + t);
24114
+ break;
24079
24115
  case CurveAdjustment.Contrast:
24080
- return buildLut(contrastCurve(clamp(t, -100, 100)));
24081
24116
  case CurveAdjustment.Highlight:
24082
24117
  case CurveAdjustment.Shadow: {
24083
- const high = adjustment === CurveAdjustment.Highlight;
24084
- const amp = clamp(t, -100, 100) * 0.32;
24085
- return buildLut((v) => {
24086
- const inside = high ? v > 128 : v < 128;
24087
- if (!inside) {
24088
- return v;
24089
- }
24090
- const u = high ? (v - 128) / 127 : v / 128;
24091
- return v + amp * Math.sin(Math.PI * u);
24092
- });
24118
+ const c = clamp(t, -100, 100);
24119
+ const name = ["contrast", "highlight", "shadow"][adjustment - CurveAdjustment.Contrast] + (c >= 0 ? "+" : "-");
24120
+ const table = CURVE_TABLES[name];
24121
+ ys = CURVE_X.map((x, k) => x + Math.abs(c) / 50 * (table[k] - x));
24122
+ break;
24123
+ }
24124
+ case CurveAdjustment.Midtone: {
24125
+ const g = midtoneGamma(clamp(t, -100, 100));
24126
+ ys = CURVE_X.map((x) => 255 * (x / 255) ** g);
24127
+ break;
24093
24128
  }
24094
- case CurveAdjustment.Midtone:
24095
- return buildLut(midtoneCurve(clamp(t, -100, 100)));
24096
24129
  case CurveAdjustment.WhiteSaturation: {
24097
24130
  const w = Math.max(1, clamp(t, 0, 255));
24098
- return buildLut((v) => v * 255 / w);
24131
+ ys = CURVE_X.map((x) => x * 255 / w);
24132
+ break;
24099
24133
  }
24100
24134
  case CurveAdjustment.BlackSaturation: {
24101
24135
  const b = Math.min(254, clamp(t, 0, 255));
24102
- return buildLut((v) => (v - b) * 255 / (255 - b));
24136
+ ys = CURVE_X.map((x) => (x - b) * 255 / (255 - b));
24137
+ break;
24103
24138
  }
24104
24139
  default:
24105
24140
  return null;
24106
24141
  }
24142
+ return splineLut(ys.map((y) => clamp(y, 0, 255)));
24107
24143
  }
24108
24144
  function levelsLut(highlight, midtone, shadow) {
24109
24145
  const black = shadow / 100 * 255;
24110
24146
  const white = Math.max(black + 1, highlight / 100 * 255);
24111
- const gamma = midtoneCurve(midtone);
24112
- return buildLut((v) => gamma(clamp((v - black) / (white - black), 0, 1) * 255));
24147
+ const gamma = midtoneGamma(midtone);
24148
+ return buildLut((v) => 255 * clamp((v - black) / (white - black), 0, 1) ** gamma, roundHalfDown);
24113
24149
  }
24114
24150
  function applyLuts(src, r, g, b, a = null) {
24115
24151
  const out = new Uint8ClampedArray(src);
@@ -24135,26 +24171,6 @@ function applyColorMatrix(src, m) {
24135
24171
  }
24136
24172
  return out;
24137
24173
  }
24138
- function rgbToHsl(r, g, b) {
24139
- const max = Math.max(r, g, b);
24140
- const min = Math.min(r, g, b);
24141
- const l = (max + min) / 2;
24142
- const d = max - min;
24143
- if (d === 0) {
24144
- return [0, 0, l];
24145
- }
24146
- const s = d / (1 - Math.abs(2 * l - 1));
24147
- let h;
24148
- if (max === r) {
24149
- h = (g - b) / d % 6;
24150
- } else if (max === g) {
24151
- h = (b - r) / d + 2;
24152
- } else {
24153
- h = (r - g) / d + 4;
24154
- }
24155
- h *= 60;
24156
- return [h < 0 ? h + 360 : h, s, l];
24157
- }
24158
24174
  function hslToRgb(h, s, l) {
24159
24175
  const c = (1 - Math.abs(2 * l - 1)) * s;
24160
24176
  const hp = (h % 360 + 360) % 360 / 60;
@@ -24163,29 +24179,80 @@ function hslToRgb(h, s, l) {
24163
24179
  const [r, g, b] = hp < 1 ? [c, x, 0] : hp < 2 ? [x, c, 0] : hp < 3 ? [0, c, x] : hp < 4 ? [0, x, c] : hp < 5 ? [x, 0, c] : [c, 0, x];
24164
24180
  return [r + m, g + m, b + m];
24165
24181
  }
24182
+ var HUE_SEXTANT = 43;
24183
+ var HUE_CIRCLE = 6 * HUE_SEXTANT;
24184
+ function gdipHue(r, g, b) {
24185
+ const max = Math.max(r, g, b);
24186
+ const d = max - Math.min(r, g, b);
24187
+ const f = (x) => Math.floor(x * HUE_SEXTANT / d);
24188
+ if (max === r && g >= b) {
24189
+ return f(g - b);
24190
+ }
24191
+ if (max === g) {
24192
+ return b >= r ? 87 + f(b - r) : 86 - f(r - b);
24193
+ }
24194
+ if (max === b && b !== r) {
24195
+ return r >= g ? 172 + f(r - g) : 171 - f(g - r);
24196
+ }
24197
+ const t = f(b - g);
24198
+ return (256 - t - (t >= 40 ? 1 : 0)) % 256;
24199
+ }
24166
24200
  function applyHueSaturationLightness(src, hue, saturation, lightness2) {
24167
24201
  const out = new Uint8ClampedArray(src);
24168
- const push = (v, t) => t >= 0 ? v + (1 - v) * (t / 100) : v * (1 + t / 100);
24202
+ const shift = Math.round(hue * HUE_CIRCLE / 360);
24203
+ const sMul = 1 + saturation / 100;
24169
24204
  for (let i = 0; i < src.length; i += 4) {
24170
- const [h, s, l] = rgbToHsl(src[i] / 255, src[i + 1] / 255, src[i + 2] / 255);
24171
- const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness2));
24172
- out[i] = Math.round(r * 255);
24173
- out[i + 1] = Math.round(g * 255);
24174
- out[i + 2] = Math.round(b * 255);
24205
+ const r = src[i];
24206
+ const g = src[i + 1];
24207
+ const b = src[i + 2];
24208
+ const max = Math.max(r, g, b);
24209
+ const min = Math.min(r, g, b);
24210
+ const l = clamp(Math.round(((max + min >> 1) * 100 + 255 * lightness2) / 100), 0, 255);
24211
+ if (max === min) {
24212
+ out[i] = out[i + 1] = out[i + 2] = l;
24213
+ continue;
24214
+ }
24215
+ const sum = max + min;
24216
+ const s = clamp((max - min) / (sum <= 255 ? sum : 510 - sum) * sMul, 0, 1);
24217
+ const hi = l <= 127 ? l * (1 + s) : l + s * (255 - l);
24218
+ const m1 = l <= 127 ? 2 * l - Math.trunc(hi) : Math.round(2 * l - hi);
24219
+ const m2 = 2 * l - m1;
24220
+ const q = ((gdipHue(r, g, b) + shift) % HUE_CIRCLE + HUE_CIRCLE) % HUE_CIRCLE;
24221
+ const sextant = Math.min(5, Math.floor(q / HUE_SEXTANT));
24222
+ const f = (q - sextant * HUE_SEXTANT) / HUE_SEXTANT;
24223
+ const up = Math.trunc(m1 + (m2 - m1) * f);
24224
+ const down = Math.trunc(m1 + (m2 - m1) * (1 - f));
24225
+ const [nr, ng, nb] = [
24226
+ [m2, up, m1],
24227
+ [down, m2, m1],
24228
+ [m1, m2, up],
24229
+ [m1, down, m2],
24230
+ [up, m1, m2],
24231
+ [m2, m1, down]
24232
+ ][sextant];
24233
+ out[i] = nr;
24234
+ out[i + 1] = ng;
24235
+ out[i + 2] = nb;
24175
24236
  }
24176
24237
  return out;
24177
24238
  }
24178
- var LUMA = [0.299, 0.587, 0.114];
24239
+ var LUMA_709 = [0.2126, 0.7152, 0.0722];
24240
+ var TINT_CHROMA_SCALE = 0.985;
24179
24241
  function applyTint(src, hue, amount) {
24180
24242
  const tint = hslToRgb(hue, 1, 0.5);
24181
- const y = tint[0] * LUMA[0] + tint[1] * LUMA[1] + tint[2] * LUMA[2];
24182
- const k = amount / 100 * 255;
24183
- const add = tint.map((c) => (c - y) * k);
24243
+ const ty = tint[0] * LUMA_709[0] + tint[1] * LUMA_709[1] + tint[2] * LUMA_709[2];
24244
+ const chroma = tint.map((c) => (c - ty) * TINT_CHROMA_SCALE);
24245
+ const a = amount / 100;
24184
24246
  const out = new Uint8ClampedArray(src);
24185
24247
  for (let i = 0; i < src.length; i += 4) {
24186
- out[i] = Math.round(src[i] + add[0]);
24187
- out[i + 1] = Math.round(src[i + 1] + add[1]);
24188
- out[i + 2] = Math.round(src[i + 2] + add[2]);
24248
+ const r = src[i];
24249
+ const g = src[i + 1];
24250
+ const b = src[i + 2];
24251
+ const y = r * LUMA_709[0] + g * LUMA_709[1] + b * LUMA_709[2];
24252
+ const v = Math.max(r, g, b);
24253
+ out[i] = Math.round((1 - a) * r + a * (y + v * chroma[0]));
24254
+ out[i + 1] = Math.round((1 - a) * g + a * (y + v * chroma[1]));
24255
+ out[i + 2] = Math.round((1 - a) * b + a * (y + v * chroma[2]));
24189
24256
  }
24190
24257
  return out;
24191
24258
  }
@@ -24202,7 +24269,7 @@ function applyRedEyeCorrection(src, width, height, areas) {
24202
24269
  const r = out[i];
24203
24270
  const g = out[i + 1];
24204
24271
  const b = out[i + 2];
24205
- if (r > g && r > b) {
24272
+ if (r >= 2 * g && r >= 2 * b && r > 64) {
24206
24273
  out[i] = Math.round((g + b) / 2);
24207
24274
  }
24208
24275
  }
@@ -24210,83 +24277,96 @@ function applyRedEyeCorrection(src, width, height, areas) {
24210
24277
  }
24211
24278
  return out;
24212
24279
  }
24213
- function boxRadiiForSigma(sigma) {
24214
- const target = sigma * sigma;
24215
- let k = 0;
24216
- while ((k + 1) * (k + 2) <= target) {
24217
- k++;
24280
+ function blurKernel(radius) {
24281
+ const taps = Math.ceil(radius);
24282
+ const k = new Float64Array(2 * taps + 1);
24283
+ if (taps === 0) {
24284
+ k[0] = 1;
24285
+ return k;
24218
24286
  }
24219
- const v = (n) => n * (n + 1) / 3;
24220
- let best = 0;
24221
- for (let m = 1; m <= 3; m++) {
24222
- if (Math.abs((3 - m) * v(k) + m * v(k + 1) - target) < Math.abs((3 - best) * v(k) + best * v(k + 1) - target)) {
24223
- best = m;
24224
- }
24225
- }
24226
- return [0, 1, 2].map((i) => i < best ? k + 1 : k);
24227
- }
24228
- function boxPass(src, w, h, r, horizontal) {
24229
- if (r <= 0) {
24230
- return src;
24231
- }
24232
- const out = new Float64Array(src.length);
24233
- const lines = horizontal ? h : w;
24234
- const len = horizontal ? w : h;
24235
- const step = horizontal ? 4 : w * 4;
24236
- const norm = 1 / (2 * r + 1);
24237
- for (let line = 0; line < lines; line++) {
24238
- const base = horizontal ? line * w * 4 : line * 4;
24239
- for (let c = 0; c < 4; c++) {
24240
- const at = (p) => src[base + clamp(p, 0, len - 1) * step + c];
24241
- let sum = 0;
24242
- for (let p = -r; p <= r; p++) {
24243
- sum += at(p);
24244
- }
24245
- for (let p = 0; p < len; p++) {
24246
- out[base + p * step + c] = sum * norm;
24247
- sum += at(p + r + 1) - at(p - r);
24248
- }
24249
- }
24287
+ const sigma = radius / 1.98;
24288
+ let sum = 0;
24289
+ for (let i = -taps; i <= taps; i++) {
24290
+ const w = Math.exp(-(i * i) / (2 * sigma * sigma));
24291
+ k[i + taps] = w;
24292
+ sum += w;
24250
24293
  }
24251
- return out;
24294
+ for (let i = 0; i < k.length; i++) {
24295
+ k[i] /= sum;
24296
+ }
24297
+ return k;
24252
24298
  }
24253
- function blurFloat(src, w, h, radius) {
24254
- let buf = new Float64Array(src.length);
24255
- for (let i = 0; i < src.length; i += 4) {
24256
- const a = src[i + 3] / 255;
24257
- buf[i] = src[i] * a;
24258
- buf[i + 1] = src[i + 1] * a;
24259
- buf[i + 2] = src[i + 2] * a;
24260
- buf[i + 3] = src[i + 3];
24299
+ function mirror(p, n) {
24300
+ if (n === 1) {
24301
+ return 0;
24261
24302
  }
24262
- for (const r of boxRadiiForSigma(radius / 2)) {
24263
- buf = boxPass(buf, w, h, r, true);
24264
- buf = boxPass(buf, w, h, r, false);
24303
+ while (p < 0 || p >= n) {
24304
+ p = p < 0 ? -p : 2 * (n - 1) - p;
24265
24305
  }
24266
- for (let i = 0; i < buf.length; i += 4) {
24267
- const a = buf[i + 3] / 255;
24268
- for (let c = 0; c < 3; c++) {
24269
- buf[i + c] = a > 0 ? buf[i + c] / a : 0;
24306
+ return p;
24307
+ }
24308
+ function gdipBlur(src, w, h, radius, vRow = 0, vZeroAbove = false) {
24309
+ const k = blurKernel(radius);
24310
+ const taps = (k.length - 1) / 2;
24311
+ const out = new Float64Array(w * h * 4);
24312
+ for (let y = 0; y < h; y++) {
24313
+ const row = y * w * 4;
24314
+ for (let x = 0; x < w; x++) {
24315
+ let s0 = 0;
24316
+ let s1 = 0;
24317
+ let s2 = 0;
24318
+ let s3 = 0;
24319
+ for (let j = -taps; j <= taps; j++) {
24320
+ const p = row + mirror(x + j, w) * 4;
24321
+ const kw = k[j + taps];
24322
+ s0 += kw * src[p];
24323
+ s1 += kw * src[p + 1];
24324
+ s2 += kw * src[p + 2];
24325
+ s3 += kw * src[p + 3];
24326
+ }
24327
+ const o = row + x * 4;
24328
+ out[o] = s0;
24329
+ out[o + 1] = s1;
24330
+ out[o + 2] = s2;
24331
+ out[o + 3] = s3;
24332
+ }
24333
+ }
24334
+ if (taps > 0 && vRow >= 0 && vRow < h) {
24335
+ const col = new Float64Array(w * 4);
24336
+ for (let j = -taps; j <= taps; j++) {
24337
+ const yy = vRow + j;
24338
+ if (vZeroAbove && yy < vRow) {
24339
+ continue;
24340
+ }
24341
+ const base = mirror(yy, h) * w * 4;
24342
+ for (let i = 0; i < w * 4; i++) {
24343
+ col[i] += k[j + taps] * out[base + i];
24344
+ }
24270
24345
  }
24346
+ out.set(col, vRow * w * 4);
24271
24347
  }
24272
- return buf;
24348
+ return out;
24273
24349
  }
24274
24350
  function applyBlur(src, w, h, radius) {
24275
- const blurred = blurFloat(src, w, h, radius);
24276
- const out = new Uint8ClampedArray(src.length);
24277
- for (let i = 0; i < src.length; i++) {
24278
- out[i] = Math.round(blurred[i]);
24351
+ return Uint8ClampedArray.from(gdipBlur(src, w, h, radius), Math.round);
24352
+ }
24353
+ var SHARPEN_FALLOFF = Math.log2(96 / 25);
24354
+ function sharpenGain(radius, amount) {
24355
+ const x = amount / 100;
24356
+ if (radius <= 3) {
24357
+ return x * 2 * radius / 3;
24279
24358
  }
24280
- return out;
24359
+ return x * 2 * (3 / radius) ** SHARPEN_FALLOFF;
24281
24360
  }
24282
24361
  function applySharpen(src, w, h, radius, amount) {
24283
- const blurred = blurFloat(src, w, h, radius);
24284
- const k = amount / 100;
24362
+ const blurred = gdipBlur(src, w, h, radius);
24363
+ const gain = sharpenGain(radius, amount);
24285
24364
  const out = new Uint8ClampedArray(src);
24286
24365
  for (let i = 0; i < src.length; i += 4) {
24287
24366
  for (let c = 0; c < 3; c++) {
24288
- out[i + c] = Math.round(src[i + c] + k * (src[i + c] - blurred[i + c]));
24367
+ out[i + c] = Math.round(src[i + c] + gain * (src[i + c] - Math.round(blurred[i + c])));
24289
24368
  }
24369
+ out[i + 3] = Math.max(src[i + 3], out[i], out[i + 1], out[i + 2]);
24290
24370
  }
24291
24371
  return out;
24292
24372
  }
@@ -24300,16 +24380,15 @@ function applyImageEffect(rgba, width, height, effect) {
24300
24380
  case "sharpen":
24301
24381
  return applySharpen(rgba, width, height, effect.radius, effect.amount);
24302
24382
  case "brightnessContrast": {
24303
- const contrast = contrastCurve(effect.contrast);
24304
- const lut = buildLut((v) => contrast(v) + effect.brightness);
24383
+ const lut = brightnessContrastLut(effect.brightness, effect.contrast);
24305
24384
  return applyLuts(rgba, lut, lut, lut);
24306
24385
  }
24307
24386
  case "colorBalance":
24308
24387
  return applyLuts(
24309
24388
  rgba,
24310
- buildLut(balanceCurve(effect.cyanRed)),
24311
- buildLut(balanceCurve(effect.magentaGreen)),
24312
- buildLut(balanceCurve(effect.yellowBlue))
24389
+ balanceLut(effect.cyanRed),
24390
+ balanceLut(effect.magentaGreen),
24391
+ balanceLut(effect.yellowBlue)
24313
24392
  );
24314
24393
  case "colorCurve": {
24315
24394
  const lut = curveAdjustmentLut(effect.adjustment, effect.intensity);
@@ -24336,6 +24415,38 @@ function applyImageEffect(rgba, width, height, effect) {
24336
24415
  return applyTint(rgba, effect.hue, effect.amount);
24337
24416
  }
24338
24417
  }
24418
+ function copyBlock(src, width, sx, sy, w, h, dst, bw, dx, dy) {
24419
+ for (let y = 0; y < h; y++) {
24420
+ const from = ((sy + y) * width + sx) * 4;
24421
+ dst.set(src.subarray(from, from + w * 4), ((dy + y) * bw + dx) * 4);
24422
+ }
24423
+ }
24424
+ function expandedBlur(rgba, width, height, radius, x0, y0, x1, y1) {
24425
+ const r = Math.ceil(radius);
24426
+ const bw = x1 - x0 + 2 * r;
24427
+ const bh = y1 - y0 + 2 * r;
24428
+ const buf = new Uint8ClampedArray(bw * bh * 4);
24429
+ const inside = x0 - r >= 0 && y0 - r >= 0 && x1 + r <= width && y1 + r <= height;
24430
+ if (inside) {
24431
+ copyBlock(rgba, width, x0, y0, x1 - x0, y1 - y0, buf, bw, r, r);
24432
+ } else {
24433
+ const gx0 = Math.max(0, x0 - r);
24434
+ const gy0 = Math.max(0, y0 - r);
24435
+ const gx1 = Math.min(width, x1 + r);
24436
+ const gy1 = Math.min(height, y1 + r);
24437
+ copyBlock(rgba, width, gx0, gy0, gx1 - gx0, gy1 - gy0, buf, bw, gx0 - (x0 - r), gy0 - (y0 - r));
24438
+ }
24439
+ const blurred = gdipBlur(buf, bw, bh, radius, inside ? r : 0, inside);
24440
+ const w = x1 - x0;
24441
+ const out = new Uint8ClampedArray(w * (y1 - y0) * 4);
24442
+ for (let y = 0; y < y1 - y0; y++) {
24443
+ const from = ((y + r) * bw + r) * 4;
24444
+ for (let i = 0; i < w * 4; i++) {
24445
+ out[y * w * 4 + i] = Math.round(blurred[from + i]);
24446
+ }
24447
+ }
24448
+ return out;
24449
+ }
24339
24450
  function applyImageEffectToRect(rgba, width, height, effect, src) {
24340
24451
  if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
24341
24452
  return null;
@@ -24347,38 +24458,43 @@ function applyImageEffectToRect(rgba, width, height, effect, src) {
24347
24458
  if (src) {
24348
24459
  x0 = clamp(Math.floor(src.x), 0, width);
24349
24460
  y0 = clamp(Math.floor(src.y), 0, height);
24350
- x1 = clamp(Math.ceil(src.x + src.w), 0, width);
24351
- y1 = clamp(Math.ceil(src.y + src.h), 0, height);
24352
- if (x1 <= x0 || y1 <= y0) {
24461
+ x1 = clamp(Math.ceil(src.x + src.w) + 1, 0, width);
24462
+ y1 = clamp(Math.ceil(src.y + src.h) + 1, 0, height);
24463
+ if (x1 <= x0 || y1 <= y0 || src.x + src.w <= x0 || src.y + src.h <= y0) {
24353
24464
  return null;
24354
24465
  }
24355
24466
  }
24356
- const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
24357
- const cw = x1 - x0;
24358
- const ch = y1 - y0;
24359
- const w = cw + 2 * pad;
24360
- const h = ch + 2 * pad;
24467
+ const w = x1 - x0;
24468
+ const h = y1 - y0;
24469
+ const shownWidth = src ? Math.min(w, Math.ceil(src.x + src.w) - x0) : w;
24470
+ const shownHeight = src ? Math.min(h, Math.ceil(src.y + src.h) - y0) : h;
24471
+ const result = (out2) => {
24472
+ if (shownWidth === w && shownHeight === h) {
24473
+ return { rgba: out2, x: x0, y: y0, width: w, height: h };
24474
+ }
24475
+ const shown = new Uint8ClampedArray(shownWidth * shownHeight * 4);
24476
+ copyBlock(out2, w, 0, 0, shownWidth, shownHeight, shown, shownWidth, 0, 0);
24477
+ return { rgba: shown, x: x0, y: y0, width: shownWidth, height: shownHeight };
24478
+ };
24479
+ if (src && effect.kind === "blur" && effect.expandEdge) {
24480
+ return result(expandedBlur(rgba, width, height, effect.radius, x0, y0, x1, y1));
24481
+ }
24361
24482
  let region = rgba;
24362
- if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
24483
+ if (w !== width || h !== height) {
24363
24484
  region = new Uint8ClampedArray(w * h * 4);
24364
- for (let y = 0; y < ch; y++) {
24365
- const from = ((y0 + y) * width + x0) * 4;
24366
- region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
24367
- }
24485
+ copyBlock(rgba, width, x0, y0, w, h, region, w, 0, 0);
24368
24486
  }
24369
- const ox = x0 - pad;
24370
- const oy = y0 - pad;
24371
24487
  const local = effect.kind === "redEyeCorrection" ? {
24372
24488
  kind: effect.kind,
24373
24489
  areas: effect.areas.map((a) => ({
24374
- left: a.left - ox,
24375
- top: a.top - oy,
24376
- right: a.right - ox,
24377
- bottom: a.bottom - oy
24490
+ left: a.left - x0,
24491
+ top: a.top - y0,
24492
+ right: a.right - x0,
24493
+ bottom: a.bottom - y0
24378
24494
  }))
24379
24495
  } : effect;
24380
24496
  const out = applyImageEffect(region, w, h, local);
24381
- return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
24497
+ return out ? result(out) : null;
24382
24498
  }
24383
24499
 
24384
24500
  // src/emf-plus-draw-image.ts
@@ -24542,11 +24658,10 @@ function effectedDraw(img, bitmap, effect, draw, source) {
24542
24658
  if (!rect) {
24543
24659
  return { bitmap: out, draw };
24544
24660
  }
24545
- const grow = region.pad;
24546
- const srcX = rect.srcX - grow - region.x;
24547
- const srcY = rect.srcY - grow - region.y;
24548
- const srcW = rect.srcW + 2 * grow;
24549
- const srcH = rect.srcH + 2 * grow;
24661
+ const srcX = rect.srcX - region.x;
24662
+ const srcY = rect.srcY - region.y;
24663
+ const srcW = rect.srcW;
24664
+ const srcH = rect.srcH;
24550
24665
  const shift = [1, 0, 0, 1, region.x, region.y];
24551
24666
  const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24552
24667
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.3.4",
3
+ "version": "4.3.5",
4
4
  "description": "Convert EMF/WMF metafile binaries to PNG data URLs or SVG (markup, data URL, React/JSX)",
5
5
  "keywords": [
6
6
  "canvas",