emf-converter 4.8.2 → 4.8.4

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**: all eleven EMF+ image effects are supported. Complete native tables cover every legal ColorCurve intensity. Deferred draws retain their effects. JPEG, GIF and TIFF have bundled decoders for environments without Canvas. HSL, tint, red-eye, blur, sharpen and GDI colour adjustment retain measured differences; see [the detailed limitations](docs/limitations.md) and the native fixture bounds.
217
+ - **Colour adjustment and image effects**: all eleven EMF+ image effects are supported. Complete native tables cover every legal ColorCurve intensity. Deferred draws retain their effects. JPEG, GIF and TIFF have bundled decoders for environments without Canvas. Blur and sharpen are exact; HSL, tint, red-eye and GDI colour adjustment retain measured differences; see [the detailed limitations](docs/limitations.md) and the native fixture bounds.
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. Native fixtures verify dashes under both uniform and non-uniform scales. GDI+ rejects singular pen transforms; the converter ignores such malformed transform data. Square, round, diamond and arrow anchor caps use their native shapes; the new centered-cap fixtures match every pixel with and without antialiasing.
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; Johab and OEM CP437 use bundled Windows mappings. Vertical `ETO_PDY` advances work with and without 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 curves follow `WidenPath` (exact for round and flat caps at one logical unit per pixel; square caps differ slightly at path ends); 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/browser.js CHANGED
@@ -16810,14 +16810,21 @@ function blurKernel(radius) {
16810
16810
  k[0] = 1;
16811
16811
  return k;
16812
16812
  }
16813
- let sum = 0;
16814
- for (let i = -taps; i <= taps; i++) {
16815
- const w = Math.exp(-1.96 * i * i / (radius * radius));
16816
- k[i + taps] = w;
16817
- sum += w;
16818
- }
16819
- for (let i = 0; i < k.length; i++) {
16820
- k[i] /= sum;
16813
+ const step = truncF32(truncF32(1.4) / Math.fround(radius));
16814
+ const w = new Float64Array(taps + 1);
16815
+ w[0] = 1;
16816
+ let offset = 0;
16817
+ let sum = 1;
16818
+ for (let i = 1; i <= taps; i++) {
16819
+ offset = truncF32(offset + step);
16820
+ w[i] = truncF32(Math.exp(-truncF32(offset * offset)));
16821
+ sum = truncF32(sum + truncF32(2 * w[i]));
16822
+ }
16823
+ const scale = truncF32(256 / sum);
16824
+ for (let i = 0; i <= taps; i++) {
16825
+ const weight = truncF32(w[i] * scale) / 256;
16826
+ k[taps + i] = weight;
16827
+ k[taps - i] = weight;
16821
16828
  }
16822
16829
  return k;
16823
16830
  }
@@ -17039,13 +17046,17 @@ function expandedBlur(rgba, width, height, radius, x0, y0, x1, y1, cropped) {
17039
17046
  const blurred = blurReduction(radius) > 1 ? effectBlur(buf, bw, bh, radius, cropped ? { x: r, y: r, w: x1 - x0, h: y1 - y0 } : void 0, true) : gdipBlur(buf, bw, bh, radius, cropped ? r : 0, cropped, Math.ceil((y1 - y0) / (x1 - x0)));
17040
17047
  const w = x1 - x0;
17041
17048
  const out = new Uint8ClampedArray(w * (y1 - y0) * 4);
17049
+ const halo = new Uint8ClampedArray((y1 - y0) * 4);
17042
17050
  for (let y = 0; y < y1 - y0; y++) {
17043
17051
  const from = ((y + r) * bw + r) * 4;
17044
17052
  for (let i = 0; i < w * 4; i++) {
17045
17053
  out[y * w * 4 + i] = Math.round(blurred[from + i]);
17046
17054
  }
17055
+ for (let i = 0; i < 4; i++) {
17056
+ halo[y * 4 + i] = Math.round(blurred[from + w * 4 + i]);
17057
+ }
17047
17058
  }
17048
- return out;
17059
+ return { out, halo };
17049
17060
  }
17050
17061
  function applyImageEffectToRect(rgba, width, height, effect, src) {
17051
17062
  if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
@@ -17078,7 +17089,12 @@ function applyImageEffectToRect(rgba, width, height, effect, src) {
17078
17089
  };
17079
17090
  if (src && effect.kind === "blur" && effect.expandEdge) {
17080
17091
  const cropped = Math.ceil(src.x + src.w) < width && Math.ceil(src.y + src.h) < height;
17081
- return result(expandedBlur(rgba, width, height, effect.radius, x0, y0, x1, y1, cropped));
17092
+ const { out: out2, halo } = expandedBlur(rgba, width, height, effect.radius, x0, y0, x1, y1, cropped);
17093
+ const region2 = result(out2);
17094
+ if (shownWidth === w && shownHeight === h) {
17095
+ region2.haloRight = halo;
17096
+ }
17097
+ return region2;
17082
17098
  }
17083
17099
  let region = rgba;
17084
17100
  if (w !== width || h !== height) {
@@ -20450,6 +20466,22 @@ function cubicIntegral(t, a) {
20450
20466
  }
20451
20467
  return Math.sign(t) * v;
20452
20468
  }
20469
+ var FAR_FADE = [1, 1, 0.985, 0.95, 0.91, 0.85, 0.8, 0.745, 0.69, 0.63, 0.56, 0.5, 0.43, 0.35, 0.28, 0.22, 0.16, 0.11, 0.065, 0.03, 0];
20470
+ function fadeAt(d, bilinear) {
20471
+ if (d >= 1) {
20472
+ return 0;
20473
+ }
20474
+ if (bilinear) {
20475
+ return 1 - Math.max(0, d);
20476
+ }
20477
+ const x = Math.max(0, d) * 20;
20478
+ const i = Math.floor(x);
20479
+ return FAR_FADE[i] + (FAR_FADE[i + 1] - FAR_FADE[i]) * (x - i);
20480
+ }
20481
+ function nearSourceExtent(deviceLen, srcLen) {
20482
+ const d = Math.ceil(deviceLen - 1e-6) - srcLen;
20483
+ return d >= -1 && d <= 1;
20484
+ }
20453
20485
  var BICUBIC_A = -0.5;
20454
20486
  var HQ_BICUBIC_A = -1;
20455
20487
  function axisFilter(kernel, scale, copy) {
@@ -20554,13 +20586,39 @@ function mirroredOrigin(o) {
20554
20586
  return f === 0 ? o : Math.floor(o) + 1 - f;
20555
20587
  }
20556
20588
  function resampleImage(rgba, width, height, spec, surface) {
20589
+ if (spec.rightHalo && spec.rightHalo.length === height * 4) {
20590
+ const grown = new Uint8ClampedArray((width + 1) * height * 4);
20591
+ for (let y = 0; y < height; y++) {
20592
+ grown.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width + 1) * 4);
20593
+ grown.set(spec.rightHalo.subarray(y * 4, y * 4 + 4), (y * (width + 1) + width) * 4);
20594
+ }
20595
+ return resampleImage(grown, width + 1, height, { ...spec, rightHalo: void 0 }, surface);
20596
+ }
20557
20597
  if (spec.kernel === "nearest" && !spec.wrap) {
20558
20598
  return resampleNearest(rgba, width, height, spec, surface);
20559
20599
  }
20560
20600
  let m = snapToDeviceGrid(spec);
20561
- const kernel = spec.kernel;
20601
+ let kernel = spec.kernel;
20562
20602
  const hq = kernel === "hq-bilinear" || kernel === "hq-bicubic";
20563
20603
  const axisAligned = m[1] === 0 && m[2] === 0;
20604
+ let farFade = false;
20605
+ let shiftX = 0;
20606
+ let shiftY = 0;
20607
+ if (hq && !axisAligned) {
20608
+ const extentU = Math.hypot(m[0], m[1]) * spec.srcW;
20609
+ const extentV = Math.hypot(m[2], m[3]) * spec.srcH;
20610
+ if (nearSourceExtent(extentU, spec.srcW) || nearSourceExtent(extentV, spec.srcH)) {
20611
+ kernel = kernel === "hq-bicubic" ? "bicubic" : "bilinear";
20612
+ } else {
20613
+ farFade = true;
20614
+ if (spec.halfPixelOffset) {
20615
+ const lu = Math.hypot(m[0], m[1]);
20616
+ const lv = Math.hypot(m[2], m[3]);
20617
+ shiftX = 0.5 * m[0] / lu + 0.5 * m[2] / lv;
20618
+ shiftY = 0.5 * m[1] / lu + 0.5 * m[3] / lv;
20619
+ }
20620
+ }
20621
+ }
20564
20622
  if (hq && axisAligned) {
20565
20623
  const ox = m[0] * spec.srcX + m[4];
20566
20624
  m = [m[0], 0, 0, m[3], m[4] + mirroredOrigin(ox) - ox, m[5]];
@@ -20615,6 +20673,8 @@ function resampleImage(rgba, width, height, spec, surface) {
20615
20673
  const vMax = spec.srcY + spec.srcH;
20616
20674
  const wu = [];
20617
20675
  const wv = [];
20676
+ const gradU = Math.hypot(inv2[0], inv2[2]);
20677
+ const gradV = Math.hypot(inv2[1], inv2[3]);
20618
20678
  const c = spec.clampArgb ?? 0;
20619
20679
  const edge = {
20620
20680
  wrap: spec.wrap,
@@ -20626,8 +20686,8 @@ function resampleImage(rgba, width, height, spec, surface) {
20626
20686
  const py = by0 + j + o;
20627
20687
  for (let i = 0; i < w; i++) {
20628
20688
  const px = bx0 + i + o;
20629
- const u = inv2[0] * px + inv2[2] * py + inv2[4];
20630
- const v = inv2[1] * px + inv2[3] * py + inv2[5];
20689
+ const u = inv2[0] * (px - shiftX) + inv2[2] * (py - shiftY) + inv2[4];
20690
+ const v = inv2[1] * (px - shiftX) + inv2[3] * (py - shiftY) + inv2[5];
20631
20691
  const cx = px + COVERAGE_NUDGE_X;
20632
20692
  const cy = py + COVERAGE_NUDGE_Y;
20633
20693
  const eu = inv2[0] * cx + inv2[2] * cy + inv2[4];
@@ -20635,31 +20695,35 @@ function resampleImage(rgba, width, height, spec, surface) {
20635
20695
  if (eu < spec.srcX || ev < spec.srcY || eu >= uMax || ev >= vMax) {
20636
20696
  continue;
20637
20697
  }
20638
- const cu = u;
20639
- const cv = v;
20640
- const iu0 = Math.ceil(cu - fu.radius);
20641
- const iu1 = Math.floor(cu + fu.radius);
20642
- const iv0 = Math.ceil(cv - fv.radius);
20643
- const iv1 = Math.floor(cv + fv.radius);
20644
- wu.length = 0;
20645
- wv.length = 0;
20646
- for (let t = iu0; t <= iu1; t++) {
20647
- wu.push(fu.weight(t, cu));
20648
- }
20649
- for (let t = iv0; t <= iv1; t++) {
20650
- wv.push(fv.weight(t, cv));
20651
- }
20652
- const [r, g, b, a] = blendSeparable(
20653
- rgba,
20654
- width,
20655
- spec.wrap ? { x0: 0, y0: 0, x1: width, y1: height } : { x0: sx0, y0: sy0, x1: sx1, y1: sy1 },
20656
- iu0,
20657
- wu,
20658
- iv0,
20659
- wv,
20660
- kernel === "bicubic",
20661
- edge
20662
- );
20698
+ const box = { x0: 0, y0: 0, x1: width, y1: height };
20699
+ const sampleAt = (cu, cv) => {
20700
+ const iu0 = Math.ceil(cu - fu.radius);
20701
+ const iu1 = Math.floor(cu + fu.radius);
20702
+ const iv0 = Math.ceil(cv - fv.radius);
20703
+ const iv1 = Math.floor(cv + fv.radius);
20704
+ wu.length = 0;
20705
+ wv.length = 0;
20706
+ for (let t = iu0; t <= iu1; t++) {
20707
+ wu.push(fu.weight(t, cu));
20708
+ }
20709
+ for (let t = iv0; t <= iv1; t++) {
20710
+ wv.push(fv.weight(t, cv));
20711
+ }
20712
+ return blendSeparable(rgba, width, box, iu0, wu, iv0, wv, kernel === "bicubic", edge);
20713
+ };
20714
+ let [r, g, b, a] = sampleAt(u, v);
20715
+ if (farFade && !spec.wrap && a > 0) {
20716
+ const du = (uMax - u) / gradU;
20717
+ const dv = (vMax - v) / gradV;
20718
+ const k = 1 - (du < 1 ? fadeAt(du, kernel === "hq-bilinear") * (sampleAt(uMax, v)[3] / a) : 0) - (dv < 1 ? fadeAt(dv, kernel === "hq-bilinear") * (sampleAt(u, vMax)[3] / a) : 0);
20719
+ if (k < 1) {
20720
+ const f = Math.max(0, k);
20721
+ r *= f;
20722
+ g *= f;
20723
+ b *= f;
20724
+ a *= f;
20725
+ }
20726
+ }
20663
20727
  if (a <= 0) {
20664
20728
  continue;
20665
20729
  }
@@ -23486,7 +23550,8 @@ function effectedDraw(img, bitmap, effect, draw, source) {
23486
23550
  srcY,
23487
23551
  srcW,
23488
23552
  srcH,
23489
- toDevice: mulMatrix(draw.resample.toDevice, shift)
23553
+ toDevice: mulMatrix(draw.resample.toDevice, shift),
23554
+ rightHalo: region.haloRight
23490
23555
  }
23491
23556
  }
23492
23557
  };
@@ -31425,6 +31490,15 @@ function illuminantCube(index) {
31425
31490
  }
31426
31491
  return cube;
31427
31492
  }
31493
+ var ILLUMINANT_MATRICES = {
31494
+ 1: [[1.64737, -0.59174, -0.05494], [0.02963, 0.96126, 881e-5], [0.2911, 0.40189, 0.30769]],
31495
+ 2: [[1.22319, -0.17765, -0.04515], [275e-5, 0.99054, 645e-5], [0.05514, 0.12795, 0.81816]],
31496
+ 3: [[1.02833, -0.01775, -0.01086], [861e-5, 0.98431, 728e-5], [-0.01616, -0.0448, 1.05995]],
31497
+ 4: [[1.17271, -0.14059, -0.03174], [-474e-5, 1.00266, 183e-5], [0.05783, 0.14663, 0.79683]],
31498
+ 5: [[1.10431, -0.08327, -0.02067], [-336e-5, 1.0025, 67e-5], [0.03344, 0.09072, 0.87702]],
31499
+ 7: [[0.92515, 0.05628, 0.01822], [318e-5, 0.9967, 34e-5], [-0.02086, -0.06817, 1.08783]],
31500
+ 8: [[1.31275, -0.26259, -0.04977], [-244e-5, 0.99695, 524e-5], [0.10854, 0.23443, 0.65827]]
31501
+ };
31428
31502
  function hasIlluminant(ca) {
31429
31503
  return illuminantCube(ca.illuminant) !== null;
31430
31504
  }
@@ -31495,9 +31569,9 @@ var BRIGHTNESS_STEP = 0.95625;
31495
31569
  var COLORFULNESS_SCALE = 1 / 100;
31496
31570
  var TINT_RADIANS = -Math.PI / 3;
31497
31571
  var RGB_TO_XYZ = [
31498
- [0.4124, 0.3576, 0.1805],
31499
- [0.2126, 0.7152, 0.0722],
31500
- [0.0193, 0.1192, 0.9505]
31572
+ [0.4123908, 0.3575843, 0.1804808],
31573
+ [0.212639, 0.7151687, 0.0721923],
31574
+ [0.0193308, 0.1191948, 0.9505322]
31501
31575
  ];
31502
31576
  var XYZ_TO_RGB = invert3(RGB_TO_XYZ);
31503
31577
  var WHITE = RGB_TO_XYZ.map((row2) => row2[0] + row2[1] + row2[2]);
@@ -31525,13 +31599,13 @@ function adjustChroma(r, g, b, scale, angle) {
31525
31599
  const z = RGB_TO_XYZ[2][0] * r + RGB_TO_XYZ[2][1] * g + RGB_TO_XYZ[2][2] * b;
31526
31600
  const d = x + 15 * y + 3 * z;
31527
31601
  if (d <= 1e-9 || y <= 1e-9) {
31528
- return [r, g, b];
31602
+ return [clamp255(r), clamp255(g), clamp255(b)];
31529
31603
  }
31530
31604
  const du = 4 * x / d - WHITE_U;
31531
31605
  const dv = 9 * y / d - WHITE_V;
31532
31606
  const cos = Math.cos(angle) * scale;
31533
31607
  const sin = Math.sin(angle) * scale;
31534
- const u = WHITE_U + du * cos - dv * sin;
31608
+ const u = Math.max(0, WHITE_U + du * cos - dv * sin);
31535
31609
  const v = Math.max(1e-6, WHITE_V + du * sin + dv * cos);
31536
31610
  const x2 = y * 9 * u / (4 * v);
31537
31611
  const z2 = y * (12 - 3 * u - 20 * v) / (4 * v);
@@ -31550,6 +31624,7 @@ function adjustChroma(r, g, b, scale, angle) {
31550
31624
  }
31551
31625
  function colorAdjustmentMapper(ca) {
31552
31626
  const illuminant = illuminantCube(ca.illuminant);
31627
+ const matrix = ILLUMINANT_MATRICES[ca.illuminant];
31553
31628
  const gammas = [ca.redGamma / 1e4, ca.greenGamma / 1e4, ca.blueGamma / 1e4];
31554
31629
  const black = ca.referenceBlack / 1e4 * 255;
31555
31630
  const span = Math.max(1e-6, ca.referenceWhite / 1e4 * 255 - black);
@@ -31578,16 +31653,17 @@ function colorAdjustmentMapper(ca) {
31578
31653
  }
31579
31654
  return (rgb2) => {
31580
31655
  let c = [rgb2 >> 16 & 255, rgb2 >> 8 & 255, rgb2 & 255];
31581
- if (illuminant) {
31582
- const mapped = lookupIlluminant(illuminant, c[0] / 255, c[1] / 255, c[2] / 255);
31583
- c = [mapped[0] * 255, mapped[1] * 255, mapped[2] * 255];
31584
- }
31585
31656
  if (chroma) {
31586
- if (!illuminant) {
31587
- c = c.map(exactPaletteLevel);
31657
+ c = c.map(exactPaletteLevel);
31658
+ if (matrix) {
31659
+ const [r, g, b] = c;
31660
+ c = matrix.map((row2) => row2[0] * r + row2[1] * g + row2[2] * b);
31588
31661
  }
31589
- c = adjustChroma(clamp255(c[0]), clamp255(c[1]), clamp255(c[2]), scale, angle);
31662
+ c = adjustChroma(c[0], c[1], c[2], scale, angle);
31590
31663
  c = c.map(roundHalfUp);
31664
+ } else if (illuminant) {
31665
+ const mapped = lookupIlluminant(illuminant, c[0] / 255, c[1] / 255, c[2] / 255);
31666
+ c = [mapped[0] * 255, mapped[1] * 255, mapped[2] * 255];
31591
31667
  }
31592
31668
  return curve(c[0], 0) << 16 | curve(c[1], 1) << 8 | curve(c[2], 2);
31593
31669
  };
@@ -31941,20 +32017,6 @@ function halftoneDespeckle(px, w, h) {
31941
32017
  }
31942
32018
  }
31943
32019
  }
31944
- function halftoneSharpen(px, w, h) {
31945
- const src = px.slice();
31946
- const at = (x, y, c) => src[(Math.max(0, Math.min(h - 1, y)) * w + Math.max(0, Math.min(w - 1, x))) * 3 + c];
31947
- for (let y = 0; y < h; y++) {
31948
- for (let x = 0; x < w; x++) {
31949
- for (let c = 0; c < 3; c++) {
31950
- const v = at(x, y, c);
31951
- const sum = at(x - 1, y, c) + at(x + 1, y, c) + at(x, y - 1, c) + at(x, y + 1, c);
31952
- const out = v + Math.floor((4 * v - sum) / 8);
31953
- px[(y * w + x) * 3 + c] = out < 0 ? 0 : out > 255 ? 255 : out;
31954
- }
31955
- }
31956
- }
31957
- }
31958
32020
  function halftonePairDespeckle(px, w, h, rows) {
31959
32021
  const n = rows ? w : h;
31960
32022
  const at = (k, side) => (rows ? side * w + k : k * w + side) * 3;
@@ -31977,6 +32039,46 @@ function halftoneKeepLast(px, w, h, rows) {
31977
32039
  }
31978
32040
  return out;
31979
32041
  }
32042
+ function halftoneReduceBoth(rect, sw, sh, dw, dh, flipX, flipY) {
32043
+ const cols = halftoneReduceTaps(sw, dw, flipX);
32044
+ const rows = halftoneReduceTaps(sh, dh, flipY);
32045
+ const wide = new Int32Array(dw * sh * 3);
32046
+ for (let y = 0; y < sh; y++) {
32047
+ for (let x = 0; x < dw; x++) {
32048
+ let total = 0;
32049
+ for (const [, w] of cols[x]) total += w;
32050
+ for (let c = 0; c < 3; c++) {
32051
+ let sum = 0;
32052
+ for (const [xx, w] of cols[x]) sum += rect[(y * sw + xx) * 3 + c] * w;
32053
+ wide[(y * dw + x) * 3 + c] = Math.floor((2 * sum + total) / (2 * total));
32054
+ }
32055
+ }
32056
+ }
32057
+ const avg = new Float64Array(dw * dh * 3);
32058
+ for (let y = 0; y < dh; y++) {
32059
+ let total = 0;
32060
+ for (const [, w] of rows[y]) total += w;
32061
+ for (let x = 0; x < dw; x++) {
32062
+ for (let c = 0; c < 3; c++) {
32063
+ let sum = 0;
32064
+ for (const [yy, w] of rows[y]) sum += wide[(yy * dw + x) * 3 + c] * w;
32065
+ avg[(y * dw + x) * 3 + c] = sum / total;
32066
+ }
32067
+ }
32068
+ }
32069
+ const at = (x, y, c) => avg[(Math.max(0, Math.min(dh - 1, y)) * dw + Math.max(0, Math.min(dw - 1, x))) * 3 + c];
32070
+ const out = new Int32Array(dw * dh * 3);
32071
+ for (let y = 0; y < dh; y++) {
32072
+ for (let x = 0; x < dw; x++) {
32073
+ for (let c = 0; c < 3; c++) {
32074
+ const v = at(x, y, c);
32075
+ const sum = at(x - 1, y, c) + at(x + 1, y, c) + at(x, y - 1, c) + at(x, y + 1, c);
32076
+ out[(y * dw + x) * 3 + c] = Math.max(0, Math.min(255, Math.floor(v + (4 * v - sum) / 8)));
32077
+ }
32078
+ }
32079
+ }
32080
+ return out;
32081
+ }
31980
32082
  function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust, adjustAfterSampling = false, directDib = false, dither, curves) {
31981
32083
  const W = Math.max(0, Math.round(Math.abs(dw)));
31982
32084
  const H = Math.max(0, Math.round(Math.abs(dh)));
@@ -32028,11 +32130,18 @@ function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust, adjustAfterSamplin
32028
32130
  }
32029
32131
  const reducedEven = W < SW ? SW % 2 === 0 && H % SH === 0 : SH % 2 === 0 && W % SW === 0;
32030
32132
  const ditherAtSource = dithered && (W >= SW && H >= SH || nativeMixed && reducedEven);
32133
+ const extendRow = ditherAtSource && nativeMixed && W < SW && H > SH && !flipY;
32134
+ if (extendRow) {
32135
+ const bigger = new Int32Array((SH + 1) * SW * 3);
32136
+ bigger.set(rect);
32137
+ bigger.copyWithin(SH * SW * 3, (SH - 1) * SW * 3, SH * SW * 3);
32138
+ rect = bigger;
32139
+ }
32031
32140
  if (ditherAtSource) {
32032
32141
  ditherPixels(
32033
32142
  rect,
32034
32143
  SW,
32035
- SH,
32144
+ extendRow ? SH + 1 : SH,
32036
32145
  dither,
32037
32146
  false,
32038
32147
  false,
@@ -32072,7 +32181,10 @@ function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust, adjustAfterSamplin
32072
32181
  linear(rows, SH, H, flipY);
32073
32182
  }
32074
32183
  let out = nativeMixed ? new Float64Array(sampleW * sampleH * 3) : new Int32Array(W * H * 3);
32075
- for (let y = 0; y < sampleH; y++) {
32184
+ if (reducing) {
32185
+ out = halftoneReduceBoth(rect, SW, SH, W, H, flipX, flipY);
32186
+ }
32187
+ for (let y = 0; !reducing && y < sampleH; y++) {
32076
32188
  const ys = rows[y];
32077
32189
  for (let x = 0; x < sampleW; x++) {
32078
32190
  const xs = cols[x];
@@ -32097,15 +32209,38 @@ function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust, adjustAfterSamplin
32097
32209
  out[o + 2] = round(b);
32098
32210
  }
32099
32211
  }
32100
- if (reducing) {
32101
- halftoneSharpen(out, W, H);
32102
- }
32103
32212
  if (nativeMixed) {
32104
32213
  const reduceHorizontal = W < SW;
32214
+ let extension;
32215
+ if (extendRow) {
32216
+ const reduced = new Float64Array(sampleW * 3);
32217
+ for (let x = 0; x < sampleW; x++) {
32218
+ let total = 0;
32219
+ const sums = [0, 0, 0];
32220
+ for (const [xx, wx] of cols[x]) {
32221
+ total += wx;
32222
+ for (let c = 0; c < 3; c++) sums[c] += rect[(SH * SW + xx) * 3 + c] * wx;
32223
+ }
32224
+ for (let c = 0; c < 3; c++) reduced[x * 3 + c] = sums[c] / total;
32225
+ }
32226
+ extension = reduced;
32227
+ }
32105
32228
  for (const horizontal of [reduceHorizontal, !reduceHorizontal]) {
32106
32229
  const source2 = out;
32107
32230
  out = new Int32Array(source2.length);
32108
- const at = (x, y, c) => source2[(Math.max(0, Math.min(sampleH - 1, y)) * sampleW + Math.max(0, Math.min(sampleW - 1, x))) * 3 + c];
32231
+ const ext = extension;
32232
+ const at = (x, y, c) => y >= sampleH && ext ? ext[Math.max(0, Math.min(sampleW - 1, x)) * 3 + c] : source2[(Math.max(0, Math.min(sampleH - 1, y)) * sampleW + Math.max(0, Math.min(sampleW - 1, x))) * 3 + c];
32233
+ if (horizontal && ext) {
32234
+ const next = new Int32Array(sampleW * 3);
32235
+ for (let x = 0; x < sampleW; x++) {
32236
+ for (let c = 0; c < 3; c++) {
32237
+ const v = ext[x * 3 + c];
32238
+ const sum = at(x - 1, sampleH, c) + at(x + 1, sampleH, c);
32239
+ next[x * 3 + c] = Math.max(0, Math.min(255, Math.floor(v + (2 * v - sum) / 4)));
32240
+ }
32241
+ }
32242
+ extension = next;
32243
+ }
32109
32244
  for (let y = 0; y < sampleH; y++) {
32110
32245
  for (let x = 0; x < sampleW; x++) {
32111
32246
  for (let c = 0; c < 3; c++) {