emf-converter 4.0.0 → 4.1.0

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
@@ -206,7 +206,7 @@ Everything below is verified against output painted by Windows itself; `src/gdi-
206
206
 
207
207
  ### Supported records
208
208
 
209
- - **EMF**: 115 of the 119 record types defined in MS-EMF, including logical palettes (`PALETTEINDEX`, `DIBPALETTEINDEX`, `PALETTERGB`, `DIB_PAL_COLORS`), `EMR_ALPHABLEND` (Windows' exact integer blend), `EMR_TRANSPARENTBLT`, `EMR_MASKBLT`, `EMR_PLGBLT`, `EMR_SETDIBITSTODEVICE`, `EMR_GRADIENTFILL` (rectangles and triangles), `EMR_FILLRGN` / `EMR_FRAMERGN` / `EMR_INVERTRGN` / `EMR_PAINTRGN`, `EMR_EXTFLOODFILL`, `EMR_ANGLEARC`, `EMR_POLYDRAW(16)`, `EMR_FLATTENPATH` / `EMR_WIDENPATH` / `EMR_ABORTPATH`, alongside shapes, paths, `EMR_EXTTEXTOUTW`, blits, clipping and transforms. Colour space, ICM, OpenGL, escape and font-driver records are consumed without effect, as on a Windows display.
209
+ - **EMF**: Record playback includes logical palettes (`PALETTEINDEX`, `DIBPALETTEINDEX`, `PALETTERGB`, `DIB_PAL_COLORS`), `EMR_ALPHABLEND` (Windows' exact integer blend), `EMR_TRANSPARENTBLT`, `EMR_MASKBLT`, `EMR_PLGBLT`, `EMR_SETDIBITSTODEVICE`, `EMR_GRADIENTFILL` (rectangles and triangles), `EMR_FILLRGN` / `EMR_FRAMERGN` / `EMR_INVERTRGN` / `EMR_PAINTRGN`, `EMR_EXTFLOODFILL`, `EMR_ANGLEARC`, `EMR_POLYDRAW(16)`, `EMR_FLATTENPATH` / `EMR_WIDENPATH` / `EMR_ABORTPATH`, alongside shapes, paths, `EMR_EXTTEXTOUTA/W`, `EMR_POLYTEXTOUTA/W`, `EMR_SMALLTEXTOUT`, text justification, blits, clipping and transforms. Colour space, ICM, OpenGL, escape and font-driver records are consumed without effect, as on a Windows display.
210
210
  - **EMF+**: every record in MS-EMFPLUS (Beziers, cardinal curves, regions, containers, save/restore, compositing mode, rendering origin, text contrast, `StrokeFillPath`, the terminal-server `SetTSGraphics` / `SetTSClip`, `MultiFormat*` played as GDI+ plays them), with 32-bit, compressed 16-bit and relative point data, and every object type (solid, hatch, texture and gradient brushes; pens with caps, joins, dash styles, dash caps, compound lines and custom line caps; paths, regions, bitmap and metafile images, fonts, string formats, image attributes). Several encodings follow what GDI+ actually does where it differs from MS-EMFPLUS (relative points, `StrokeFillPath`, `SetTSGraphics`, `SetTSClip`).
211
211
  - **WMF**: META_ANIMATEPALETTE, META_ARC, META_BITBLT, META_CHORD, META_CREATEBITMAP, META_CREATEBITMAPINDIRECT, META_CREATEBRUSH, META_CREATEBRUSHINDIRECT, META_CREATEFONTINDIRECT, META_CREATEPALETTE, META_CREATEPATTERNBRUSH, META_CREATEPENINDIRECT, META_CREATEREGION, META_DELETEOBJECT, META_DIBBITBLT, META_DIBCREATEPATTERNBRUSH, META_DIBSTRETCHBLT, META_ELLIPSE, META_EOF, META_ESCAPE, META_EXCLUDECLIPRECT, META_EXTFLOODFILL, META_EXTTEXTOUT, META_FILLREGION, META_FLOODFILL, META_FRAMEREGION, META_INTERSECTCLIPRECT, META_INVERTREGION, META_LINETO, META_MOVETO, META_OFFSETCLIPRGN, META_OFFSETVIEWPORTORG, META_OFFSETWINDOWORG, META_PAINTREGION, META_PATBLT, META_PIE, META_POLYGON, META_POLYLINE, META_POLYPOLYGON, META_REALIZEPALETTE, META_RECTANGLE, META_RESIZEPALETTE, META_RESTOREDC, META_ROUNDRECT, META_SAVEDC, META_SCALEVIEWPORTEXT, META_SCALEWINDOWEXT, META_SELECTCLIPREGION, META_SELECTOBJECT, META_SELECTPALETTE, META_SETBKCOLOR, META_SETBKMODE, META_SETDIBTODEV, META_SETLAYOUT, META_SETMAPMODE, META_SETMAPPERFLAGS, META_SETPALENTRIES, META_SETPIXEL, META_SETPOLYFILLMODE, META_SETRELABS, META_SETROP2, META_SETSTRETCHBLTMODE, META_SETTEXTALIGN, META_SETTEXTCHAREXTRA, META_SETTEXTCOLOR, META_SETTEXTJUSTIFICATION, META_SETVIEWPORTEXT, META_SETVIEWPORTORG, META_SETWINDOWEXT, META_SETWINDOWORG, META_STRETCHBLT, META_STRETCHDIB, META_TEXTOUT. Where Windows no longer plays a record the way MS-WMF describes it (Win16 device bitmaps in META_BITBLT / META_STRETCHBLT, META_CREATEPATTERNBRUSH, banded META_SETDIBTODEV), the converter follows Windows.
212
212
 
@@ -214,7 +214,9 @@ Everything below is verified against output painted by Windows itself; `src/gdi-
214
214
 
215
215
  Everything is measured against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the exact per-fixture bounds.
216
216
 
217
- - **Unhandled records**: `EMR_EXTTEXTOUTA`, `EMR_POLYTEXTOUTA`, `EMR_POLYTEXTOUTW` and `EMR_SMALLTEXTOUT` (ANSI, multi-string and small-glyph text records, rarely written by modern recorders) are skipped with a console warning. `EMR_SETTEXTJUSTIFICATION` and `EMR_SETCOLORADJUSTMENT` are read but not yet applied (Windows' own recorder bakes justification into `EMR_EXTTEXTOUTW` spacing arrays, so only other writers emit the former), and the `HALFTONE` stretch mode is not bit-exact. EMF+ image effects (`SerializableObject`: blur, sharpen, colour matrix and the like) are not applied, so the image is drawn without the effect, and an EMF+ pen's own transform is ignored.
217
+ - **Colour adjustment and image effects**: `EMR_SETCOLORADJUSTMENT` is read but not yet applied, and the `HALFTONE` stretch mode is not bit-exact. EMF+ image effects (`SerializableObject`: blur, sharpen, colour matrix and the like) are not applied, so the image is drawn without the effect.
218
+ - **Pen transforms**: EMF+ pen transforms support uniform scaling with rotation and translation; nonuniform scaling and skew remain unsupported.
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.
218
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.
219
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.
220
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.
package/dist/index.js CHANGED
@@ -493,6 +493,23 @@ function totalGlyphAdvance(dx) {
493
493
  }
494
494
  return sum;
495
495
  }
496
+ function applyTextJustification(advances, characters, extra, count) {
497
+ const result = advances.slice();
498
+ if (extra === 0 || count <= 0) {
499
+ return result;
500
+ }
501
+ let seen = 0;
502
+ for (let i = 0; i < result.length && i < characters.length; i++) {
503
+ if (characters[i] !== 32) {
504
+ continue;
505
+ }
506
+ const before = Math.round(seen * extra / count);
507
+ seen++;
508
+ const after = Math.round(seen * extra / count);
509
+ result[i] += after - before;
510
+ }
511
+ return result;
512
+ }
496
513
  function alignmentStartOffset(totalWidth, align) {
497
514
  if (align === "center") {
498
515
  return -totalWidth / 2;
@@ -629,8 +646,11 @@ var EMR_PAINTRGN = 74;
629
646
  var EMR_MASKBLT = 78;
630
647
  var EMR_PLGBLT = 79;
631
648
  var EMR_SETDIBITSTODEVICE = 80;
649
+ var EMR_EXTTEXTOUTA = 83;
632
650
  var EMR_POLYPOLYLINE16 = 90;
633
651
  var EMR_POLYDRAW16 = 92;
652
+ var EMR_POLYTEXTOUTA = 96;
653
+ var EMR_POLYTEXTOUTW = 97;
634
654
  var EMR_CREATECOLORSPACE = 99;
635
655
  var EMR_SETCOLORSPACE = 100;
636
656
  var EMR_DELETECOLORSPACE = 101;
@@ -639,6 +659,7 @@ var EMR_GLSBOUNDEDRECORD = 103;
639
659
  var EMR_PIXELFORMAT = 104;
640
660
  var EMR_DRAWESCAPE = 105;
641
661
  var EMR_EXTESCAPE = 106;
662
+ var EMR_SMALLTEXTOUT = 108;
642
663
  var EMR_FORCEUFIMAPPING = 109;
643
664
  var EMR_NAMEDESCAPE = 110;
644
665
  var EMR_COLORCORRECTPALETTE = 111;
@@ -13102,7 +13123,7 @@ function parseEmfPlusPenObject(view, dataOff, recDataSize, textureCache, cacheKe
13102
13123
  o += 4;
13103
13124
  return v;
13104
13125
  };
13105
- const pen = { kind: "plus-pen", color: "rgba(0,0,0,1)", width: penWidth || 1, dashStyle: 0 };
13126
+ const pen = { kind: "plus-pen", color: "rgba(0,0,0,1)", width: penWidth, dashStyle: 0 };
13106
13127
  if (penFlags & PEN_TRANSFORM) {
13107
13128
  if (o + 24 <= end) {
13108
13129
  pen.transform = [0, 4, 8, 12, 16, 20].map((k) => view.getFloat32(o + k, true));
@@ -14938,19 +14959,7 @@ var SvgContext = class {
14938
14959
  }
14939
14960
  }
14940
14961
  // ---- text ---------------------------------------------------------------
14941
- measurer() {
14942
- if (this.measureCtx === void 0) {
14943
- const temp = createTempCanvas(1, 1);
14944
- this.measureCtx = temp && !isSoftwareRaster(temp.canvas) ? temp.ctx : null;
14945
- }
14946
- return this.measureCtx;
14947
- }
14948
14962
  measureText(text) {
14949
- const real = this.shadow ?? this.measurer();
14950
- if (real) {
14951
- real.font = this.state.font;
14952
- return real.measureText(text);
14953
- }
14954
14963
  const size = parseFont(this.state.font).size;
14955
14964
  return { width: estimateTextWidth(text, size) };
14956
14965
  }
@@ -20955,10 +20964,109 @@ function handleEmfGdiBlendBlitRecord(rCtx, recType, offset, dataOff, recSize) {
20955
20964
  }
20956
20965
  }
20957
20966
 
20967
+ // src/emf-ansi.ts
20968
+ var CP1252_HIGH2 = [
20969
+ 8364,
20970
+ 129,
20971
+ 8218,
20972
+ 402,
20973
+ 8222,
20974
+ 8230,
20975
+ 8224,
20976
+ 8225,
20977
+ 710,
20978
+ 8240,
20979
+ 352,
20980
+ 8249,
20981
+ 338,
20982
+ 141,
20983
+ 381,
20984
+ 143,
20985
+ 144,
20986
+ 8216,
20987
+ 8217,
20988
+ 8220,
20989
+ 8221,
20990
+ 8226,
20991
+ 8211,
20992
+ 8212,
20993
+ 732,
20994
+ 8482,
20995
+ 353,
20996
+ 8250,
20997
+ 339,
20998
+ 157,
20999
+ 382,
21000
+ 376
21001
+ ];
21002
+ function ansiToCode(b, charSet) {
21003
+ return charSet === 2 ? b : b >= 128 && b <= 159 ? CP1252_HIGH2[b - 128] : b;
21004
+ }
21005
+ var CHARSET_ENCODING = {
21006
+ 0: "windows-1252",
21007
+ 1: "windows-1252",
21008
+ 128: "shift_jis",
21009
+ 129: "euc-kr",
21010
+ 130: "johab",
21011
+ 134: "gbk",
21012
+ 136: "big5",
21013
+ 161: "windows-1253",
21014
+ 162: "windows-1254",
21015
+ 163: "windows-1258",
21016
+ 177: "windows-1255",
21017
+ 178: "windows-1256",
21018
+ 186: "windows-1257",
21019
+ 204: "windows-1251",
21020
+ 222: "windows-874",
21021
+ 238: "windows-1250",
21022
+ 255: "ibm437"
21023
+ };
21024
+ function decodeAnsiRecord(bytes, charSet) {
21025
+ if (charSet === 2) {
21026
+ return { codes: [...bytes], byteLengths: bytes.map(() => 1) };
21027
+ }
21028
+ const label = CHARSET_ENCODING[charSet] ?? "windows-1252";
21029
+ try {
21030
+ const decoder = new TextDecoder(label, { fatal: true });
21031
+ const replacementDecoder = new TextDecoder(label);
21032
+ const source = new Uint8Array(bytes);
21033
+ const codes = [];
21034
+ const byteLengths = [];
21035
+ let i = 0;
21036
+ while (i < bytes.length) {
21037
+ let decoded = null;
21038
+ let consumed = 1;
21039
+ for (let n = 1; n <= Math.min(4, bytes.length - i); n++) {
21040
+ try {
21041
+ const candidate = decoder.decode(source.subarray(i, i + n));
21042
+ if (candidate.length > 0) {
21043
+ decoded = candidate;
21044
+ consumed = n;
21045
+ break;
21046
+ }
21047
+ } catch {
21048
+ }
21049
+ }
21050
+ if (decoded === null) {
21051
+ decoded = replacementDecoder.decode(source.subarray(i, i + 1));
21052
+ }
21053
+ for (let j = 0; j < decoded.length; j++) {
21054
+ codes.push(decoded.charCodeAt(j));
21055
+ byteLengths.push(j === decoded.length - 1 ? consumed : 0);
21056
+ }
21057
+ i += consumed;
21058
+ }
21059
+ return { codes, byteLengths };
21060
+ } catch {
21061
+ return { codes: bytes.map((b) => ansiToCode(b, charSet)), byteLengths: bytes.map(() => 1) };
21062
+ }
21063
+ }
21064
+
20958
21065
  // src/gdi-text-render.ts
20959
21066
  var ETO_OPAQUE = 2;
20960
21067
  var ETO_CLIPPED = 4;
20961
21068
  var ETO_GLYPH_INDEX = 16;
21069
+ var ETO_IGNORELANGUAGE = 4096;
20962
21070
  var ETO_PDY = 8192;
20963
21071
  var GRAY_INV_GAMMA = 0.43;
20964
21072
  var GRAY_BLACK_ON_WHITE = [255, 255, 240, 233, 225, 217, 208, 199, 189, 178, 167, 154, 140, 124, 104, 77, 0];
@@ -21039,15 +21147,24 @@ function fillDeviceQuad(ctx, pts, color, clip) {
21039
21147
  ctx.fill();
21040
21148
  ctx.restore();
21041
21149
  }
21150
+ function isC1Control(code) {
21151
+ return code >= 128 && code <= 159;
21152
+ }
21153
+ function suppressC1Glyph(run, index) {
21154
+ return !run.glyphIndices && !(run.options & ETO_IGNORELANGUAGE) && isC1Control(run.codes[index]);
21155
+ }
21042
21156
  function layoutGdiRun(font, run) {
21043
21157
  const n = run.codes.length;
21044
21158
  const glyphs = run.codes.map((c) => run.glyphIndices ? c : font.glyphIndex(c));
21045
- const adv = [];
21159
+ let adv = [];
21046
21160
  for (let i = 0; i < n; i++) {
21047
21161
  adv.push(
21048
- run.dx && i < run.dx.length ? run.dx[i] : run.matrix ? font.rotatedAdvance(glyphs[i]) : font.advance(glyphs[i])
21162
+ suppressC1Glyph(run, i) && !run.dx ? 0 : run.dx && i < run.dx.length ? run.dx[i] : run.matrix ? font.rotatedAdvance(glyphs[i]) : font.advance(glyphs[i])
21049
21163
  );
21050
21164
  }
21165
+ if (!run.dx && run.textJustification && !run.glyphIndices) {
21166
+ adv = applyTextJustification(adv, run.codes, run.textJustification.extra, run.textJustification.count);
21167
+ }
21051
21168
  const advY = [];
21052
21169
  for (let i = 0; i < n; i++) {
21053
21170
  advY.push(run.dy && i < run.dy.length ? run.dy[i] : 0);
@@ -21090,9 +21207,11 @@ function layoutGdiRun(font, run) {
21090
21207
  const subX = font.gridFit ? 0 : Math.round((origin.x - ox) * 64);
21091
21208
  const oy = Math.round(origin.y);
21092
21209
  origins.push({ x: ox + subX / 64, y: oy, along, down });
21093
- const g = font.glyph(glyphs[i], gm, subX);
21094
- if (g.bitmap) {
21095
- placed.push({ x: ox + g.bitmap.left, y: oy - g.bitmap.top, bitmap: g.bitmap });
21210
+ if (!suppressC1Glyph(run, i)) {
21211
+ const g = font.glyph(glyphs[i], gm, subX);
21212
+ if (g.bitmap) {
21213
+ placed.push({ x: ox + g.bitmap.left, y: oy - g.bitmap.top, bitmap: g.bitmap });
21214
+ }
21096
21215
  }
21097
21216
  along += adv[i];
21098
21217
  down += advY[i];
@@ -21177,21 +21296,25 @@ function paintGdiTextRun(ctx, font, run, fontFamilyMap) {
21177
21296
  }
21178
21297
  function emitSvgRun(ctx, font, run, glyphs, origins, fontFamilyMap) {
21179
21298
  let text = "";
21299
+ const visibleOrigins = [];
21180
21300
  for (let i = 0; i < origins.length; i++) {
21181
21301
  const code = run.glyphIndices ? font.charForGlyph(glyphs[i]) : run.codes[i];
21302
+ if (!run.glyphIndices && !(run.options & ETO_IGNORELANGUAGE) && isC1Control(code)) continue;
21182
21303
  text += String.fromCharCode(code);
21304
+ visibleOrigins.push(origins[i]);
21183
21305
  }
21306
+ if (text.length === 0) return;
21184
21307
  const m = run.matrix;
21185
21308
  const ttf = font.ttf;
21186
21309
  const bold = font.syntheticBold || ttf.weightClass >= 600;
21187
21310
  const italic = font.syntheticItalic || (ttf.fsSelection & 1) !== 0 || (ttf.macStyle & 2) !== 0;
21188
- const first = origins[0];
21311
+ const first = visibleOrigins[0];
21189
21312
  ctx.fillGlyphRun({
21190
21313
  text,
21191
21314
  // Upright runs: device positions. Rotated runs: offsets along/across
21192
21315
  // the baseline from the first glyph, under the frame matrix.
21193
- xs: origins.map((o) => m ? o.along - first.along : o.x),
21194
- ys: origins.map((o) => m ? o.down - first.down : o.y),
21316
+ xs: visibleOrigins.map((o) => m ? o.along - first.along : o.x),
21317
+ ys: visibleOrigins.map((o) => m ? o.down - first.down : o.y),
21195
21318
  matrix: m ? [m[0], m[1], m[2], m[3], first.x, first.y] : null,
21196
21319
  scaleX: font.ppemX !== font.ppem ? font.ppemX / font.ppem : 1,
21197
21320
  fontFamily: mapFontFamily(ttf.family, fontFamilyMap),
@@ -21362,6 +21485,7 @@ function drawGdiTextCall(ctx, fonts, state, call) {
21362
21485
  y: ref.y,
21363
21486
  dx: call.dx ? call.dx.map((v) => v * advanceScale) : null,
21364
21487
  dy: call.dy ? call.dy.map((v) => -v * fontScale) : null,
21488
+ textJustification: call.textJustification ? { ...call.textJustification, extra: call.textJustification.extra * advanceScale } : void 0,
21365
21489
  textAlign: state.textAlign,
21366
21490
  textColor: state.textColor,
21367
21491
  bkColor: state.bkColor,
@@ -21498,11 +21622,11 @@ function readDxArray(view, offset, dataOff, nChars, viewEnd) {
21498
21622
  }
21499
21623
  const dx = [];
21500
21624
  for (let i = 0; i < nChars; i++) {
21501
- dx.push(view.getUint32(start + i * 4, true));
21625
+ dx.push(view.getInt32(start + i * 4, true));
21502
21626
  }
21503
21627
  return dx;
21504
21628
  }
21505
- function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21629
+ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString, viewEnd) {
21506
21630
  const { ctx, view, state } = rCtx;
21507
21631
  const fonts = rCtx.fonts;
21508
21632
  if (!fonts) {
@@ -21514,7 +21638,7 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21514
21638
  codes.push(view.getUint16(offset + offString + i * 2, true));
21515
21639
  }
21516
21640
  const pdy = (options & ETO_PDY) !== 0;
21517
- const raw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, view.byteLength);
21641
+ const raw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, viewEnd);
21518
21642
  let dx = raw;
21519
21643
  let dy = null;
21520
21644
  if (raw && pdy) {
@@ -21539,7 +21663,8 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21539
21663
  },
21540
21664
  dx,
21541
21665
  dy,
21542
- matrix: gdiDeviceMatrix(rCtx)
21666
+ matrix: gdiDeviceMatrix(rCtx),
21667
+ textJustification: state.textJustification
21543
21668
  });
21544
21669
  if (!adv) {
21545
21670
  return false;
@@ -21551,12 +21676,12 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21551
21676
  return true;
21552
21677
  }
21553
21678
  function horizontalAlign(textAlign) {
21679
+ if ((textAlign & 6) === 6) {
21680
+ return "center";
21681
+ }
21554
21682
  if (textAlign & 2) {
21555
21683
  return "right";
21556
21684
  }
21557
- if (textAlign & 6) {
21558
- return "center";
21559
- }
21560
21685
  return "left";
21561
21686
  }
21562
21687
  function verticalBaseline(textAlign) {
@@ -21598,11 +21723,11 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21598
21723
  const refY = view.getInt32(dataOff + 32, true);
21599
21724
  const nChars = view.getUint32(dataOff + 36, true);
21600
21725
  const offString = view.getUint32(dataOff + 40, true);
21601
- const viewEnd = view.byteLength;
21602
- if (nChars === 0 || offString === 0 || offset + offString + nChars * 2 > viewEnd) {
21726
+ const viewEnd = Math.min(view.byteLength, offset + recSize);
21727
+ if (nChars === 0 || offString === 0 || nChars > Math.floor((viewEnd - offset - offString) / 2)) {
21603
21728
  return true;
21604
21729
  }
21605
- if (drawWithFontEngine(rCtx, offset, dataOff, nChars, offString)) {
21730
+ if (drawWithFontEngine(rCtx, offset, dataOff, nChars, offString, viewEnd)) {
21606
21731
  return true;
21607
21732
  }
21608
21733
  const text = readUtf16LE(view, offset + offString, nChars);
@@ -21617,8 +21742,16 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21617
21742
  const align = horizontalAlign(state.textAlign);
21618
21743
  ctx.textBaseline = verticalBaseline(state.textAlign);
21619
21744
  ctx.textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
21620
- const dxLogical = readDxArray(view, offset, dataOff, nChars, viewEnd);
21621
- const dxDevice = dxLogical ? dxLogical.map((v) => advanceScale !== null ? v * advanceScale : gmw(rCtx, v)) : null;
21745
+ const pdy = (view.getUint32(dataOff + 44, true) & ETO_PDY) !== 0;
21746
+ const dxRaw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, viewEnd);
21747
+ const dxLogical = dxRaw && pdy ? dxRaw.filter((_, i) => i % 2 === 0) : dxRaw;
21748
+ let dxDevice = dxLogical ? dxLogical.map((v) => advanceScale !== null ? v * advanceScale : gmw(rCtx, v)) : null;
21749
+ if (!dxDevice && state.textJustification && !pdy) {
21750
+ const units = text.split("");
21751
+ const natural = units.map((ch) => ctx.measureText(ch).width);
21752
+ const scale = advanceScale ?? gmw(rCtx, 1);
21753
+ dxDevice = applyTextJustification(natural, units.map((ch) => ch.charCodeAt(0)), state.textJustification.extra * scale, state.textJustification.count);
21754
+ }
21622
21755
  if (dxDevice) {
21623
21756
  ctx.textAlign = "left";
21624
21757
  }
@@ -21651,10 +21784,118 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21651
21784
  }
21652
21785
  return true;
21653
21786
  }
21787
+ function drawTextCall(rCtx, x, y, codes, options, rect, dx) {
21788
+ const n = codes.length;
21789
+ const offString = 76;
21790
+ const offDx = offString + n * 2 + 3 & -4;
21791
+ const dxCount = dx ? dx.length : 0;
21792
+ const size = offDx + dxCount * 4;
21793
+ const view = new DataView(new ArrayBuffer(size));
21794
+ view.setUint32(44, n, true);
21795
+ view.setUint32(48, offString, true);
21796
+ view.setUint32(52, options, true);
21797
+ view.setInt32(36, x, true);
21798
+ view.setInt32(40, y, true);
21799
+ view.setInt32(56, rect[0], true);
21800
+ view.setInt32(60, rect[1], true);
21801
+ view.setInt32(64, rect[2], true);
21802
+ view.setInt32(68, rect[3], true);
21803
+ view.setUint32(72, dx ? offDx : 0, true);
21804
+ for (let i = 0; i < n; i++) view.setUint16(offString + i * 2, codes[i], true);
21805
+ if (dx) for (let i = 0; i < dxCount; i++) view.setInt32(offDx + i * 4, dx[i], true);
21806
+ const child = { ...rCtx, view };
21807
+ handleExtTextOutW(child, 0, 8, size);
21808
+ }
21809
+ function readAnsiRecord(view, start, count, charSet) {
21810
+ return decodeAnsiRecord(Array.from({ length: count }, (_, i) => view.getUint8(start + i)), charSet);
21811
+ }
21812
+ function collapseAnsiDx(dx, byteLengths, pdy) {
21813
+ if (!dx) return null;
21814
+ const result = [];
21815
+ let at = 0;
21816
+ for (const bytes of byteLengths) {
21817
+ let x = 0, y = 0;
21818
+ for (let i = 0; i < bytes; i++) {
21819
+ x += dx[at + i * (pdy ? 2 : 1)] ?? 0;
21820
+ if (pdy) y += dx[at + i * 2 + 1] ?? 0;
21821
+ }
21822
+ result.push(x);
21823
+ if (pdy) result.push(y);
21824
+ at += bytes * (pdy ? 2 : 1);
21825
+ }
21826
+ return result;
21827
+ }
21828
+ function handlePolyText(rCtx, offset, dataOff, recSize, wide) {
21829
+ const { view, state } = rCtx;
21830
+ const end = Math.min(view.byteLength, offset + recSize);
21831
+ if (recSize < 40) return true;
21832
+ const count = view.getUint32(dataOff + 28, true);
21833
+ const arrayStart = dataOff + 32;
21834
+ if (count > Math.floor((end - arrayStart) / 40)) return true;
21835
+ const charSet = state.fontDetails?.charSet ?? 1;
21836
+ for (let i = 0; i < count; i++) {
21837
+ const e = arrayStart + i * 40;
21838
+ const x = view.getInt32(e, true), y = view.getInt32(e + 4, true);
21839
+ const n = view.getUint32(e + 8, true), offString = view.getUint32(e + 12, true);
21840
+ const options = view.getUint32(e + 16, true);
21841
+ const rect = [view.getInt32(e + 20, true), view.getInt32(e + 24, true), view.getInt32(e + 28, true), view.getInt32(e + 32, true)];
21842
+ const offDx = view.getUint32(e + 36, true);
21843
+ const unit = wide ? 2 : 1;
21844
+ if (!offString || offString > end - offset || n > Math.floor((end - (offset + offString)) / unit)) continue;
21845
+ const ansi = wide ? null : readAnsiRecord(view, offset + offString, n, charSet);
21846
+ const codes = wide ? Array.from({ length: n }, (_, j) => view.getUint16(offset + offString + j * 2, true)) : ansi.codes;
21847
+ let dx = null;
21848
+ const pdy = (options & ETO_PDY) !== 0;
21849
+ const dxCount = n * (pdy ? 2 : 1);
21850
+ if (offDx && offDx <= end - offset && dxCount <= Math.floor((end - (offset + offDx)) / 4)) {
21851
+ dx = Array.from({ length: dxCount }, (_, j) => view.getInt32(offset + offDx + j * 4, true));
21852
+ if (!wide) dx = collapseAnsiDx(dx, ansi.byteLengths, pdy);
21853
+ }
21854
+ drawTextCall(rCtx, x, y, codes, options, rect, dx);
21855
+ }
21856
+ return true;
21857
+ }
21858
+ function handleSmallTextOut(rCtx, offset, dataOff, recSize) {
21859
+ const { view } = rCtx;
21860
+ const end = Math.min(view.byteLength, offset + recSize);
21861
+ if (recSize < 36) return true;
21862
+ const x = view.getInt32(dataOff, true), y = view.getInt32(dataOff + 4, true);
21863
+ const n = view.getUint32(dataOff + 8, true), options = view.getUint32(dataOff + 12, true);
21864
+ const small = (options & 512) !== 0, noRect = (options & 256) !== 0;
21865
+ const base = dataOff + 28 + (noRect ? 0 : 16);
21866
+ if (base > end || n > Math.floor((end - base) / (small ? 1 : 2))) return true;
21867
+ const rect = noRect ? [0, 0, 0, 0] : [view.getInt32(dataOff + 28, true), view.getInt32(dataOff + 32, true), view.getInt32(dataOff + 36, true), view.getInt32(dataOff + 40, true)];
21868
+ const codes = small ? Array.from({ length: n }, (_, i) => view.getUint8(base + i)) : Array.from({ length: n }, (_, i) => view.getUint16(base + i * 2, true));
21869
+ drawTextCall(rCtx, x, y, codes, options, rect, null);
21870
+ return true;
21871
+ }
21654
21872
  function handleEmfGdiDrawTextRecord(rCtx, recType, offset, dataOff, recSize) {
21655
21873
  if (recType === EMR_EXTTEXTOUTW) {
21656
21874
  return handleExtTextOutW(rCtx, offset, dataOff, recSize);
21657
21875
  }
21876
+ if (recType === EMR_EXTTEXTOUTA || recType === EMR_POLYTEXTOUTA || recType === EMR_POLYTEXTOUTW || recType === EMR_SMALLTEXTOUT) {
21877
+ if (recType === EMR_POLYTEXTOUTA || recType === EMR_POLYTEXTOUTW) return handlePolyText(rCtx, offset, dataOff, recSize, recType === EMR_POLYTEXTOUTW);
21878
+ if (recType === EMR_SMALLTEXTOUT) return handleSmallTextOut(rCtx, offset, dataOff, recSize);
21879
+ const { view, state } = rCtx;
21880
+ const end = Math.min(view.byteLength, offset + recSize);
21881
+ if (recSize < 76) return true;
21882
+ const x = view.getInt32(dataOff + 28, true), y = view.getInt32(dataOff + 32, true);
21883
+ const n = view.getUint32(dataOff + 36, true), strOff = view.getUint32(dataOff + 40, true), options = view.getUint32(dataOff + 44, true);
21884
+ if (!strOff || strOff > end - offset) return true;
21885
+ if (n > end - (offset + strOff)) return true;
21886
+ const ansi = readAnsiRecord(view, offset + strOff, n, state.fontDetails?.charSet ?? 1);
21887
+ const rect = [view.getInt32(dataOff + 48, true), view.getInt32(dataOff + 52, true), view.getInt32(dataOff + 56, true), view.getInt32(dataOff + 60, true)];
21888
+ const offDx = view.getUint32(dataOff + 64, true);
21889
+ let dx = null;
21890
+ const pdy = (options & ETO_PDY) !== 0;
21891
+ const dxCount = n * (pdy ? 2 : 1);
21892
+ if (offDx && offDx <= end - offset && dxCount <= Math.floor((end - (offset + offDx)) / 4)) {
21893
+ dx = Array.from({ length: dxCount }, (_, i) => view.getInt32(offset + offDx + i * 4, true));
21894
+ dx = collapseAnsiDx(dx, ansi.byteLengths, pdy);
21895
+ }
21896
+ drawTextCall(rCtx, x, y, ansi.codes, options, rect, dx);
21897
+ return true;
21898
+ }
21658
21899
  return false;
21659
21900
  }
21660
21901
 
@@ -23667,7 +23908,7 @@ function penDashArray(pen) {
23667
23908
  return pattern.map((v) => v * w);
23668
23909
  }
23669
23910
  function applyPlusPenStyle(ctx, pen, widthScale = 1) {
23670
- ctx.lineWidth = pen.width * widthScale;
23911
+ ctx.lineWidth = (pen.width || 1) * widthScale;
23671
23912
  const dashes = penDashArray(pen);
23672
23913
  ctx.lineCap = canvasLineCap(dashes.length > 0 ? pen.dashCap ?? 0 : pen.startCap ?? pen.endCap);
23673
23914
  ctx.lineJoin = canvasLineJoin(pen.lineJoin);
@@ -23679,6 +23920,7 @@ function applyPlusPenStyle(ctx, pen, widthScale = 1) {
23679
23920
  }
23680
23921
  function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false) {
23681
23922
  const { ctx } = rCtx;
23923
+ pen = applySimilarityPenTransform(pen);
23682
23924
  const inset = !!pen && pen.alignment === 1 && closedFigure;
23683
23925
  const widthScale = inset ? 2 : 1;
23684
23926
  const stroke = (c) => {
@@ -23740,6 +23982,22 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
23740
23982
  applyPlusWorldTransform(rCtx);
23741
23983
  stroke(ctx);
23742
23984
  }
23985
+ function applySimilarityPenTransform(pen) {
23986
+ if (!pen?.transform) {
23987
+ return pen;
23988
+ }
23989
+ if (isIdentity2(pen.transform)) {
23990
+ return { ...pen, transform: null };
23991
+ }
23992
+ const [a, b, c, d] = pen.transform;
23993
+ const sx = Math.hypot(a, b);
23994
+ const sy = Math.hypot(c, d);
23995
+ const dot = a * c + b * d;
23996
+ if (!Number.isFinite(sx + sy + dot) || !(sx > 0) || Math.abs(sx - sy) > 1e-6 * sx || Math.abs(dot) > 1e-6 * sx * sy) {
23997
+ return pen;
23998
+ }
23999
+ return { ...pen, width: pen.width === 0 ? 1 : pen.width * sx, transform: null };
24000
+ }
23743
24001
  function strokeGdiplus(rCtx, pen, sampler, buildPath, device, size, mode, closedFigure) {
23744
24002
  if (pen.transform && !isIdentity2(pen.transform)) {
23745
24003
  return false;
@@ -24153,6 +24411,7 @@ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, ali
24153
24411
  dx,
24154
24412
  dy: null,
24155
24413
  textAlign: 24,
24414
+ options: 0,
24156
24415
  matrix: null});
24157
24416
  if (!cov) {
24158
24417
  return true;
@@ -26867,46 +27126,6 @@ function penIsNull(p) {
26867
27126
  }
26868
27127
 
26869
27128
  // src/wmf-text.ts
26870
- var CP1252_HIGH2 = [
26871
- 8364,
26872
- 129,
26873
- 8218,
26874
- 402,
26875
- 8222,
26876
- 8230,
26877
- 8224,
26878
- 8225,
26879
- 710,
26880
- 8240,
26881
- 352,
26882
- 8249,
26883
- 338,
26884
- 141,
26885
- 381,
26886
- 143,
26887
- 144,
26888
- 8216,
26889
- 8217,
26890
- 8220,
26891
- 8221,
26892
- 8226,
26893
- 8211,
26894
- 8212,
26895
- 732,
26896
- 8482,
26897
- 353,
26898
- 8250,
26899
- 339,
26900
- 157,
26901
- 382,
26902
- 376
26903
- ];
26904
- function ansiToCode(b, charSet) {
26905
- if (charSet === 2) {
26906
- return b;
26907
- }
26908
- return b >= 128 && b <= 159 ? CP1252_HIGH2[b - 128] : b;
26909
- }
26910
27129
  function fontAdvances(p, codes) {
26911
27130
  const fonts = p.rCtx.fonts;
26912
27131
  if (!fonts) {
package/dist/index.mjs CHANGED
@@ -491,6 +491,23 @@ function totalGlyphAdvance(dx) {
491
491
  }
492
492
  return sum;
493
493
  }
494
+ function applyTextJustification(advances, characters, extra, count) {
495
+ const result = advances.slice();
496
+ if (extra === 0 || count <= 0) {
497
+ return result;
498
+ }
499
+ let seen = 0;
500
+ for (let i = 0; i < result.length && i < characters.length; i++) {
501
+ if (characters[i] !== 32) {
502
+ continue;
503
+ }
504
+ const before = Math.round(seen * extra / count);
505
+ seen++;
506
+ const after = Math.round(seen * extra / count);
507
+ result[i] += after - before;
508
+ }
509
+ return result;
510
+ }
494
511
  function alignmentStartOffset(totalWidth, align) {
495
512
  if (align === "center") {
496
513
  return -totalWidth / 2;
@@ -627,8 +644,11 @@ var EMR_PAINTRGN = 74;
627
644
  var EMR_MASKBLT = 78;
628
645
  var EMR_PLGBLT = 79;
629
646
  var EMR_SETDIBITSTODEVICE = 80;
647
+ var EMR_EXTTEXTOUTA = 83;
630
648
  var EMR_POLYPOLYLINE16 = 90;
631
649
  var EMR_POLYDRAW16 = 92;
650
+ var EMR_POLYTEXTOUTA = 96;
651
+ var EMR_POLYTEXTOUTW = 97;
632
652
  var EMR_CREATECOLORSPACE = 99;
633
653
  var EMR_SETCOLORSPACE = 100;
634
654
  var EMR_DELETECOLORSPACE = 101;
@@ -637,6 +657,7 @@ var EMR_GLSBOUNDEDRECORD = 103;
637
657
  var EMR_PIXELFORMAT = 104;
638
658
  var EMR_DRAWESCAPE = 105;
639
659
  var EMR_EXTESCAPE = 106;
660
+ var EMR_SMALLTEXTOUT = 108;
640
661
  var EMR_FORCEUFIMAPPING = 109;
641
662
  var EMR_NAMEDESCAPE = 110;
642
663
  var EMR_COLORCORRECTPALETTE = 111;
@@ -13100,7 +13121,7 @@ function parseEmfPlusPenObject(view, dataOff, recDataSize, textureCache, cacheKe
13100
13121
  o += 4;
13101
13122
  return v;
13102
13123
  };
13103
- const pen = { kind: "plus-pen", color: "rgba(0,0,0,1)", width: penWidth || 1, dashStyle: 0 };
13124
+ const pen = { kind: "plus-pen", color: "rgba(0,0,0,1)", width: penWidth, dashStyle: 0 };
13104
13125
  if (penFlags & PEN_TRANSFORM) {
13105
13126
  if (o + 24 <= end) {
13106
13127
  pen.transform = [0, 4, 8, 12, 16, 20].map((k) => view.getFloat32(o + k, true));
@@ -14936,19 +14957,7 @@ var SvgContext = class {
14936
14957
  }
14937
14958
  }
14938
14959
  // ---- text ---------------------------------------------------------------
14939
- measurer() {
14940
- if (this.measureCtx === void 0) {
14941
- const temp = createTempCanvas(1, 1);
14942
- this.measureCtx = temp && !isSoftwareRaster(temp.canvas) ? temp.ctx : null;
14943
- }
14944
- return this.measureCtx;
14945
- }
14946
14960
  measureText(text) {
14947
- const real = this.shadow ?? this.measurer();
14948
- if (real) {
14949
- real.font = this.state.font;
14950
- return real.measureText(text);
14951
- }
14952
14961
  const size = parseFont(this.state.font).size;
14953
14962
  return { width: estimateTextWidth(text, size) };
14954
14963
  }
@@ -20953,10 +20962,109 @@ function handleEmfGdiBlendBlitRecord(rCtx, recType, offset, dataOff, recSize) {
20953
20962
  }
20954
20963
  }
20955
20964
 
20965
+ // src/emf-ansi.ts
20966
+ var CP1252_HIGH2 = [
20967
+ 8364,
20968
+ 129,
20969
+ 8218,
20970
+ 402,
20971
+ 8222,
20972
+ 8230,
20973
+ 8224,
20974
+ 8225,
20975
+ 710,
20976
+ 8240,
20977
+ 352,
20978
+ 8249,
20979
+ 338,
20980
+ 141,
20981
+ 381,
20982
+ 143,
20983
+ 144,
20984
+ 8216,
20985
+ 8217,
20986
+ 8220,
20987
+ 8221,
20988
+ 8226,
20989
+ 8211,
20990
+ 8212,
20991
+ 732,
20992
+ 8482,
20993
+ 353,
20994
+ 8250,
20995
+ 339,
20996
+ 157,
20997
+ 382,
20998
+ 376
20999
+ ];
21000
+ function ansiToCode(b, charSet) {
21001
+ return charSet === 2 ? b : b >= 128 && b <= 159 ? CP1252_HIGH2[b - 128] : b;
21002
+ }
21003
+ var CHARSET_ENCODING = {
21004
+ 0: "windows-1252",
21005
+ 1: "windows-1252",
21006
+ 128: "shift_jis",
21007
+ 129: "euc-kr",
21008
+ 130: "johab",
21009
+ 134: "gbk",
21010
+ 136: "big5",
21011
+ 161: "windows-1253",
21012
+ 162: "windows-1254",
21013
+ 163: "windows-1258",
21014
+ 177: "windows-1255",
21015
+ 178: "windows-1256",
21016
+ 186: "windows-1257",
21017
+ 204: "windows-1251",
21018
+ 222: "windows-874",
21019
+ 238: "windows-1250",
21020
+ 255: "ibm437"
21021
+ };
21022
+ function decodeAnsiRecord(bytes, charSet) {
21023
+ if (charSet === 2) {
21024
+ return { codes: [...bytes], byteLengths: bytes.map(() => 1) };
21025
+ }
21026
+ const label = CHARSET_ENCODING[charSet] ?? "windows-1252";
21027
+ try {
21028
+ const decoder = new TextDecoder(label, { fatal: true });
21029
+ const replacementDecoder = new TextDecoder(label);
21030
+ const source = new Uint8Array(bytes);
21031
+ const codes = [];
21032
+ const byteLengths = [];
21033
+ let i = 0;
21034
+ while (i < bytes.length) {
21035
+ let decoded = null;
21036
+ let consumed = 1;
21037
+ for (let n = 1; n <= Math.min(4, bytes.length - i); n++) {
21038
+ try {
21039
+ const candidate = decoder.decode(source.subarray(i, i + n));
21040
+ if (candidate.length > 0) {
21041
+ decoded = candidate;
21042
+ consumed = n;
21043
+ break;
21044
+ }
21045
+ } catch {
21046
+ }
21047
+ }
21048
+ if (decoded === null) {
21049
+ decoded = replacementDecoder.decode(source.subarray(i, i + 1));
21050
+ }
21051
+ for (let j = 0; j < decoded.length; j++) {
21052
+ codes.push(decoded.charCodeAt(j));
21053
+ byteLengths.push(j === decoded.length - 1 ? consumed : 0);
21054
+ }
21055
+ i += consumed;
21056
+ }
21057
+ return { codes, byteLengths };
21058
+ } catch {
21059
+ return { codes: bytes.map((b) => ansiToCode(b, charSet)), byteLengths: bytes.map(() => 1) };
21060
+ }
21061
+ }
21062
+
20956
21063
  // src/gdi-text-render.ts
20957
21064
  var ETO_OPAQUE = 2;
20958
21065
  var ETO_CLIPPED = 4;
20959
21066
  var ETO_GLYPH_INDEX = 16;
21067
+ var ETO_IGNORELANGUAGE = 4096;
20960
21068
  var ETO_PDY = 8192;
20961
21069
  var GRAY_INV_GAMMA = 0.43;
20962
21070
  var GRAY_BLACK_ON_WHITE = [255, 255, 240, 233, 225, 217, 208, 199, 189, 178, 167, 154, 140, 124, 104, 77, 0];
@@ -21037,15 +21145,24 @@ function fillDeviceQuad(ctx, pts, color, clip) {
21037
21145
  ctx.fill();
21038
21146
  ctx.restore();
21039
21147
  }
21148
+ function isC1Control(code) {
21149
+ return code >= 128 && code <= 159;
21150
+ }
21151
+ function suppressC1Glyph(run, index) {
21152
+ return !run.glyphIndices && !(run.options & ETO_IGNORELANGUAGE) && isC1Control(run.codes[index]);
21153
+ }
21040
21154
  function layoutGdiRun(font, run) {
21041
21155
  const n = run.codes.length;
21042
21156
  const glyphs = run.codes.map((c) => run.glyphIndices ? c : font.glyphIndex(c));
21043
- const adv = [];
21157
+ let adv = [];
21044
21158
  for (let i = 0; i < n; i++) {
21045
21159
  adv.push(
21046
- run.dx && i < run.dx.length ? run.dx[i] : run.matrix ? font.rotatedAdvance(glyphs[i]) : font.advance(glyphs[i])
21160
+ suppressC1Glyph(run, i) && !run.dx ? 0 : run.dx && i < run.dx.length ? run.dx[i] : run.matrix ? font.rotatedAdvance(glyphs[i]) : font.advance(glyphs[i])
21047
21161
  );
21048
21162
  }
21163
+ if (!run.dx && run.textJustification && !run.glyphIndices) {
21164
+ adv = applyTextJustification(adv, run.codes, run.textJustification.extra, run.textJustification.count);
21165
+ }
21049
21166
  const advY = [];
21050
21167
  for (let i = 0; i < n; i++) {
21051
21168
  advY.push(run.dy && i < run.dy.length ? run.dy[i] : 0);
@@ -21088,9 +21205,11 @@ function layoutGdiRun(font, run) {
21088
21205
  const subX = font.gridFit ? 0 : Math.round((origin.x - ox) * 64);
21089
21206
  const oy = Math.round(origin.y);
21090
21207
  origins.push({ x: ox + subX / 64, y: oy, along, down });
21091
- const g = font.glyph(glyphs[i], gm, subX);
21092
- if (g.bitmap) {
21093
- placed.push({ x: ox + g.bitmap.left, y: oy - g.bitmap.top, bitmap: g.bitmap });
21208
+ if (!suppressC1Glyph(run, i)) {
21209
+ const g = font.glyph(glyphs[i], gm, subX);
21210
+ if (g.bitmap) {
21211
+ placed.push({ x: ox + g.bitmap.left, y: oy - g.bitmap.top, bitmap: g.bitmap });
21212
+ }
21094
21213
  }
21095
21214
  along += adv[i];
21096
21215
  down += advY[i];
@@ -21175,21 +21294,25 @@ function paintGdiTextRun(ctx, font, run, fontFamilyMap) {
21175
21294
  }
21176
21295
  function emitSvgRun(ctx, font, run, glyphs, origins, fontFamilyMap) {
21177
21296
  let text = "";
21297
+ const visibleOrigins = [];
21178
21298
  for (let i = 0; i < origins.length; i++) {
21179
21299
  const code = run.glyphIndices ? font.charForGlyph(glyphs[i]) : run.codes[i];
21300
+ if (!run.glyphIndices && !(run.options & ETO_IGNORELANGUAGE) && isC1Control(code)) continue;
21180
21301
  text += String.fromCharCode(code);
21302
+ visibleOrigins.push(origins[i]);
21181
21303
  }
21304
+ if (text.length === 0) return;
21182
21305
  const m = run.matrix;
21183
21306
  const ttf = font.ttf;
21184
21307
  const bold = font.syntheticBold || ttf.weightClass >= 600;
21185
21308
  const italic = font.syntheticItalic || (ttf.fsSelection & 1) !== 0 || (ttf.macStyle & 2) !== 0;
21186
- const first = origins[0];
21309
+ const first = visibleOrigins[0];
21187
21310
  ctx.fillGlyphRun({
21188
21311
  text,
21189
21312
  // Upright runs: device positions. Rotated runs: offsets along/across
21190
21313
  // the baseline from the first glyph, under the frame matrix.
21191
- xs: origins.map((o) => m ? o.along - first.along : o.x),
21192
- ys: origins.map((o) => m ? o.down - first.down : o.y),
21314
+ xs: visibleOrigins.map((o) => m ? o.along - first.along : o.x),
21315
+ ys: visibleOrigins.map((o) => m ? o.down - first.down : o.y),
21193
21316
  matrix: m ? [m[0], m[1], m[2], m[3], first.x, first.y] : null,
21194
21317
  scaleX: font.ppemX !== font.ppem ? font.ppemX / font.ppem : 1,
21195
21318
  fontFamily: mapFontFamily(ttf.family, fontFamilyMap),
@@ -21360,6 +21483,7 @@ function drawGdiTextCall(ctx, fonts, state, call) {
21360
21483
  y: ref.y,
21361
21484
  dx: call.dx ? call.dx.map((v) => v * advanceScale) : null,
21362
21485
  dy: call.dy ? call.dy.map((v) => -v * fontScale) : null,
21486
+ textJustification: call.textJustification ? { ...call.textJustification, extra: call.textJustification.extra * advanceScale } : void 0,
21363
21487
  textAlign: state.textAlign,
21364
21488
  textColor: state.textColor,
21365
21489
  bkColor: state.bkColor,
@@ -21496,11 +21620,11 @@ function readDxArray(view, offset, dataOff, nChars, viewEnd) {
21496
21620
  }
21497
21621
  const dx = [];
21498
21622
  for (let i = 0; i < nChars; i++) {
21499
- dx.push(view.getUint32(start + i * 4, true));
21623
+ dx.push(view.getInt32(start + i * 4, true));
21500
21624
  }
21501
21625
  return dx;
21502
21626
  }
21503
- function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21627
+ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString, viewEnd) {
21504
21628
  const { ctx, view, state } = rCtx;
21505
21629
  const fonts = rCtx.fonts;
21506
21630
  if (!fonts) {
@@ -21512,7 +21636,7 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21512
21636
  codes.push(view.getUint16(offset + offString + i * 2, true));
21513
21637
  }
21514
21638
  const pdy = (options & ETO_PDY) !== 0;
21515
- const raw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, view.byteLength);
21639
+ const raw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, viewEnd);
21516
21640
  let dx = raw;
21517
21641
  let dy = null;
21518
21642
  if (raw && pdy) {
@@ -21537,7 +21661,8 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21537
21661
  },
21538
21662
  dx,
21539
21663
  dy,
21540
- matrix: gdiDeviceMatrix(rCtx)
21664
+ matrix: gdiDeviceMatrix(rCtx),
21665
+ textJustification: state.textJustification
21541
21666
  });
21542
21667
  if (!adv) {
21543
21668
  return false;
@@ -21549,12 +21674,12 @@ function drawWithFontEngine(rCtx, offset, dataOff, nChars, offString) {
21549
21674
  return true;
21550
21675
  }
21551
21676
  function horizontalAlign(textAlign) {
21677
+ if ((textAlign & 6) === 6) {
21678
+ return "center";
21679
+ }
21552
21680
  if (textAlign & 2) {
21553
21681
  return "right";
21554
21682
  }
21555
- if (textAlign & 6) {
21556
- return "center";
21557
- }
21558
21683
  return "left";
21559
21684
  }
21560
21685
  function verticalBaseline(textAlign) {
@@ -21596,11 +21721,11 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21596
21721
  const refY = view.getInt32(dataOff + 32, true);
21597
21722
  const nChars = view.getUint32(dataOff + 36, true);
21598
21723
  const offString = view.getUint32(dataOff + 40, true);
21599
- const viewEnd = view.byteLength;
21600
- if (nChars === 0 || offString === 0 || offset + offString + nChars * 2 > viewEnd) {
21724
+ const viewEnd = Math.min(view.byteLength, offset + recSize);
21725
+ if (nChars === 0 || offString === 0 || nChars > Math.floor((viewEnd - offset - offString) / 2)) {
21601
21726
  return true;
21602
21727
  }
21603
- if (drawWithFontEngine(rCtx, offset, dataOff, nChars, offString)) {
21728
+ if (drawWithFontEngine(rCtx, offset, dataOff, nChars, offString, viewEnd)) {
21604
21729
  return true;
21605
21730
  }
21606
21731
  const text = readUtf16LE(view, offset + offString, nChars);
@@ -21615,8 +21740,16 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21615
21740
  const align = horizontalAlign(state.textAlign);
21616
21741
  ctx.textBaseline = verticalBaseline(state.textAlign);
21617
21742
  ctx.textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
21618
- const dxLogical = readDxArray(view, offset, dataOff, nChars, viewEnd);
21619
- const dxDevice = dxLogical ? dxLogical.map((v) => advanceScale !== null ? v * advanceScale : gmw(rCtx, v)) : null;
21743
+ const pdy = (view.getUint32(dataOff + 44, true) & ETO_PDY) !== 0;
21744
+ const dxRaw = readDxArray(view, offset, dataOff, pdy ? nChars * 2 : nChars, viewEnd);
21745
+ const dxLogical = dxRaw && pdy ? dxRaw.filter((_, i) => i % 2 === 0) : dxRaw;
21746
+ let dxDevice = dxLogical ? dxLogical.map((v) => advanceScale !== null ? v * advanceScale : gmw(rCtx, v)) : null;
21747
+ if (!dxDevice && state.textJustification && !pdy) {
21748
+ const units = text.split("");
21749
+ const natural = units.map((ch) => ctx.measureText(ch).width);
21750
+ const scale = advanceScale ?? gmw(rCtx, 1);
21751
+ dxDevice = applyTextJustification(natural, units.map((ch) => ch.charCodeAt(0)), state.textJustification.extra * scale, state.textJustification.count);
21752
+ }
21620
21753
  if (dxDevice) {
21621
21754
  ctx.textAlign = "left";
21622
21755
  }
@@ -21649,10 +21782,118 @@ function handleExtTextOutW(rCtx, offset, dataOff, recSize) {
21649
21782
  }
21650
21783
  return true;
21651
21784
  }
21785
+ function drawTextCall(rCtx, x, y, codes, options, rect, dx) {
21786
+ const n = codes.length;
21787
+ const offString = 76;
21788
+ const offDx = offString + n * 2 + 3 & -4;
21789
+ const dxCount = dx ? dx.length : 0;
21790
+ const size = offDx + dxCount * 4;
21791
+ const view = new DataView(new ArrayBuffer(size));
21792
+ view.setUint32(44, n, true);
21793
+ view.setUint32(48, offString, true);
21794
+ view.setUint32(52, options, true);
21795
+ view.setInt32(36, x, true);
21796
+ view.setInt32(40, y, true);
21797
+ view.setInt32(56, rect[0], true);
21798
+ view.setInt32(60, rect[1], true);
21799
+ view.setInt32(64, rect[2], true);
21800
+ view.setInt32(68, rect[3], true);
21801
+ view.setUint32(72, dx ? offDx : 0, true);
21802
+ for (let i = 0; i < n; i++) view.setUint16(offString + i * 2, codes[i], true);
21803
+ if (dx) for (let i = 0; i < dxCount; i++) view.setInt32(offDx + i * 4, dx[i], true);
21804
+ const child = { ...rCtx, view };
21805
+ handleExtTextOutW(child, 0, 8, size);
21806
+ }
21807
+ function readAnsiRecord(view, start, count, charSet) {
21808
+ return decodeAnsiRecord(Array.from({ length: count }, (_, i) => view.getUint8(start + i)), charSet);
21809
+ }
21810
+ function collapseAnsiDx(dx, byteLengths, pdy) {
21811
+ if (!dx) return null;
21812
+ const result = [];
21813
+ let at = 0;
21814
+ for (const bytes of byteLengths) {
21815
+ let x = 0, y = 0;
21816
+ for (let i = 0; i < bytes; i++) {
21817
+ x += dx[at + i * (pdy ? 2 : 1)] ?? 0;
21818
+ if (pdy) y += dx[at + i * 2 + 1] ?? 0;
21819
+ }
21820
+ result.push(x);
21821
+ if (pdy) result.push(y);
21822
+ at += bytes * (pdy ? 2 : 1);
21823
+ }
21824
+ return result;
21825
+ }
21826
+ function handlePolyText(rCtx, offset, dataOff, recSize, wide) {
21827
+ const { view, state } = rCtx;
21828
+ const end = Math.min(view.byteLength, offset + recSize);
21829
+ if (recSize < 40) return true;
21830
+ const count = view.getUint32(dataOff + 28, true);
21831
+ const arrayStart = dataOff + 32;
21832
+ if (count > Math.floor((end - arrayStart) / 40)) return true;
21833
+ const charSet = state.fontDetails?.charSet ?? 1;
21834
+ for (let i = 0; i < count; i++) {
21835
+ const e = arrayStart + i * 40;
21836
+ const x = view.getInt32(e, true), y = view.getInt32(e + 4, true);
21837
+ const n = view.getUint32(e + 8, true), offString = view.getUint32(e + 12, true);
21838
+ const options = view.getUint32(e + 16, true);
21839
+ const rect = [view.getInt32(e + 20, true), view.getInt32(e + 24, true), view.getInt32(e + 28, true), view.getInt32(e + 32, true)];
21840
+ const offDx = view.getUint32(e + 36, true);
21841
+ const unit = wide ? 2 : 1;
21842
+ if (!offString || offString > end - offset || n > Math.floor((end - (offset + offString)) / unit)) continue;
21843
+ const ansi = wide ? null : readAnsiRecord(view, offset + offString, n, charSet);
21844
+ const codes = wide ? Array.from({ length: n }, (_, j) => view.getUint16(offset + offString + j * 2, true)) : ansi.codes;
21845
+ let dx = null;
21846
+ const pdy = (options & ETO_PDY) !== 0;
21847
+ const dxCount = n * (pdy ? 2 : 1);
21848
+ if (offDx && offDx <= end - offset && dxCount <= Math.floor((end - (offset + offDx)) / 4)) {
21849
+ dx = Array.from({ length: dxCount }, (_, j) => view.getInt32(offset + offDx + j * 4, true));
21850
+ if (!wide) dx = collapseAnsiDx(dx, ansi.byteLengths, pdy);
21851
+ }
21852
+ drawTextCall(rCtx, x, y, codes, options, rect, dx);
21853
+ }
21854
+ return true;
21855
+ }
21856
+ function handleSmallTextOut(rCtx, offset, dataOff, recSize) {
21857
+ const { view } = rCtx;
21858
+ const end = Math.min(view.byteLength, offset + recSize);
21859
+ if (recSize < 36) return true;
21860
+ const x = view.getInt32(dataOff, true), y = view.getInt32(dataOff + 4, true);
21861
+ const n = view.getUint32(dataOff + 8, true), options = view.getUint32(dataOff + 12, true);
21862
+ const small = (options & 512) !== 0, noRect = (options & 256) !== 0;
21863
+ const base = dataOff + 28 + (noRect ? 0 : 16);
21864
+ if (base > end || n > Math.floor((end - base) / (small ? 1 : 2))) return true;
21865
+ const rect = noRect ? [0, 0, 0, 0] : [view.getInt32(dataOff + 28, true), view.getInt32(dataOff + 32, true), view.getInt32(dataOff + 36, true), view.getInt32(dataOff + 40, true)];
21866
+ const codes = small ? Array.from({ length: n }, (_, i) => view.getUint8(base + i)) : Array.from({ length: n }, (_, i) => view.getUint16(base + i * 2, true));
21867
+ drawTextCall(rCtx, x, y, codes, options, rect, null);
21868
+ return true;
21869
+ }
21652
21870
  function handleEmfGdiDrawTextRecord(rCtx, recType, offset, dataOff, recSize) {
21653
21871
  if (recType === EMR_EXTTEXTOUTW) {
21654
21872
  return handleExtTextOutW(rCtx, offset, dataOff, recSize);
21655
21873
  }
21874
+ if (recType === EMR_EXTTEXTOUTA || recType === EMR_POLYTEXTOUTA || recType === EMR_POLYTEXTOUTW || recType === EMR_SMALLTEXTOUT) {
21875
+ if (recType === EMR_POLYTEXTOUTA || recType === EMR_POLYTEXTOUTW) return handlePolyText(rCtx, offset, dataOff, recSize, recType === EMR_POLYTEXTOUTW);
21876
+ if (recType === EMR_SMALLTEXTOUT) return handleSmallTextOut(rCtx, offset, dataOff, recSize);
21877
+ const { view, state } = rCtx;
21878
+ const end = Math.min(view.byteLength, offset + recSize);
21879
+ if (recSize < 76) return true;
21880
+ const x = view.getInt32(dataOff + 28, true), y = view.getInt32(dataOff + 32, true);
21881
+ const n = view.getUint32(dataOff + 36, true), strOff = view.getUint32(dataOff + 40, true), options = view.getUint32(dataOff + 44, true);
21882
+ if (!strOff || strOff > end - offset) return true;
21883
+ if (n > end - (offset + strOff)) return true;
21884
+ const ansi = readAnsiRecord(view, offset + strOff, n, state.fontDetails?.charSet ?? 1);
21885
+ const rect = [view.getInt32(dataOff + 48, true), view.getInt32(dataOff + 52, true), view.getInt32(dataOff + 56, true), view.getInt32(dataOff + 60, true)];
21886
+ const offDx = view.getUint32(dataOff + 64, true);
21887
+ let dx = null;
21888
+ const pdy = (options & ETO_PDY) !== 0;
21889
+ const dxCount = n * (pdy ? 2 : 1);
21890
+ if (offDx && offDx <= end - offset && dxCount <= Math.floor((end - (offset + offDx)) / 4)) {
21891
+ dx = Array.from({ length: dxCount }, (_, i) => view.getInt32(offset + offDx + i * 4, true));
21892
+ dx = collapseAnsiDx(dx, ansi.byteLengths, pdy);
21893
+ }
21894
+ drawTextCall(rCtx, x, y, ansi.codes, options, rect, dx);
21895
+ return true;
21896
+ }
21656
21897
  return false;
21657
21898
  }
21658
21899
 
@@ -23665,7 +23906,7 @@ function penDashArray(pen) {
23665
23906
  return pattern.map((v) => v * w);
23666
23907
  }
23667
23908
  function applyPlusPenStyle(ctx, pen, widthScale = 1) {
23668
- ctx.lineWidth = pen.width * widthScale;
23909
+ ctx.lineWidth = (pen.width || 1) * widthScale;
23669
23910
  const dashes = penDashArray(pen);
23670
23911
  ctx.lineCap = canvasLineCap(dashes.length > 0 ? pen.dashCap ?? 0 : pen.startCap ?? pen.endCap);
23671
23912
  ctx.lineJoin = canvasLineJoin(pen.lineJoin);
@@ -23677,6 +23918,7 @@ function applyPlusPenStyle(ctx, pen, widthScale = 1) {
23677
23918
  }
23678
23919
  function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false) {
23679
23920
  const { ctx } = rCtx;
23921
+ pen = applySimilarityPenTransform(pen);
23680
23922
  const inset = !!pen && pen.alignment === 1 && closedFigure;
23681
23923
  const widthScale = inset ? 2 : 1;
23682
23924
  const stroke = (c) => {
@@ -23738,6 +23980,22 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
23738
23980
  applyPlusWorldTransform(rCtx);
23739
23981
  stroke(ctx);
23740
23982
  }
23983
+ function applySimilarityPenTransform(pen) {
23984
+ if (!pen?.transform) {
23985
+ return pen;
23986
+ }
23987
+ if (isIdentity2(pen.transform)) {
23988
+ return { ...pen, transform: null };
23989
+ }
23990
+ const [a, b, c, d] = pen.transform;
23991
+ const sx = Math.hypot(a, b);
23992
+ const sy = Math.hypot(c, d);
23993
+ const dot = a * c + b * d;
23994
+ if (!Number.isFinite(sx + sy + dot) || !(sx > 0) || Math.abs(sx - sy) > 1e-6 * sx || Math.abs(dot) > 1e-6 * sx * sy) {
23995
+ return pen;
23996
+ }
23997
+ return { ...pen, width: pen.width === 0 ? 1 : pen.width * sx, transform: null };
23998
+ }
23741
23999
  function strokeGdiplus(rCtx, pen, sampler, buildPath, device, size, mode, closedFigure) {
23742
24000
  if (pen.transform && !isIdentity2(pen.transform)) {
23743
24001
  return false;
@@ -24151,6 +24409,7 @@ function drawPlusStringWithEngine(rCtx, font, text, layoutX, layoutY, paint, ali
24151
24409
  dx,
24152
24410
  dy: null,
24153
24411
  textAlign: 24,
24412
+ options: 0,
24154
24413
  matrix: null});
24155
24414
  if (!cov) {
24156
24415
  return true;
@@ -26865,46 +27124,6 @@ function penIsNull(p) {
26865
27124
  }
26866
27125
 
26867
27126
  // src/wmf-text.ts
26868
- var CP1252_HIGH2 = [
26869
- 8364,
26870
- 129,
26871
- 8218,
26872
- 402,
26873
- 8222,
26874
- 8230,
26875
- 8224,
26876
- 8225,
26877
- 710,
26878
- 8240,
26879
- 352,
26880
- 8249,
26881
- 338,
26882
- 141,
26883
- 381,
26884
- 143,
26885
- 144,
26886
- 8216,
26887
- 8217,
26888
- 8220,
26889
- 8221,
26890
- 8226,
26891
- 8211,
26892
- 8212,
26893
- 732,
26894
- 8482,
26895
- 353,
26896
- 8250,
26897
- 339,
26898
- 157,
26899
- 382,
26900
- 376
26901
- ];
26902
- function ansiToCode(b, charSet) {
26903
- if (charSet === 2) {
26904
- return b;
26905
- }
26906
- return b >= 128 && b <= 159 ? CP1252_HIGH2[b - 128] : b;
26907
- }
26908
27127
  function fontAdvances(p, codes) {
26909
27128
  const fonts = p.rCtx.fonts;
26910
27129
  if (!fonts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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",