emf-converter 4.8.0 → 4.8.2

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
@@ -217,7 +217,7 @@ Everything is measured against output painted by Windows itself; `src/gdi-parity
217
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.
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
- - **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.
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.
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.
222
222
  - **Small EMF+ residuals**: rotated `HighQualityBicubic` `DrawImage` edge pixels (0.14%), one-level differences at exact half-level `Blend` knots, and a few pixels of a metafile nested in `DrawImage` under a scale.
223
223
  - **WMF**: `PS_INSIDEFRAME` boxes can come out a pixel short at non-integer scales, right-to-left (`LAYOUT_RTL`) layouts differ by single pixels on mirrored diagonals, and metric map modes assume a 96 dpi reference device (Windows derives them from the physical display, so its own output varies per machine).
package/dist/browser.js CHANGED
@@ -16403,8 +16403,8 @@ function midtoneGamma(t) {
16403
16403
  low = high;
16404
16404
  high = (high + 100) / 2;
16405
16405
  }
16406
- gamma = Math.abs(t) === 100 ? 10 : Math.min(10, gamma + (Math.abs(t) - low) / (high - low));
16407
- return Math.fround(t < 0 ? gamma : 1 / gamma);
16406
+ gamma = Math.abs(t) === 100 ? 10 : Math.min(10, truncF32(gamma + truncF32(truncF32(Math.abs(t) - low) / truncF32(high - low))));
16407
+ return t < 0 ? truncF32(gamma) : truncF32(1 / truncF32(gamma));
16408
16408
  }
16409
16409
  function brightnessContrastLut(brightness, contrast) {
16410
16410
  const gain = contrast >= 100 ? 1e6 : contrast >= 0 ? 100 / (100 - contrast) : (100 + contrast) / 100;
@@ -16421,6 +16421,23 @@ function curveAdjustmentLut(adjustment, intensity) {
16421
16421
  return nativeCurveLookup(adjustment, clamp(Math.trunc(intensity), min, max)).slice();
16422
16422
  }
16423
16423
  function levelsLut(highlight, midtone, shadow) {
16424
+ if (highlight !== shadow) {
16425
+ const forward = highlight > shadow;
16426
+ const lo = truncF32((forward ? shadow : highlight) * 255 / 100);
16427
+ const hi = truncF32((forward ? highlight : shadow) * 255 / 100);
16428
+ const scale = truncF32(1 / truncF32(hi - lo));
16429
+ const exponent2 = midtoneGamma(midtone);
16430
+ const lut = new Uint8Array(256);
16431
+ for (let v = 0; v < 256; v++) {
16432
+ let level = clamp(truncF32(truncF32(v - lo) * scale), 0, 1);
16433
+ if (midtone !== 0) {
16434
+ level = truncF32(level ** exponent2);
16435
+ }
16436
+ const out = forward ? truncF32(255 * level) : truncF32(255 - truncF32(255 * level));
16437
+ lut[v] = clamp(Math.floor(out + 0.5), 0, 255);
16438
+ }
16439
+ return lut;
16440
+ }
16424
16441
  const base = shadow * 255 / 100 - (highlight === shadow && shadow < 50 ? 1 : 0);
16425
16442
  const span = highlight === shadow ? 1 : (highlight - shadow) * 255 / 100;
16426
16443
  const exponent = midtoneGamma(midtone);
@@ -16453,14 +16470,6 @@ function applyColorMatrix(src, m) {
16453
16470
  }
16454
16471
  return out;
16455
16472
  }
16456
- function hslToRgb(h, s, l) {
16457
- const c = (1 - Math.abs(2 * l - 1)) * s;
16458
- const hp = (h % 360 + 360) % 360 / 60;
16459
- const x = c * (1 - Math.abs(hp % 2 - 1));
16460
- const m = l - c / 2;
16461
- 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];
16462
- return [r + m, g + m, b + m];
16463
- }
16464
16473
  var HUE_SEXTANT = 43;
16465
16474
  var HUE_CIRCLE = 255;
16466
16475
  function gdipHue(r, g, b) {
@@ -16483,29 +16492,30 @@ function gdipHue(r, g, b) {
16483
16492
  function applyHueSaturationLightness(src, hue, saturation, lightness) {
16484
16493
  const out = new Uint8ClampedArray(src);
16485
16494
  const shift = Math.round(hue * HUE_CIRCLE / 360);
16486
- const sMul = 1 + saturation / 100;
16495
+ const satScale = 65536 + Math.floor(saturation * 65536 / 100);
16496
+ const lightOffset = Math.round(255 * Math.floor(lightness * 65536 / 100) / 65536);
16487
16497
  for (let i = 0; i < src.length; i += 4) {
16488
16498
  const r = src[i];
16489
16499
  const g = src[i + 1];
16490
16500
  const b = src[i + 2];
16491
16501
  const max = Math.max(r, g, b);
16492
16502
  const min = Math.min(r, g, b);
16493
- const l = clamp(Math.round(((max + min >> 1) * 100 + 255 * lightness) / 100), 0, 255);
16503
+ const l = clamp((max + min >> 1) + lightOffset, 0, 255);
16494
16504
  if (max === min) {
16495
16505
  out[i] = out[i + 1] = out[i + 2] = l;
16496
16506
  continue;
16497
16507
  }
16498
16508
  const sum = max + min;
16499
- const s = Math.fround(clamp((max - min) / (sum <= 255 ? sum : 510 - sum) * sMul, 0, 1));
16500
- const hi = l <= 127 ? l * (1 + s) : l + s * (255 - l);
16501
- const m1 = l <= 127 ? 2 * l - Math.trunc(hi) : Math.round(2 * l - hi);
16502
- const m2 = 2 * l - m1;
16509
+ const sat2 = clamp(Math.round(Math.floor((max - min) * 255 / (sum <= 255 ? sum : 510 - sum)) * satScale / 65536), 0, 255);
16510
+ const hi = l <= 127 ? Math.floor(l * (255 + sat2) / 255) : l + sat2 - Math.floor(l * sat2 / 255);
16511
+ const m1 = 2 * l - hi;
16512
+ const m2 = hi;
16503
16513
  const index = ((gdipHue(r, g, b) + shift) % HUE_CIRCLE + HUE_CIRCLE) % HUE_CIRCLE;
16504
16514
  const q = index + Math.floor((index + 41) / 85);
16505
16515
  const sextant = Math.min(5, Math.floor(q / HUE_SEXTANT));
16506
- const f = (q - sextant * HUE_SEXTANT) / HUE_SEXTANT;
16507
- const up = Math.trunc(m1 + (m2 - m1) * f);
16508
- const down = Math.trunc(m1 + (m2 - m1) * (1 - f));
16516
+ const position = q - sextant * HUE_SEXTANT;
16517
+ const up = m1 + Math.floor((m2 - m1) * position / HUE_SEXTANT);
16518
+ const down = m1 + Math.floor((m2 - m1) * (HUE_SEXTANT - position) / HUE_SEXTANT);
16509
16519
  const [nr, ng, nb] = [
16510
16520
  [m2, up, m1],
16511
16521
  [down, m2, m1],
@@ -16520,28 +16530,40 @@ function applyHueSaturationLightness(src, hue, saturation, lightness) {
16520
16530
  }
16521
16531
  return out;
16522
16532
  }
16523
- var LUMA_709 = [0.2126, 0.7152, 0.0722];
16524
- var TINT_CHROMA_SCALE = 0.985;
16525
16533
  function applyTint(src, hue, amount) {
16526
16534
  const index = (Math.round(hue * 255 / 360) + 256) % 256;
16527
16535
  const q = (index + Math.floor((index + 41) / 85)) % 258;
16528
- const tint = hslToRgb(q * 60 / 43, 1, 0.5);
16529
- const ty = tint[0] * LUMA_709[0] + tint[1] * LUMA_709[1] + tint[2] * LUMA_709[2];
16530
- const chroma = tint.map((c) => (c - ty) * TINT_CHROMA_SCALE);
16531
- const a = amount / 100;
16536
+ const sextant = Math.min(5, Math.floor(q / HUE_SEXTANT));
16537
+ const position = q - sextant * HUE_SEXTANT;
16538
+ const up = Math.floor(TINT_FULL * position / HUE_SEXTANT);
16539
+ const down = Math.floor(TINT_FULL * (HUE_SEXTANT - position) / HUE_SEXTANT);
16540
+ const [tr, tg, tb] = [
16541
+ [TINT_FULL, up, 0],
16542
+ [down, TINT_FULL, 0],
16543
+ [0, TINT_FULL, up],
16544
+ [0, down, TINT_FULL],
16545
+ [up, 0, TINT_FULL],
16546
+ [TINT_FULL, 0, down]
16547
+ ][sextant];
16548
+ const weight = Math.sign(amount) * Math.floor((255 * Math.abs(amount) + 49) / 100);
16549
+ const original = 255 - weight;
16532
16550
  const out = new Uint8ClampedArray(src);
16533
16551
  for (let i = 0; i < src.length; i += 4) {
16534
16552
  const r = src[i];
16535
16553
  const g = src[i + 1];
16536
16554
  const b = src[i + 2];
16537
- const y = r * LUMA_709[0] + g * LUMA_709[1] + b * LUMA_709[2];
16538
- const v = Math.max(r, g, b);
16539
- out[i] = Math.round((1 - a) * r + a * (y + v * chroma[0]));
16540
- out[i + 1] = Math.round((1 - a) * g + a * (y + v * chroma[1]));
16541
- out[i + 2] = Math.round((1 - a) * b + a * (y + v * chroma[2]));
16555
+ const u = weight * Math.max(r, g, b) >> 8;
16556
+ const x0 = original * r + tr * u;
16557
+ const x1 = original * g + tg * u;
16558
+ const x2 = original * b + tb * u;
16559
+ const base = 54 * r + 183 * g + 19 * b - (54 * (x0 >> 8) + 183 * (x1 >> 8) + 19 * (x2 >> 8));
16560
+ out[i] = base + x0 >> 8;
16561
+ out[i + 1] = base + x1 >> 8;
16562
+ out[i + 2] = base + x2 >> 8;
16542
16563
  }
16543
16564
  return out;
16544
16565
  }
16566
+ var TINT_FULL = 254;
16545
16567
  function applyRedEyeCorrection(src, width, height, areas) {
16546
16568
  const out = new Uint8ClampedArray(src);
16547
16569
  for (const a of areas) {
@@ -20541,8 +20563,7 @@ function resampleImage(rgba, width, height, spec, surface) {
20541
20563
  const axisAligned = m[1] === 0 && m[2] === 0;
20542
20564
  if (hq && axisAligned) {
20543
20565
  const ox = m[0] * spec.srcX + m[4];
20544
- const oy = m[3] * spec.srcY + m[5];
20545
- m = [m[0], 0, 0, m[3], m[4] + mirroredOrigin(ox) - ox, m[5] + mirroredOrigin(oy) - oy];
20566
+ m = [m[0], 0, 0, m[3], m[4] + mirroredOrigin(ox) - ox, m[5]];
20546
20567
  }
20547
20568
  const inv2 = invert2(m);
20548
20569
  if (!inv2) {
@@ -23355,6 +23376,21 @@ var nestedReplayer = null;
23355
23376
  function registerNestedMetafileReplayer(fn) {
23356
23377
  nestedReplayer = fn;
23357
23378
  }
23379
+ function inclusiveNestedMapping(toDevice, srcX, srcY, srcW, srcH) {
23380
+ const [a, b, c, d, e, f] = toDevice;
23381
+ const lengthU = Math.hypot(a, b) * srcW;
23382
+ const lengthV = Math.hypot(c, d) * srcH;
23383
+ if (!(srcW > 1 && srcH > 1 && lengthU > 1 && lengthV > 1)) {
23384
+ return toDevice;
23385
+ }
23386
+ const ku = (lengthU - 1) / lengthU * (srcW / (srcW - 1));
23387
+ const kv = (lengthV - 1) / lengthV * (srcH / (srcH - 1));
23388
+ const na = a * ku;
23389
+ const nb = b * ku;
23390
+ const nc = c * kv;
23391
+ const nd = d * kv;
23392
+ return [na, nb, nc, nd, e + (a - na) * srcX + (c - nc) * srcY, f + (b - nb) * srcX + (d - nd) * srcY];
23393
+ }
23358
23394
  function drawNestedMetafile(rCtx, img, cached, source) {
23359
23395
  const depth = rCtx.nestingDepth ?? 0;
23360
23396
  if (!nestedReplayer || !source || !img.data || source.unit !== UNIT_PIXEL || depth >= MAX_NESTED_METAFILE_DEPTH) {
@@ -23368,9 +23404,12 @@ function drawNestedMetafile(rCtx, img, cached, source) {
23368
23404
  if (!size) {
23369
23405
  return false;
23370
23406
  }
23371
- const toCanvas = mulMatrix(
23372
- [1, 0, 0, 1, -0.5, -0.5],
23373
- mulMatrix(mulMatrix(plusWorldMatrix(rCtx), source.toWorld(srcX, srcY, srcW, srcH)), [1, 0, 0, 1, 0.5, 0.5])
23407
+ const toCanvas = inclusiveNestedMapping(
23408
+ mulMatrix(plusWorldMatrix(rCtx), source.toWorld(srcX, srcY, srcW, srcH)),
23409
+ srcX,
23410
+ srcY,
23411
+ srcW,
23412
+ srcH
23374
23413
  );
23375
23414
  const bytes = img.data.slice(0);
23376
23415
  const caches = cached && cached.kind === "metafile" ? cached.caches : void 0;
@@ -29189,9 +29228,9 @@ function perpendicularVectors(width, dx0, dy0) {
29189
29228
  const vx = rounded[0], vy = rounded[1];
29190
29229
  return { v: flip ? [-vx, -vy] : [vx, vy], ray: flip ? [-x, -y] : [x, y] };
29191
29230
  }
29192
- function squareExtension(width, dx, dy) {
29231
+ function squareExtension(width, dx, dy, scale = 1) {
29193
29232
  const len = Math.hypot(dx, dy);
29194
- const r = width / 2;
29233
+ const r = width / 2 * scale;
29195
29234
  return [Math.floor(dx / len * r + 0.5), Math.floor(dy / len * r + 0.5)];
29196
29235
  }
29197
29236
  function turnSign(ax, ay, bx, by) {
@@ -29230,7 +29269,7 @@ var Outliner = class {
29230
29269
  const [L, R] = drawVertices(this.pen, d[0], d[1]);
29231
29270
  const perpendicular = curve && drawDir ? drawDir : [dx, dy];
29232
29271
  const vectors = perpendicularVectors(this.opts.width, perpendicular[0], perpendicular[1]);
29233
- return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1]), curveEnd: !!drawDir, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
29272
+ return { dx, dy, L, R, v: vectors.v, vRaw: vectors.ray, e: squareExtension(this.opts.width, perpendicular[0], perpendicular[1]), pe: [perpendicular[0], perpendicular[1]], curveEnd: !!drawDir, tangentLength: drawDir ? drawDir[0] * drawDir[0] + drawDir[1] * drawDir[1] : 0, curve };
29234
29273
  }
29235
29274
  push(p, v) {
29236
29275
  this.pts.push([p[0] + v[0], p[1] + v[1]]);
@@ -29325,7 +29364,7 @@ var Outliner = class {
29325
29364
  return side === "R" ? s.v : [-s.v[0], -s.v[1]];
29326
29365
  }
29327
29366
  /** A cap at `p` from the `from` side of `s` round to the other side (`start`: around the back). */
29328
- cap(p, s, start) {
29367
+ cap(p, s, start, scale = 1) {
29329
29368
  const from = start ? "L" : "R";
29330
29369
  const to = start ? "R" : "L";
29331
29370
  const { cap } = this.opts;
@@ -29343,7 +29382,8 @@ var Outliner = class {
29343
29382
  const sv = this.capSide(s, from);
29344
29383
  const ev = this.capSide(s, to);
29345
29384
  if (cap === "square") {
29346
- const e = start ? [-s.e[0], -s.e[1]] : s.e;
29385
+ const e0 = scale === 1 ? s.e : squareExtension(this.opts.width, s.pe[0], s.pe[1], scale);
29386
+ const e = start ? [-e0[0], -e0[1]] : e0;
29347
29387
  this.push(p, [sv[0] + e[0], sv[1] + e[1]]);
29348
29388
  this.push(p, [ev[0] + e[0], ev[1] + e[1]]);
29349
29389
  } else {
@@ -29439,7 +29479,7 @@ var Outliner = class {
29439
29479
  * lets a single point stand for a zero-length dash along `dirs[0]`;
29440
29480
  * `draws` (curve end tangents) picks draw vertices only.
29441
29481
  */
29442
- open(P, dirs, draws, curves) {
29482
+ open(P, dirs, draws, curves, startScale = 1, endScale = 1) {
29443
29483
  if (P.length < 2 && !dirs?.[0]) {
29444
29484
  this.dot(P[0]);
29445
29485
  return;
@@ -29450,12 +29490,12 @@ var Outliner = class {
29450
29490
  }
29451
29491
  if (segs2.length === 0) {
29452
29492
  const s = this.seg(P[0], P[0], dirs?.[0], draws?.[0], curves?.[0]);
29453
- this.cap(P[0], s, true);
29454
- this.cap(P[0], s, false);
29493
+ this.cap(P[0], s, true, startScale);
29494
+ this.cap(P[0], s, false, endScale);
29455
29495
  this.flush();
29456
29496
  return;
29457
29497
  }
29458
- this.cap(P[0], segs2[0], true);
29498
+ this.cap(P[0], segs2[0], true, startScale);
29459
29499
  for (let i = 0; i + 1 < segs2.length; i++) {
29460
29500
  const s = segs2[i];
29461
29501
  const t = segs2[i + 1];
@@ -29466,7 +29506,7 @@ var Outliner = class {
29466
29506
  this.join(P[i + 1], s, t, "R", tr < 0);
29467
29507
  }
29468
29508
  }
29469
- this.cap(P[P.length - 1], segs2[segs2.length - 1], false);
29509
+ this.cap(P[P.length - 1], segs2[segs2.length - 1], false, endScale);
29470
29510
  for (let i = segs2.length - 1; i > 0; i--) {
29471
29511
  const s = segs2[i];
29472
29512
  const t = segs2[i - 1];
@@ -29522,7 +29562,7 @@ var Outliner = class {
29522
29562
  this.flush();
29523
29563
  }
29524
29564
  };
29525
- function dashPieces(P, tangents, pattern, shorten) {
29565
+ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors) {
29526
29566
  const out = [];
29527
29567
  const dashes = [];
29528
29568
  for (let i = 0; i < pattern.length; i += 2) {
@@ -29537,12 +29577,22 @@ function dashPieces(P, tangents, pattern, shorten) {
29537
29577
  let idx = 0;
29538
29578
  let left = dashes[0];
29539
29579
  let on = true;
29540
- let cur = { pts: [P[0]], dirs: [], draws: [] };
29580
+ let cur = { pts: [P[0]], dirs: [], draws: [], curves: [], startScale: 1, endScale: 1 };
29541
29581
  for (let i = 0; i + 1 < P.length; i++) {
29542
29582
  const [x0, y0] = P[i];
29543
29583
  const [x1, y1] = P[i + 1];
29544
29584
  const dir = [x1 - x0, y1 - y0];
29545
- const len = Math.hypot(dir[0], dir[1]);
29585
+ const real = Math.hypot(dir[0], dir[1]);
29586
+ const len = wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
29587
+ if (len === 0) {
29588
+ if (on && cur) {
29589
+ cur.pts.push(P[i + 1]);
29590
+ cur.dirs.push(dir);
29591
+ cur.draws.push(tangents[i]);
29592
+ cur.curves.push(curveFlags[i]);
29593
+ }
29594
+ continue;
29595
+ }
29546
29596
  let t = 0;
29547
29597
  while (len - t > left || left === 0 && on) {
29548
29598
  t += left;
@@ -29551,10 +29601,12 @@ function dashPieces(P, tangents, pattern, shorten) {
29551
29601
  cur.pts.push(q);
29552
29602
  cur.dirs.push(dir);
29553
29603
  cur.draws.push(tangents[i]);
29604
+ cur.curves.push(curveFlags[i]);
29605
+ cur.endScale = cur.pts[0] === P[0] && q[0] === P[0][0] && q[1] === P[0][1] ? 1 : real / len;
29554
29606
  out.push(cur);
29555
29607
  cur = null;
29556
29608
  } else {
29557
- cur = { pts: [q], dirs: [], draws: [] };
29609
+ cur = { pts: [q], dirs: [], draws: [], curves: [], startScale: real / len, endScale: 1 };
29558
29610
  }
29559
29611
  on = !on;
29560
29612
  idx = (idx + 1) % dashes.length;
@@ -29565,9 +29617,12 @@ function dashPieces(P, tangents, pattern, shorten) {
29565
29617
  cur.pts.push(P[i + 1]);
29566
29618
  cur.dirs.push(dir);
29567
29619
  cur.draws.push(tangents[i]);
29620
+ cur.curves.push(curveFlags[i]);
29621
+ cur.endScale = real / len;
29568
29622
  }
29569
29623
  }
29570
29624
  if (on && cur && cur.dirs.length > 0) {
29625
+ cur.endScale = 1;
29571
29626
  out.push(cur);
29572
29627
  }
29573
29628
  return out;
@@ -29602,10 +29657,13 @@ function widenPath(path, opts) {
29602
29657
  if (dashed) {
29603
29658
  const run = closed ? [...P, P[0]] : P;
29604
29659
  const shorten = opts.cap === "flat" || opts.shortenDashes === false ? 0 : opts.width;
29605
- for (const piece of dashPieces(run, closed ? [...dirs, void 0] : dirs, opts.dashes, shorten)) {
29660
+ const runTangents = closed ? [...dirs, void 0] : dirs;
29661
+ const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
29662
+ for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors)) {
29606
29663
  const pts = [piece.pts[0]];
29607
29664
  const pdirs = [];
29608
29665
  const pdraws = [];
29666
+ const pcurves = [];
29609
29667
  for (let i = 1; i < piece.pts.length; i++) {
29610
29668
  const q = piece.pts[i];
29611
29669
  const last = pts[pts.length - 1];
@@ -29613,9 +29671,10 @@ function widenPath(path, opts) {
29613
29671
  pts.push(q);
29614
29672
  pdirs.push(piece.dirs[i - 1]);
29615
29673
  pdraws.push(piece.draws[i - 1]);
29674
+ pcurves.push(piece.curves[i - 1]);
29616
29675
  }
29617
29676
  }
29618
- outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pts.map(() => !!fig.roundWiden));
29677
+ outliner.open(pts, pdirs.length > 0 ? pdirs : [piece.dirs[0]], pdirs.length > 0 ? pdraws : [piece.draws[0]], pdirs.length > 0 ? pcurves : [piece.curves[0]], piece.startScale, piece.endScale);
29619
29678
  }
29620
29679
  } else if (closed && P.length >= 3) {
29621
29680
  outliner.closed(P, dirs, curves);
@@ -29823,7 +29882,10 @@ function penWidenOptions(rCtx, opts = {}) {
29823
29882
  join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
29824
29883
  miterLimit: state.miterLimit ?? 10,
29825
29884
  dashes: dashes ? dashes.map((v) => v * 16) : null,
29826
- shortenDashes: (flags & 15) !== 7
29885
+ shortenDashes: (flags & 15) !== 7,
29886
+ // One logical unit per device pixel, unrotated: GDI measures dash
29887
+ // segments in whole pixels.
29888
+ wholePixelDashVectors: Math.abs(matrix[0] - 1) < 1e-6 && Math.abs(matrix[3] - 1) < 1e-6 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6
29827
29889
  };
29828
29890
  }
29829
29891
  function hatchBackgroundMask(hatch) {