@bendyline/squisq-formats 2.6.14 → 2.6.16
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 +41 -13
- package/dist/{chunk-MIK4NMOP.js → chunk-EMAPNC32.js} +45 -6
- package/dist/{chunk-XJNOZTAY.js → chunk-EQAHFGGX.js} +17 -44
- package/dist/{chunk-3ITGQRL5.js → chunk-NVMYRBOJ.js} +1 -1
- package/dist/chunk-RSU3V5IQ.js +73 -0
- package/dist/{chunk-XG6KFCPM.js → chunk-SAMQZRJU.js} +1 -1
- package/dist/{chunk-C7SPLITE.js → chunk-TLDBJH7D.js} +1 -1
- package/dist/{chunk-2SUIARE4.js → chunk-VE2ADQ5T.js} +4 -1
- package/dist/{chunk-VPWPEMZJ.js → chunk-YVCUNZB5.js} +1 -1
- package/dist/{chunk-ROF7SSQP.js → chunk-ZDRSDYXN.js} +73 -1
- package/dist/diagrams/index.d.ts +86 -0
- package/dist/diagrams/index.js +222 -0
- package/dist/docx/index.d.ts +7 -0
- package/dist/docx/index.js +2 -2
- package/dist/epub/index.js +3 -3
- package/dist/html/index.js +3 -3
- package/dist/images-CAWESWRF.js +10 -0
- package/dist/{import-C16E8Y4X.d.ts → import-Cn_V1_7e.d.ts} +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -11
- package/dist/infer/index.js +6 -6
- package/dist/outside-in/index.d.ts +2 -2
- package/dist/outside-in/index.js +2 -2
- package/dist/pdf/index.d.ts +16 -0
- package/dist/pdf/index.js +2 -1
- package/dist/pptx/index.d.ts +2 -2
- package/dist/pptx/index.js +3 -3
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.js +1 -1
- package/dist/{types-Ba_aUHZJ.d.ts → types-Cubj9A8c.d.ts} +1 -1
- package/package.json +7 -2
- package/dist/chunk-B7GEAZBI.js +0 -37
- package/dist/images-JLBFCDD4.js +0 -6
- package/dist/{chunk-7DQP2I57.js → chunk-3KXEE7KH.js} +5 -5
package/README.md
CHANGED
|
@@ -87,7 +87,34 @@ const imported = await pdfToMarkdownDoc(pdfBuffer);
|
|
|
87
87
|
const container = await pdfToContainer(pdfBuffer);
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
**Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the `detectTables` / `detectCodeBlocks` / `detectBlockquotes` / `detectLinks` options (all default true). `pdfToMarkdownDoc` is text-only; `pdfToContainer` also extracts embedded XObject and inline raster images in browsers and Node, placed **by page** — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only; masks, vector artwork, and images that PDF.js cannot expose as decoded pixels are not emitted.
|
|
90
|
+
**Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). Pass `images` (keyed by the markdown URL, as for DOCX) to draw PNG and JPEG images that stand alone in a paragraph; other images print as an `[Image: alt]` note. PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the `detectTables` / `detectCodeBlocks` / `detectBlockquotes` / `detectLinks` options (all default true). `pdfToMarkdownDoc` is text-only; `pdfToContainer` also extracts embedded XObject and inline raster images in browsers and Node, placed **by page** — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only; masks, vector artwork, and images that PDF.js cannot expose as decoded pixels are not emitted.
|
|
91
|
+
|
|
92
|
+
### Diagrams as pictures
|
|
93
|
+
|
|
94
|
+
DOCX, EPUB, PDF and PPTX cannot draw Mermaid or Squisq diagrams, so on their
|
|
95
|
+
own they export diagram source as text. `rasterizeDiagrams` finds every
|
|
96
|
+
diagram — Mermaid fences, ASCII `diagram`/`timeline`/`tree` fences, and
|
|
97
|
+
`{[drawing]}`/`{[layout]}`/`{[diagram]}` heading blocks — asks a renderer for
|
|
98
|
+
a PNG of each, and returns the document with each diagram replaced by an
|
|
99
|
+
image plus the bytes for the exporters' `images` option. Drawing needs a
|
|
100
|
+
browser; `@bendyline/squisq-react/diagram-pictures` provides the renderer.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { rasterizeDiagrams } from '@bendyline/squisq-formats/diagrams';
|
|
104
|
+
import { createDiagramPictureRenderer } from '@bendyline/squisq-react/diagram-pictures';
|
|
105
|
+
|
|
106
|
+
const pictured = await rasterizeDiagrams(markdownDoc, createDiagramPictureRenderer());
|
|
107
|
+
const docx = await markdownDocToDocx(pictured.markdownDoc, { images: pictured.images });
|
|
108
|
+
const pdf = await markdownDocToPdf(pictured.markdownDoc, { images: pictured.images });
|
|
109
|
+
|
|
110
|
+
// PPTX keeps drawings, timelines and ASCII diagrams as native shapes;
|
|
111
|
+
// only Mermaid needs a picture.
|
|
112
|
+
const pptx = await docToPptx(doc, { diagramImages: pictured.mermaid });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A diagram the renderer cannot draw stays as source; the export never fails on
|
|
116
|
+
it. Image entries carry their display size (`width`/`height` in CSS pixels),
|
|
117
|
+
so a picture rendered at 2× prints at 1×.
|
|
91
118
|
|
|
92
119
|
### HTML
|
|
93
120
|
|
|
@@ -289,18 +316,19 @@ Also exported: `createRegistry` / `defaultRegistry` / `defaultFormats`, `BUILTIN
|
|
|
289
316
|
|
|
290
317
|
## Subpath Exports
|
|
291
318
|
|
|
292
|
-
| Subpath | Contents
|
|
293
|
-
| ------------------------------------- |
|
|
294
|
-
| `@bendyline/squisq-formats/docx` | DOCX import/export (+ `docxToContainer`)
|
|
295
|
-
| `@bendyline/squisq-formats/pdf` | PDF import/export (+ `pdfToContainer`, `configurePdfWorker`)
|
|
296
|
-
| `@bendyline/squisq-formats/html` | Player HTML export, static plain-HTML export + bundles, HTML import
|
|
297
|
-
| `@bendyline/squisq-formats/epub` | EPUB 3 e-book export (with optional Media Overlays)
|
|
298
|
-
| `@bendyline/squisq-formats/pptx` | PPTX export + import (text/lists/tables + slide images)
|
|
299
|
-
| `@bendyline/squisq-formats/xlsx` | XLSX import + tables-only export
|
|
300
|
-
| `@bendyline/squisq-formats/csv` | CSV import/export (RFC 4180)
|
|
301
|
-
| `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (package reader/writer, XML utilities, namespaces)
|
|
302
|
-
| `@bendyline/squisq-formats/
|
|
303
|
-
| `@bendyline/squisq-formats/
|
|
319
|
+
| Subpath | Contents |
|
|
320
|
+
| ------------------------------------- | ------------------------------------------------------------------------------- |
|
|
321
|
+
| `@bendyline/squisq-formats/docx` | DOCX import/export (+ `docxToContainer`) |
|
|
322
|
+
| `@bendyline/squisq-formats/pdf` | PDF import/export (+ `pdfToContainer`, `configurePdfWorker`) |
|
|
323
|
+
| `@bendyline/squisq-formats/html` | Player HTML export, static plain-HTML export + bundles, HTML import |
|
|
324
|
+
| `@bendyline/squisq-formats/epub` | EPUB 3 e-book export (with optional Media Overlays) |
|
|
325
|
+
| `@bendyline/squisq-formats/pptx` | PPTX export + import (text/lists/tables + slide images) |
|
|
326
|
+
| `@bendyline/squisq-formats/xlsx` | XLSX import + tables-only export |
|
|
327
|
+
| `@bendyline/squisq-formats/csv` | CSV import/export (RFC 4180) |
|
|
328
|
+
| `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (package reader/writer, XML utilities, namespaces) |
|
|
329
|
+
| `@bendyline/squisq-formats/diagrams` | `rasterizeDiagrams` / `findDiagrams`: diagrams as pictures for document exports |
|
|
330
|
+
| `@bendyline/squisq-formats/container` | `ContentContainer` ↔ ZIP serialization |
|
|
331
|
+
| `@bendyline/squisq-formats/registry` | Format registry + programmatic `convert()` (also re-exported from the root) |
|
|
304
332
|
|
|
305
333
|
The package root re-exports the common converters; `./container`, the plain-HTML/bundle functions, `docxToContainer`, `pdfToContainer`, `PdfPageSize`, and the image utilities are subpath-only.
|
|
306
334
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
inferMimeType
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YVCUNZB5.js";
|
|
4
4
|
import {
|
|
5
5
|
RelIdAllocator,
|
|
6
6
|
inlineNodeToRuns,
|
|
@@ -8,9 +8,6 @@ import {
|
|
|
8
8
|
sanitizeOfficeHyperlink,
|
|
9
9
|
toOoxmlHex
|
|
10
10
|
} from "./chunk-A6N6IN3I.js";
|
|
11
|
-
import {
|
|
12
|
-
extToMime
|
|
13
|
-
} from "./chunk-B7GEAZBI.js";
|
|
14
11
|
import {
|
|
15
12
|
extractPlainText,
|
|
16
13
|
stripHtmlTags
|
|
@@ -59,6 +56,9 @@ import {
|
|
|
59
56
|
fontAwesomeFaces,
|
|
60
57
|
fontAwesomeGlyph
|
|
61
58
|
} from "./chunk-USU6HTKB.js";
|
|
59
|
+
import {
|
|
60
|
+
extToMime
|
|
61
|
+
} from "./chunk-RSU3V5IQ.js";
|
|
62
62
|
|
|
63
63
|
// src/pptx/export.ts
|
|
64
64
|
import { resolveFontFamily, VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
|
|
@@ -162,12 +162,19 @@ async function docToPptx(doc, options = {}) {
|
|
|
162
162
|
{ persistentLayers: doc.persistentLayers },
|
|
163
163
|
theme
|
|
164
164
|
);
|
|
165
|
+
const mediaAspectRatios = {};
|
|
166
|
+
for (const [source, picture] of effectiveOptions.diagramImages ?? []) {
|
|
167
|
+
if (picture.width > 0 && picture.height > 0) {
|
|
168
|
+
mediaAspectRatios[source] = picture.width / picture.height;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
165
171
|
const expanded = expandDocBlocks(flatBlocks, {
|
|
166
172
|
audioSegments,
|
|
167
173
|
viewport: VIEWPORT_PRESETS.landscape,
|
|
168
174
|
persistentLayers,
|
|
169
175
|
theme,
|
|
170
176
|
customTemplates: doc.customTemplates,
|
|
177
|
+
mediaAspectRatios,
|
|
171
178
|
// A .pptx is a discrete deck: the >20s pacing split would emit N identical
|
|
172
179
|
// copies of one authored slide (a long single block became 18 duplicates),
|
|
173
180
|
// and the <5s pacing merge would delete authored slides outright. Keep one
|
|
@@ -206,6 +213,7 @@ async function docToPptx(doc, options = {}) {
|
|
|
206
213
|
effectiveOptions.allowRelativeHyperlinks ?? false,
|
|
207
214
|
effectiveOptions.signal
|
|
208
215
|
);
|
|
216
|
+
ctx.diagramImages = effectiveOptions.diagramImages;
|
|
209
217
|
slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx));
|
|
210
218
|
slideContexts.push(ctx);
|
|
211
219
|
}
|
|
@@ -545,15 +553,46 @@ function convertLayerToShapes(layer, ctx, textBottom) {
|
|
|
545
553
|
)
|
|
546
554
|
];
|
|
547
555
|
}
|
|
548
|
-
case "mermaid":
|
|
556
|
+
case "mermaid": {
|
|
557
|
+
const picture = ctx.diagramImages?.get(layer.content.source.trim());
|
|
558
|
+
if (picture && picture.width > 0 && picture.height > 0) {
|
|
559
|
+
const data = picture.data instanceof Uint8Array ? picture.data.buffer.slice(
|
|
560
|
+
picture.data.byteOffset,
|
|
561
|
+
picture.data.byteOffset + picture.data.byteLength
|
|
562
|
+
) : picture.data;
|
|
563
|
+
const relId = ctx.addImage(
|
|
564
|
+
`diagram-${ctx.embeddedImages.length + 1}.png`,
|
|
565
|
+
data,
|
|
566
|
+
mermaidTitle(layer.content.source)
|
|
567
|
+
);
|
|
568
|
+
const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
|
|
569
|
+
const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
|
|
570
|
+
const scale = Math.min(area.width / picture.width, area.height / picture.height);
|
|
571
|
+
const width = Math.round(picture.width * scale);
|
|
572
|
+
const height = Math.round(picture.height * scale);
|
|
573
|
+
return [
|
|
574
|
+
buildImageShape(
|
|
575
|
+
embedded,
|
|
576
|
+
ctx.allocShapeId(),
|
|
577
|
+
area.x + Math.round((area.width - width) / 2),
|
|
578
|
+
area.y + Math.round((area.height - height) / 2),
|
|
579
|
+
width,
|
|
580
|
+
height
|
|
581
|
+
)
|
|
582
|
+
];
|
|
583
|
+
}
|
|
549
584
|
return [
|
|
550
585
|
buildLayerTextShape(
|
|
551
586
|
textLayerForPlaceholder(layer.position, layer.content.source, ctx),
|
|
552
587
|
ctx.allocShapeId()
|
|
553
588
|
)
|
|
554
589
|
];
|
|
590
|
+
}
|
|
555
591
|
}
|
|
556
592
|
}
|
|
593
|
+
function mermaidTitle(source) {
|
|
594
|
+
return /^\s*accTitle\s*:\s*(.+?)\s*$/mu.exec(source)?.[1] ?? "Diagram";
|
|
595
|
+
}
|
|
557
596
|
function resolvePositionValue(value, dimension) {
|
|
558
597
|
if (typeof value === "number") return value;
|
|
559
598
|
if (typeof value === "string") {
|
|
@@ -1075,7 +1114,7 @@ function buildBodyShapeCustom(bodyContent, top, height, style) {
|
|
|
1075
1114
|
return `<p:sp><p:nvSpPr><p:cNvPr id="3" name="Content"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="${BODY_LEFT}" y="${top}"/><a:ext cx="${BODY_WIDTH}" cy="${height}"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr>${defRPr}</a:lvl1pPr></a:lstStyle>` + bodyContent + `</p:txBody></p:sp>`;
|
|
1076
1115
|
}
|
|
1077
1116
|
function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight) {
|
|
1078
|
-
return `<p:pic><p:nvPicPr><p:cNvPr id="${shapeId}" name="${escapeXml(img.alt || "Picture")}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${img.relId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
|
|
1117
|
+
return `<p:pic><p:nvPicPr><p:cNvPr id="${shapeId}" name="${escapeXml(img.alt || "Picture")}"${img.alt ? ` descr="${escapeXml(img.alt)}"` : ""}/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${img.relId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
|
|
1079
1118
|
}
|
|
1080
1119
|
function convertBodyBlocks(nodes, ctx) {
|
|
1081
1120
|
const parts = [];
|
|
@@ -5,9 +5,6 @@ import {
|
|
|
5
5
|
normalizeOoxmlHex,
|
|
6
6
|
sanitizeOfficeHyperlink
|
|
7
7
|
} from "./chunk-A6N6IN3I.js";
|
|
8
|
-
import {
|
|
9
|
-
extToMime
|
|
10
|
-
} from "./chunk-B7GEAZBI.js";
|
|
11
8
|
import {
|
|
12
9
|
stripHtmlTags
|
|
13
10
|
} from "./chunk-AVOZAKGP.js";
|
|
@@ -62,6 +59,11 @@ import {
|
|
|
62
59
|
fontAwesomeGlyph,
|
|
63
60
|
obfuscateDocxFont
|
|
64
61
|
} from "./chunk-USU6HTKB.js";
|
|
62
|
+
import {
|
|
63
|
+
extToMime,
|
|
64
|
+
fitWithin,
|
|
65
|
+
readImageDimensions
|
|
66
|
+
} from "./chunk-RSU3V5IQ.js";
|
|
65
67
|
|
|
66
68
|
// src/docx/export.ts
|
|
67
69
|
import { resolveFontFamily } from "@bendyline/squisq/schemas";
|
|
@@ -526,59 +528,30 @@ function convertImage(node, ctx) {
|
|
|
526
528
|
const ext = contentType.split("/")[1]?.replace("jpeg", "jpg") || "png";
|
|
527
529
|
const filename = `image${ctx.images.length + 1}.${ext}`;
|
|
528
530
|
const { relId, docPrId } = ctx.addImage(data, contentType, filename);
|
|
529
|
-
const dims = readImageDimensions(data);
|
|
531
|
+
const dims = imageEntry.width && imageEntry.height ? { width: imageEntry.width, height: imageEntry.height } : readImageDimensions(data);
|
|
530
532
|
const EMU_PER_INCH = 914400;
|
|
531
533
|
const MAX_WIDTH_EMU = 6 * EMU_PER_INCH;
|
|
534
|
+
const MAX_HEIGHT_EMU = 8 * EMU_PER_INCH;
|
|
532
535
|
let cx;
|
|
533
536
|
let cy;
|
|
534
537
|
if (dims) {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
cy = Math.round(heightEmu);
|
|
544
|
-
}
|
|
538
|
+
const fitted = fitWithin(
|
|
539
|
+
dims.width / 96 * EMU_PER_INCH,
|
|
540
|
+
dims.height / 96 * EMU_PER_INCH,
|
|
541
|
+
MAX_WIDTH_EMU,
|
|
542
|
+
MAX_HEIGHT_EMU
|
|
543
|
+
);
|
|
544
|
+
cx = Math.round(fitted.width);
|
|
545
|
+
cy = Math.round(fitted.height);
|
|
545
546
|
} else {
|
|
546
547
|
cx = 5 * EMU_PER_INCH;
|
|
547
548
|
cy = 3 * EMU_PER_INCH;
|
|
548
549
|
}
|
|
549
550
|
const name = escapeXml(node.alt || filename);
|
|
551
|
+
const description = node.alt ? ` descr="${escapeXml(node.alt)}"` : "";
|
|
550
552
|
const NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
551
553
|
const NS_PIC = "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
|
552
|
-
return `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${docPrId}" name="${name}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${NS_A}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A}"><a:graphicData uri="${NS_PIC}"><pic:pic xmlns:pic="${NS_PIC}"><pic:nvPicPr><pic:cNvPr id="0" name="${name}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
|
|
553
|
-
}
|
|
554
|
-
function readImageDimensions(data) {
|
|
555
|
-
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
556
|
-
if (bytes.length < 24) return null;
|
|
557
|
-
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
558
|
-
const width = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
|
|
559
|
-
const height = bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23];
|
|
560
|
-
return { width, height };
|
|
561
|
-
}
|
|
562
|
-
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
563
|
-
let offset = 2;
|
|
564
|
-
while (offset < bytes.length - 9) {
|
|
565
|
-
if (bytes[offset] !== 255) break;
|
|
566
|
-
const marker = bytes[offset + 1];
|
|
567
|
-
if (marker === 192 || marker === 194) {
|
|
568
|
-
const height = bytes[offset + 5] << 8 | bytes[offset + 6];
|
|
569
|
-
const width = bytes[offset + 7] << 8 | bytes[offset + 8];
|
|
570
|
-
return { width, height };
|
|
571
|
-
}
|
|
572
|
-
const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
573
|
-
offset += 2 + segLen;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
|
|
577
|
-
const width = bytes[6] | bytes[7] << 8;
|
|
578
|
-
const height = bytes[8] | bytes[9] << 8;
|
|
579
|
-
return { width, height };
|
|
580
|
-
}
|
|
581
|
-
return null;
|
|
554
|
+
return `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${docPrId}" name="${name}"${description}/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${NS_A}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A}"><a:graphicData uri="${NS_PIC}"><pic:pic xmlns:pic="${NS_PIC}"><pic:nvPicPr><pic:cNvPr id="0" name="${name}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
|
|
582
555
|
}
|
|
583
556
|
function convertFootnoteRef(node, ctx) {
|
|
584
557
|
const fnId = ctx.getFootnoteId(node.identifier);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/shared/images.ts
|
|
2
|
+
var EXT_TO_MIME = {
|
|
3
|
+
// Raster / vector images
|
|
4
|
+
png: "image/png",
|
|
5
|
+
jpg: "image/jpeg",
|
|
6
|
+
jpeg: "image/jpeg",
|
|
7
|
+
gif: "image/gif",
|
|
8
|
+
webp: "image/webp",
|
|
9
|
+
bmp: "image/bmp",
|
|
10
|
+
svg: "image/svg+xml",
|
|
11
|
+
tiff: "image/tiff",
|
|
12
|
+
tif: "image/tiff",
|
|
13
|
+
ico: "image/x-icon",
|
|
14
|
+
avif: "image/avif",
|
|
15
|
+
// Office vector formats (docx/pptx embedded metafiles)
|
|
16
|
+
emf: "image/emf",
|
|
17
|
+
wmf: "image/wmf",
|
|
18
|
+
// Audio / video (html export inlines these as data URIs)
|
|
19
|
+
mp3: "audio/mpeg",
|
|
20
|
+
wav: "audio/wav",
|
|
21
|
+
ogg: "audio/ogg",
|
|
22
|
+
mp4: "video/mp4",
|
|
23
|
+
webm: "video/webm",
|
|
24
|
+
// Sidecar data files (`{[dataTable src=…]}` references)
|
|
25
|
+
csv: "text/csv",
|
|
26
|
+
tsv: "text/tab-separated-values",
|
|
27
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
28
|
+
parquet: "application/vnd.apache.parquet"
|
|
29
|
+
};
|
|
30
|
+
function extToMime(ext) {
|
|
31
|
+
const key = ext.replace(/^\./, "").toLowerCase();
|
|
32
|
+
return EXT_TO_MIME[key] ?? "application/octet-stream";
|
|
33
|
+
}
|
|
34
|
+
function readImageDimensions(data) {
|
|
35
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
36
|
+
if (bytes.length < 24) return null;
|
|
37
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
38
|
+
const width = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
|
|
39
|
+
const height = bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23];
|
|
40
|
+
return { width, height };
|
|
41
|
+
}
|
|
42
|
+
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
43
|
+
let offset = 2;
|
|
44
|
+
while (offset < bytes.length - 9) {
|
|
45
|
+
if (bytes[offset] !== 255) break;
|
|
46
|
+
const marker = bytes[offset + 1];
|
|
47
|
+
if (marker === 192 || marker === 194) {
|
|
48
|
+
const height = bytes[offset + 5] << 8 | bytes[offset + 6];
|
|
49
|
+
const width = bytes[offset + 7] << 8 | bytes[offset + 8];
|
|
50
|
+
return { width, height };
|
|
51
|
+
}
|
|
52
|
+
const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
53
|
+
offset += 2 + segLen;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
|
|
57
|
+
const width = bytes[6] | bytes[7] << 8;
|
|
58
|
+
const height = bytes[8] | bytes[9] << 8;
|
|
59
|
+
return { width, height };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function fitWithin(width, height, maxWidth, maxHeight) {
|
|
64
|
+
if (width <= 0 || height <= 0) return { width: 0, height: 0 };
|
|
65
|
+
const scale = Math.min(1, maxWidth / width, maxHeight / height);
|
|
66
|
+
return { width: width * scale, height: height * scale };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
extToMime,
|
|
71
|
+
readImageDimensions,
|
|
72
|
+
fitWithin
|
|
73
|
+
};
|
|
@@ -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-
|
|
265
|
+
const { extToMime } = await import("./images-CAWESWRF.js");
|
|
266
266
|
return new Map(
|
|
267
267
|
[...images].map(([path, data]) => [
|
|
268
268
|
path,
|
|
@@ -382,11 +382,14 @@ function defaultFormats() {
|
|
|
382
382
|
const raw = optionsFor(options, "pdf");
|
|
383
383
|
const markdownDoc = await markdownOf(input);
|
|
384
384
|
const pdfWarnings = [];
|
|
385
|
+
const containerImages = await collectContainerDocxImages(input.container, options.signal);
|
|
386
|
+
const images = new Map([...containerImages, ...raw.images ?? /* @__PURE__ */ new Map()]);
|
|
385
387
|
const buf = await markdownDocToPdf(markdownDoc, {
|
|
386
388
|
...raw,
|
|
387
389
|
...options.title !== void 0 ? { title: options.title } : {},
|
|
388
390
|
themeId: resolveThemeId(input, options),
|
|
389
391
|
themeRegistry: options.themeRegistry ?? raw.themeRegistry,
|
|
392
|
+
images,
|
|
390
393
|
onWarning: (message) => pdfWarnings.push(message)
|
|
391
394
|
});
|
|
392
395
|
return ok(await toBytes(buf), MIME.pdf, [
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
fontAwesomeFaces,
|
|
10
10
|
fontAwesomeGlyph
|
|
11
11
|
} from "./chunk-USU6HTKB.js";
|
|
12
|
+
import {
|
|
13
|
+
fitWithin,
|
|
14
|
+
readImageDimensions
|
|
15
|
+
} from "./chunk-RSU3V5IQ.js";
|
|
12
16
|
|
|
13
17
|
// src/pdf/export.ts
|
|
14
18
|
import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
|
|
@@ -391,6 +395,7 @@ async function markdownDocToPdf(doc, options = {}) {
|
|
|
391
395
|
pdfDoc.setCreationDate(/* @__PURE__ */ new Date());
|
|
392
396
|
pdfDoc.setModificationDate(/* @__PURE__ */ new Date());
|
|
393
397
|
const ctx = await createExportContext(pdfDoc, options, doc);
|
|
398
|
+
ctx.images = await embedDocumentImages(pdfDoc, doc, options.images);
|
|
394
399
|
renderBlocks(doc.children, ctx, 0);
|
|
395
400
|
const warning = ctx.text.warning();
|
|
396
401
|
if (warning) {
|
|
@@ -404,6 +409,39 @@ async function docToPdf(doc, options = {}) {
|
|
|
404
409
|
const markdownDoc = docToMarkdown(doc);
|
|
405
410
|
return markdownDocToPdf(markdownDoc, options);
|
|
406
411
|
}
|
|
412
|
+
var POINTS_PER_CSS_PIXEL = 0.75;
|
|
413
|
+
function collectImageUrls(nodes, into) {
|
|
414
|
+
for (const node of nodes) {
|
|
415
|
+
if (node.type === "image") into.add(node.url);
|
|
416
|
+
const children = node.children;
|
|
417
|
+
if (Array.isArray(children)) collectImageUrls(children, into);
|
|
418
|
+
}
|
|
419
|
+
return into;
|
|
420
|
+
}
|
|
421
|
+
async function embedDocumentImages(pdfDoc, doc, images) {
|
|
422
|
+
const embedded = /* @__PURE__ */ new Map();
|
|
423
|
+
if (!images || images.size === 0) return embedded;
|
|
424
|
+
for (const url of collectImageUrls(doc.children, /* @__PURE__ */ new Set())) {
|
|
425
|
+
const entry = images.get(url);
|
|
426
|
+
if (!entry) continue;
|
|
427
|
+
const bytes = entry.data instanceof Uint8Array ? entry.data : new Uint8Array(entry.data);
|
|
428
|
+
const isPng = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
429
|
+
const isJpeg = bytes[0] === 255 && bytes[1] === 216;
|
|
430
|
+
if (!isPng && !isJpeg) continue;
|
|
431
|
+
try {
|
|
432
|
+
const image = isPng ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes);
|
|
433
|
+
const pixels = readImageDimensions(bytes) ?? { width: image.width, height: image.height };
|
|
434
|
+
const sized = Boolean(entry.width && entry.height);
|
|
435
|
+
embedded.set(url, {
|
|
436
|
+
image,
|
|
437
|
+
width: sized ? entry.width : pixels.width,
|
|
438
|
+
height: sized ? entry.height : pixels.height
|
|
439
|
+
});
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return embedded;
|
|
444
|
+
}
|
|
407
445
|
async function createExportContext(pdfDoc, options, doc) {
|
|
408
446
|
const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
|
|
409
447
|
pdfDoc.embedFont(StandardFonts.Helvetica),
|
|
@@ -455,7 +493,8 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
455
493
|
colors: { text: colorText, heading: colorHeading, link: colorLink },
|
|
456
494
|
footnotes: new FootnoteIndex(doc),
|
|
457
495
|
text: new WinAnsiTracker(),
|
|
458
|
-
signal: options.signal
|
|
496
|
+
signal: options.signal,
|
|
497
|
+
images: /* @__PURE__ */ new Map()
|
|
459
498
|
};
|
|
460
499
|
}
|
|
461
500
|
function ensureSpace(ctx, needed) {
|
|
@@ -812,6 +851,7 @@ function renderHeading(node, ctx, extraIndent) {
|
|
|
812
851
|
function renderParagraph(node, ctx, extraIndent, colorOverride) {
|
|
813
852
|
const x0 = ctx.margin + extraIndent;
|
|
814
853
|
const w = ctx.contentWidth - extraIndent;
|
|
854
|
+
if (renderImageParagraph(node, ctx, x0, w)) return;
|
|
815
855
|
const spans = flattenInlines(node.children, ctx, {
|
|
816
856
|
bold: false,
|
|
817
857
|
italic: false,
|
|
@@ -821,6 +861,38 @@ function renderParagraph(node, ctx, extraIndent, colorOverride) {
|
|
|
821
861
|
drawSpans(spans, ctx, w, x0);
|
|
822
862
|
ctx.y -= PARAGRAPH_SPACING;
|
|
823
863
|
}
|
|
864
|
+
function renderImageParagraph(node, ctx, x0, width) {
|
|
865
|
+
const images = node.children.filter((child) => child.type === "image");
|
|
866
|
+
const onlyImages = node.children.every(
|
|
867
|
+
(child) => child.type === "image" || child.type === "break" || child.type === "text" && !child.value.trim()
|
|
868
|
+
);
|
|
869
|
+
if (!onlyImages || images.length === 0) return false;
|
|
870
|
+
if (!images.some((image) => ctx.images.has(image.url))) return false;
|
|
871
|
+
const maxHeight = ctx.pageHeight - 2 * ctx.margin;
|
|
872
|
+
for (const imageNode of images) {
|
|
873
|
+
const embedded = ctx.images.get(imageNode.url);
|
|
874
|
+
if (!embedded) {
|
|
875
|
+
const note = flattenInlines([imageNode], ctx, { bold: false, italic: false, code: false });
|
|
876
|
+
drawSpans(note, ctx, width, x0);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const size = fitWithin(
|
|
880
|
+
embedded.width * POINTS_PER_CSS_PIXEL,
|
|
881
|
+
embedded.height * POINTS_PER_CSS_PIXEL,
|
|
882
|
+
width,
|
|
883
|
+
maxHeight
|
|
884
|
+
);
|
|
885
|
+
ensureSpace(ctx, size.height);
|
|
886
|
+
ctx.page.drawImage(embedded.image, {
|
|
887
|
+
x: x0 + (width - size.width) / 2,
|
|
888
|
+
y: ctx.y - size.height,
|
|
889
|
+
width: size.width,
|
|
890
|
+
height: size.height
|
|
891
|
+
});
|
|
892
|
+
ctx.y -= size.height + PARAGRAPH_SPACING;
|
|
893
|
+
}
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
824
896
|
function renderBlockquote(node, ctx, extraIndent) {
|
|
825
897
|
const barX = ctx.margin + extraIndent + 4;
|
|
826
898
|
const indent = extraIndent + BLOCKQUOTE_INDENT;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MarkdownDocument, MarkdownCodeBlock } from '@bendyline/squisq/markdown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Diagrams as pictures for document exports.
|
|
5
|
+
*
|
|
6
|
+
* DOCX, EPUB and PDF have no Mermaid or Squisq renderer, so a diagram would
|
|
7
|
+
* export as its source text. `rasterizeDiagrams` finds every diagram in a
|
|
8
|
+
* MarkdownDocument, asks a host-supplied renderer for a picture of it, and
|
|
9
|
+
* returns the document with each pictured diagram swapped for an image plus
|
|
10
|
+
* the picture bytes, ready for the exporters' `images` option. The renderer
|
|
11
|
+
* is supplied by the host because drawing needs a browser (Mermaid lays out
|
|
12
|
+
* text with the DOM); this module is environment-neutral.
|
|
13
|
+
*
|
|
14
|
+
* Diagrams are recognized the way the player recognizes them:
|
|
15
|
+
* - ```mermaid fences;
|
|
16
|
+
* - ASCII-art ```diagram, ```tree, and ```timeline fences (also unlabeled or
|
|
17
|
+
* `text` fences whose content the detectors accept);
|
|
18
|
+
* - heading blocks with a container template — `{[drawing]}`, `{[layout]}`,
|
|
19
|
+
* `{[diagram]}` — together with every child heading they own.
|
|
20
|
+
*
|
|
21
|
+
* `docToPptx` keeps drawings, timelines and ASCII diagrams as native shapes;
|
|
22
|
+
* for it, use the returned `mermaid` map as its `diagramImages` option.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** What kind of diagram a picture is wanted for. */
|
|
26
|
+
type ExportDiagramKind = 'mermaid' | 'diagram' | 'timeline' | 'tree' | 'container';
|
|
27
|
+
interface ExportDiagram {
|
|
28
|
+
readonly kind: ExportDiagramKind;
|
|
29
|
+
/**
|
|
30
|
+
* The diagram's own markdown: its fence, or for a container its heading
|
|
31
|
+
* and every child heading and body it owns.
|
|
32
|
+
*/
|
|
33
|
+
readonly markdown: string;
|
|
34
|
+
/** The fence content for fence kinds; '' for containers. */
|
|
35
|
+
readonly source: string;
|
|
36
|
+
/** The container's template (`drawing`, `layout`, `diagram`); '' for fences. */
|
|
37
|
+
readonly template: string;
|
|
38
|
+
/** A text alternative for the picture. */
|
|
39
|
+
readonly alt: string;
|
|
40
|
+
}
|
|
41
|
+
/** A PNG picture of a diagram, with the size it should display at. */
|
|
42
|
+
interface DiagramPicture {
|
|
43
|
+
readonly data: ArrayBuffer | Uint8Array;
|
|
44
|
+
/** Display size in CSS pixels (96 per inch); the PNG may be denser. */
|
|
45
|
+
readonly width: number;
|
|
46
|
+
readonly height: number;
|
|
47
|
+
}
|
|
48
|
+
/** Draws one diagram; resolves to null to leave that diagram as source. */
|
|
49
|
+
type DiagramRasterizer = (diagram: ExportDiagram) => Promise<DiagramPicture | null>;
|
|
50
|
+
interface RasterizeDiagramsOptions {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/** Limit the kinds pictured. Default: all of them. */
|
|
53
|
+
kinds?: readonly ExportDiagramKind[];
|
|
54
|
+
/** Prefix for the image URLs written into the document. Default: `diagram`. */
|
|
55
|
+
urlPrefix?: string;
|
|
56
|
+
/** Receives a note for each diagram the renderer could not draw. */
|
|
57
|
+
onWarning?: (message: string) => void;
|
|
58
|
+
}
|
|
59
|
+
interface RasterizedDiagrams {
|
|
60
|
+
/** The document with each pictured diagram replaced by an image of it. */
|
|
61
|
+
readonly markdownDoc: MarkdownDocument;
|
|
62
|
+
/**
|
|
63
|
+
* Picture bytes keyed by the image URL written into `markdownDoc`, in the
|
|
64
|
+
* shape the DOCX and PDF exporters take; EPUB takes `data` alone.
|
|
65
|
+
*/
|
|
66
|
+
readonly images: Map<string, {
|
|
67
|
+
data: Uint8Array;
|
|
68
|
+
contentType: 'image/png';
|
|
69
|
+
width: number;
|
|
70
|
+
height: number;
|
|
71
|
+
}>;
|
|
72
|
+
/** Pictures of Mermaid diagrams keyed by trimmed source, for `docToPptx`'s `diagramImages`. */
|
|
73
|
+
readonly mermaid: Map<string, DiagramPicture>;
|
|
74
|
+
}
|
|
75
|
+
/** The diagram kind of a fence, or null for ordinary code. */
|
|
76
|
+
declare function fenceDiagramKind(node: MarkdownCodeBlock): ExportDiagramKind | null;
|
|
77
|
+
/** Every diagram in `doc`, in document order. */
|
|
78
|
+
declare function findDiagrams(doc: MarkdownDocument): ExportDiagram[];
|
|
79
|
+
/**
|
|
80
|
+
* Picture every diagram in `doc` with `render`, one at a time, and return
|
|
81
|
+
* the document with each pictured diagram replaced by an image of it. A
|
|
82
|
+
* diagram the renderer declines or fails on stays as it was.
|
|
83
|
+
*/
|
|
84
|
+
declare function rasterizeDiagrams(doc: MarkdownDocument, render: DiagramRasterizer, options?: RasterizeDiagramsOptions): Promise<RasterizedDiagrams>;
|
|
85
|
+
|
|
86
|
+
export { type DiagramPicture, type DiagramRasterizer, type ExportDiagram, type ExportDiagramKind, type RasterizeDiagramsOptions, type RasterizedDiagrams, fenceDiagramKind, findDiagrams, rasterizeDiagrams };
|