emf-converter 4.3.2 → 4.3.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
@@ -212,10 +212,10 @@ Everything below is verified against output painted by Windows itself; `src/gdi-
212
212
 
213
213
  ## Limitations
214
214
 
215
- Everything is measured against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the exact per-fixture bounds.
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 resamples with an area-averaging box filter. Integer enlargements match Windows exactly; reductions do not, because Windows' halftone engine uses a narrower kernel and its own colour mapping (1.05% of pixels differ on the fixture). `EMR_SETCOLORADJUSTMENT` is applied to the source of `HALFTONE` `StretchBlt` and `StretchDIBits` calls, as in GDI. Windows does not publish its adjustment formulas, so gamma, reference black/white, contrast, brightness, colourfulness, red-green tint, negative and log filter use documented approximations, and Windows' dithering of the adjusted colours is not reproduced (6.6% of pixels differ by more than 24 on the fixture). 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`).
218
- - **Pen transforms**: nonuniform and skewed EMF+ pen transforms are drawn with the transformed (elliptical or sheared) nib, but unlike uniform ones they are not yet verified against Windows output, and a singular pen transform is ignored.
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`).
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.
221
221
  - **GM_COMPATIBLE recordings**: EMF files do not record the graphics mode, and Windows plays RoundRect, Arc, Chord, Pie and null-pen Ellipse records back differently from how a GM_COMPATIBLE application drew them on screen; the converter follows Windows' playback.
package/dist/index.js CHANGED
@@ -12702,7 +12702,10 @@ function dashFigure(pts, closed, pen) {
12702
12702
  for (let i = 0; i + 1 < path.length; i++) {
12703
12703
  const a = path[i];
12704
12704
  const b = path[i + 1];
12705
- let segLen = Math.hypot(b.x - a.x, b.y - a.y);
12705
+ const m = pen.dashMetric;
12706
+ const vx = b.x - a.x;
12707
+ const vy = b.y - a.y;
12708
+ let segLen = m ? Math.hypot(m[0] * vx + m[2] * vy, m[1] * vx + m[3] * vy) : Math.hypot(vx, vy);
12706
12709
  let t0 = 0;
12707
12710
  while (segLen - t0 > remaining + 1e-9) {
12708
12711
  t0 += remaining;
@@ -12730,6 +12733,26 @@ function dashFigure(pts, closed, pen) {
12730
12733
  }
12731
12734
  return out.filter((d) => d.pts.length > 1);
12732
12735
  }
12736
+ var ARROW_ANCHOR = 20;
12737
+ function arrowAnchor(pp, atEnd, half) {
12738
+ const w = 2 * half;
12739
+ const tip = atEnd ? pp[pp.length - 1] : pp[0];
12740
+ const next = atEnd ? pp[pp.length - 2] : pp[1];
12741
+ const len = Math.hypot(next.x - tip.x, next.y - tip.y);
12742
+ const ux = (next.x - tip.x) / len;
12743
+ const uy = (next.y - tip.y) / len;
12744
+ const depth = 2 * w;
12745
+ const across = 2 * w / Math.sqrt(3);
12746
+ const base = { x: tip.x + ux * depth, y: tip.y + uy * depth };
12747
+ if (len > depth) {
12748
+ if (atEnd) {
12749
+ pp[pp.length - 1] = base;
12750
+ } else {
12751
+ pp[0] = base;
12752
+ }
12753
+ }
12754
+ return [tip, { x: base.x - uy * across, y: base.y + ux * across }, { x: base.x + uy * across, y: base.y - ux * across }];
12755
+ }
12733
12756
  var MIN_DASH = 1 / 256;
12734
12757
  function signedArea(pts) {
12735
12758
  let a = 0;
@@ -12780,6 +12803,19 @@ function widenFigures(figures, pen) {
12780
12803
  if (pp.length < 2) {
12781
12804
  continue;
12782
12805
  }
12806
+ const arrows = [];
12807
+ let startCapOverride = null;
12808
+ let endCapOverride = null;
12809
+ if (!pieceClosed && !(pen.dash && closed)) {
12810
+ if (piece.first && pen.startCap === ARROW_ANCHOR) {
12811
+ arrows.push(arrowAnchor(pp, false, h));
12812
+ startCapOverride = 0;
12813
+ }
12814
+ if (piece.last && pen.endCap === ARROW_ANCHOR) {
12815
+ arrows.push(arrowAnchor(pp, true, h));
12816
+ endCapOverride = 0;
12817
+ }
12818
+ }
12783
12819
  for (const [o1, o2] of bands) {
12784
12820
  if (pieceClosed) {
12785
12821
  polys.push(sidePoints(pp, true, o2, pen));
@@ -12790,11 +12826,16 @@ function widenFigures(figures, pen) {
12790
12826
  const right = sidePoints(pp, false, o1, pen).reverse();
12791
12827
  const fEnd = frame(pp[pp.length - 2], pp[pp.length - 1]);
12792
12828
  const f0 = frame(pp[1], pp[0]);
12793
- const endCap = piece.last && !(pen.dash && closed) ? pen.endCap : pen.dashCap;
12794
- const startCap = piece.first && !(pen.dash && closed) ? pen.startCap : pen.dashCap;
12829
+ const endCap = endCapOverride ?? (piece.last && !(pen.dash && closed) ? pen.endCap : pen.dashCap);
12830
+ const startCap = startCapOverride ?? (piece.first && !(pen.dash && closed) ? pen.startCap : pen.dashCap);
12795
12831
  const capEnd = capPoints(pp[pp.length - 1], fEnd, o1, o2, endCap, h);
12796
12832
  const capStart = capPoints(pp[0], f0, -o2, -o1, startCap, h);
12797
- polys.push([...left.slice(0, -1), ...capEnd, ...right.slice(1, -1), ...capStart]);
12833
+ const poly = [...left.slice(0, -1), ...capEnd, ...right.slice(1, -1), ...capStart];
12834
+ polys.push(poly);
12835
+ const sign = Math.sign(signedArea(poly));
12836
+ for (const arrow of arrows) {
12837
+ polys.push(Math.sign(signedArea(arrow)) === sign ? arrow : arrow.slice().reverse());
12838
+ }
12798
12839
  }
12799
12840
  }
12800
12841
  }
@@ -20085,50 +20126,111 @@ function readColorAdjustment(view, off) {
20085
20126
  function isIdentityColorAdjustment(ca) {
20086
20127
  return !ca || ca.flags === 0 && ca.redGamma === 1e4 && ca.greenGamma === 1e4 && ca.blueGamma === 1e4 && ca.referenceBlack === 0 && ca.referenceWhite === 1e4 && ca.contrast === 0 && ca.brightness === 0 && ca.colorfulness === 0 && ca.redGreenTint === 0;
20087
20128
  }
20088
- function colorfulnessScale(c) {
20089
- return c > 0 ? 1 + 2 * c / 100 : 1 + c / 100;
20129
+ var CONTRAST_GAIN = 1.88;
20130
+ var BRIGHTNESS_GAIN = 78.5;
20131
+ var COLORFULNESS_GAIN = 1.335;
20132
+ var TINT_RADIANS = -1.135;
20133
+ var RGB_TO_XYZ = [
20134
+ [0.4124, 0.3576, 0.1805],
20135
+ [0.2126, 0.7152, 0.0722],
20136
+ [0.0193, 0.1192, 0.9505]
20137
+ ];
20138
+ var XYZ_TO_RGB = invert3(RGB_TO_XYZ);
20139
+ var WHITE = RGB_TO_XYZ.map((row) => row[0] + row[1] + row[2]);
20140
+ var WHITE_D = WHITE[0] + 15 * WHITE[1] + 3 * WHITE[2];
20141
+ var WHITE_U = 4 * WHITE[0] / WHITE_D;
20142
+ var WHITE_V = 9 * WHITE[1] / WHITE_D;
20143
+ function invert3(m) {
20144
+ const [[a, b, c], [d, e, f], [g, h, i]] = m;
20145
+ const A = e * i - f * h;
20146
+ const B = f * g - d * i;
20147
+ const C = d * h - e * g;
20148
+ const det = a * A + b * B + c * C;
20149
+ return [
20150
+ [A / det, (c * h - b * i) / det, (b * f - c * e) / det],
20151
+ [B / det, (a * i - c * g) / det, (c * d - a * f) / det],
20152
+ [C / det, (b * g - a * h) / det, (a * e - b * d) / det]
20153
+ ];
20154
+ }
20155
+ function lightness(t) {
20156
+ return t > 8856e-6 ? 116 * Math.cbrt(t) - 16 : 903.3 * t;
20157
+ }
20158
+ function fromLightness(l) {
20159
+ return l > 8 ? ((l + 16) / 116) ** 3 : l / 903.3;
20090
20160
  }
20091
20161
  var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
20092
20162
  function colorAdjustmentMapper(ca) {
20093
20163
  const black = ca.referenceBlack / 1e4;
20094
20164
  const span = Math.max(1e-6, ca.referenceWhite / 1e4 - black);
20095
20165
  const gammas = [ca.redGamma / 1e4, ca.greenGamma / 1e4, ca.blueGamma / 1e4];
20096
- const curves = gammas.map((gamma) => {
20166
+ const negative = (ca.flags & CA_NEGATIVE) !== 0;
20167
+ const linear = gammas.map((gamma) => {
20097
20168
  const table = new Float64Array(256);
20098
20169
  for (let i = 0; i < 256; i++) {
20099
- let v = clamp012((i / 255 - black) / span);
20100
- v = Math.pow(v, gamma);
20101
- table[i] = ca.flags & CA_NEGATIVE ? 1 - v : v;
20170
+ const v = negative ? 1 - i / 255 : i / 255;
20171
+ table[i] = clamp012((v - black) / span) ** gamma;
20102
20172
  }
20103
20173
  return table;
20104
20174
  });
20105
- const chroma = colorfulnessScale(ca.colorfulness);
20106
- const contrast = 1 + ca.contrast / 100;
20107
- const brightness = 0.8 * ca.brightness / 100;
20108
- const tint = 0.3 * ca.redGreenTint / 100;
20175
+ const chroma = ca.colorfulness < 0 ? 1 + ca.colorfulness / 100 : 1 + COLORFULNESS_GAIN * ca.colorfulness / 100;
20176
+ const angle = TINT_RADIANS * ca.redGreenTint / 100;
20177
+ const cos = Math.cos(angle) * chroma;
20178
+ const sin = Math.sin(angle) * chroma;
20179
+ const slope = Math.exp(CONTRAST_GAIN * ca.contrast / 100);
20180
+ const offset = 50 - 50 * slope + BRIGHTNESS_GAIN * ca.brightness / 100;
20109
20181
  const log = (ca.flags & CA_LOG_FILTER) !== 0;
20110
- const out = (v) => {
20111
- v = clamp012(v);
20182
+ const colour = ca.colorfulness !== 0 || ca.redGreenTint !== 0;
20183
+ const out = (v, c) => {
20184
+ v = clamp012(fromLightness(slope * lightness(clamp012(v)) + offset)) ** (1 / gammas[c]);
20112
20185
  if (log) {
20113
20186
  v = Math.log10(1 + 9 * v);
20114
20187
  }
20115
20188
  return Math.round(v * 255);
20116
20189
  };
20117
20190
  return (rgb2) => {
20118
- const r = curves[0][rgb2 >> 16 & 255];
20119
- const g = curves[1][rgb2 >> 8 & 255];
20120
- const b = curves[2][rgb2 & 255];
20121
- let y = 0.299 * r + 0.587 * g + 0.114 * b;
20122
- let cr = (r - y) * chroma;
20123
- const cb = (b - y) * chroma;
20124
- y = (y - 0.5) * contrast + 0.5 + brightness;
20125
- cr += tint;
20126
- const r2 = y + cr;
20127
- const b2 = y + cb;
20128
- const g2 = (y - 0.299 * r2 - 0.114 * b2) / 0.587;
20129
- return out(r2) << 16 | out(g2) << 8 | out(b2);
20191
+ let r = linear[0][rgb2 >> 16 & 255];
20192
+ let g = linear[1][rgb2 >> 8 & 255];
20193
+ let b = linear[2][rgb2 & 255];
20194
+ if (colour) {
20195
+ const x = RGB_TO_XYZ[0][0] * r + RGB_TO_XYZ[0][1] * g + RGB_TO_XYZ[0][2] * b;
20196
+ const y = RGB_TO_XYZ[1][0] * r + RGB_TO_XYZ[1][1] * g + RGB_TO_XYZ[1][2] * b;
20197
+ const z = RGB_TO_XYZ[2][0] * r + RGB_TO_XYZ[2][1] * g + RGB_TO_XYZ[2][2] * b;
20198
+ const d = x + 15 * y + 3 * z;
20199
+ if (d > 1e-12 && y > 1e-12) {
20200
+ const du = 4 * x / d - WHITE_U;
20201
+ const dv = 9 * y / d - WHITE_V;
20202
+ const u = WHITE_U + du * cos - dv * sin;
20203
+ const v = Math.max(1e-6, WHITE_V + du * sin + dv * cos);
20204
+ const x2 = y * 9 * u / (4 * v);
20205
+ const z2 = y * (12 - 3 * u - 20 * v) / (4 * v);
20206
+ r = XYZ_TO_RGB[0][0] * x2 + XYZ_TO_RGB[0][1] * y + XYZ_TO_RGB[0][2] * z2;
20207
+ g = XYZ_TO_RGB[1][0] * x2 + XYZ_TO_RGB[1][1] * y + XYZ_TO_RGB[1][2] * z2;
20208
+ b = XYZ_TO_RGB[2][0] * x2 + XYZ_TO_RGB[2][1] * y + XYZ_TO_RGB[2][2] * z2;
20209
+ }
20210
+ }
20211
+ return out(r, 0) << 16 | out(g, 1) << 8 | out(b, 2);
20130
20212
  };
20131
20213
  }
20214
+ function colorAdjustRgb(rgbs, ca) {
20215
+ if (!ca || isIdentityColorAdjustment(ca)) {
20216
+ return;
20217
+ }
20218
+ const map = colorAdjustmentMapper(ca);
20219
+ const cache = /* @__PURE__ */ new Map();
20220
+ for (let i = 0; i + 2 < rgbs.length; i += 3) {
20221
+ const rgb2 = rgbs[i] << 16 | rgbs[i + 1] << 8 | rgbs[i + 2];
20222
+ let v = cache.get(rgb2);
20223
+ if (v === void 0) {
20224
+ v = map(rgb2);
20225
+ if (cache.size < 65536) {
20226
+ cache.set(rgb2, v);
20227
+ }
20228
+ }
20229
+ rgbs[i] = v >> 16 & 255;
20230
+ rgbs[i + 1] = v >> 8 & 255;
20231
+ rgbs[i + 2] = v & 255;
20232
+ }
20233
+ }
20132
20234
  function applyColorAdjustment(pixels, ca) {
20133
20235
  if (!ca || isIdentityColorAdjustment(ca)) {
20134
20236
  return;
@@ -20223,64 +20325,160 @@ function stretchGdi(src, sx, sy, sw, sh, dw, dh, mode) {
20223
20325
  }
20224
20326
  return { width: W, height: H, data };
20225
20327
  }
20226
- function axisBoxWeights(srcStart, srcLen, dstLen, reverse) {
20227
- const out = [];
20328
+ var FIX = 65536;
20329
+ function halftoneNearest(i, srcLen, dstLen) {
20330
+ const num = (2 * i + 1) * srcLen;
20331
+ const den = 2 * dstLen;
20332
+ let k = Math.floor(num / den);
20333
+ if (k * den === num) {
20334
+ k--;
20335
+ }
20336
+ return Math.max(0, Math.min(srcLen - 1, k));
20337
+ }
20338
+ function halftoneAxis(start, srcLen, dstLen, reverse) {
20339
+ const taps = [];
20228
20340
  if (dstLen <= 0 || srcLen <= 0) {
20229
- return out;
20341
+ return taps;
20230
20342
  }
20231
- const at = (k) => reverse ? srcStart + srcLen - 1 - k : srcStart + k;
20343
+ const at = (k) => reverse ? start + srcLen - 1 - k : start + k;
20344
+ if (dstLen >= srcLen) {
20345
+ for (let i = 0; i < dstLen; i++) {
20346
+ taps.push([[at(halftoneNearest(i, srcLen, dstLen)), 1]]);
20347
+ }
20348
+ return taps;
20349
+ }
20350
+ const step = Math.ceil(srcLen * FIX / dstLen);
20232
20351
  for (let i = 0; i < dstLen; i++) {
20233
- const a = i * srcLen;
20234
- const b = (i + 1) * srcLen;
20235
- const taps = [];
20236
- for (let k = Math.floor(a / dstLen); k * dstLen < b; k++) {
20237
- const overlap = Math.min(b, (k + 1) * dstLen) - Math.max(a, k * dstLen);
20352
+ const a = i * step;
20353
+ const b = (i + 1) * step;
20354
+ const run = [];
20355
+ for (let k = Math.floor(a / FIX); k * FIX < b && k < srcLen; k++) {
20356
+ const overlap = Math.min(b, (k + 1) * FIX) - Math.max(a, k * FIX);
20238
20357
  if (overlap > 0) {
20239
- taps.push([at(k), overlap / srcLen]);
20358
+ run.push([at(k), overlap]);
20359
+ }
20360
+ }
20361
+ taps.push(run);
20362
+ }
20363
+ return taps;
20364
+ }
20365
+ var SPECKLE_OVERSHOOT = 4;
20366
+ function halftoneDespeckle(px, w, h) {
20367
+ const src = px.slice();
20368
+ const at = (x, y, c) => src[(y * w + x) * 3 + c];
20369
+ const over = (v) => v < -SPECKLE_OVERSHOOT * 4 || v > (255 + SPECKLE_OVERSHOOT) * 4;
20370
+ for (let y = 0; y < h; y++) {
20371
+ const up = y > 0 ? y - 1 : Math.min(h - 1, y + 1);
20372
+ const down = y < h - 1 ? y + 1 : Math.max(0, y - 1);
20373
+ for (let x = 0; x < w; x++) {
20374
+ const left = x > 0 ? x - 1 : Math.min(w - 1, x + 1);
20375
+ const right = x < w - 1 ? x + 1 : Math.max(0, x - 1);
20376
+ let hClip = 0;
20377
+ let vClip = 0;
20378
+ for (let c = 0; c < 3; c++) {
20379
+ const v = at(x, y, c);
20380
+ if (over(6 * v - at(left, y, c) - at(right, y, c))) {
20381
+ hClip++;
20382
+ }
20383
+ if (over(6 * v - at(x, up, c) - at(x, down, c))) {
20384
+ vClip++;
20385
+ }
20386
+ }
20387
+ if (hClip < 2 || vClip < 2) {
20388
+ continue;
20389
+ }
20390
+ for (let c = 0; c < 3; c++) {
20391
+ px[(y * w + x) * 3 + c] = at(left, y, c) + 2 * at(x, y, c) + at(right, y, c) + 2 >> 2;
20392
+ }
20393
+ }
20394
+ }
20395
+ }
20396
+ function halftoneSharpen(px, w, h) {
20397
+ const src = px.slice();
20398
+ 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];
20399
+ for (let y = 0; y < h; y++) {
20400
+ for (let x = 0; x < w; x++) {
20401
+ for (let c = 0; c < 3; c++) {
20402
+ const v = at(x, y, c);
20403
+ const sum = at(x - 1, y, c) + at(x + 1, y, c) + at(x, y - 1, c) + at(x, y + 1, c);
20404
+ const out = v + Math.floor((4 * v - sum) / 8);
20405
+ px[(y * w + x) * 3 + c] = out < 0 ? 0 : out > 255 ? 255 : out;
20240
20406
  }
20241
20407
  }
20242
- out.push(taps);
20243
20408
  }
20244
- return out;
20245
20409
  }
20246
- function stretchHalftone(src, sx, sy, sw, sh, dw, dh) {
20410
+ function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust) {
20247
20411
  const W = Math.max(0, Math.round(Math.abs(dw)));
20248
20412
  const H = Math.max(0, Math.round(Math.abs(dh)));
20413
+ const SW = Math.round(Math.abs(sw));
20414
+ const SH = Math.round(Math.abs(sh));
20415
+ const x0 = Math.round(Math.min(sx, sx + sw));
20416
+ const y0 = Math.round(Math.min(sy, sy + sh));
20249
20417
  const flipX = dw < 0 !== sw < 0;
20250
20418
  const flipY = dh < 0 !== sh < 0;
20251
- const cols = axisBoxWeights(Math.round(Math.min(sx, sx + sw)), Math.round(Math.abs(sw)), W, flipX);
20252
- const rows = axisBoxWeights(Math.round(Math.min(sy, sy + sh)), Math.round(Math.abs(sh)), H, flipY);
20253
20419
  const data = new Uint8ClampedArray(W * H * 4);
20254
- const s = src.data;
20420
+ if (W === 0 || H === 0 || SW === 0 || SH === 0) {
20421
+ return { width: W, height: H, data };
20422
+ }
20423
+ const rect = new Int32Array(SW * SH * 3);
20424
+ for (let y = 0; y < SH; y++) {
20425
+ const yy = y0 + y;
20426
+ for (let x = 0; x < SW; x++) {
20427
+ const xx = x0 + x;
20428
+ if (yy < 0 || yy >= src.height || xx < 0 || xx >= src.width) {
20429
+ continue;
20430
+ }
20431
+ const i = (yy * src.width + xx) * 4;
20432
+ const o = (y * SW + x) * 3;
20433
+ rect[o] = src.data[i];
20434
+ rect[o + 1] = src.data[i + 1];
20435
+ rect[o + 2] = src.data[i + 2];
20436
+ }
20437
+ }
20438
+ const enlarging = W >= SW && H >= SH && (W > SW || H > SH);
20439
+ const reducing = W < SW && H < SH;
20440
+ if (enlarging) {
20441
+ halftoneDespeckle(rect, SW, SH);
20442
+ }
20443
+ if (adjust) {
20444
+ adjust(rect);
20445
+ }
20446
+ const cols = halftoneAxis(0, SW, W, flipX);
20447
+ const rows = halftoneAxis(0, SH, H, flipY);
20448
+ const out = new Int32Array(W * H * 3);
20255
20449
  for (let y = 0; y < H; y++) {
20256
- const ys = rows[y] ?? [];
20450
+ const ys = rows[y];
20257
20451
  for (let x = 0; x < W; x++) {
20258
- const xs = cols[x] ?? [];
20452
+ const xs = cols[x];
20259
20453
  let r = 0;
20260
20454
  let g = 0;
20261
20455
  let b = 0;
20456
+ let total = 0;
20262
20457
  for (const [yy, wy] of ys) {
20263
- if (yy < 0 || yy >= src.height) {
20264
- continue;
20265
- }
20266
20458
  for (const [xx, wx] of xs) {
20267
- if (xx < 0 || xx >= src.width) {
20268
- continue;
20269
- }
20270
20459
  const w = wx * wy;
20271
- const i = (yy * src.width + xx) * 4;
20272
- r += s[i] * w;
20273
- g += s[i + 1] * w;
20274
- b += s[i + 2] * w;
20460
+ total += w;
20461
+ const i = (yy * SW + xx) * 3;
20462
+ r += rect[i] * w;
20463
+ g += rect[i + 1] * w;
20464
+ b += rect[i + 2] * w;
20275
20465
  }
20276
20466
  }
20277
- const o = (y * W + x) * 4;
20278
- data[o] = Math.floor(r + 1e-6);
20279
- data[o + 1] = Math.floor(g + 1e-6);
20280
- data[o + 2] = Math.floor(b + 1e-6);
20281
- data[o + 3] = 255;
20467
+ const o = (y * W + x) * 3;
20468
+ out[o] = Math.floor((2 * r + total) / (2 * total));
20469
+ out[o + 1] = Math.floor((2 * g + total) / (2 * total));
20470
+ out[o + 2] = Math.floor((2 * b + total) / (2 * total));
20282
20471
  }
20283
20472
  }
20473
+ if (reducing) {
20474
+ halftoneSharpen(out, W, H);
20475
+ }
20476
+ for (let i = 0, o = 0; i < W * H; i++, o += 3) {
20477
+ data[i * 4] = out[o];
20478
+ data[i * 4 + 1] = out[o + 1];
20479
+ data[i * 4 + 2] = out[o + 2];
20480
+ data[i * 4 + 3] = 255;
20481
+ }
20284
20482
  return { width: W, height: H, data };
20285
20483
  }
20286
20484
 
@@ -20306,7 +20504,7 @@ function patternOperand(rCtx, brush) {
20306
20504
  function drawSourceMapped(target, decoded, req, offsetX, offsetY, mode) {
20307
20505
  const dLeft = Math.min(req.dx, req.dx + req.dw) - offsetX;
20308
20506
  const dTop = Math.min(req.dy, req.dy + req.dh) - offsetY;
20309
- const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20507
+ const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, decoded.adjust) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20310
20508
  if (px.width === 0 || px.height === 0) {
20311
20509
  return;
20312
20510
  }
@@ -20334,15 +20532,14 @@ function decodeSource(rCtx, req) {
20334
20532
  if (!image) {
20335
20533
  return null;
20336
20534
  }
20337
- if (req.stretch && rCtx.state.stretchBltMode === HALFTONE) {
20338
- applyColorAdjustment(image, rCtx.state.colorAdjustment);
20339
- }
20535
+ const ca = rCtx.state.colorAdjustment;
20536
+ const adjust = req.stretch && rCtx.state.stretchBltMode === HALFTONE && ca ? (rgb2) => colorAdjustRgb(rgb2, ca) : void 0;
20340
20537
  let { sy } = req;
20341
20538
  if (req.dibOrigin === "bottom-left" && rCtx.view.getInt32(req.source.bmi + 8, true) > 0) {
20342
20539
  sy = image.height - sy - req.sh;
20343
20540
  }
20344
20541
  return {
20345
- decoded: { pixels: image },
20542
+ decoded: { pixels: image, adjust },
20346
20543
  req: { ...req, sy }
20347
20544
  };
20348
20545
  }
@@ -20498,6 +20695,9 @@ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, s
20498
20695
  return;
20499
20696
  }
20500
20697
  src = decoded.decoded.pixels;
20698
+ if (decoded.decoded.adjust) {
20699
+ applyColorAdjustment(src, rCtx.state.colorAdjustment);
20700
+ }
20501
20701
  srcX = decoded.req.sx;
20502
20702
  srcY = decoded.req.sy;
20503
20703
  }
@@ -23965,12 +24165,12 @@ function hslToRgb(h, s, l) {
23965
24165
  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];
23966
24166
  return [r + m, g + m, b + m];
23967
24167
  }
23968
- function applyHueSaturationLightness(src, hue, saturation, lightness) {
24168
+ function applyHueSaturationLightness(src, hue, saturation, lightness2) {
23969
24169
  const out = new Uint8ClampedArray(src);
23970
24170
  const push = (v, t) => t >= 0 ? v + (1 - v) * (t / 100) : v * (1 + t / 100);
23971
24171
  for (let i = 0; i < src.length; i += 4) {
23972
24172
  const [h, s, l] = rgbToHsl(src[i] / 255, src[i + 1] / 255, src[i + 2] / 255);
23973
- const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness));
24173
+ const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness2));
23974
24174
  out[i] = Math.round(r * 255);
23975
24175
  out[i + 1] = Math.round(g * 255);
23976
24176
  out[i + 2] = Math.round(b * 255);
@@ -24661,6 +24861,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24661
24861
  }
24662
24862
  };
24663
24863
  if (pen) {
24864
+ const nibDashed = !!nib && penDashArray(pen).length > 0;
24664
24865
  applyPlusPenStyle(ctx, pen, widthScale);
24665
24866
  const mode = plusRasterMode(rCtx);
24666
24867
  let sampler = pen.brush && !isSvgContext(ctx) ? brushSampler(rCtx, pen.brush) : null;
@@ -24675,7 +24876,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24675
24876
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24676
24877
  return;
24677
24878
  }
24678
- const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24879
+ const outline = hasCustomCap(pen) || nibDashed ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24679
24880
  let box = null;
24680
24881
  if (size && outline) {
24681
24882
  box = deviceBounds(outline.flat(), device, size, 2);
@@ -24705,7 +24906,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24705
24906
  }
24706
24907
  }
24707
24908
  const paint = pen.brush ? brushPaint2(rCtx, pen.brush) : pen.color;
24708
- if ((hasCustomCap(pen) || nib && typeof paint !== "string") && fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure)) {
24909
+ if ((hasCustomCap(pen) || nibDashed || nib && typeof paint !== "string") && fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure)) {
24709
24910
  return;
24710
24911
  }
24711
24912
  ctx.strokeStyle = paint;
@@ -24727,7 +24928,9 @@ function resolvePenTransform(pen) {
24727
24928
  return { pen: { ...plain, width: pen.width === 0 ? 1 : pen.width }, nib: null };
24728
24929
  }
24729
24930
  if (Math.abs(sx - sy) <= 1e-6 * sx && Math.abs(dot) <= 1e-6 * sx * sy) {
24730
- return { pen: { ...plain, width: pen.width * sx }, nib: null };
24931
+ const pattern = pen.dashStyle === 5 ? pen.dashPattern : DASH_PATTERNS[pen.dashStyle];
24932
+ const dashed = pattern && pattern.length > 0 ? { dashStyle: 5, dashPattern: pattern.map((v) => v / sx), dashOffset: (pen.dashOffset ?? 0) / sx } : {};
24933
+ return { pen: { ...plain, ...dashed, width: pen.width * sx }, nib: null };
24731
24934
  }
24732
24935
  return { pen: plain, nib: [a, b, c, d, 0, 0] };
24733
24936
  }
@@ -24752,7 +24955,7 @@ function penFrame(device, nib) {
24752
24955
  toDevice: mulMatrix(full, [1 / unit, 0, 0, 1 / unit, 0, 0])
24753
24956
  };
24754
24957
  }
24755
- function framePen(pen, unit) {
24958
+ function framePen(pen, unit, nib = null) {
24756
24959
  const dash = penDashArray(pen).map((v) => v * unit);
24757
24960
  return {
24758
24961
  half: (pen.width || 1) * unit / 2,
@@ -24763,6 +24966,7 @@ function framePen(pen, unit) {
24763
24966
  dashCap: pen.dashCap ?? 0,
24764
24967
  dash: dash.length > 0 ? dash : null,
24765
24968
  dashOffset: (pen.dashOffset ?? 0) * (pen.width || 1) * unit,
24969
+ dashMetric: nib ? [nib[0], nib[1], nib[2], nib[3]] : void 0,
24766
24970
  compound: pen.compound ?? null,
24767
24971
  inset: pen.alignment === 1
24768
24972
  };
@@ -24790,7 +24994,7 @@ function strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, c
24790
24994
  figures = figures.map((f) => ({ ...f, closed: true }));
24791
24995
  }
24792
24996
  const width = (pen.width || 1) * frame2.unit;
24793
- const devicePen = framePen(pen, frame2.unit);
24997
+ const devicePen = framePen(pen, frame2.unit, nib);
24794
24998
  const antialias = mode === "gdiplus-aa";
24795
24999
  const toFix = (poly) => {
24796
25000
  const m = frame2.toDevice;
@@ -24882,7 +25086,7 @@ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24882
25086
  figures = figures.map((f) => ({ ...f, closed: true }));
24883
25087
  }
24884
25088
  const width = (pen.width || 1) * frame2.unit;
24885
- const devicePen = framePen(pen, frame2.unit);
25089
+ const devicePen = framePen(pen, frame2.unit, nib);
24886
25090
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24887
25091
  const m = mulMatrix(deviceInv, frame2.toDevice);
24888
25092
  return outline.map((poly) => poly.map((p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] })));
package/dist/index.mjs CHANGED
@@ -12700,7 +12700,10 @@ function dashFigure(pts, closed, pen) {
12700
12700
  for (let i = 0; i + 1 < path.length; i++) {
12701
12701
  const a = path[i];
12702
12702
  const b = path[i + 1];
12703
- let segLen = Math.hypot(b.x - a.x, b.y - a.y);
12703
+ const m = pen.dashMetric;
12704
+ const vx = b.x - a.x;
12705
+ const vy = b.y - a.y;
12706
+ let segLen = m ? Math.hypot(m[0] * vx + m[2] * vy, m[1] * vx + m[3] * vy) : Math.hypot(vx, vy);
12704
12707
  let t0 = 0;
12705
12708
  while (segLen - t0 > remaining + 1e-9) {
12706
12709
  t0 += remaining;
@@ -12728,6 +12731,26 @@ function dashFigure(pts, closed, pen) {
12728
12731
  }
12729
12732
  return out.filter((d) => d.pts.length > 1);
12730
12733
  }
12734
+ var ARROW_ANCHOR = 20;
12735
+ function arrowAnchor(pp, atEnd, half) {
12736
+ const w = 2 * half;
12737
+ const tip = atEnd ? pp[pp.length - 1] : pp[0];
12738
+ const next = atEnd ? pp[pp.length - 2] : pp[1];
12739
+ const len = Math.hypot(next.x - tip.x, next.y - tip.y);
12740
+ const ux = (next.x - tip.x) / len;
12741
+ const uy = (next.y - tip.y) / len;
12742
+ const depth = 2 * w;
12743
+ const across = 2 * w / Math.sqrt(3);
12744
+ const base = { x: tip.x + ux * depth, y: tip.y + uy * depth };
12745
+ if (len > depth) {
12746
+ if (atEnd) {
12747
+ pp[pp.length - 1] = base;
12748
+ } else {
12749
+ pp[0] = base;
12750
+ }
12751
+ }
12752
+ return [tip, { x: base.x - uy * across, y: base.y + ux * across }, { x: base.x + uy * across, y: base.y - ux * across }];
12753
+ }
12731
12754
  var MIN_DASH = 1 / 256;
12732
12755
  function signedArea(pts) {
12733
12756
  let a = 0;
@@ -12778,6 +12801,19 @@ function widenFigures(figures, pen) {
12778
12801
  if (pp.length < 2) {
12779
12802
  continue;
12780
12803
  }
12804
+ const arrows = [];
12805
+ let startCapOverride = null;
12806
+ let endCapOverride = null;
12807
+ if (!pieceClosed && !(pen.dash && closed)) {
12808
+ if (piece.first && pen.startCap === ARROW_ANCHOR) {
12809
+ arrows.push(arrowAnchor(pp, false, h));
12810
+ startCapOverride = 0;
12811
+ }
12812
+ if (piece.last && pen.endCap === ARROW_ANCHOR) {
12813
+ arrows.push(arrowAnchor(pp, true, h));
12814
+ endCapOverride = 0;
12815
+ }
12816
+ }
12781
12817
  for (const [o1, o2] of bands) {
12782
12818
  if (pieceClosed) {
12783
12819
  polys.push(sidePoints(pp, true, o2, pen));
@@ -12788,11 +12824,16 @@ function widenFigures(figures, pen) {
12788
12824
  const right = sidePoints(pp, false, o1, pen).reverse();
12789
12825
  const fEnd = frame(pp[pp.length - 2], pp[pp.length - 1]);
12790
12826
  const f0 = frame(pp[1], pp[0]);
12791
- const endCap = piece.last && !(pen.dash && closed) ? pen.endCap : pen.dashCap;
12792
- const startCap = piece.first && !(pen.dash && closed) ? pen.startCap : pen.dashCap;
12827
+ const endCap = endCapOverride ?? (piece.last && !(pen.dash && closed) ? pen.endCap : pen.dashCap);
12828
+ const startCap = startCapOverride ?? (piece.first && !(pen.dash && closed) ? pen.startCap : pen.dashCap);
12793
12829
  const capEnd = capPoints(pp[pp.length - 1], fEnd, o1, o2, endCap, h);
12794
12830
  const capStart = capPoints(pp[0], f0, -o2, -o1, startCap, h);
12795
- polys.push([...left.slice(0, -1), ...capEnd, ...right.slice(1, -1), ...capStart]);
12831
+ const poly = [...left.slice(0, -1), ...capEnd, ...right.slice(1, -1), ...capStart];
12832
+ polys.push(poly);
12833
+ const sign = Math.sign(signedArea(poly));
12834
+ for (const arrow of arrows) {
12835
+ polys.push(Math.sign(signedArea(arrow)) === sign ? arrow : arrow.slice().reverse());
12836
+ }
12796
12837
  }
12797
12838
  }
12798
12839
  }
@@ -20083,50 +20124,111 @@ function readColorAdjustment(view, off) {
20083
20124
  function isIdentityColorAdjustment(ca) {
20084
20125
  return !ca || ca.flags === 0 && ca.redGamma === 1e4 && ca.greenGamma === 1e4 && ca.blueGamma === 1e4 && ca.referenceBlack === 0 && ca.referenceWhite === 1e4 && ca.contrast === 0 && ca.brightness === 0 && ca.colorfulness === 0 && ca.redGreenTint === 0;
20085
20126
  }
20086
- function colorfulnessScale(c) {
20087
- return c > 0 ? 1 + 2 * c / 100 : 1 + c / 100;
20127
+ var CONTRAST_GAIN = 1.88;
20128
+ var BRIGHTNESS_GAIN = 78.5;
20129
+ var COLORFULNESS_GAIN = 1.335;
20130
+ var TINT_RADIANS = -1.135;
20131
+ var RGB_TO_XYZ = [
20132
+ [0.4124, 0.3576, 0.1805],
20133
+ [0.2126, 0.7152, 0.0722],
20134
+ [0.0193, 0.1192, 0.9505]
20135
+ ];
20136
+ var XYZ_TO_RGB = invert3(RGB_TO_XYZ);
20137
+ var WHITE = RGB_TO_XYZ.map((row) => row[0] + row[1] + row[2]);
20138
+ var WHITE_D = WHITE[0] + 15 * WHITE[1] + 3 * WHITE[2];
20139
+ var WHITE_U = 4 * WHITE[0] / WHITE_D;
20140
+ var WHITE_V = 9 * WHITE[1] / WHITE_D;
20141
+ function invert3(m) {
20142
+ const [[a, b, c], [d, e, f], [g, h, i]] = m;
20143
+ const A = e * i - f * h;
20144
+ const B = f * g - d * i;
20145
+ const C = d * h - e * g;
20146
+ const det = a * A + b * B + c * C;
20147
+ return [
20148
+ [A / det, (c * h - b * i) / det, (b * f - c * e) / det],
20149
+ [B / det, (a * i - c * g) / det, (c * d - a * f) / det],
20150
+ [C / det, (b * g - a * h) / det, (a * e - b * d) / det]
20151
+ ];
20152
+ }
20153
+ function lightness(t) {
20154
+ return t > 8856e-6 ? 116 * Math.cbrt(t) - 16 : 903.3 * t;
20155
+ }
20156
+ function fromLightness(l) {
20157
+ return l > 8 ? ((l + 16) / 116) ** 3 : l / 903.3;
20088
20158
  }
20089
20159
  var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
20090
20160
  function colorAdjustmentMapper(ca) {
20091
20161
  const black = ca.referenceBlack / 1e4;
20092
20162
  const span = Math.max(1e-6, ca.referenceWhite / 1e4 - black);
20093
20163
  const gammas = [ca.redGamma / 1e4, ca.greenGamma / 1e4, ca.blueGamma / 1e4];
20094
- const curves = gammas.map((gamma) => {
20164
+ const negative = (ca.flags & CA_NEGATIVE) !== 0;
20165
+ const linear = gammas.map((gamma) => {
20095
20166
  const table = new Float64Array(256);
20096
20167
  for (let i = 0; i < 256; i++) {
20097
- let v = clamp012((i / 255 - black) / span);
20098
- v = Math.pow(v, gamma);
20099
- table[i] = ca.flags & CA_NEGATIVE ? 1 - v : v;
20168
+ const v = negative ? 1 - i / 255 : i / 255;
20169
+ table[i] = clamp012((v - black) / span) ** gamma;
20100
20170
  }
20101
20171
  return table;
20102
20172
  });
20103
- const chroma = colorfulnessScale(ca.colorfulness);
20104
- const contrast = 1 + ca.contrast / 100;
20105
- const brightness = 0.8 * ca.brightness / 100;
20106
- const tint = 0.3 * ca.redGreenTint / 100;
20173
+ const chroma = ca.colorfulness < 0 ? 1 + ca.colorfulness / 100 : 1 + COLORFULNESS_GAIN * ca.colorfulness / 100;
20174
+ const angle = TINT_RADIANS * ca.redGreenTint / 100;
20175
+ const cos = Math.cos(angle) * chroma;
20176
+ const sin = Math.sin(angle) * chroma;
20177
+ const slope = Math.exp(CONTRAST_GAIN * ca.contrast / 100);
20178
+ const offset = 50 - 50 * slope + BRIGHTNESS_GAIN * ca.brightness / 100;
20107
20179
  const log = (ca.flags & CA_LOG_FILTER) !== 0;
20108
- const out = (v) => {
20109
- v = clamp012(v);
20180
+ const colour = ca.colorfulness !== 0 || ca.redGreenTint !== 0;
20181
+ const out = (v, c) => {
20182
+ v = clamp012(fromLightness(slope * lightness(clamp012(v)) + offset)) ** (1 / gammas[c]);
20110
20183
  if (log) {
20111
20184
  v = Math.log10(1 + 9 * v);
20112
20185
  }
20113
20186
  return Math.round(v * 255);
20114
20187
  };
20115
20188
  return (rgb2) => {
20116
- const r = curves[0][rgb2 >> 16 & 255];
20117
- const g = curves[1][rgb2 >> 8 & 255];
20118
- const b = curves[2][rgb2 & 255];
20119
- let y = 0.299 * r + 0.587 * g + 0.114 * b;
20120
- let cr = (r - y) * chroma;
20121
- const cb = (b - y) * chroma;
20122
- y = (y - 0.5) * contrast + 0.5 + brightness;
20123
- cr += tint;
20124
- const r2 = y + cr;
20125
- const b2 = y + cb;
20126
- const g2 = (y - 0.299 * r2 - 0.114 * b2) / 0.587;
20127
- return out(r2) << 16 | out(g2) << 8 | out(b2);
20189
+ let r = linear[0][rgb2 >> 16 & 255];
20190
+ let g = linear[1][rgb2 >> 8 & 255];
20191
+ let b = linear[2][rgb2 & 255];
20192
+ if (colour) {
20193
+ const x = RGB_TO_XYZ[0][0] * r + RGB_TO_XYZ[0][1] * g + RGB_TO_XYZ[0][2] * b;
20194
+ const y = RGB_TO_XYZ[1][0] * r + RGB_TO_XYZ[1][1] * g + RGB_TO_XYZ[1][2] * b;
20195
+ const z = RGB_TO_XYZ[2][0] * r + RGB_TO_XYZ[2][1] * g + RGB_TO_XYZ[2][2] * b;
20196
+ const d = x + 15 * y + 3 * z;
20197
+ if (d > 1e-12 && y > 1e-12) {
20198
+ const du = 4 * x / d - WHITE_U;
20199
+ const dv = 9 * y / d - WHITE_V;
20200
+ const u = WHITE_U + du * cos - dv * sin;
20201
+ const v = Math.max(1e-6, WHITE_V + du * sin + dv * cos);
20202
+ const x2 = y * 9 * u / (4 * v);
20203
+ const z2 = y * (12 - 3 * u - 20 * v) / (4 * v);
20204
+ r = XYZ_TO_RGB[0][0] * x2 + XYZ_TO_RGB[0][1] * y + XYZ_TO_RGB[0][2] * z2;
20205
+ g = XYZ_TO_RGB[1][0] * x2 + XYZ_TO_RGB[1][1] * y + XYZ_TO_RGB[1][2] * z2;
20206
+ b = XYZ_TO_RGB[2][0] * x2 + XYZ_TO_RGB[2][1] * y + XYZ_TO_RGB[2][2] * z2;
20207
+ }
20208
+ }
20209
+ return out(r, 0) << 16 | out(g, 1) << 8 | out(b, 2);
20128
20210
  };
20129
20211
  }
20212
+ function colorAdjustRgb(rgbs, ca) {
20213
+ if (!ca || isIdentityColorAdjustment(ca)) {
20214
+ return;
20215
+ }
20216
+ const map = colorAdjustmentMapper(ca);
20217
+ const cache = /* @__PURE__ */ new Map();
20218
+ for (let i = 0; i + 2 < rgbs.length; i += 3) {
20219
+ const rgb2 = rgbs[i] << 16 | rgbs[i + 1] << 8 | rgbs[i + 2];
20220
+ let v = cache.get(rgb2);
20221
+ if (v === void 0) {
20222
+ v = map(rgb2);
20223
+ if (cache.size < 65536) {
20224
+ cache.set(rgb2, v);
20225
+ }
20226
+ }
20227
+ rgbs[i] = v >> 16 & 255;
20228
+ rgbs[i + 1] = v >> 8 & 255;
20229
+ rgbs[i + 2] = v & 255;
20230
+ }
20231
+ }
20130
20232
  function applyColorAdjustment(pixels, ca) {
20131
20233
  if (!ca || isIdentityColorAdjustment(ca)) {
20132
20234
  return;
@@ -20221,64 +20323,160 @@ function stretchGdi(src, sx, sy, sw, sh, dw, dh, mode) {
20221
20323
  }
20222
20324
  return { width: W, height: H, data };
20223
20325
  }
20224
- function axisBoxWeights(srcStart, srcLen, dstLen, reverse) {
20225
- const out = [];
20326
+ var FIX = 65536;
20327
+ function halftoneNearest(i, srcLen, dstLen) {
20328
+ const num = (2 * i + 1) * srcLen;
20329
+ const den = 2 * dstLen;
20330
+ let k = Math.floor(num / den);
20331
+ if (k * den === num) {
20332
+ k--;
20333
+ }
20334
+ return Math.max(0, Math.min(srcLen - 1, k));
20335
+ }
20336
+ function halftoneAxis(start, srcLen, dstLen, reverse) {
20337
+ const taps = [];
20226
20338
  if (dstLen <= 0 || srcLen <= 0) {
20227
- return out;
20339
+ return taps;
20228
20340
  }
20229
- const at = (k) => reverse ? srcStart + srcLen - 1 - k : srcStart + k;
20341
+ const at = (k) => reverse ? start + srcLen - 1 - k : start + k;
20342
+ if (dstLen >= srcLen) {
20343
+ for (let i = 0; i < dstLen; i++) {
20344
+ taps.push([[at(halftoneNearest(i, srcLen, dstLen)), 1]]);
20345
+ }
20346
+ return taps;
20347
+ }
20348
+ const step = Math.ceil(srcLen * FIX / dstLen);
20230
20349
  for (let i = 0; i < dstLen; i++) {
20231
- const a = i * srcLen;
20232
- const b = (i + 1) * srcLen;
20233
- const taps = [];
20234
- for (let k = Math.floor(a / dstLen); k * dstLen < b; k++) {
20235
- const overlap = Math.min(b, (k + 1) * dstLen) - Math.max(a, k * dstLen);
20350
+ const a = i * step;
20351
+ const b = (i + 1) * step;
20352
+ const run = [];
20353
+ for (let k = Math.floor(a / FIX); k * FIX < b && k < srcLen; k++) {
20354
+ const overlap = Math.min(b, (k + 1) * FIX) - Math.max(a, k * FIX);
20236
20355
  if (overlap > 0) {
20237
- taps.push([at(k), overlap / srcLen]);
20356
+ run.push([at(k), overlap]);
20357
+ }
20358
+ }
20359
+ taps.push(run);
20360
+ }
20361
+ return taps;
20362
+ }
20363
+ var SPECKLE_OVERSHOOT = 4;
20364
+ function halftoneDespeckle(px, w, h) {
20365
+ const src = px.slice();
20366
+ const at = (x, y, c) => src[(y * w + x) * 3 + c];
20367
+ const over = (v) => v < -SPECKLE_OVERSHOOT * 4 || v > (255 + SPECKLE_OVERSHOOT) * 4;
20368
+ for (let y = 0; y < h; y++) {
20369
+ const up = y > 0 ? y - 1 : Math.min(h - 1, y + 1);
20370
+ const down = y < h - 1 ? y + 1 : Math.max(0, y - 1);
20371
+ for (let x = 0; x < w; x++) {
20372
+ const left = x > 0 ? x - 1 : Math.min(w - 1, x + 1);
20373
+ const right = x < w - 1 ? x + 1 : Math.max(0, x - 1);
20374
+ let hClip = 0;
20375
+ let vClip = 0;
20376
+ for (let c = 0; c < 3; c++) {
20377
+ const v = at(x, y, c);
20378
+ if (over(6 * v - at(left, y, c) - at(right, y, c))) {
20379
+ hClip++;
20380
+ }
20381
+ if (over(6 * v - at(x, up, c) - at(x, down, c))) {
20382
+ vClip++;
20383
+ }
20384
+ }
20385
+ if (hClip < 2 || vClip < 2) {
20386
+ continue;
20387
+ }
20388
+ for (let c = 0; c < 3; c++) {
20389
+ px[(y * w + x) * 3 + c] = at(left, y, c) + 2 * at(x, y, c) + at(right, y, c) + 2 >> 2;
20390
+ }
20391
+ }
20392
+ }
20393
+ }
20394
+ function halftoneSharpen(px, w, h) {
20395
+ const src = px.slice();
20396
+ 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];
20397
+ for (let y = 0; y < h; y++) {
20398
+ for (let x = 0; x < w; x++) {
20399
+ for (let c = 0; c < 3; c++) {
20400
+ const v = at(x, y, c);
20401
+ const sum = at(x - 1, y, c) + at(x + 1, y, c) + at(x, y - 1, c) + at(x, y + 1, c);
20402
+ const out = v + Math.floor((4 * v - sum) / 8);
20403
+ px[(y * w + x) * 3 + c] = out < 0 ? 0 : out > 255 ? 255 : out;
20238
20404
  }
20239
20405
  }
20240
- out.push(taps);
20241
20406
  }
20242
- return out;
20243
20407
  }
20244
- function stretchHalftone(src, sx, sy, sw, sh, dw, dh) {
20408
+ function stretchHalftone(src, sx, sy, sw, sh, dw, dh, adjust) {
20245
20409
  const W = Math.max(0, Math.round(Math.abs(dw)));
20246
20410
  const H = Math.max(0, Math.round(Math.abs(dh)));
20411
+ const SW = Math.round(Math.abs(sw));
20412
+ const SH = Math.round(Math.abs(sh));
20413
+ const x0 = Math.round(Math.min(sx, sx + sw));
20414
+ const y0 = Math.round(Math.min(sy, sy + sh));
20247
20415
  const flipX = dw < 0 !== sw < 0;
20248
20416
  const flipY = dh < 0 !== sh < 0;
20249
- const cols = axisBoxWeights(Math.round(Math.min(sx, sx + sw)), Math.round(Math.abs(sw)), W, flipX);
20250
- const rows = axisBoxWeights(Math.round(Math.min(sy, sy + sh)), Math.round(Math.abs(sh)), H, flipY);
20251
20417
  const data = new Uint8ClampedArray(W * H * 4);
20252
- const s = src.data;
20418
+ if (W === 0 || H === 0 || SW === 0 || SH === 0) {
20419
+ return { width: W, height: H, data };
20420
+ }
20421
+ const rect = new Int32Array(SW * SH * 3);
20422
+ for (let y = 0; y < SH; y++) {
20423
+ const yy = y0 + y;
20424
+ for (let x = 0; x < SW; x++) {
20425
+ const xx = x0 + x;
20426
+ if (yy < 0 || yy >= src.height || xx < 0 || xx >= src.width) {
20427
+ continue;
20428
+ }
20429
+ const i = (yy * src.width + xx) * 4;
20430
+ const o = (y * SW + x) * 3;
20431
+ rect[o] = src.data[i];
20432
+ rect[o + 1] = src.data[i + 1];
20433
+ rect[o + 2] = src.data[i + 2];
20434
+ }
20435
+ }
20436
+ const enlarging = W >= SW && H >= SH && (W > SW || H > SH);
20437
+ const reducing = W < SW && H < SH;
20438
+ if (enlarging) {
20439
+ halftoneDespeckle(rect, SW, SH);
20440
+ }
20441
+ if (adjust) {
20442
+ adjust(rect);
20443
+ }
20444
+ const cols = halftoneAxis(0, SW, W, flipX);
20445
+ const rows = halftoneAxis(0, SH, H, flipY);
20446
+ const out = new Int32Array(W * H * 3);
20253
20447
  for (let y = 0; y < H; y++) {
20254
- const ys = rows[y] ?? [];
20448
+ const ys = rows[y];
20255
20449
  for (let x = 0; x < W; x++) {
20256
- const xs = cols[x] ?? [];
20450
+ const xs = cols[x];
20257
20451
  let r = 0;
20258
20452
  let g = 0;
20259
20453
  let b = 0;
20454
+ let total = 0;
20260
20455
  for (const [yy, wy] of ys) {
20261
- if (yy < 0 || yy >= src.height) {
20262
- continue;
20263
- }
20264
20456
  for (const [xx, wx] of xs) {
20265
- if (xx < 0 || xx >= src.width) {
20266
- continue;
20267
- }
20268
20457
  const w = wx * wy;
20269
- const i = (yy * src.width + xx) * 4;
20270
- r += s[i] * w;
20271
- g += s[i + 1] * w;
20272
- b += s[i + 2] * w;
20458
+ total += w;
20459
+ const i = (yy * SW + xx) * 3;
20460
+ r += rect[i] * w;
20461
+ g += rect[i + 1] * w;
20462
+ b += rect[i + 2] * w;
20273
20463
  }
20274
20464
  }
20275
- const o = (y * W + x) * 4;
20276
- data[o] = Math.floor(r + 1e-6);
20277
- data[o + 1] = Math.floor(g + 1e-6);
20278
- data[o + 2] = Math.floor(b + 1e-6);
20279
- data[o + 3] = 255;
20465
+ const o = (y * W + x) * 3;
20466
+ out[o] = Math.floor((2 * r + total) / (2 * total));
20467
+ out[o + 1] = Math.floor((2 * g + total) / (2 * total));
20468
+ out[o + 2] = Math.floor((2 * b + total) / (2 * total));
20280
20469
  }
20281
20470
  }
20471
+ if (reducing) {
20472
+ halftoneSharpen(out, W, H);
20473
+ }
20474
+ for (let i = 0, o = 0; i < W * H; i++, o += 3) {
20475
+ data[i * 4] = out[o];
20476
+ data[i * 4 + 1] = out[o + 1];
20477
+ data[i * 4 + 2] = out[o + 2];
20478
+ data[i * 4 + 3] = 255;
20479
+ }
20282
20480
  return { width: W, height: H, data };
20283
20481
  }
20284
20482
 
@@ -20304,7 +20502,7 @@ function patternOperand(rCtx, brush) {
20304
20502
  function drawSourceMapped(target, decoded, req, offsetX, offsetY, mode) {
20305
20503
  const dLeft = Math.min(req.dx, req.dx + req.dw) - offsetX;
20306
20504
  const dTop = Math.min(req.dy, req.dy + req.dh) - offsetY;
20307
- const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20505
+ const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, decoded.adjust) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20308
20506
  if (px.width === 0 || px.height === 0) {
20309
20507
  return;
20310
20508
  }
@@ -20332,15 +20530,14 @@ function decodeSource(rCtx, req) {
20332
20530
  if (!image) {
20333
20531
  return null;
20334
20532
  }
20335
- if (req.stretch && rCtx.state.stretchBltMode === HALFTONE) {
20336
- applyColorAdjustment(image, rCtx.state.colorAdjustment);
20337
- }
20533
+ const ca = rCtx.state.colorAdjustment;
20534
+ const adjust = req.stretch && rCtx.state.stretchBltMode === HALFTONE && ca ? (rgb2) => colorAdjustRgb(rgb2, ca) : void 0;
20338
20535
  let { sy } = req;
20339
20536
  if (req.dibOrigin === "bottom-left" && rCtx.view.getInt32(req.source.bmi + 8, true) > 0) {
20340
20537
  sy = image.height - sy - req.sh;
20341
20538
  }
20342
20539
  return {
20343
- decoded: { pixels: image },
20540
+ decoded: { pixels: image, adjust },
20344
20541
  req: { ...req, sy }
20345
20542
  };
20346
20543
  }
@@ -20496,6 +20693,9 @@ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, s
20496
20693
  return;
20497
20694
  }
20498
20695
  src = decoded.decoded.pixels;
20696
+ if (decoded.decoded.adjust) {
20697
+ applyColorAdjustment(src, rCtx.state.colorAdjustment);
20698
+ }
20499
20699
  srcX = decoded.req.sx;
20500
20700
  srcY = decoded.req.sy;
20501
20701
  }
@@ -23963,12 +24163,12 @@ function hslToRgb(h, s, l) {
23963
24163
  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];
23964
24164
  return [r + m, g + m, b + m];
23965
24165
  }
23966
- function applyHueSaturationLightness(src, hue, saturation, lightness) {
24166
+ function applyHueSaturationLightness(src, hue, saturation, lightness2) {
23967
24167
  const out = new Uint8ClampedArray(src);
23968
24168
  const push = (v, t) => t >= 0 ? v + (1 - v) * (t / 100) : v * (1 + t / 100);
23969
24169
  for (let i = 0; i < src.length; i += 4) {
23970
24170
  const [h, s, l] = rgbToHsl(src[i] / 255, src[i + 1] / 255, src[i + 2] / 255);
23971
- const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness));
24171
+ const [r, g, b] = hslToRgb(h + hue, push(s, saturation), push(l, lightness2));
23972
24172
  out[i] = Math.round(r * 255);
23973
24173
  out[i + 1] = Math.round(g * 255);
23974
24174
  out[i + 2] = Math.round(b * 255);
@@ -24659,6 +24859,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24659
24859
  }
24660
24860
  };
24661
24861
  if (pen) {
24862
+ const nibDashed = !!nib && penDashArray(pen).length > 0;
24662
24863
  applyPlusPenStyle(ctx, pen, widthScale);
24663
24864
  const mode = plusRasterMode(rCtx);
24664
24865
  let sampler = pen.brush && !isSvgContext(ctx) ? brushSampler(rCtx, pen.brush) : null;
@@ -24673,7 +24874,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24673
24874
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24674
24875
  return;
24675
24876
  }
24676
- const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24877
+ const outline = hasCustomCap(pen) || nibDashed ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24677
24878
  let box = null;
24678
24879
  if (size && outline) {
24679
24880
  box = deviceBounds(outline.flat(), device, size, 2);
@@ -24703,7 +24904,7 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24703
24904
  }
24704
24905
  }
24705
24906
  const paint = pen.brush ? brushPaint2(rCtx, pen.brush) : pen.color;
24706
- if ((hasCustomCap(pen) || nib && typeof paint !== "string") && fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure)) {
24907
+ if ((hasCustomCap(pen) || nibDashed || nib && typeof paint !== "string") && fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure)) {
24707
24908
  return;
24708
24909
  }
24709
24910
  ctx.strokeStyle = paint;
@@ -24725,7 +24926,9 @@ function resolvePenTransform(pen) {
24725
24926
  return { pen: { ...plain, width: pen.width === 0 ? 1 : pen.width }, nib: null };
24726
24927
  }
24727
24928
  if (Math.abs(sx - sy) <= 1e-6 * sx && Math.abs(dot) <= 1e-6 * sx * sy) {
24728
- return { pen: { ...plain, width: pen.width * sx }, nib: null };
24929
+ const pattern = pen.dashStyle === 5 ? pen.dashPattern : DASH_PATTERNS[pen.dashStyle];
24930
+ const dashed = pattern && pattern.length > 0 ? { dashStyle: 5, dashPattern: pattern.map((v) => v / sx), dashOffset: (pen.dashOffset ?? 0) / sx } : {};
24931
+ return { pen: { ...plain, ...dashed, width: pen.width * sx }, nib: null };
24729
24932
  }
24730
24933
  return { pen: plain, nib: [a, b, c, d, 0, 0] };
24731
24934
  }
@@ -24750,7 +24953,7 @@ function penFrame(device, nib) {
24750
24953
  toDevice: mulMatrix(full, [1 / unit, 0, 0, 1 / unit, 0, 0])
24751
24954
  };
24752
24955
  }
24753
- function framePen(pen, unit) {
24956
+ function framePen(pen, unit, nib = null) {
24754
24957
  const dash = penDashArray(pen).map((v) => v * unit);
24755
24958
  return {
24756
24959
  half: (pen.width || 1) * unit / 2,
@@ -24761,6 +24964,7 @@ function framePen(pen, unit) {
24761
24964
  dashCap: pen.dashCap ?? 0,
24762
24965
  dash: dash.length > 0 ? dash : null,
24763
24966
  dashOffset: (pen.dashOffset ?? 0) * (pen.width || 1) * unit,
24967
+ dashMetric: nib ? [nib[0], nib[1], nib[2], nib[3]] : void 0,
24764
24968
  compound: pen.compound ?? null,
24765
24969
  inset: pen.alignment === 1
24766
24970
  };
@@ -24788,7 +24992,7 @@ function strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, c
24788
24992
  figures = figures.map((f) => ({ ...f, closed: true }));
24789
24993
  }
24790
24994
  const width = (pen.width || 1) * frame2.unit;
24791
- const devicePen = framePen(pen, frame2.unit);
24995
+ const devicePen = framePen(pen, frame2.unit, nib);
24792
24996
  const antialias = mode === "gdiplus-aa";
24793
24997
  const toFix = (poly) => {
24794
24998
  const m = frame2.toDevice;
@@ -24880,7 +25084,7 @@ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24880
25084
  figures = figures.map((f) => ({ ...f, closed: true }));
24881
25085
  }
24882
25086
  const width = (pen.width || 1) * frame2.unit;
24883
- const devicePen = framePen(pen, frame2.unit);
25087
+ const devicePen = framePen(pen, frame2.unit, nib);
24884
25088
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24885
25089
  const m = mulMatrix(deviceInv, frame2.toDevice);
24886
25090
  return outline.map((poly) => poly.map((p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.3.2",
3
+ "version": "4.3.4",
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",