emf-converter 4.20.1 → 4.20.3

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/dist/browser.js CHANGED
@@ -29019,7 +29019,7 @@ var MonoStubContour = class {
29019
29019
  constructor(o) {
29020
29020
  this.points = Array.from(o.xs, (x, i) => [x / 64 - 0.5, o.ys[i] / 64 - 0.5]);
29021
29021
  }
29022
- isStub(flipped, scan, crossing, lo, hi, precision) {
29022
+ isStub(flipped, scan, crossing, lo, hi, precision, _opposite) {
29023
29023
  const points = this.points;
29024
29024
  const n = points.length;
29025
29025
  const point = (i) => flipped ? [points[i][1], points[i][0]] : points[i];
@@ -29055,6 +29055,82 @@ var MonoStubContour = class {
29055
29055
  return !(a.side * b.side < 0 && a.continues && b.continues);
29056
29056
  }
29057
29057
  };
29058
+ function convexContour(o) {
29059
+ const edges = [];
29060
+ for (let i = 0; i < o.xs.length; i++) {
29061
+ const j = (i + 1) % o.xs.length;
29062
+ const dx = o.xs[j] - o.xs[i], dy = o.ys[j] - o.ys[i];
29063
+ if (dx || dy) edges.push([dx, dy]);
29064
+ }
29065
+ if (edges.length < 3) return false;
29066
+ const half = (p) => p[1] > 0 || p[1] === 0 && p[0] >= 0 ? 0 : 1;
29067
+ let direction = 0, revolutions = 0;
29068
+ for (let i = 0; i < edges.length; i++) {
29069
+ const a = edges[i], b = edges[(i + 1) % edges.length];
29070
+ const cross = a[0] * b[1] - a[1] * b[0];
29071
+ if (!cross) {
29072
+ if (a[0] * b[0] + a[1] * b[1] < 0) return false;
29073
+ continue;
29074
+ }
29075
+ const turn = Math.sign(cross);
29076
+ if (direction && direction !== turn) return false;
29077
+ direction = turn;
29078
+ if (half(a) !== half(b) && half(a) === (turn > 0 ? 1 : 0)) revolutions++;
29079
+ }
29080
+ return direction !== 0 && revolutions === 1;
29081
+ }
29082
+ var MonoStubContours = class _MonoStubContours {
29083
+ constructor(contours) {
29084
+ this.contours = contours;
29085
+ }
29086
+ static create(o) {
29087
+ if (o.endPts.length > 64) return null;
29088
+ if (Array.from(o.onCurve).some((p) => !p)) return null;
29089
+ if (o.endPts.length === 1) return new MonoStubContour(o);
29090
+ const contours = [];
29091
+ const boxes = [];
29092
+ let start = 0;
29093
+ for (const end of o.endPts) {
29094
+ const xs = [], ys = [], onCurve = [];
29095
+ for (let i = start; i <= end; i++) {
29096
+ xs.push(o.xs[i]);
29097
+ ys.push(o.ys[i]);
29098
+ onCurve.push(o.onCurve[i]);
29099
+ }
29100
+ const part = { xs, ys, onCurve, endPts: [end - start] };
29101
+ start = end + 1;
29102
+ if (!convexContour(part)) return null;
29103
+ const box = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity };
29104
+ for (let i = 0; i < part.xs.length; i++) {
29105
+ box.xMin = Math.min(box.xMin, part.xs[i]);
29106
+ box.xMax = Math.max(box.xMax, part.xs[i]);
29107
+ box.yMin = Math.min(box.yMin, part.ys[i]);
29108
+ box.yMax = Math.max(box.yMax, part.ys[i]);
29109
+ }
29110
+ if (boxes.some((b) => box.xMin <= b.xMax + 64 && b.xMin <= box.xMax + 64 && box.yMin <= b.yMax + 64 && b.yMin <= box.yMax + 64)) return null;
29111
+ boxes.push(box);
29112
+ contours.push(new MonoStubContour(part));
29113
+ }
29114
+ return new _MonoStubContours(contours);
29115
+ }
29116
+ isStub(flipped, scan, crossing, lo, hi, precision, opposite) {
29117
+ let owner = -1, otherOwner = -1, result = null;
29118
+ for (let i = 0; i < this.contours.length; i++) {
29119
+ const a = this.contours[i].isStub(flipped, scan, crossing, lo, hi, precision);
29120
+ const b = this.contours[i].isStub(flipped, scan, opposite, lo, hi, precision);
29121
+ if (a !== null) {
29122
+ if (owner >= 0) return null;
29123
+ owner = i;
29124
+ result = a;
29125
+ }
29126
+ if (b !== null) {
29127
+ if (otherOwner >= 0) return null;
29128
+ otherOwner = i;
29129
+ }
29130
+ }
29131
+ return owner >= 0 && owner === otherOwner ? result : null;
29132
+ }
29133
+ };
29058
29134
  function monoBox(o) {
29059
29135
  const n = o.xs.length;
29060
29136
  if (n === 0) {
@@ -29076,16 +29152,11 @@ function monoBox(o) {
29076
29152
  let xMax = Math.floor((cxMax + 32) / 64);
29077
29153
  let yMin = Math.floor((cyMin + 31) / 64);
29078
29154
  let yMax = Math.floor((cyMax + 32) / 64);
29079
- const rem = (v, add) => (v + add & 63) - add;
29080
- if (xMin === xMax) {
29081
- if (rem(cxMin, 31) + rem(cxMax, 32) < 0) xMin -= 1;
29082
- else xMax += 1;
29083
- }
29084
- if (yMin === yMax) {
29085
- if (rem(cyMin, 31) + rem(cyMax, 32) < 0) yMin -= 1;
29086
- else yMax += 1;
29087
- }
29088
- return { xMin, xMax, yMin, yMax };
29155
+ const collapsedX = xMin === xMax;
29156
+ const collapsedY = yMin === yMax;
29157
+ if (collapsedX) xMax += 1;
29158
+ if (collapsedY) yMax += 1;
29159
+ return { xMin, xMax, yMin, yMax, collapsedX, collapsedY };
29089
29160
  }
29090
29161
  function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossings26Dot6 = false) {
29091
29162
  const box = fixedBox ?? monoBox(o);
@@ -29109,7 +29180,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29109
29180
  const at = (col, row2) => (height - 1 - row2) * width + col;
29110
29181
  const r = new Raster(dropout);
29111
29182
  const P = r.precision;
29112
- const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) && local.endPts.length === 1 && Array.from(local.onCurve).every(Boolean) ? new MonoStubContour(local) : null;
29183
+ const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) ? MonoStubContours.create(local) : null;
29113
29184
  const smartPixel = (raster, x1, x2) => {
29114
29185
  if (monoContour && smartCrossings26Dot6) {
29115
29186
  const step = P / 64;
@@ -29152,7 +29223,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29152
29223
  break;
29153
29224
  case 1:
29154
29225
  case 5:
29155
- const stub = monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P);
29226
+ const stub = monoContour && box.collapsedX ? false : monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P, x2 / P);
29156
29227
  if (stub === true) return;
29157
29228
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= r.half)) {
29158
29229
  return;
@@ -29213,7 +29284,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29213
29284
  break;
29214
29285
  case 1:
29215
29286
  case 5:
29216
- const stub = monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P);
29287
+ const stub = monoContour && box.collapsedY ? false : monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P, x2 / P);
29217
29288
  if (stub === true) return;
29218
29289
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= h.half)) {
29219
29290
  return;
@@ -29577,6 +29648,7 @@ var RasterRealizedFont = class {
29577
29648
  fsSelection: face.italic ? 1 : 0,
29578
29649
  macStyle: 0,
29579
29650
  winAscent: this.ascent,
29651
+ winDescent: this.descent,
29580
29652
  unitsPerEm: this.ppem || 1
29581
29653
  };
29582
29654
  }
@@ -29764,6 +29836,11 @@ var RealizedFont = class {
29764
29836
  }
29765
29837
  return base + (this.syntheticBold ? 1 : 0);
29766
29838
  }
29839
+ /** GDI+ rounds tracked hmtx advances to font units before device scaling. */
29840
+ designAdvance(index, tracking) {
29841
+ const { advance } = this.ttf.hMetrics(index < this.ttf.numGlyphs ? index : NOTDEF);
29842
+ return Math.round(advance * tracking) * this.ppemX / this.ttf.unitsPerEm + (this.syntheticBold ? 1 : 0);
29843
+ }
29767
29844
  /**
29768
29845
  * Advance of glyph `index` in a font realised with an escapement: GDI
29769
29846
  * does not use the hinted/hdmx widths there but the linearly scaled
@@ -40451,6 +40528,35 @@ function surfaceSize5(ctx) {
40451
40528
  return typeof w === "number" && typeof h === "number" && w > 0 && h > 0 ? { w, h } : null;
40452
40529
  }
40453
40530
 
40531
+ // src/emf-plus-text-alignment.ts
40532
+ var SIMPLE_SCRIPT = /^(?:\p{Script=Latin}|\p{Script=Common}|\p{Co})$/u;
40533
+ var SHAPING_CHARACTER = /[\p{M}\p{Cf}\p{Emoji_Presentation}]/u;
40534
+ function layoutPlusAlignedString(font, codes, format, x, y, width, height) {
40535
+ const horizontal = format.alignment, vertical = format.lineAlignment;
40536
+ if (!horizontal && !vertical || horizontal < 0 || horizontal > 2 || vertical < 0 || vertical > 2 || font.gridFit || font.mode !== "gray" || !font.designAdvance || font.ttf.winDescent === void 0 || font.syntheticBold || font.syntheticItalic || format.flags & 3 || ![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0 || codes.some((c) => !Number.isInteger(c) || c < 32 || c > 1114111 || c >= 127 && c <= 159 || c === 8232 || c === 8233)) return null;
40537
+ if (codes.some((c) => {
40538
+ const character = String.fromCodePoint(c);
40539
+ return !SIMPLE_SCRIPT.test(character) || SHAPING_CHARACTER.test(character) || font.glyphIndex(c) === 0;
40540
+ })) return null;
40541
+ const em = font.ppem;
40542
+ const tracking = format.tracking ?? 1.03;
40543
+ const leading = em * (format.leadingMargin ?? 1 / 6);
40544
+ const trailing = em * (format.trailingMargin ?? 1 / 6);
40545
+ if (![tracking, leading, trailing].every((v) => Number.isFinite(v) && v >= 0)) return null;
40546
+ if (!(format.flags & 4) && (font.ttf.fsSelection & 1 || font.ttf.macStyle & 2)) return null;
40547
+ const dx = codes.map((c) => font.designAdvance(font.glyphIndex(c), tracking));
40548
+ let measuredCount = codes.length;
40549
+ if (!(format.flags & 2048)) while (measuredCount > 0 && codes[measuredCount - 1] === 32) measuredCount--;
40550
+ const measuredWidth = dx.slice(0, measuredCount).reduce((a, b) => a + b, 0) + leading + trailing;
40551
+ const measuredHeight = em * (font.ttf.winAscent + font.ttf.winDescent) / font.ttf.unitsPerEm + (format.flags & 4 ? 0 : em / 8);
40552
+ if (measuredWidth > width || measuredHeight > height) return null;
40553
+ return {
40554
+ x: Math.fround(x + horizontal / 2 * (width - measuredWidth) + leading),
40555
+ y: Math.fround(y + vertical / 2 * (height - measuredHeight)) + Math.round(em * font.ttf.winAscent / font.ttf.unitsPerEm),
40556
+ dx
40557
+ };
40558
+ }
40559
+
40454
40560
  // src/emf-plus-text-image-handlers.ts
40455
40561
  function imageAttributesWrap(rCtx, attributesId) {
40456
40562
  const obj = attributesId <= 255 ? rCtx.objectTable.get(attributesId) : void 0;
@@ -40719,25 +40825,33 @@ function paintPlusEngineRun(rCtx, ef, run) {
40719
40825
  );
40720
40826
  return true;
40721
40827
  }
40722
- function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, alignment, format, brushSampler2 = null) {
40723
- if (alignment !== 0) {
40724
- return false;
40725
- }
40828
+ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, layoutW, layoutH, paint, alignment, format, brushSampler2 = null) {
40726
40829
  const ef = realizePlusEngineFont(rCtx, font, paint, brushSampler2);
40727
40830
  if (!ef) {
40728
40831
  return false;
40729
40832
  }
40730
40833
  const { realized, emPx, m, unhinted } = ef;
40834
+ const codes = Array.from(text, (character) => character.codePointAt(0));
40835
+ if (format && (alignment !== 0 || format.lineAlignment !== 0) && unhinted && m[0] === m[3] && Math.fround(emPx) === realized.ppem) {
40836
+ const aligned = layoutPlusAlignedString(
40837
+ realized,
40838
+ codes,
40839
+ format,
40840
+ m[0] * layoutX + m[4],
40841
+ m[3] * layoutY + m[5],
40842
+ m[0] * layoutW,
40843
+ m[3] * layoutH
40844
+ );
40845
+ if (aligned) return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, ...aligned, dy: null });
40846
+ }
40847
+ if (alignment !== 0) return false;
40731
40848
  const ttf = realized.ttf;
40732
40849
  const exactAscent = emPx * ttf.winAscent / ttf.unitsPerEm;
40733
- const ascent = unhinted ? Math.round(exactAscent) : Math.ceil(exactAscent);
40850
+ const clearTypeCell = realized.mode === "cleartype" && !realized.syntheticBold && !realized.syntheticItalic && emPx === realized.ppem && m[0] === 1 && m[3] === 1 && Number.isInteger(layoutY + m[5]) && (format?.lineAlignment ?? 0) === 0;
40851
+ const ascent = unhinted ? Math.round(exactAscent) : clearTypeCell ? Math.max(Math.round(exactAscent), realized.ascent) : Math.ceil(exactAscent);
40734
40852
  let x = m[0] * layoutX + m[4] + emPx * (format?.leadingMargin ?? PLUS_LEADING_EM);
40735
40853
  if (!unhinted) x = Math.round(x);
40736
40854
  const y = m[3] * layoutY + m[5] + ascent;
40737
- const codes = [];
40738
- for (const character of text) {
40739
- codes.push(character.codePointAt(0));
40740
- }
40741
40855
  const tracking = format?.tracking ?? PLUS_DEFAULT_TRACKING;
40742
40856
  const dx = unhinted ? codes.map((c) => realized.advance(realized.glyphIndex(c)) * tracking) : null;
40743
40857
  return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, x, y, dx, dy: null });
@@ -40819,8 +40933,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40819
40933
  const strLen = view.getUint32(dataOff + 8, true);
40820
40934
  const layoutX = view.getFloat32(dataOff + 12, true);
40821
40935
  const layoutY = view.getFloat32(dataOff + 16, true);
40822
- view.getFloat32(dataOff + 20, true);
40823
- view.getFloat32(dataOff + 24, true);
40936
+ const layoutW = view.getFloat32(dataOff + 20, true);
40937
+ const layoutH = view.getFloat32(dataOff + 24, true);
40824
40938
  const fontId = recFlags & 255;
40825
40939
  const font = objectTable.get(fontId);
40826
40940
  if (strLen > 0 && dataOff + 28 + strLen * 2 <= dataOff + recDataSize) {
@@ -40836,6 +40950,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40836
40950
  text,
40837
40951
  layoutX,
40838
40952
  layoutY,
40953
+ layoutW,
40954
+ layoutH,
40839
40955
  paint,
40840
40956
  alignment,
40841
40957
  format,
package/dist/browser.mjs CHANGED
@@ -29017,7 +29017,7 @@ var MonoStubContour = class {
29017
29017
  constructor(o) {
29018
29018
  this.points = Array.from(o.xs, (x, i) => [x / 64 - 0.5, o.ys[i] / 64 - 0.5]);
29019
29019
  }
29020
- isStub(flipped, scan, crossing, lo, hi, precision) {
29020
+ isStub(flipped, scan, crossing, lo, hi, precision, _opposite) {
29021
29021
  const points = this.points;
29022
29022
  const n = points.length;
29023
29023
  const point = (i) => flipped ? [points[i][1], points[i][0]] : points[i];
@@ -29053,6 +29053,82 @@ var MonoStubContour = class {
29053
29053
  return !(a.side * b.side < 0 && a.continues && b.continues);
29054
29054
  }
29055
29055
  };
29056
+ function convexContour(o) {
29057
+ const edges = [];
29058
+ for (let i = 0; i < o.xs.length; i++) {
29059
+ const j = (i + 1) % o.xs.length;
29060
+ const dx = o.xs[j] - o.xs[i], dy = o.ys[j] - o.ys[i];
29061
+ if (dx || dy) edges.push([dx, dy]);
29062
+ }
29063
+ if (edges.length < 3) return false;
29064
+ const half = (p) => p[1] > 0 || p[1] === 0 && p[0] >= 0 ? 0 : 1;
29065
+ let direction = 0, revolutions = 0;
29066
+ for (let i = 0; i < edges.length; i++) {
29067
+ const a = edges[i], b = edges[(i + 1) % edges.length];
29068
+ const cross = a[0] * b[1] - a[1] * b[0];
29069
+ if (!cross) {
29070
+ if (a[0] * b[0] + a[1] * b[1] < 0) return false;
29071
+ continue;
29072
+ }
29073
+ const turn = Math.sign(cross);
29074
+ if (direction && direction !== turn) return false;
29075
+ direction = turn;
29076
+ if (half(a) !== half(b) && half(a) === (turn > 0 ? 1 : 0)) revolutions++;
29077
+ }
29078
+ return direction !== 0 && revolutions === 1;
29079
+ }
29080
+ var MonoStubContours = class _MonoStubContours {
29081
+ constructor(contours) {
29082
+ this.contours = contours;
29083
+ }
29084
+ static create(o) {
29085
+ if (o.endPts.length > 64) return null;
29086
+ if (Array.from(o.onCurve).some((p) => !p)) return null;
29087
+ if (o.endPts.length === 1) return new MonoStubContour(o);
29088
+ const contours = [];
29089
+ const boxes = [];
29090
+ let start = 0;
29091
+ for (const end of o.endPts) {
29092
+ const xs = [], ys = [], onCurve = [];
29093
+ for (let i = start; i <= end; i++) {
29094
+ xs.push(o.xs[i]);
29095
+ ys.push(o.ys[i]);
29096
+ onCurve.push(o.onCurve[i]);
29097
+ }
29098
+ const part = { xs, ys, onCurve, endPts: [end - start] };
29099
+ start = end + 1;
29100
+ if (!convexContour(part)) return null;
29101
+ const box = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity };
29102
+ for (let i = 0; i < part.xs.length; i++) {
29103
+ box.xMin = Math.min(box.xMin, part.xs[i]);
29104
+ box.xMax = Math.max(box.xMax, part.xs[i]);
29105
+ box.yMin = Math.min(box.yMin, part.ys[i]);
29106
+ box.yMax = Math.max(box.yMax, part.ys[i]);
29107
+ }
29108
+ if (boxes.some((b) => box.xMin <= b.xMax + 64 && b.xMin <= box.xMax + 64 && box.yMin <= b.yMax + 64 && b.yMin <= box.yMax + 64)) return null;
29109
+ boxes.push(box);
29110
+ contours.push(new MonoStubContour(part));
29111
+ }
29112
+ return new _MonoStubContours(contours);
29113
+ }
29114
+ isStub(flipped, scan, crossing, lo, hi, precision, opposite) {
29115
+ let owner = -1, otherOwner = -1, result = null;
29116
+ for (let i = 0; i < this.contours.length; i++) {
29117
+ const a = this.contours[i].isStub(flipped, scan, crossing, lo, hi, precision);
29118
+ const b = this.contours[i].isStub(flipped, scan, opposite, lo, hi, precision);
29119
+ if (a !== null) {
29120
+ if (owner >= 0) return null;
29121
+ owner = i;
29122
+ result = a;
29123
+ }
29124
+ if (b !== null) {
29125
+ if (otherOwner >= 0) return null;
29126
+ otherOwner = i;
29127
+ }
29128
+ }
29129
+ return owner >= 0 && owner === otherOwner ? result : null;
29130
+ }
29131
+ };
29056
29132
  function monoBox(o) {
29057
29133
  const n = o.xs.length;
29058
29134
  if (n === 0) {
@@ -29074,16 +29150,11 @@ function monoBox(o) {
29074
29150
  let xMax = Math.floor((cxMax + 32) / 64);
29075
29151
  let yMin = Math.floor((cyMin + 31) / 64);
29076
29152
  let yMax = Math.floor((cyMax + 32) / 64);
29077
- const rem = (v, add) => (v + add & 63) - add;
29078
- if (xMin === xMax) {
29079
- if (rem(cxMin, 31) + rem(cxMax, 32) < 0) xMin -= 1;
29080
- else xMax += 1;
29081
- }
29082
- if (yMin === yMax) {
29083
- if (rem(cyMin, 31) + rem(cyMax, 32) < 0) yMin -= 1;
29084
- else yMax += 1;
29085
- }
29086
- return { xMin, xMax, yMin, yMax };
29153
+ const collapsedX = xMin === xMax;
29154
+ const collapsedY = yMin === yMax;
29155
+ if (collapsedX) xMax += 1;
29156
+ if (collapsedY) yMax += 1;
29157
+ return { xMin, xMax, yMin, yMax, collapsedX, collapsedY };
29087
29158
  }
29088
29159
  function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossings26Dot6 = false) {
29089
29160
  const box = fixedBox ?? monoBox(o);
@@ -29107,7 +29178,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29107
29178
  const at = (col, row2) => (height - 1 - row2) * width + col;
29108
29179
  const r = new Raster(dropout);
29109
29180
  const P = r.precision;
29110
- const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) && local.endPts.length === 1 && Array.from(local.onCurve).every(Boolean) ? new MonoStubContour(local) : null;
29181
+ const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) ? MonoStubContours.create(local) : null;
29111
29182
  const smartPixel = (raster, x1, x2) => {
29112
29183
  if (monoContour && smartCrossings26Dot6) {
29113
29184
  const step = P / 64;
@@ -29150,7 +29221,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29150
29221
  break;
29151
29222
  case 1:
29152
29223
  case 5:
29153
- const stub = monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P);
29224
+ const stub = monoContour && box.collapsedX ? false : monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P, x2 / P);
29154
29225
  if (stub === true) return;
29155
29226
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= r.half)) {
29156
29227
  return;
@@ -29211,7 +29282,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29211
29282
  break;
29212
29283
  case 1:
29213
29284
  case 5:
29214
- const stub = monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P);
29285
+ const stub = monoContour && box.collapsedY ? false : monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P, x2 / P);
29215
29286
  if (stub === true) return;
29216
29287
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= h.half)) {
29217
29288
  return;
@@ -29575,6 +29646,7 @@ var RasterRealizedFont = class {
29575
29646
  fsSelection: face.italic ? 1 : 0,
29576
29647
  macStyle: 0,
29577
29648
  winAscent: this.ascent,
29649
+ winDescent: this.descent,
29578
29650
  unitsPerEm: this.ppem || 1
29579
29651
  };
29580
29652
  }
@@ -29762,6 +29834,11 @@ var RealizedFont = class {
29762
29834
  }
29763
29835
  return base + (this.syntheticBold ? 1 : 0);
29764
29836
  }
29837
+ /** GDI+ rounds tracked hmtx advances to font units before device scaling. */
29838
+ designAdvance(index, tracking) {
29839
+ const { advance } = this.ttf.hMetrics(index < this.ttf.numGlyphs ? index : NOTDEF);
29840
+ return Math.round(advance * tracking) * this.ppemX / this.ttf.unitsPerEm + (this.syntheticBold ? 1 : 0);
29841
+ }
29765
29842
  /**
29766
29843
  * Advance of glyph `index` in a font realised with an escapement: GDI
29767
29844
  * does not use the hinted/hdmx widths there but the linearly scaled
@@ -40449,6 +40526,35 @@ function surfaceSize5(ctx) {
40449
40526
  return typeof w === "number" && typeof h === "number" && w > 0 && h > 0 ? { w, h } : null;
40450
40527
  }
40451
40528
 
40529
+ // src/emf-plus-text-alignment.ts
40530
+ var SIMPLE_SCRIPT = /^(?:\p{Script=Latin}|\p{Script=Common}|\p{Co})$/u;
40531
+ var SHAPING_CHARACTER = /[\p{M}\p{Cf}\p{Emoji_Presentation}]/u;
40532
+ function layoutPlusAlignedString(font, codes, format, x, y, width, height) {
40533
+ const horizontal = format.alignment, vertical = format.lineAlignment;
40534
+ if (!horizontal && !vertical || horizontal < 0 || horizontal > 2 || vertical < 0 || vertical > 2 || font.gridFit || font.mode !== "gray" || !font.designAdvance || font.ttf.winDescent === void 0 || font.syntheticBold || font.syntheticItalic || format.flags & 3 || ![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0 || codes.some((c) => !Number.isInteger(c) || c < 32 || c > 1114111 || c >= 127 && c <= 159 || c === 8232 || c === 8233)) return null;
40535
+ if (codes.some((c) => {
40536
+ const character = String.fromCodePoint(c);
40537
+ return !SIMPLE_SCRIPT.test(character) || SHAPING_CHARACTER.test(character) || font.glyphIndex(c) === 0;
40538
+ })) return null;
40539
+ const em = font.ppem;
40540
+ const tracking = format.tracking ?? 1.03;
40541
+ const leading = em * (format.leadingMargin ?? 1 / 6);
40542
+ const trailing = em * (format.trailingMargin ?? 1 / 6);
40543
+ if (![tracking, leading, trailing].every((v) => Number.isFinite(v) && v >= 0)) return null;
40544
+ if (!(format.flags & 4) && (font.ttf.fsSelection & 1 || font.ttf.macStyle & 2)) return null;
40545
+ const dx = codes.map((c) => font.designAdvance(font.glyphIndex(c), tracking));
40546
+ let measuredCount = codes.length;
40547
+ if (!(format.flags & 2048)) while (measuredCount > 0 && codes[measuredCount - 1] === 32) measuredCount--;
40548
+ const measuredWidth = dx.slice(0, measuredCount).reduce((a, b) => a + b, 0) + leading + trailing;
40549
+ const measuredHeight = em * (font.ttf.winAscent + font.ttf.winDescent) / font.ttf.unitsPerEm + (format.flags & 4 ? 0 : em / 8);
40550
+ if (measuredWidth > width || measuredHeight > height) return null;
40551
+ return {
40552
+ x: Math.fround(x + horizontal / 2 * (width - measuredWidth) + leading),
40553
+ y: Math.fround(y + vertical / 2 * (height - measuredHeight)) + Math.round(em * font.ttf.winAscent / font.ttf.unitsPerEm),
40554
+ dx
40555
+ };
40556
+ }
40557
+
40452
40558
  // src/emf-plus-text-image-handlers.ts
40453
40559
  function imageAttributesWrap(rCtx, attributesId) {
40454
40560
  const obj = attributesId <= 255 ? rCtx.objectTable.get(attributesId) : void 0;
@@ -40717,25 +40823,33 @@ function paintPlusEngineRun(rCtx, ef, run) {
40717
40823
  );
40718
40824
  return true;
40719
40825
  }
40720
- function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, alignment, format, brushSampler2 = null) {
40721
- if (alignment !== 0) {
40722
- return false;
40723
- }
40826
+ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, layoutW, layoutH, paint, alignment, format, brushSampler2 = null) {
40724
40827
  const ef = realizePlusEngineFont(rCtx, font, paint, brushSampler2);
40725
40828
  if (!ef) {
40726
40829
  return false;
40727
40830
  }
40728
40831
  const { realized, emPx, m, unhinted } = ef;
40832
+ const codes = Array.from(text, (character) => character.codePointAt(0));
40833
+ if (format && (alignment !== 0 || format.lineAlignment !== 0) && unhinted && m[0] === m[3] && Math.fround(emPx) === realized.ppem) {
40834
+ const aligned = layoutPlusAlignedString(
40835
+ realized,
40836
+ codes,
40837
+ format,
40838
+ m[0] * layoutX + m[4],
40839
+ m[3] * layoutY + m[5],
40840
+ m[0] * layoutW,
40841
+ m[3] * layoutH
40842
+ );
40843
+ if (aligned) return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, ...aligned, dy: null });
40844
+ }
40845
+ if (alignment !== 0) return false;
40729
40846
  const ttf = realized.ttf;
40730
40847
  const exactAscent = emPx * ttf.winAscent / ttf.unitsPerEm;
40731
- const ascent = unhinted ? Math.round(exactAscent) : Math.ceil(exactAscent);
40848
+ const clearTypeCell = realized.mode === "cleartype" && !realized.syntheticBold && !realized.syntheticItalic && emPx === realized.ppem && m[0] === 1 && m[3] === 1 && Number.isInteger(layoutY + m[5]) && (format?.lineAlignment ?? 0) === 0;
40849
+ const ascent = unhinted ? Math.round(exactAscent) : clearTypeCell ? Math.max(Math.round(exactAscent), realized.ascent) : Math.ceil(exactAscent);
40732
40850
  let x = m[0] * layoutX + m[4] + emPx * (format?.leadingMargin ?? PLUS_LEADING_EM);
40733
40851
  if (!unhinted) x = Math.round(x);
40734
40852
  const y = m[3] * layoutY + m[5] + ascent;
40735
- const codes = [];
40736
- for (const character of text) {
40737
- codes.push(character.codePointAt(0));
40738
- }
40739
40853
  const tracking = format?.tracking ?? PLUS_DEFAULT_TRACKING;
40740
40854
  const dx = unhinted ? codes.map((c) => realized.advance(realized.glyphIndex(c)) * tracking) : null;
40741
40855
  return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, x, y, dx, dy: null });
@@ -40817,8 +40931,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40817
40931
  const strLen = view.getUint32(dataOff + 8, true);
40818
40932
  const layoutX = view.getFloat32(dataOff + 12, true);
40819
40933
  const layoutY = view.getFloat32(dataOff + 16, true);
40820
- view.getFloat32(dataOff + 20, true);
40821
- view.getFloat32(dataOff + 24, true);
40934
+ const layoutW = view.getFloat32(dataOff + 20, true);
40935
+ const layoutH = view.getFloat32(dataOff + 24, true);
40822
40936
  const fontId = recFlags & 255;
40823
40937
  const font = objectTable.get(fontId);
40824
40938
  if (strLen > 0 && dataOff + 28 + strLen * 2 <= dataOff + recDataSize) {
@@ -40834,6 +40948,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40834
40948
  text,
40835
40949
  layoutX,
40836
40950
  layoutY,
40951
+ layoutW,
40952
+ layoutH,
40837
40953
  paint,
40838
40954
  alignment,
40839
40955
  format,
package/dist/index.js CHANGED
@@ -29020,7 +29020,7 @@ var MonoStubContour = class {
29020
29020
  constructor(o) {
29021
29021
  this.points = Array.from(o.xs, (x, i) => [x / 64 - 0.5, o.ys[i] / 64 - 0.5]);
29022
29022
  }
29023
- isStub(flipped, scan, crossing, lo, hi, precision) {
29023
+ isStub(flipped, scan, crossing, lo, hi, precision, _opposite) {
29024
29024
  const points = this.points;
29025
29025
  const n = points.length;
29026
29026
  const point = (i) => flipped ? [points[i][1], points[i][0]] : points[i];
@@ -29056,6 +29056,82 @@ var MonoStubContour = class {
29056
29056
  return !(a.side * b.side < 0 && a.continues && b.continues);
29057
29057
  }
29058
29058
  };
29059
+ function convexContour(o) {
29060
+ const edges = [];
29061
+ for (let i = 0; i < o.xs.length; i++) {
29062
+ const j = (i + 1) % o.xs.length;
29063
+ const dx = o.xs[j] - o.xs[i], dy = o.ys[j] - o.ys[i];
29064
+ if (dx || dy) edges.push([dx, dy]);
29065
+ }
29066
+ if (edges.length < 3) return false;
29067
+ const half = (p) => p[1] > 0 || p[1] === 0 && p[0] >= 0 ? 0 : 1;
29068
+ let direction = 0, revolutions = 0;
29069
+ for (let i = 0; i < edges.length; i++) {
29070
+ const a = edges[i], b = edges[(i + 1) % edges.length];
29071
+ const cross = a[0] * b[1] - a[1] * b[0];
29072
+ if (!cross) {
29073
+ if (a[0] * b[0] + a[1] * b[1] < 0) return false;
29074
+ continue;
29075
+ }
29076
+ const turn = Math.sign(cross);
29077
+ if (direction && direction !== turn) return false;
29078
+ direction = turn;
29079
+ if (half(a) !== half(b) && half(a) === (turn > 0 ? 1 : 0)) revolutions++;
29080
+ }
29081
+ return direction !== 0 && revolutions === 1;
29082
+ }
29083
+ var MonoStubContours = class _MonoStubContours {
29084
+ constructor(contours) {
29085
+ this.contours = contours;
29086
+ }
29087
+ static create(o) {
29088
+ if (o.endPts.length > 64) return null;
29089
+ if (Array.from(o.onCurve).some((p) => !p)) return null;
29090
+ if (o.endPts.length === 1) return new MonoStubContour(o);
29091
+ const contours = [];
29092
+ const boxes = [];
29093
+ let start = 0;
29094
+ for (const end of o.endPts) {
29095
+ const xs = [], ys = [], onCurve = [];
29096
+ for (let i = start; i <= end; i++) {
29097
+ xs.push(o.xs[i]);
29098
+ ys.push(o.ys[i]);
29099
+ onCurve.push(o.onCurve[i]);
29100
+ }
29101
+ const part = { xs, ys, onCurve, endPts: [end - start] };
29102
+ start = end + 1;
29103
+ if (!convexContour(part)) return null;
29104
+ const box = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity };
29105
+ for (let i = 0; i < part.xs.length; i++) {
29106
+ box.xMin = Math.min(box.xMin, part.xs[i]);
29107
+ box.xMax = Math.max(box.xMax, part.xs[i]);
29108
+ box.yMin = Math.min(box.yMin, part.ys[i]);
29109
+ box.yMax = Math.max(box.yMax, part.ys[i]);
29110
+ }
29111
+ if (boxes.some((b) => box.xMin <= b.xMax + 64 && b.xMin <= box.xMax + 64 && box.yMin <= b.yMax + 64 && b.yMin <= box.yMax + 64)) return null;
29112
+ boxes.push(box);
29113
+ contours.push(new MonoStubContour(part));
29114
+ }
29115
+ return new _MonoStubContours(contours);
29116
+ }
29117
+ isStub(flipped, scan, crossing, lo, hi, precision, opposite) {
29118
+ let owner = -1, otherOwner = -1, result = null;
29119
+ for (let i = 0; i < this.contours.length; i++) {
29120
+ const a = this.contours[i].isStub(flipped, scan, crossing, lo, hi, precision);
29121
+ const b = this.contours[i].isStub(flipped, scan, opposite, lo, hi, precision);
29122
+ if (a !== null) {
29123
+ if (owner >= 0) return null;
29124
+ owner = i;
29125
+ result = a;
29126
+ }
29127
+ if (b !== null) {
29128
+ if (otherOwner >= 0) return null;
29129
+ otherOwner = i;
29130
+ }
29131
+ }
29132
+ return owner >= 0 && owner === otherOwner ? result : null;
29133
+ }
29134
+ };
29059
29135
  function monoBox(o) {
29060
29136
  const n = o.xs.length;
29061
29137
  if (n === 0) {
@@ -29077,16 +29153,11 @@ function monoBox(o) {
29077
29153
  let xMax = Math.floor((cxMax + 32) / 64);
29078
29154
  let yMin = Math.floor((cyMin + 31) / 64);
29079
29155
  let yMax = Math.floor((cyMax + 32) / 64);
29080
- const rem = (v, add) => (v + add & 63) - add;
29081
- if (xMin === xMax) {
29082
- if (rem(cxMin, 31) + rem(cxMax, 32) < 0) xMin -= 1;
29083
- else xMax += 1;
29084
- }
29085
- if (yMin === yMax) {
29086
- if (rem(cyMin, 31) + rem(cyMax, 32) < 0) yMin -= 1;
29087
- else yMax += 1;
29088
- }
29089
- return { xMin, xMax, yMin, yMax };
29156
+ const collapsedX = xMin === xMax;
29157
+ const collapsedY = yMin === yMax;
29158
+ if (collapsedX) xMax += 1;
29159
+ if (collapsedY) yMax += 1;
29160
+ return { xMin, xMax, yMin, yMax, collapsedX, collapsedY };
29090
29161
  }
29091
29162
  function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossings26Dot6 = false) {
29092
29163
  const box = fixedBox ?? monoBox(o);
@@ -29110,7 +29181,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29110
29181
  const at = (col, row2) => (height - 1 - row2) * width + col;
29111
29182
  const r = new Raster(dropout);
29112
29183
  const P = r.precision;
29113
- const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) && local.endPts.length === 1 && Array.from(local.onCurve).every(Boolean) ? new MonoStubContour(local) : null;
29184
+ const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) ? MonoStubContours.create(local) : null;
29114
29185
  const smartPixel = (raster, x1, x2) => {
29115
29186
  if (monoContour && smartCrossings26Dot6) {
29116
29187
  const step = P / 64;
@@ -29153,7 +29224,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29153
29224
  break;
29154
29225
  case 1:
29155
29226
  case 5:
29156
- const stub = monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P);
29227
+ const stub = monoContour && box.collapsedX ? false : monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P, x2 / P);
29157
29228
  if (stub === true) return;
29158
29229
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= r.half)) {
29159
29230
  return;
@@ -29214,7 +29285,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29214
29285
  break;
29215
29286
  case 1:
29216
29287
  case 5:
29217
- const stub = monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P);
29288
+ const stub = monoContour && box.collapsedY ? false : monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P, x2 / P);
29218
29289
  if (stub === true) return;
29219
29290
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= h.half)) {
29220
29291
  return;
@@ -29578,6 +29649,7 @@ var RasterRealizedFont = class {
29578
29649
  fsSelection: face.italic ? 1 : 0,
29579
29650
  macStyle: 0,
29580
29651
  winAscent: this.ascent,
29652
+ winDescent: this.descent,
29581
29653
  unitsPerEm: this.ppem || 1
29582
29654
  };
29583
29655
  }
@@ -29765,6 +29837,11 @@ var RealizedFont = class {
29765
29837
  }
29766
29838
  return base + (this.syntheticBold ? 1 : 0);
29767
29839
  }
29840
+ /** GDI+ rounds tracked hmtx advances to font units before device scaling. */
29841
+ designAdvance(index, tracking) {
29842
+ const { advance } = this.ttf.hMetrics(index < this.ttf.numGlyphs ? index : NOTDEF);
29843
+ return Math.round(advance * tracking) * this.ppemX / this.ttf.unitsPerEm + (this.syntheticBold ? 1 : 0);
29844
+ }
29768
29845
  /**
29769
29846
  * Advance of glyph `index` in a font realised with an escapement: GDI
29770
29847
  * does not use the hinted/hdmx widths there but the linearly scaled
@@ -40452,6 +40529,35 @@ function surfaceSize5(ctx) {
40452
40529
  return typeof w === "number" && typeof h === "number" && w > 0 && h > 0 ? { w, h } : null;
40453
40530
  }
40454
40531
 
40532
+ // src/emf-plus-text-alignment.ts
40533
+ var SIMPLE_SCRIPT = /^(?:\p{Script=Latin}|\p{Script=Common}|\p{Co})$/u;
40534
+ var SHAPING_CHARACTER = /[\p{M}\p{Cf}\p{Emoji_Presentation}]/u;
40535
+ function layoutPlusAlignedString(font, codes, format, x, y, width, height) {
40536
+ const horizontal = format.alignment, vertical = format.lineAlignment;
40537
+ if (!horizontal && !vertical || horizontal < 0 || horizontal > 2 || vertical < 0 || vertical > 2 || font.gridFit || font.mode !== "gray" || !font.designAdvance || font.ttf.winDescent === void 0 || font.syntheticBold || font.syntheticItalic || format.flags & 3 || ![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0 || codes.some((c) => !Number.isInteger(c) || c < 32 || c > 1114111 || c >= 127 && c <= 159 || c === 8232 || c === 8233)) return null;
40538
+ if (codes.some((c) => {
40539
+ const character = String.fromCodePoint(c);
40540
+ return !SIMPLE_SCRIPT.test(character) || SHAPING_CHARACTER.test(character) || font.glyphIndex(c) === 0;
40541
+ })) return null;
40542
+ const em = font.ppem;
40543
+ const tracking = format.tracking ?? 1.03;
40544
+ const leading = em * (format.leadingMargin ?? 1 / 6);
40545
+ const trailing = em * (format.trailingMargin ?? 1 / 6);
40546
+ if (![tracking, leading, trailing].every((v) => Number.isFinite(v) && v >= 0)) return null;
40547
+ if (!(format.flags & 4) && (font.ttf.fsSelection & 1 || font.ttf.macStyle & 2)) return null;
40548
+ const dx = codes.map((c) => font.designAdvance(font.glyphIndex(c), tracking));
40549
+ let measuredCount = codes.length;
40550
+ if (!(format.flags & 2048)) while (measuredCount > 0 && codes[measuredCount - 1] === 32) measuredCount--;
40551
+ const measuredWidth = dx.slice(0, measuredCount).reduce((a, b) => a + b, 0) + leading + trailing;
40552
+ const measuredHeight = em * (font.ttf.winAscent + font.ttf.winDescent) / font.ttf.unitsPerEm + (format.flags & 4 ? 0 : em / 8);
40553
+ if (measuredWidth > width || measuredHeight > height) return null;
40554
+ return {
40555
+ x: Math.fround(x + horizontal / 2 * (width - measuredWidth) + leading),
40556
+ y: Math.fround(y + vertical / 2 * (height - measuredHeight)) + Math.round(em * font.ttf.winAscent / font.ttf.unitsPerEm),
40557
+ dx
40558
+ };
40559
+ }
40560
+
40455
40561
  // src/emf-plus-text-image-handlers.ts
40456
40562
  function imageAttributesWrap(rCtx, attributesId) {
40457
40563
  const obj = attributesId <= 255 ? rCtx.objectTable.get(attributesId) : void 0;
@@ -40720,25 +40826,33 @@ function paintPlusEngineRun(rCtx, ef, run) {
40720
40826
  );
40721
40827
  return true;
40722
40828
  }
40723
- function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, alignment, format, brushSampler2 = null) {
40724
- if (alignment !== 0) {
40725
- return false;
40726
- }
40829
+ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, layoutW, layoutH, paint, alignment, format, brushSampler2 = null) {
40727
40830
  const ef = realizePlusEngineFont(rCtx, font, paint, brushSampler2);
40728
40831
  if (!ef) {
40729
40832
  return false;
40730
40833
  }
40731
40834
  const { realized, emPx, m, unhinted } = ef;
40835
+ const codes = Array.from(text, (character) => character.codePointAt(0));
40836
+ if (format && (alignment !== 0 || format.lineAlignment !== 0) && unhinted && m[0] === m[3] && Math.fround(emPx) === realized.ppem) {
40837
+ const aligned = layoutPlusAlignedString(
40838
+ realized,
40839
+ codes,
40840
+ format,
40841
+ m[0] * layoutX + m[4],
40842
+ m[3] * layoutY + m[5],
40843
+ m[0] * layoutW,
40844
+ m[3] * layoutH
40845
+ );
40846
+ if (aligned) return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, ...aligned, dy: null });
40847
+ }
40848
+ if (alignment !== 0) return false;
40732
40849
  const ttf = realized.ttf;
40733
40850
  const exactAscent = emPx * ttf.winAscent / ttf.unitsPerEm;
40734
- const ascent = unhinted ? Math.round(exactAscent) : Math.ceil(exactAscent);
40851
+ const clearTypeCell = realized.mode === "cleartype" && !realized.syntheticBold && !realized.syntheticItalic && emPx === realized.ppem && m[0] === 1 && m[3] === 1 && Number.isInteger(layoutY + m[5]) && (format?.lineAlignment ?? 0) === 0;
40852
+ const ascent = unhinted ? Math.round(exactAscent) : clearTypeCell ? Math.max(Math.round(exactAscent), realized.ascent) : Math.ceil(exactAscent);
40735
40853
  let x = m[0] * layoutX + m[4] + emPx * (format?.leadingMargin ?? PLUS_LEADING_EM);
40736
40854
  if (!unhinted) x = Math.round(x);
40737
40855
  const y = m[3] * layoutY + m[5] + ascent;
40738
- const codes = [];
40739
- for (const character of text) {
40740
- codes.push(character.codePointAt(0));
40741
- }
40742
40856
  const tracking = format?.tracking ?? PLUS_DEFAULT_TRACKING;
40743
40857
  const dx = unhinted ? codes.map((c) => realized.advance(realized.glyphIndex(c)) * tracking) : null;
40744
40858
  return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, x, y, dx, dy: null });
@@ -40820,8 +40934,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40820
40934
  const strLen = view.getUint32(dataOff + 8, true);
40821
40935
  const layoutX = view.getFloat32(dataOff + 12, true);
40822
40936
  const layoutY = view.getFloat32(dataOff + 16, true);
40823
- view.getFloat32(dataOff + 20, true);
40824
- view.getFloat32(dataOff + 24, true);
40937
+ const layoutW = view.getFloat32(dataOff + 20, true);
40938
+ const layoutH = view.getFloat32(dataOff + 24, true);
40825
40939
  const fontId = recFlags & 255;
40826
40940
  const font = objectTable.get(fontId);
40827
40941
  if (strLen > 0 && dataOff + 28 + strLen * 2 <= dataOff + recDataSize) {
@@ -40837,6 +40951,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40837
40951
  text,
40838
40952
  layoutX,
40839
40953
  layoutY,
40954
+ layoutW,
40955
+ layoutH,
40840
40956
  paint,
40841
40957
  alignment,
40842
40958
  format,
package/dist/index.mjs CHANGED
@@ -29018,7 +29018,7 @@ var MonoStubContour = class {
29018
29018
  constructor(o) {
29019
29019
  this.points = Array.from(o.xs, (x, i) => [x / 64 - 0.5, o.ys[i] / 64 - 0.5]);
29020
29020
  }
29021
- isStub(flipped, scan, crossing, lo, hi, precision) {
29021
+ isStub(flipped, scan, crossing, lo, hi, precision, _opposite) {
29022
29022
  const points = this.points;
29023
29023
  const n = points.length;
29024
29024
  const point = (i) => flipped ? [points[i][1], points[i][0]] : points[i];
@@ -29054,6 +29054,82 @@ var MonoStubContour = class {
29054
29054
  return !(a.side * b.side < 0 && a.continues && b.continues);
29055
29055
  }
29056
29056
  };
29057
+ function convexContour(o) {
29058
+ const edges = [];
29059
+ for (let i = 0; i < o.xs.length; i++) {
29060
+ const j = (i + 1) % o.xs.length;
29061
+ const dx = o.xs[j] - o.xs[i], dy = o.ys[j] - o.ys[i];
29062
+ if (dx || dy) edges.push([dx, dy]);
29063
+ }
29064
+ if (edges.length < 3) return false;
29065
+ const half = (p) => p[1] > 0 || p[1] === 0 && p[0] >= 0 ? 0 : 1;
29066
+ let direction = 0, revolutions = 0;
29067
+ for (let i = 0; i < edges.length; i++) {
29068
+ const a = edges[i], b = edges[(i + 1) % edges.length];
29069
+ const cross = a[0] * b[1] - a[1] * b[0];
29070
+ if (!cross) {
29071
+ if (a[0] * b[0] + a[1] * b[1] < 0) return false;
29072
+ continue;
29073
+ }
29074
+ const turn = Math.sign(cross);
29075
+ if (direction && direction !== turn) return false;
29076
+ direction = turn;
29077
+ if (half(a) !== half(b) && half(a) === (turn > 0 ? 1 : 0)) revolutions++;
29078
+ }
29079
+ return direction !== 0 && revolutions === 1;
29080
+ }
29081
+ var MonoStubContours = class _MonoStubContours {
29082
+ constructor(contours) {
29083
+ this.contours = contours;
29084
+ }
29085
+ static create(o) {
29086
+ if (o.endPts.length > 64) return null;
29087
+ if (Array.from(o.onCurve).some((p) => !p)) return null;
29088
+ if (o.endPts.length === 1) return new MonoStubContour(o);
29089
+ const contours = [];
29090
+ const boxes = [];
29091
+ let start = 0;
29092
+ for (const end of o.endPts) {
29093
+ const xs = [], ys = [], onCurve = [];
29094
+ for (let i = start; i <= end; i++) {
29095
+ xs.push(o.xs[i]);
29096
+ ys.push(o.ys[i]);
29097
+ onCurve.push(o.onCurve[i]);
29098
+ }
29099
+ const part = { xs, ys, onCurve, endPts: [end - start] };
29100
+ start = end + 1;
29101
+ if (!convexContour(part)) return null;
29102
+ const box = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity };
29103
+ for (let i = 0; i < part.xs.length; i++) {
29104
+ box.xMin = Math.min(box.xMin, part.xs[i]);
29105
+ box.xMax = Math.max(box.xMax, part.xs[i]);
29106
+ box.yMin = Math.min(box.yMin, part.ys[i]);
29107
+ box.yMax = Math.max(box.yMax, part.ys[i]);
29108
+ }
29109
+ if (boxes.some((b) => box.xMin <= b.xMax + 64 && b.xMin <= box.xMax + 64 && box.yMin <= b.yMax + 64 && b.yMin <= box.yMax + 64)) return null;
29110
+ boxes.push(box);
29111
+ contours.push(new MonoStubContour(part));
29112
+ }
29113
+ return new _MonoStubContours(contours);
29114
+ }
29115
+ isStub(flipped, scan, crossing, lo, hi, precision, opposite) {
29116
+ let owner = -1, otherOwner = -1, result = null;
29117
+ for (let i = 0; i < this.contours.length; i++) {
29118
+ const a = this.contours[i].isStub(flipped, scan, crossing, lo, hi, precision);
29119
+ const b = this.contours[i].isStub(flipped, scan, opposite, lo, hi, precision);
29120
+ if (a !== null) {
29121
+ if (owner >= 0) return null;
29122
+ owner = i;
29123
+ result = a;
29124
+ }
29125
+ if (b !== null) {
29126
+ if (otherOwner >= 0) return null;
29127
+ otherOwner = i;
29128
+ }
29129
+ }
29130
+ return owner >= 0 && owner === otherOwner ? result : null;
29131
+ }
29132
+ };
29057
29133
  function monoBox(o) {
29058
29134
  const n = o.xs.length;
29059
29135
  if (n === 0) {
@@ -29075,16 +29151,11 @@ function monoBox(o) {
29075
29151
  let xMax = Math.floor((cxMax + 32) / 64);
29076
29152
  let yMin = Math.floor((cyMin + 31) / 64);
29077
29153
  let yMax = Math.floor((cyMax + 32) / 64);
29078
- const rem = (v, add) => (v + add & 63) - add;
29079
- if (xMin === xMax) {
29080
- if (rem(cxMin, 31) + rem(cxMax, 32) < 0) xMin -= 1;
29081
- else xMax += 1;
29082
- }
29083
- if (yMin === yMax) {
29084
- if (rem(cyMin, 31) + rem(cyMax, 32) < 0) yMin -= 1;
29085
- else yMax += 1;
29086
- }
29087
- return { xMin, xMax, yMin, yMax };
29154
+ const collapsedX = xMin === xMax;
29155
+ const collapsedY = yMin === yMax;
29156
+ if (collapsedX) xMax += 1;
29157
+ if (collapsedY) yMax += 1;
29158
+ return { xMin, xMax, yMin, yMax, collapsedX, collapsedY };
29088
29159
  }
29089
29160
  function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossings26Dot6 = false) {
29090
29161
  const box = fixedBox ?? monoBox(o);
@@ -29108,7 +29179,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29108
29179
  const at = (col, row2) => (height - 1 - row2) * width + col;
29109
29180
  const r = new Raster(dropout);
29110
29181
  const P = r.precision;
29111
- const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) && local.endPts.length === 1 && Array.from(local.onCurve).every(Boolean) ? new MonoStubContour(local) : null;
29182
+ const monoContour = stubOvershoot && (dropout === 1 || dropout === 4 || dropout === 5) ? MonoStubContours.create(local) : null;
29112
29183
  const smartPixel = (raster, x1, x2) => {
29113
29184
  if (monoContour && smartCrossings26Dot6) {
29114
29185
  const step = P / 64;
@@ -29151,7 +29222,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29151
29222
  break;
29152
29223
  case 1:
29153
29224
  case 5:
29154
- const stub = monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P);
29225
+ const stub = monoContour && box.collapsedX ? false : monoContour?.isStub(false, y, x1 / P, e2 / P, e1 / P, P, x2 / P);
29155
29226
  if (stub === true) return;
29156
29227
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= r.half)) {
29157
29228
  return;
@@ -29212,7 +29283,7 @@ function rasterizeMono(o, dropout, fixedBox, stubOvershoot = true, smartCrossing
29212
29283
  break;
29213
29284
  case 1:
29214
29285
  case 5:
29215
- const stub = monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P);
29286
+ const stub = monoContour && box.collapsedY ? false : monoContour?.isStub(true, col, x1 / P, e2 / P, e1 / P, P, x2 / P);
29216
29287
  if (stub === true) return;
29217
29288
  if (stub == null && left.next === right && left.height <= 0 && !(stubOvershoot && left.flags & OVERSHOOT_TOP && x2 - x1 >= h.half)) {
29218
29289
  return;
@@ -29576,6 +29647,7 @@ var RasterRealizedFont = class {
29576
29647
  fsSelection: face.italic ? 1 : 0,
29577
29648
  macStyle: 0,
29578
29649
  winAscent: this.ascent,
29650
+ winDescent: this.descent,
29579
29651
  unitsPerEm: this.ppem || 1
29580
29652
  };
29581
29653
  }
@@ -29763,6 +29835,11 @@ var RealizedFont = class {
29763
29835
  }
29764
29836
  return base + (this.syntheticBold ? 1 : 0);
29765
29837
  }
29838
+ /** GDI+ rounds tracked hmtx advances to font units before device scaling. */
29839
+ designAdvance(index, tracking) {
29840
+ const { advance } = this.ttf.hMetrics(index < this.ttf.numGlyphs ? index : NOTDEF);
29841
+ return Math.round(advance * tracking) * this.ppemX / this.ttf.unitsPerEm + (this.syntheticBold ? 1 : 0);
29842
+ }
29766
29843
  /**
29767
29844
  * Advance of glyph `index` in a font realised with an escapement: GDI
29768
29845
  * does not use the hinted/hdmx widths there but the linearly scaled
@@ -40450,6 +40527,35 @@ function surfaceSize5(ctx) {
40450
40527
  return typeof w === "number" && typeof h === "number" && w > 0 && h > 0 ? { w, h } : null;
40451
40528
  }
40452
40529
 
40530
+ // src/emf-plus-text-alignment.ts
40531
+ var SIMPLE_SCRIPT = /^(?:\p{Script=Latin}|\p{Script=Common}|\p{Co})$/u;
40532
+ var SHAPING_CHARACTER = /[\p{M}\p{Cf}\p{Emoji_Presentation}]/u;
40533
+ function layoutPlusAlignedString(font, codes, format, x, y, width, height) {
40534
+ const horizontal = format.alignment, vertical = format.lineAlignment;
40535
+ if (!horizontal && !vertical || horizontal < 0 || horizontal > 2 || vertical < 0 || vertical > 2 || font.gridFit || font.mode !== "gray" || !font.designAdvance || font.ttf.winDescent === void 0 || font.syntheticBold || font.syntheticItalic || format.flags & 3 || ![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0 || codes.some((c) => !Number.isInteger(c) || c < 32 || c > 1114111 || c >= 127 && c <= 159 || c === 8232 || c === 8233)) return null;
40536
+ if (codes.some((c) => {
40537
+ const character = String.fromCodePoint(c);
40538
+ return !SIMPLE_SCRIPT.test(character) || SHAPING_CHARACTER.test(character) || font.glyphIndex(c) === 0;
40539
+ })) return null;
40540
+ const em = font.ppem;
40541
+ const tracking = format.tracking ?? 1.03;
40542
+ const leading = em * (format.leadingMargin ?? 1 / 6);
40543
+ const trailing = em * (format.trailingMargin ?? 1 / 6);
40544
+ if (![tracking, leading, trailing].every((v) => Number.isFinite(v) && v >= 0)) return null;
40545
+ if (!(format.flags & 4) && (font.ttf.fsSelection & 1 || font.ttf.macStyle & 2)) return null;
40546
+ const dx = codes.map((c) => font.designAdvance(font.glyphIndex(c), tracking));
40547
+ let measuredCount = codes.length;
40548
+ if (!(format.flags & 2048)) while (measuredCount > 0 && codes[measuredCount - 1] === 32) measuredCount--;
40549
+ const measuredWidth = dx.slice(0, measuredCount).reduce((a, b) => a + b, 0) + leading + trailing;
40550
+ const measuredHeight = em * (font.ttf.winAscent + font.ttf.winDescent) / font.ttf.unitsPerEm + (format.flags & 4 ? 0 : em / 8);
40551
+ if (measuredWidth > width || measuredHeight > height) return null;
40552
+ return {
40553
+ x: Math.fround(x + horizontal / 2 * (width - measuredWidth) + leading),
40554
+ y: Math.fround(y + vertical / 2 * (height - measuredHeight)) + Math.round(em * font.ttf.winAscent / font.ttf.unitsPerEm),
40555
+ dx
40556
+ };
40557
+ }
40558
+
40453
40559
  // src/emf-plus-text-image-handlers.ts
40454
40560
  function imageAttributesWrap(rCtx, attributesId) {
40455
40561
  const obj = attributesId <= 255 ? rCtx.objectTable.get(attributesId) : void 0;
@@ -40718,25 +40824,33 @@ function paintPlusEngineRun(rCtx, ef, run) {
40718
40824
  );
40719
40825
  return true;
40720
40826
  }
40721
- function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, alignment, format, brushSampler2 = null) {
40722
- if (alignment !== 0) {
40723
- return false;
40724
- }
40827
+ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, layoutW, layoutH, paint, alignment, format, brushSampler2 = null) {
40725
40828
  const ef = realizePlusEngineFont(rCtx, font, paint, brushSampler2);
40726
40829
  if (!ef) {
40727
40830
  return false;
40728
40831
  }
40729
40832
  const { realized, emPx, m, unhinted } = ef;
40833
+ const codes = Array.from(text, (character) => character.codePointAt(0));
40834
+ if (format && (alignment !== 0 || format.lineAlignment !== 0) && unhinted && m[0] === m[3] && Math.fround(emPx) === realized.ppem) {
40835
+ const aligned = layoutPlusAlignedString(
40836
+ realized,
40837
+ codes,
40838
+ format,
40839
+ m[0] * layoutX + m[4],
40840
+ m[3] * layoutY + m[5],
40841
+ m[0] * layoutW,
40842
+ m[3] * layoutH
40843
+ );
40844
+ if (aligned) return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, ...aligned, dy: null });
40845
+ }
40846
+ if (alignment !== 0) return false;
40730
40847
  const ttf = realized.ttf;
40731
40848
  const exactAscent = emPx * ttf.winAscent / ttf.unitsPerEm;
40732
- const ascent = unhinted ? Math.round(exactAscent) : Math.ceil(exactAscent);
40849
+ const clearTypeCell = realized.mode === "cleartype" && !realized.syntheticBold && !realized.syntheticItalic && emPx === realized.ppem && m[0] === 1 && m[3] === 1 && Number.isInteger(layoutY + m[5]) && (format?.lineAlignment ?? 0) === 0;
40850
+ const ascent = unhinted ? Math.round(exactAscent) : clearTypeCell ? Math.max(Math.round(exactAscent), realized.ascent) : Math.ceil(exactAscent);
40733
40851
  let x = m[0] * layoutX + m[4] + emPx * (format?.leadingMargin ?? PLUS_LEADING_EM);
40734
40852
  if (!unhinted) x = Math.round(x);
40735
40853
  const y = m[3] * layoutY + m[5] + ascent;
40736
- const codes = [];
40737
- for (const character of text) {
40738
- codes.push(character.codePointAt(0));
40739
- }
40740
40854
  const tracking = format?.tracking ?? PLUS_DEFAULT_TRACKING;
40741
40855
  const dx = unhinted ? codes.map((c) => realized.advance(realized.glyphIndex(c)) * tracking) : null;
40742
40856
  return paintPlusEngineRun(rCtx, ef, { codes, glyphIndices: false, x, y, dx, dy: null });
@@ -40818,8 +40932,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40818
40932
  const strLen = view.getUint32(dataOff + 8, true);
40819
40933
  const layoutX = view.getFloat32(dataOff + 12, true);
40820
40934
  const layoutY = view.getFloat32(dataOff + 16, true);
40821
- view.getFloat32(dataOff + 20, true);
40822
- view.getFloat32(dataOff + 24, true);
40935
+ const layoutW = view.getFloat32(dataOff + 20, true);
40936
+ const layoutH = view.getFloat32(dataOff + 24, true);
40823
40937
  const fontId = recFlags & 255;
40824
40938
  const font = objectTable.get(fontId);
40825
40939
  if (strLen > 0 && dataOff + 28 + strLen * 2 <= dataOff + recDataSize) {
@@ -40835,6 +40949,8 @@ function handleEmfPlusTextImageRecord(rCtx, recType, recFlags, dataOff, recDataS
40835
40949
  text,
40836
40950
  layoutX,
40837
40951
  layoutY,
40952
+ layoutW,
40953
+ layoutH,
40838
40954
  paint,
40839
40955
  alignment,
40840
40956
  format,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.20.1",
3
+ "version": "4.20.3",
4
4
  "description": "Convert EMF/WMF metafile binaries to PNG or JPEG data URLs, or SVG (markup, data URL, React/JSX)",
5
5
  "keywords": [
6
6
  "canvas",