@bendyline/squisq-formats 2.7.2 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,13 +13,14 @@ import {
13
13
  fontAwesomeGlyph
14
14
  } from "./chunk-LBXBRFBZ.js";
15
15
  import {
16
+ authoredImageSize,
16
17
  fitWithin,
17
18
  readImageDimensions
18
- } from "./chunk-RSU3V5IQ.js";
19
+ } from "./chunk-JZR32FON.js";
19
20
 
20
21
  // src/pdf/export.ts
21
22
  import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
22
- import fontkit from "@pdf-lib/fontkit";
23
+ import fontkit2 from "@pdf-lib/fontkit";
23
24
  import { contrastRatio } from "@bendyline/squisq/schemas";
24
25
  import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
25
26
  import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
@@ -269,6 +270,10 @@ var WHITESPACE_LIKE = /* @__PURE__ */ new Set([
269
270
  8288,
270
271
  65279
271
272
  ]);
273
+ var INVISIBLE_OR_SPACE = /^[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]$/u;
274
+ function isInvisibleOrSpace(codePoint) {
275
+ return WHITESPACE_LIKE.has(codePoint) || INVISIBLE_OR_SPACE.test(String.fromCodePoint(codePoint));
276
+ }
272
277
  var MAX_SAMPLES = 5;
273
278
  var ASCII_ONLY = /^[\x20-\x7e]*$/;
274
279
  var COMBINING_MARKS = /[̀-ͯ]/g;
@@ -320,13 +325,116 @@ var WinAnsiTracker = class {
320
325
  get substitutions() {
321
326
  return this.count;
322
327
  }
323
- /** One-shot summary for the export, or undefined when nothing was lost. */
324
- warning() {
328
+ /**
329
+ * One-shot summary for the export, or undefined when nothing was lost.
330
+ * `unicodeFont` says an embedded Unicode font already drew what it could, so
331
+ * what remains is beyond it too (CJK and emoji, with the bundled faces).
332
+ */
333
+ warning(options = {}) {
325
334
  if (this.count === 0) return void 0;
335
+ if (options.unicodeFont) {
336
+ return `PDF export replaced ${this.count} character(s) that its fonts cannot draw (${this.samples.join(", ")}). Scripts such as CJK and emoji are not available in PDF export.`;
337
+ }
326
338
  return `PDF export replaced ${this.count} character(s) that the standard PDF fonts cannot encode (${this.samples.join(", ")}). Scripts such as emoji, CJK and Cyrillic require an embedded Unicode font.`;
327
339
  }
328
340
  };
329
341
 
342
+ // src/pdf/unicodeText.ts
343
+ import fontkit from "@pdf-lib/fontkit";
344
+ async function loadBundledUnicodeFonts() {
345
+ const { dejaVuFontData } = await import("./unicodeFontData.generated-YY6T2K5S.js");
346
+ return dejaVuFontData();
347
+ }
348
+ function hasUnencodableText(value) {
349
+ for (const ch of value) {
350
+ const codePoint = ch.codePointAt(0);
351
+ if (!isWinAnsiEncodable(codePoint) && !isInvisibleOrSpace(codePoint)) return true;
352
+ }
353
+ return false;
354
+ }
355
+ function needsUnicodeFont(root) {
356
+ const pending = [root];
357
+ while (pending.length > 0) {
358
+ const value = pending.pop();
359
+ if (typeof value === "string") {
360
+ if (hasUnencodableText(value)) return true;
361
+ } else if (Array.isArray(value)) {
362
+ pending.push(...value);
363
+ } else if (value && typeof value === "object") {
364
+ pending.push(...Object.values(value));
365
+ }
366
+ }
367
+ return false;
368
+ }
369
+ function asBytes(data) {
370
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
371
+ }
372
+ async function embedUnicodeFaces(pdfDoc, fonts, standard) {
373
+ pdfDoc.registerFontkit(fontkit);
374
+ const faces = /* @__PURE__ */ new Map();
375
+ const faceFor = (data) => {
376
+ let face = faces.get(data);
377
+ if (!face) {
378
+ const bytes = asBytes(data);
379
+ const metrics = fontkit.create(bytes);
380
+ face = pdfDoc.embedFont(bytes, { subset: true }).then((font) => ({
381
+ font,
382
+ covers: (codePoint) => metrics.hasGlyphForCodePoint(codePoint)
383
+ }));
384
+ faces.set(data, face);
385
+ }
386
+ return face;
387
+ };
388
+ const [regular, bold, mono] = await Promise.all([
389
+ faceFor(fonts.regular),
390
+ faceFor(fonts.bold ?? fonts.regular),
391
+ faceFor(fonts.mono ?? fonts.regular)
392
+ ]);
393
+ return /* @__PURE__ */ new Map([
394
+ [standard.regular, regular],
395
+ [standard.italic, regular],
396
+ [standard.bold, bold],
397
+ [standard.boldItalic, bold],
398
+ [standard.mono, mono],
399
+ [standard.monoBold, mono]
400
+ ]);
401
+ }
402
+ var PdfTextRuns = class {
403
+ constructor(tracker, faces = /* @__PURE__ */ new Map()) {
404
+ this.tracker = tracker;
405
+ this.faces = faces;
406
+ }
407
+ split(text, font) {
408
+ const face = this.faces.get(font);
409
+ if (!face || !hasUnencodableText(text)) return [{ text: this.tracker.clean(text), font }];
410
+ const runs = [];
411
+ const push = (value, runFont) => {
412
+ if (value.length === 0) return;
413
+ const last = runs[runs.length - 1];
414
+ if (last && last.font === runFont) last.text += value;
415
+ else runs.push({ text: value, font: runFont });
416
+ };
417
+ const drawable = (codePoint) => !isInvisibleOrSpace(codePoint) && face.covers(codePoint);
418
+ for (const word of text.normalize("NFC").split(/(\s+)/u)) {
419
+ if (word.length === 0) continue;
420
+ const codePoints = Array.from(word, (ch) => ch.codePointAt(0));
421
+ if (codePoints.every(isWinAnsiEncodable)) {
422
+ push(word, font);
423
+ } else if (codePoints.every(drawable)) {
424
+ push(word, face.font);
425
+ } else {
426
+ for (const ch of word) {
427
+ const codePoint = ch.codePointAt(0);
428
+ if (isWinAnsiEncodable(codePoint)) push(ch, font);
429
+ else if (drawable(codePoint)) push(ch, face.font);
430
+ else push(this.tracker.clean(ch), font);
431
+ }
432
+ }
433
+ }
434
+ return runs.length > 0 ? runs : [{ text: "", font }];
435
+ }
436
+ };
437
+
330
438
  // src/pdf/styles.ts
331
439
  var PAGE_WIDTH_LETTER = 612;
332
440
  var PAGE_HEIGHT_LETTER = 792;
@@ -401,7 +509,7 @@ async function markdownDocToPdf(doc, options = {}) {
401
509
  const ctx = await createExportContext(pdfDoc, options, doc);
402
510
  ctx.images = await embedDocumentImages(pdfDoc, doc, options.images);
403
511
  renderBlocks(doc.children, ctx, 0);
404
- const warning = ctx.text.warning();
512
+ const warning = ctx.text.warning({ unicodeFont: ctx.unicodeFont });
405
513
  if (warning) {
406
514
  if (options.onWarning) options.onWarning(warning);
407
515
  else console.warn(warning);
@@ -455,10 +563,22 @@ async function createExportContext(pdfDoc, options, doc) {
455
563
  pdfDoc.embedFont(StandardFonts.Courier),
456
564
  pdfDoc.embedFont(StandardFonts.CourierBold)
457
565
  ]);
566
+ let unicodeFaces = /* @__PURE__ */ new Map();
567
+ if (options.unicodeFonts !== false && needsUnicodeFont(doc)) {
568
+ const faces = options.unicodeFonts ?? await loadBundledUnicodeFonts();
569
+ unicodeFaces = await embedUnicodeFaces(pdfDoc, faces, {
570
+ regular,
571
+ bold,
572
+ italic,
573
+ boldItalic,
574
+ mono,
575
+ monoBold
576
+ });
577
+ }
458
578
  const icons = {};
459
579
  const iconFamilies = collectInlineIconFamilies(doc);
460
580
  if (iconFamilies.size > 0) {
461
- pdfDoc.registerFontkit(fontkit);
581
+ pdfDoc.registerFontkit(fontkit2);
462
582
  await Promise.all(
463
583
  fontAwesomeFaces(iconFamilies).map(async (face) => {
464
584
  icons[face.family] = await pdfDoc.embedFont(face.data, { subset: true });
@@ -483,6 +603,7 @@ async function createExportContext(pdfDoc, options, doc) {
483
603
  colorLink = paperInk(theme.colors.highlight || theme.colors.secondary, COLOR_LINK);
484
604
  }
485
605
  }
606
+ const text = new WinAnsiTracker();
486
607
  return {
487
608
  pdfDoc,
488
609
  fonts: { regular, bold, italic, boldItalic, mono, monoBold, icons },
@@ -496,7 +617,9 @@ async function createExportContext(pdfDoc, options, doc) {
496
617
  bottomY: margin,
497
618
  colors: { text: colorText, heading: colorHeading, link: colorLink },
498
619
  footnotes: new FootnoteIndex(doc),
499
- text: new WinAnsiTracker(),
620
+ text,
621
+ runs: new PdfTextRuns(text, unicodeFaces),
622
+ unicodeFont: unicodeFaces.size > 0,
500
623
  signal: options.signal,
501
624
  images: /* @__PURE__ */ new Map()
502
625
  };
@@ -522,15 +645,15 @@ function flattenInlines(nodes, ctx, state) {
522
645
  case "text": {
523
646
  const font = state.code ? state.bold ? ctx.fonts.monoBold : ctx.fonts.mono : pickFont(ctx, state.bold, state.italic);
524
647
  const baseSize = state.code ? CODE_FONT_SIZE : ctx.fontSize;
525
- spans.push({
526
- text: ctx.text.clean(node.value),
527
- font,
528
- fontSize: state.vertAlign ? baseSize * VERT_ALIGN_SCALE : baseSize,
529
- color: state.code ? COLOR_CODE_TEXT : state.color ?? ctx.colors.text,
530
- link: state.link,
531
- strikethrough: state.strikethrough,
532
- ...state.vertAlign ? { baselineShift: vertAlignShift(state.vertAlign, baseSize) } : {}
533
- });
648
+ spans.push(
649
+ ...textSpans(ctx, node.value, font, {
650
+ fontSize: state.vertAlign ? baseSize * VERT_ALIGN_SCALE : baseSize,
651
+ color: state.code ? COLOR_CODE_TEXT : state.color ?? ctx.colors.text,
652
+ link: state.link,
653
+ strikethrough: state.strikethrough,
654
+ ...state.vertAlign ? { baselineShift: vertAlignShift(state.vertAlign, baseSize) } : {}
655
+ })
656
+ );
534
657
  break;
535
658
  }
536
659
  case "strong":
@@ -561,13 +684,13 @@ function flattenInlines(nodes, ctx, state) {
561
684
  );
562
685
  break;
563
686
  case "inlineCode": {
564
- spans.push({
565
- text: ctx.text.clean(node.value),
566
- font: ctx.fonts.mono,
567
- fontSize: CODE_FONT_SIZE,
568
- color: COLOR_CODE_TEXT,
569
- link: state.link
570
- });
687
+ spans.push(
688
+ ...textSpans(ctx, node.value, ctx.fonts.mono, {
689
+ fontSize: CODE_FONT_SIZE,
690
+ color: COLOR_CODE_TEXT,
691
+ link: state.link
692
+ })
693
+ );
571
694
  break;
572
695
  }
573
696
  case "link": {
@@ -584,12 +707,12 @@ function flattenInlines(nodes, ctx, state) {
584
707
  }
585
708
  case "image": {
586
709
  const imgNode = node;
587
- spans.push({
588
- text: ctx.text.clean(imgNode.alt ? `[Image: ${imgNode.alt}]` : "[Image]"),
589
- font: ctx.fonts.italic,
590
- fontSize: ctx.fontSize,
591
- color: COLOR_BLOCKQUOTE_TEXT
592
- });
710
+ spans.push(
711
+ ...textSpans(ctx, imgNode.alt ? `[Image: ${imgNode.alt}]` : "[Image]", ctx.fonts.italic, {
712
+ fontSize: ctx.fontSize,
713
+ color: COLOR_BLOCKQUOTE_TEXT
714
+ })
715
+ );
593
716
  break;
594
717
  }
595
718
  case "inlineIcon": {
@@ -606,14 +729,14 @@ function flattenInlines(nodes, ctx, state) {
606
729
  strikethrough: state.strikethrough
607
730
  });
608
731
  } else {
609
- spans.push({
610
- text: ctx.text.clean(`{[${iconNode.token}]}`),
611
- font: pickFont(ctx, state.bold, state.italic),
612
- fontSize: ctx.fontSize,
613
- color: state.color ?? ctx.colors.text,
614
- link: state.link,
615
- strikethrough: state.strikethrough
616
- });
732
+ spans.push(
733
+ ...textSpans(ctx, `{[${iconNode.token}]}`, pickFont(ctx, state.bold, state.italic), {
734
+ fontSize: ctx.fontSize,
735
+ color: state.color ?? ctx.colors.text,
736
+ link: state.link,
737
+ strikethrough: state.strikethrough
738
+ })
739
+ );
617
740
  }
618
741
  break;
619
742
  }
@@ -628,33 +751,33 @@ function flattenInlines(nodes, ctx, state) {
628
751
  case "htmlInline": {
629
752
  const html = node.rawHtml;
630
753
  if (html) {
631
- spans.push({
632
- text: ctx.text.clean(html),
633
- font: ctx.fonts.mono,
634
- fontSize: CODE_FONT_SIZE,
635
- color: COLOR_CODE_TEXT
636
- });
754
+ spans.push(
755
+ ...textSpans(ctx, html, ctx.fonts.mono, {
756
+ fontSize: CODE_FONT_SIZE,
757
+ color: COLOR_CODE_TEXT
758
+ })
759
+ );
637
760
  }
638
761
  break;
639
762
  }
640
763
  case "inlineMath":
641
- spans.push({
642
- text: ctx.text.clean(node.value),
643
- font: ctx.fonts.mono,
644
- fontSize: CODE_FONT_SIZE,
645
- color: COLOR_CODE_TEXT
646
- });
764
+ spans.push(
765
+ ...textSpans(ctx, node.value, ctx.fonts.mono, {
766
+ fontSize: CODE_FONT_SIZE,
767
+ color: COLOR_CODE_TEXT
768
+ })
769
+ );
647
770
  break;
648
771
  case "footnoteReference": {
649
772
  const ref = node;
650
773
  const marker = ctx.footnotes ? String(ctx.footnotes.cite(ref.identifier).number) : ref.label ?? ref.identifier;
651
- spans.push({
652
- text: ctx.text.clean(marker),
653
- font: ctx.fonts.regular,
654
- fontSize: ctx.fontSize * VERT_ALIGN_SCALE,
655
- color: ctx.colors.link,
656
- baselineShift: vertAlignShift("superscript", ctx.fontSize)
657
- });
774
+ spans.push(
775
+ ...textSpans(ctx, marker, ctx.fonts.regular, {
776
+ fontSize: ctx.fontSize * VERT_ALIGN_SCALE,
777
+ color: ctx.colors.link,
778
+ baselineShift: vertAlignShift("superscript", ctx.fontSize)
779
+ })
780
+ );
658
781
  break;
659
782
  }
660
783
  // linkReference, imageReference, textDirective — render children or identifier
@@ -663,12 +786,12 @@ function flattenInlines(nodes, ctx, state) {
663
786
  if (fallback.children) {
664
787
  spans.push(...flattenInlines(fallback.children, ctx, state));
665
788
  } else if (fallback.value) {
666
- spans.push({
667
- text: ctx.text.clean(fallback.value),
668
- font: pickFont(ctx, state.bold, state.italic),
669
- fontSize: ctx.fontSize,
670
- color: state.color ?? ctx.colors.text
671
- });
789
+ spans.push(
790
+ ...textSpans(ctx, fallback.value, pickFont(ctx, state.bold, state.italic), {
791
+ fontSize: ctx.fontSize,
792
+ color: state.color ?? ctx.colors.text
793
+ })
794
+ );
672
795
  }
673
796
  break;
674
797
  }
@@ -676,6 +799,23 @@ function flattenInlines(nodes, ctx, state) {
676
799
  }
677
800
  return spans;
678
801
  }
802
+ function textSpans(ctx, text, font, style) {
803
+ return ctx.runs.split(text, font).map((run) => ({ ...style, text: run.text, font: run.font }));
804
+ }
805
+ function drawTextLine(ctx, text, font, at) {
806
+ let x = at.x;
807
+ for (const run of ctx.runs.split(text, font)) {
808
+ if (run.text.length === 0) continue;
809
+ ctx.page.drawText(run.text, {
810
+ x,
811
+ y: at.y,
812
+ size: at.size,
813
+ font: run.font,
814
+ color: rgb(at.color.r, at.color.g, at.color.b)
815
+ });
816
+ x += run.font.widthOfTextAtSize(run.text, at.size);
817
+ }
818
+ }
679
819
  function pickFont(ctx, bold, italic) {
680
820
  if (bold && italic) return ctx.fonts.boldItalic;
681
821
  if (bold) return ctx.fonts.bold;
@@ -880,9 +1020,10 @@ function renderImageParagraph(node, ctx, x0, width) {
880
1020
  drawSpans(note, ctx, width, x0);
881
1021
  continue;
882
1022
  }
1023
+ const display = authoredImageSize(imageNode, embedded) ?? embedded;
883
1024
  const size = fitWithin(
884
- embedded.width * POINTS_PER_CSS_PIXEL,
885
- embedded.height * POINTS_PER_CSS_PIXEL,
1025
+ display.width * POINTS_PER_CSS_PIXEL,
1026
+ display.height * POINTS_PER_CSS_PIXEL,
886
1027
  width,
887
1028
  maxHeight
888
1029
  );
@@ -967,7 +1108,7 @@ function renderListItem(item, ctx, extraIndent, depth, ordered, counter) {
967
1108
  function renderCodeBlock(node, ctx, extraIndent) {
968
1109
  const x0 = ctx.margin + extraIndent;
969
1110
  const _w = ctx.contentWidth - extraIndent;
970
- const lines = node.value.split("\n").map((line) => ctx.text.clean(line));
1111
+ const lines = node.value.split("\n");
971
1112
  const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
972
1113
  const totalHeight = lines.length * lineH + 12;
973
1114
  ensureSpace(ctx, Math.min(totalHeight, ctx.y - ctx.bottomY));
@@ -977,12 +1118,11 @@ function renderCodeBlock(node, ctx, extraIndent) {
977
1118
  for (const line of lines) {
978
1119
  ensureSpace(ctx, lineH);
979
1120
  if (line.length > 0) {
980
- ctx.page.drawText(line, {
1121
+ drawTextLine(ctx, line, ctx.fonts.mono, {
981
1122
  x: x0 + 8,
982
1123
  y: ctx.y - CODE_FONT_SIZE,
983
1124
  size: CODE_FONT_SIZE,
984
- font: ctx.fonts.mono,
985
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
1125
+ color: COLOR_CODE_TEXT
986
1126
  });
987
1127
  }
988
1128
  ctx.y -= lineH;
@@ -1083,36 +1223,34 @@ function renderThematicBreak(ctx, extraIndent) {
1083
1223
  }
1084
1224
  function renderHtmlBlock(node, ctx, extraIndent) {
1085
1225
  if (!node.rawHtml) return;
1086
- const lines = node.rawHtml.split("\n").map((line) => ctx.text.clean(line));
1226
+ const lines = node.rawHtml.split("\n");
1087
1227
  const x0 = ctx.margin + extraIndent;
1088
1228
  for (const line of lines) {
1089
1229
  if (line.trim().length === 0) continue;
1090
1230
  const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
1091
1231
  ensureSpace(ctx, lineH);
1092
- ctx.page.drawText(line, {
1232
+ drawTextLine(ctx, line, ctx.fonts.mono, {
1093
1233
  x: x0,
1094
1234
  y: ctx.y - CODE_FONT_SIZE,
1095
1235
  size: CODE_FONT_SIZE,
1096
- font: ctx.fonts.mono,
1097
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
1236
+ color: COLOR_CODE_TEXT
1098
1237
  });
1099
1238
  ctx.y -= lineH;
1100
1239
  }
1101
1240
  ctx.y -= PARAGRAPH_SPACING;
1102
1241
  }
1103
1242
  function renderMathBlock(node, ctx, extraIndent) {
1104
- const lines = node.value.split("\n").map((line) => ctx.text.clean(line));
1243
+ const lines = node.value.split("\n");
1105
1244
  const x0 = ctx.margin + extraIndent;
1106
1245
  for (const line of lines) {
1107
1246
  const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
1108
1247
  ensureSpace(ctx, lineH);
1109
1248
  if (line.length > 0) {
1110
- ctx.page.drawText(line, {
1249
+ drawTextLine(ctx, line, ctx.fonts.mono, {
1111
1250
  x: x0,
1112
1251
  y: ctx.y - CODE_FONT_SIZE,
1113
1252
  size: CODE_FONT_SIZE,
1114
- font: ctx.fonts.mono,
1115
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
1253
+ color: COLOR_CODE_TEXT
1116
1254
  });
1117
1255
  }
1118
1256
  ctx.y -= lineH;
@@ -1121,15 +1259,14 @@ function renderMathBlock(node, ctx, extraIndent) {
1121
1259
  }
1122
1260
  function renderFootnoteDefinition(node, ctx, extraIndent) {
1123
1261
  const number = ctx.footnotes?.numberFor(node.identifier);
1124
- const label = ctx.text.clean(number !== void 0 ? `${number}.` : `[${node.identifier}]`);
1262
+ const label = number !== void 0 ? `${number}.` : `[${node.identifier}]`;
1125
1263
  const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
1126
1264
  ensureSpace(ctx, lineH);
1127
- ctx.page.drawText(label, {
1265
+ drawTextLine(ctx, label, ctx.fonts.bold, {
1128
1266
  x: ctx.margin + extraIndent,
1129
1267
  y: ctx.y - ctx.fontSize * 0.75,
1130
1268
  size: ctx.fontSize * 0.75,
1131
- font: ctx.fonts.bold,
1132
- color: rgb(ctx.colors.link.r, ctx.colors.link.g, ctx.colors.link.b)
1269
+ color: ctx.colors.link
1133
1270
  });
1134
1271
  renderBlocks(node.children, ctx, extraIndent + LIST_INDENT);
1135
1272
  }
@@ -1163,12 +1300,11 @@ function renderFallbackBlock(node, ctx, extraIndent) {
1163
1300
  } else if (fallback.value && typeof fallback.value === "string") {
1164
1301
  const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
1165
1302
  ensureSpace(ctx, lineH);
1166
- ctx.page.drawText(ctx.text.clean(fallback.value), {
1303
+ drawTextLine(ctx, fallback.value, ctx.fonts.regular, {
1167
1304
  x: ctx.margin + extraIndent,
1168
1305
  y: ctx.y - ctx.fontSize,
1169
1306
  size: ctx.fontSize,
1170
- font: ctx.fonts.regular,
1171
- color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b)
1307
+ color: ctx.colors.text
1172
1308
  });
1173
1309
  ctx.y -= lineH + PARAGRAPH_SPACING;
1174
1310
  }
@@ -67,10 +67,11 @@ import {
67
67
  obfuscateDocxFont
68
68
  } from "./chunk-LBXBRFBZ.js";
69
69
  import {
70
+ authoredImageSize,
70
71
  extToMime,
71
72
  fitWithin,
72
73
  readImageDimensions
73
- } from "./chunk-RSU3V5IQ.js";
74
+ } from "./chunk-JZR32FON.js";
74
75
 
75
76
  // src/docx/export.ts
76
77
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
@@ -703,7 +704,8 @@ function convertImage(node, ctx) {
703
704
  const ext = contentType.split("/")[1]?.replace("jpeg", "jpg") || "png";
704
705
  const filename = `image${ctx.images.length + 1}.${ext}`;
705
706
  const { relId, docPrId } = ctx.addImage(data, contentType, filename);
706
- const dims = imageEntry.width && imageEntry.height ? { width: imageEntry.width, height: imageEntry.height } : readImageDimensions(data);
707
+ const natural = imageEntry.width && imageEntry.height ? { width: imageEntry.width, height: imageEntry.height } : readImageDimensions(data);
708
+ const dims = authoredImageSize(node, natural) ?? natural;
707
709
  const EMU_PER_INCH3 = 914400;
708
710
  const MAX_WIDTH_EMU2 = 6 * EMU_PER_INCH3;
709
711
  const MAX_HEIGHT_EMU2 = 8 * EMU_PER_INCH3;
@@ -2,7 +2,7 @@ import {
2
2
  arrayBufferToBase64DataUrl,
3
3
  extractFilename,
4
4
  inferMimeType
5
- } from "./chunk-YVCUNZB5.js";
5
+ } from "./chunk-WMIDXNGR.js";
6
6
  import {
7
7
  exportImageBytes
8
8
  } from "./chunk-VCZUXQUL.js";
@@ -10,6 +10,9 @@ import {
10
10
  FootnoteIndex,
11
11
  footnoteIds
12
12
  } from "./chunk-BXWNU4T5.js";
13
+ import {
14
+ imageSizeAttributes
15
+ } from "./chunk-JZR32FON.js";
13
16
 
14
17
  // src/html/index.ts
15
18
  import JSZip3 from "jszip";
@@ -504,7 +507,8 @@ function extractFirstImageFromBlock(block) {
504
507
  );
505
508
  const replacement = remainingKids.length === 0 ? null : { ...b, children: remainingKids };
506
509
  return {
507
- image: { src: img.url, alt: img.alt ?? "" },
510
+ // A resized image parses with the size it was given in the editor.
511
+ image: { src: img.url, alt: img.alt ?? "", width: img.width, height: img.height },
508
512
  replacement
509
513
  };
510
514
  }
@@ -780,7 +784,7 @@ function nodeToHtml(node, ctx) {
780
784
  const resolved = ctx?.images?.get(original) ?? original;
781
785
  const safeSrc = sanitizeUrl(resolved, "media") ?? svgDataImageToHtmlSrc(resolved);
782
786
  if (!safeSrc) return escapeHtml2(node.alt ?? "");
783
- return `<img src="${escapeAttr(safeSrc)}" alt="${escapeAttr(node.alt ?? "")}" />`;
787
+ return `<img src="${escapeAttr(safeSrc)}" alt="${escapeAttr(node.alt ?? "")}"${imageSizeAttributes(node)} />`;
784
788
  }
785
789
  case "thematicBreak":
786
790
  return "<hr />";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  extractFilename,
3
3
  inferMimeType
4
- } from "./chunk-YVCUNZB5.js";
4
+ } from "./chunk-WMIDXNGR.js";
5
5
  import {
6
6
  exportImageBytes,
7
7
  svgIslandsAsImages
@@ -21,6 +21,9 @@ import {
21
21
  fontAwesomeFaces,
22
22
  fontAwesomeGlyph
23
23
  } from "./chunk-LBXBRFBZ.js";
24
+ import {
25
+ imageSizeAttributes
26
+ } from "./chunk-JZR32FON.js";
24
27
 
25
28
  // src/epub/export.ts
26
29
  import JSZip from "jszip";
@@ -432,7 +435,7 @@ function inlineToXhtml(node, ctx) {
432
435
  const safeSrc = resolved ? `../images/${resolved.filename}` : sanitizeUrl(node.url, "media");
433
436
  if (!safeSrc) return alt;
434
437
  const src = escapeXml(safeSrc);
435
- return `<img src="${src}" alt="${alt}"/>`;
438
+ return `<img src="${src}" alt="${alt}"${imageSizeAttributes(node)}/>`;
436
439
  }
437
440
  case "break":
438
441
  return "<br/>";
@@ -65,9 +65,22 @@ function fitWithin(width, height, maxWidth, maxHeight) {
65
65
  const scale = Math.min(1, maxWidth / width, maxHeight / height);
66
66
  return { width: width * scale, height: height * scale };
67
67
  }
68
+ function imageSizeAttributes(image) {
69
+ return (image.width !== void 0 ? ` width="${image.width}"` : "") + (image.height !== void 0 ? ` height="${image.height}"` : "");
70
+ }
71
+ function authoredImageSize(image, intrinsic) {
72
+ const { width, height } = image;
73
+ if (width !== void 0 && height !== void 0) return { width, height };
74
+ if (!intrinsic || intrinsic.width <= 0 || intrinsic.height <= 0) return void 0;
75
+ if (width !== void 0) return { width, height: width * intrinsic.height / intrinsic.width };
76
+ if (height !== void 0) return { width: height * intrinsic.width / intrinsic.height, height };
77
+ return void 0;
78
+ }
68
79
 
69
80
  export {
70
81
  extToMime,
71
82
  readImageDimensions,
72
- fitWithin
83
+ fitWithin,
84
+ imageSizeAttributes,
85
+ authoredImageSize
73
86
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  inferMimeType
3
- } from "./chunk-YVCUNZB5.js";
3
+ } from "./chunk-WMIDXNGR.js";
4
4
  import {
5
5
  RelIdAllocator,
6
6
  blipXml,
@@ -63,7 +63,7 @@ import {
63
63
  } from "./chunk-LBXBRFBZ.js";
64
64
  import {
65
65
  extToMime
66
- } from "./chunk-RSU3V5IQ.js";
66
+ } from "./chunk-JZR32FON.js";
67
67
 
68
68
  // src/pptx/export.ts
69
69
  import { resolveFontFamily, VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
@@ -262,7 +262,7 @@ function dataReferenceWarnings(doc, formatLabel) {
262
262
  }
263
263
  async function collectContainerDocxImages(container, signal) {
264
264
  const images = await collectContainerImages(container, signal);
265
- const { extToMime } = await import("./images-CAWESWRF.js");
265
+ const { extToMime } = await import("./images-PQCYRJJ5.js");
266
266
  return new Map(
267
267
  [...images].map(([path, data]) => [
268
268
  path,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-XCHIYF3A.js";
4
+ } from "./chunk-RRULGPWV.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extToMime
3
- } from "./chunk-RSU3V5IQ.js";
3
+ } from "./chunk-JZR32FON.js";
4
4
 
5
5
  // src/html/imageUtils.ts
6
6
  function inferMimeType(filename) {
@@ -4,7 +4,7 @@ import {
4
4
  docxToDoc,
5
5
  docxToMarkdownDoc,
6
6
  markdownDocToDocx
7
- } from "../chunk-YITBSNPM.js";
7
+ } from "../chunk-5YYF3Z6C.js";
8
8
  import "../chunk-XVBQB2SL.js";
9
9
  import "../chunk-VCZUXQUL.js";
10
10
  import "../chunk-AVOZAKGP.js";
@@ -15,7 +15,7 @@ import "../chunk-7AWFHP5U.js";
15
15
  import "../chunk-BXWNU4T5.js";
16
16
  import "../chunk-IIQYS2YH.js";
17
17
  import "../chunk-LBXBRFBZ.js";
18
- import "../chunk-RSU3V5IQ.js";
18
+ import "../chunk-JZR32FON.js";
19
19
  export {
20
20
  docToDocx,
21
21
  docxToContainer,