@bendyline/squisq-formats 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -43
- package/dist/{chunk-CRTC6DEX.js → chunk-46FMDOWZ.js} +2 -2
- package/dist/{chunk-ON232HJR.js → chunk-5LSSWZPU.js} +2 -2
- package/dist/{chunk-APSIAEXL.js → chunk-D7IFPWNZ.js} +87 -43
- package/dist/chunk-D7IFPWNZ.js.map +1 -0
- package/dist/chunk-DQAZR57U.js +33 -0
- package/dist/chunk-DQAZR57U.js.map +1 -0
- package/dist/chunk-EHLGMSTE.js +273 -0
- package/dist/chunk-EHLGMSTE.js.map +1 -0
- package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
- package/dist/chunk-MJGRI6XR.js.map +1 -0
- package/dist/{chunk-JBNVE2GV.js → chunk-RFAPOKHJ.js} +9 -2
- package/dist/chunk-RFAPOKHJ.js.map +1 -0
- package/dist/chunk-RTS5XBZ7.js +526 -0
- package/dist/chunk-RTS5XBZ7.js.map +1 -0
- package/dist/{chunk-FMOIGA5T.js → chunk-SHLPSADL.js} +178 -5
- package/dist/chunk-SHLPSADL.js.map +1 -0
- package/dist/chunk-SSUPBUF5.js +15 -0
- package/dist/chunk-SSUPBUF5.js.map +1 -0
- package/dist/{chunk-BJQIJ4S4.js → chunk-WWBIKRNP.js} +17 -41
- package/dist/chunk-WWBIKRNP.js.map +1 -0
- package/dist/{chunk-I2GFTEOV.js → chunk-YESS7VY3.js} +77 -15
- package/dist/{chunk-I2GFTEOV.js.map → chunk-YESS7VY3.js.map} +1 -1
- package/dist/csv/index.d.ts +14 -1
- package/dist/csv/index.js +1 -1
- package/dist/docx/index.js +4 -3
- package/dist/epub/index.js +3 -2
- package/dist/html/index.d.ts +3 -0
- package/dist/html/index.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -11
- package/dist/ooxml/index.d.ts +5 -1
- package/dist/ooxml/index.js +16 -12
- package/dist/pdf/index.js +2 -1
- package/dist/pptx/index.d.ts +18 -3
- package/dist/pptx/index.js +7 -4
- package/dist/registry/index.d.ts +167 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/xlsx/index.d.ts +44 -14
- package/dist/xlsx/index.js +4 -2
- package/package.json +7 -2
- package/src/__tests__/convert.test.ts +186 -0
- package/src/__tests__/csvImport.test.ts +34 -1
- package/src/__tests__/formatRegistry.test.ts +174 -0
- package/src/__tests__/lossyWarnings.test.ts +105 -0
- package/src/__tests__/pdfImport.test.ts +95 -1
- package/src/__tests__/pptxImport.test.ts +46 -1
- package/src/__tests__/roundTripAssets.test.ts +50 -0
- package/src/__tests__/roundTripMatrix.test.ts +10 -4
- package/src/__tests__/sharedContainer.test.ts +41 -0
- package/src/__tests__/sharedImages.test.ts +61 -0
- package/src/__tests__/xlsxExport.test.ts +164 -0
- package/src/csv/index.ts +25 -2
- package/src/docx/import.ts +6 -37
- package/src/html/imageUtils.ts +5 -19
- package/src/index.ts +29 -5
- package/src/ooxml/index.ts +3 -0
- package/src/ooxml/namespaces.ts +10 -0
- package/src/pdf/import.ts +145 -50
- package/src/pptx/import.ts +127 -10
- package/src/pptx/index.ts +1 -1
- package/src/registry/convert.ts +318 -0
- package/src/registry/defaultFormats.ts +323 -0
- package/src/registry/errors.ts +46 -0
- package/src/registry/index.ts +38 -0
- package/src/registry/registry.ts +48 -0
- package/src/registry/types.ts +107 -0
- package/src/shared/container.ts +28 -0
- package/src/shared/images.ts +44 -0
- package/src/xlsx/export.ts +253 -0
- package/src/xlsx/index.ts +10 -36
- package/dist/chunk-A3FHLTY5.js.map +0 -1
- package/dist/chunk-APSIAEXL.js.map +0 -1
- package/dist/chunk-BJQIJ4S4.js.map +0 -1
- package/dist/chunk-FMOIGA5T.js.map +0 -1
- package/dist/chunk-JBNVE2GV.js.map +0 -1
- package/dist/chunk-L53YIGYS.js +0 -141
- package/dist/chunk-L53YIGYS.js.map +0 -1
- package/dist/chunk-QSE7EWE7.js +0 -181
- package/dist/chunk-QSE7EWE7.js.map +0 -1
- /package/dist/{chunk-CRTC6DEX.js.map → chunk-46FMDOWZ.js.map} +0 -0
- /package/dist/{chunk-ON232HJR.js.map → chunk-5LSSWZPU.js.map} +0 -0
package/src/pdf/import.ts
CHANGED
|
@@ -44,8 +44,8 @@ import type {
|
|
|
44
44
|
MarkdownImage,
|
|
45
45
|
} from '@bendyline/squisq/markdown';
|
|
46
46
|
|
|
47
|
-
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
48
47
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
48
|
+
import { buildContainer } from '../shared/container.js';
|
|
49
49
|
|
|
50
50
|
import {
|
|
51
51
|
DEFAULT_FONT_SIZE,
|
|
@@ -160,30 +160,44 @@ export async function pdfToContainer(
|
|
|
160
160
|
const textLines = await extractTextLines(bytes);
|
|
161
161
|
const images = await extractImages(bytes);
|
|
162
162
|
|
|
163
|
+
// Under Node there is no DOM/canvas, so extractImages() returns [] via its
|
|
164
|
+
// environment guard and embedded images can't be decoded. The registry's
|
|
165
|
+
// `importContainer` wrapper surfaces this (typeof document === 'undefined')
|
|
166
|
+
// as a ConversionResult warning; direct callers of pdfToContainer get no
|
|
167
|
+
// signal, which is an accepted trade-off for the degraded Node path.
|
|
168
|
+
|
|
163
169
|
const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
|
|
164
170
|
|
|
165
|
-
//
|
|
166
|
-
|
|
171
|
+
// Classify text into blocks, capturing the originating page of each block
|
|
172
|
+
// (parallel array) so images can be placed on their own page rather than
|
|
173
|
+
// dumped at the document end.
|
|
174
|
+
const blockPages: number[] = [];
|
|
175
|
+
let blocks = classifyLines(textLines, bodySize, options, blockPages);
|
|
167
176
|
if (images.length > 0) {
|
|
168
|
-
blocks = insertImageBlocks(blocks, images);
|
|
177
|
+
blocks = insertImageBlocks(blocks, blockPages, images);
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
|
|
172
181
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await container.writeFile(img.path, new Uint8Array(img.data), 'image/png');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return container;
|
|
182
|
+
// pdf image extraction only ever produces PNG (canvas re-encode), so every
|
|
183
|
+
// entry gets image/png.
|
|
184
|
+
return buildContainer(
|
|
185
|
+
stringifyMarkdown(markdownDoc),
|
|
186
|
+
images.map((img) => [img.path, { data: img.data, mimeType: 'image/png' }] as const),
|
|
187
|
+
);
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Extracted image with position info for placement.
|
|
192
|
+
*
|
|
193
|
+
* `page` is captured reliably from the operator-list walk. `y` is currently
|
|
194
|
+
* always 0 — recovering a real y within the page would require tracking the
|
|
195
|
+
* current transformation matrix (CTM) during the paint operator, which is out
|
|
196
|
+
* of scope here (see the placement note in `insertImageBlocks`).
|
|
197
|
+
*
|
|
198
|
+
* Exported for direct unit testing of `insertImageBlocks`.
|
|
199
|
+
*/
|
|
200
|
+
export interface ExtractedImage {
|
|
187
201
|
path: string;
|
|
188
202
|
data: ArrayBuffer;
|
|
189
203
|
page: number;
|
|
@@ -328,29 +342,86 @@ function imageDataToPng(img: PdfjsImageData): ArrayBuffer | null {
|
|
|
328
342
|
}
|
|
329
343
|
}
|
|
330
344
|
|
|
345
|
+
/** Wrap an extracted image as a paragraph containing an image node. */
|
|
346
|
+
function imageParagraph(img: ExtractedImage): MarkdownParagraph {
|
|
347
|
+
const imgNode: MarkdownImage = {
|
|
348
|
+
type: 'image',
|
|
349
|
+
url: img.path,
|
|
350
|
+
alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
|
|
351
|
+
};
|
|
352
|
+
return { type: 'paragraph', children: [imgNode] };
|
|
353
|
+
}
|
|
354
|
+
|
|
331
355
|
/**
|
|
332
|
-
* Insert image reference blocks
|
|
333
|
-
*
|
|
356
|
+
* Insert image reference blocks among the text blocks, page by page.
|
|
357
|
+
*
|
|
358
|
+
* Each image is placed immediately after the LAST content block that
|
|
359
|
+
* originated from the same page (per the parallel `blockPages` array).
|
|
360
|
+
* Images on a page that produced no text blocks (e.g. an image-only page)
|
|
361
|
+
* fall back to the last block of the nearest preceding page that did; if no
|
|
362
|
+
* such page exists, they are appended at the document end.
|
|
363
|
+
*
|
|
364
|
+
* Placement is intentionally page-level only. Ordering *within* a page follows
|
|
365
|
+
* the extraction order of the images (roughly the paint-operator order) — real
|
|
366
|
+
* vertical (y) ordering within a page is future work, since `ExtractedImage.y`
|
|
367
|
+
* is not yet populated (it needs CTM tracking during the paint operator).
|
|
368
|
+
*
|
|
369
|
+
* @param blocks - The classified content blocks, in document order.
|
|
370
|
+
* @param blockPages - Parallel array: `blockPages[i]` is the 0-based page that
|
|
371
|
+
* `blocks[i]` came from. Must be the same length as `blocks`.
|
|
372
|
+
* @param images - Extracted images with a reliable `page` field.
|
|
334
373
|
*/
|
|
335
|
-
function insertImageBlocks(
|
|
374
|
+
export function insertImageBlocks(
|
|
336
375
|
blocks: MarkdownBlockNode[],
|
|
376
|
+
blockPages: number[],
|
|
337
377
|
images: ExtractedImage[],
|
|
338
378
|
): MarkdownBlockNode[] {
|
|
339
379
|
if (images.length === 0) return blocks;
|
|
340
380
|
|
|
341
|
-
//
|
|
342
|
-
|
|
381
|
+
// No text blocks at all → every image simply appends in order.
|
|
382
|
+
if (blocks.length === 0) {
|
|
383
|
+
return images.map(imageParagraph);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Map each page → index of its LAST block, and remember which pages have
|
|
387
|
+
// blocks (sorted ascending) for the image-only-page fallback lookup.
|
|
388
|
+
const lastBlockIndexByPage = new Map<number, number>();
|
|
389
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
390
|
+
lastBlockIndexByPage.set(blockPages[i], i);
|
|
391
|
+
}
|
|
392
|
+
const pagesWithBlocks = [...lastBlockIndexByPage.keys()].sort((a, b) => a - b);
|
|
393
|
+
|
|
394
|
+
const lastIndex = blocks.length - 1;
|
|
395
|
+
|
|
396
|
+
/** Resolve the block index after which an image on `page` should be inserted. */
|
|
397
|
+
const anchorFor = (page: number): number => {
|
|
398
|
+
const direct = lastBlockIndexByPage.get(page);
|
|
399
|
+
if (direct !== undefined) return direct;
|
|
400
|
+
// Image-only page: fall back to the nearest preceding page with blocks.
|
|
401
|
+
let anchor = -1;
|
|
402
|
+
for (const p of pagesWithBlocks) {
|
|
403
|
+
if (p < page) anchor = lastBlockIndexByPage.get(p)!;
|
|
404
|
+
else break;
|
|
405
|
+
}
|
|
406
|
+
// No preceding page with blocks → append at the document end.
|
|
407
|
+
return anchor === -1 ? lastIndex : anchor;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Group image paragraphs by the block index they should follow, preserving
|
|
411
|
+
// image order within each group.
|
|
412
|
+
const insertAfter = new Map<number, MarkdownParagraph[]>();
|
|
343
413
|
for (const img of images) {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
414
|
+
const anchor = anchorFor(img.page);
|
|
415
|
+
const group = insertAfter.get(anchor);
|
|
416
|
+
if (group) group.push(imageParagraph(img));
|
|
417
|
+
else insertAfter.set(anchor, [imageParagraph(img)]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const result: MarkdownBlockNode[] = [];
|
|
421
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
422
|
+
result.push(blocks[i]);
|
|
423
|
+
const imgs = insertAfter.get(i);
|
|
424
|
+
if (imgs) result.push(...imgs);
|
|
354
425
|
}
|
|
355
426
|
return result;
|
|
356
427
|
}
|
|
@@ -592,8 +663,20 @@ function classifyLines(
|
|
|
592
663
|
lines: TextLine[],
|
|
593
664
|
bodySize: number,
|
|
594
665
|
options: PdfImportOptions,
|
|
666
|
+
/**
|
|
667
|
+
* Optional out-parameter: when provided, receives one entry per produced
|
|
668
|
+
* block giving the 0-based page that block originated from. Kept parallel
|
|
669
|
+
* to the returned block array so image placement can be page-aware without
|
|
670
|
+
* mutating the (strictly-typed) block nodes themselves.
|
|
671
|
+
*/
|
|
672
|
+
blockPages?: number[],
|
|
595
673
|
): MarkdownBlockNode[] {
|
|
596
674
|
const blocks: MarkdownBlockNode[] = [];
|
|
675
|
+
/** Push a block and record its originating page in the parallel array. */
|
|
676
|
+
const pushBlock = (block: MarkdownBlockNode, page: number): void => {
|
|
677
|
+
blocks.push(block);
|
|
678
|
+
if (blockPages) blockPages.push(page);
|
|
679
|
+
};
|
|
597
680
|
const detectTables = options.detectTables !== false;
|
|
598
681
|
const detectCodeBlocks = options.detectCodeBlocks !== false;
|
|
599
682
|
const detectBlockquotes = options.detectBlockquotes !== false;
|
|
@@ -610,11 +693,14 @@ function classifyLines(
|
|
|
610
693
|
// --- Heading detection ---
|
|
611
694
|
if (line.fontSize >= IMPORT_HEADING_MIN_SIZE && line.fontSize > bodySize + 1) {
|
|
612
695
|
const depth = sizeToHeadingDepth(line.fontSize);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
696
|
+
pushBlock(
|
|
697
|
+
{
|
|
698
|
+
type: 'heading',
|
|
699
|
+
depth,
|
|
700
|
+
children: buildInlineNodes(line, options),
|
|
701
|
+
} as MarkdownHeading,
|
|
702
|
+
line.page,
|
|
703
|
+
);
|
|
618
704
|
i++;
|
|
619
705
|
continue;
|
|
620
706
|
}
|
|
@@ -626,10 +712,13 @@ function classifyLines(
|
|
|
626
712
|
codeLines.push(lines[i].text);
|
|
627
713
|
i++;
|
|
628
714
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
715
|
+
pushBlock(
|
|
716
|
+
{
|
|
717
|
+
type: 'code',
|
|
718
|
+
value: codeLines.join('\n'),
|
|
719
|
+
} as MarkdownCodeBlock,
|
|
720
|
+
line.page,
|
|
721
|
+
);
|
|
633
722
|
continue;
|
|
634
723
|
}
|
|
635
724
|
|
|
@@ -639,7 +728,7 @@ function classifyLines(
|
|
|
639
728
|
if (tableLines > 0) {
|
|
640
729
|
const table = buildTable(lines.slice(i, i + tableLines), options);
|
|
641
730
|
if (table) {
|
|
642
|
-
|
|
731
|
+
pushBlock(table, line.page);
|
|
643
732
|
i += tableLines;
|
|
644
733
|
continue;
|
|
645
734
|
}
|
|
@@ -651,7 +740,7 @@ function classifyLines(
|
|
|
651
740
|
const orderedMatch = line.text.match(IMPORT_ORDERED_PREFIX);
|
|
652
741
|
if (bulletMatch || orderedMatch) {
|
|
653
742
|
const listResult = consumeList(lines, i, typicalLeftMargin, bodySize, options);
|
|
654
|
-
|
|
743
|
+
pushBlock(listResult.list, line.page);
|
|
655
744
|
i = listResult.nextIndex;
|
|
656
745
|
continue;
|
|
657
746
|
}
|
|
@@ -675,10 +764,13 @@ function classifyLines(
|
|
|
675
764
|
children: buildInlineNodes(ql, options),
|
|
676
765
|
}) as MarkdownParagraph,
|
|
677
766
|
);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
767
|
+
pushBlock(
|
|
768
|
+
{
|
|
769
|
+
type: 'blockquote',
|
|
770
|
+
children: quoteBlocks,
|
|
771
|
+
} as MarkdownBlockquote,
|
|
772
|
+
line.page,
|
|
773
|
+
);
|
|
682
774
|
continue;
|
|
683
775
|
}
|
|
684
776
|
|
|
@@ -721,10 +813,13 @@ function classifyLines(
|
|
|
721
813
|
}
|
|
722
814
|
|
|
723
815
|
if (allInlines.length > 0) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
816
|
+
pushBlock(
|
|
817
|
+
{
|
|
818
|
+
type: 'paragraph',
|
|
819
|
+
children: mergeAdjacentText(allInlines),
|
|
820
|
+
} as MarkdownParagraph,
|
|
821
|
+
line.page,
|
|
822
|
+
);
|
|
728
823
|
}
|
|
729
824
|
}
|
|
730
825
|
|
package/src/pptx/import.ts
CHANGED
|
@@ -12,20 +12,42 @@
|
|
|
12
12
|
import type {
|
|
13
13
|
MarkdownBlockNode,
|
|
14
14
|
MarkdownDocument,
|
|
15
|
+
MarkdownImage,
|
|
15
16
|
MarkdownListItem,
|
|
16
17
|
MarkdownTable,
|
|
17
18
|
MarkdownTableCell,
|
|
18
19
|
MarkdownTableRow,
|
|
19
20
|
} from '@bendyline/squisq/markdown';
|
|
20
|
-
import {
|
|
21
|
+
import { stringifyMarkdown } from '@bendyline/squisq/markdown';
|
|
22
|
+
import { getPartBinary, getPartRelationships, getPartXml, openPackage } from '../ooxml/reader.js';
|
|
21
23
|
import type { OoxmlPackage } from '../ooxml/types.js';
|
|
22
24
|
import { NS_DRAWINGML, NS_PML, NS_R } from '../ooxml/namespaces.js';
|
|
25
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
26
|
+
import { buildContainer } from '../shared/container.js';
|
|
27
|
+
import { extToMime } from '../shared/images.js';
|
|
23
28
|
|
|
24
29
|
export interface PptxImportOptions {
|
|
25
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Whether to extract embedded slide images into the document as image nodes
|
|
32
|
+
* (referencing `images/imageN.ext`). When false, pictures are ignored so the
|
|
33
|
+
* markdown never carries dangling image references with no backing container.
|
|
34
|
+
* `pptxToContainer` forces this on. Default: false.
|
|
35
|
+
*/
|
|
26
36
|
extractImages?: boolean;
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Per-import mutable state used to collect embedded images across slides.
|
|
41
|
+
* Mirrors the docx import context's `extractedImages` / `imageCounter`.
|
|
42
|
+
*/
|
|
43
|
+
interface ImportContext {
|
|
44
|
+
pkg: OoxmlPackage;
|
|
45
|
+
extractImages: boolean;
|
|
46
|
+
/** Collected image files: `images/imageN.ext` → { data, mimeType } */
|
|
47
|
+
extractedImages: Map<string, { data: ArrayBuffer; mimeType: string }>;
|
|
48
|
+
imageCounter: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
function attrNS(el: Element, ns: string, local: string, fallback: string): string | null {
|
|
30
52
|
return el.getAttributeNS(ns, local) ?? el.getAttribute(fallback);
|
|
31
53
|
}
|
|
@@ -92,11 +114,66 @@ function tableToMarkdown(tbl: Element): MarkdownTable {
|
|
|
92
114
|
return { type: 'table', children: rows };
|
|
93
115
|
}
|
|
94
116
|
|
|
117
|
+
/** Directory portion of a part path, e.g. `ppt/slides/slide1.xml` → `ppt/slides`. */
|
|
118
|
+
function baseDirOf(path: string): string {
|
|
119
|
+
const slash = path.lastIndexOf('/');
|
|
120
|
+
return slash === -1 ? '' : path.slice(0, slash);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract every `<p:pic>` picture in a slide as an image node, reading the
|
|
125
|
+
* `<a:blip r:embed>` relationship, resolving it to the media part, and copying
|
|
126
|
+
* the bytes into `ctx.extractedImages` under `images/imageN.ext`.
|
|
127
|
+
*/
|
|
128
|
+
async function extractSlideImages(
|
|
129
|
+
doc: Document,
|
|
130
|
+
slidePath: string,
|
|
131
|
+
ctx: ImportContext,
|
|
132
|
+
): Promise<MarkdownImage[]> {
|
|
133
|
+
const rels = await getPartRelationships(ctx.pkg, slidePath);
|
|
134
|
+
const relById = new Map(rels.map((r) => [r.id, r.target]));
|
|
135
|
+
const baseDir = baseDirOf(slidePath);
|
|
136
|
+
|
|
137
|
+
const images: MarkdownImage[] = [];
|
|
138
|
+
const pics = doc.getElementsByTagNameNS(NS_PML, 'pic');
|
|
139
|
+
for (let i = 0; i < pics.length; i++) {
|
|
140
|
+
const pic = pics[i]!;
|
|
141
|
+
const blips = pic.getElementsByTagNameNS(NS_DRAWINGML, 'blip');
|
|
142
|
+
if (!blips.length) continue;
|
|
143
|
+
const embed = attrNS(blips[0]!, NS_R, 'embed', 'r:embed');
|
|
144
|
+
if (!embed) continue;
|
|
145
|
+
const target = relById.get(embed);
|
|
146
|
+
if (!target) continue;
|
|
147
|
+
|
|
148
|
+
const mediaPath = resolveTarget(baseDir, target);
|
|
149
|
+
const data = await getPartBinary(ctx.pkg, mediaPath);
|
|
150
|
+
if (!data) continue;
|
|
151
|
+
|
|
152
|
+
const dot = mediaPath.lastIndexOf('.');
|
|
153
|
+
const ext = dot !== -1 ? mediaPath.slice(dot).toLowerCase() : '.png';
|
|
154
|
+
const mimeType = extToMime(ext);
|
|
155
|
+
|
|
156
|
+
ctx.imageCounter++;
|
|
157
|
+
const imagePath = `images/image${ctx.imageCounter}${ext}`;
|
|
158
|
+
ctx.extractedImages.set(imagePath, { data, mimeType });
|
|
159
|
+
|
|
160
|
+
// Alt text from the picture's non-visual properties (descr, then name).
|
|
161
|
+
const cNvPrs = pic.getElementsByTagNameNS(NS_PML, 'cNvPr');
|
|
162
|
+
const alt =
|
|
163
|
+
(cNvPrs.length ? cNvPrs[0]!.getAttribute('descr') || cNvPrs[0]!.getAttribute('name') : '') ||
|
|
164
|
+
'Image';
|
|
165
|
+
|
|
166
|
+
images.push({ type: 'image', url: imagePath, alt });
|
|
167
|
+
}
|
|
168
|
+
return images;
|
|
169
|
+
}
|
|
170
|
+
|
|
95
171
|
async function convertSlide(
|
|
96
|
-
pkg: OoxmlPackage,
|
|
97
172
|
path: string,
|
|
98
173
|
index: number,
|
|
174
|
+
ctx: ImportContext,
|
|
99
175
|
): Promise<MarkdownBlockNode[]> {
|
|
176
|
+
const pkg = ctx.pkg;
|
|
100
177
|
const doc = await getPartXml(pkg, path);
|
|
101
178
|
if (!doc) return [];
|
|
102
179
|
const out: MarkdownBlockNode[] = [];
|
|
@@ -136,21 +213,61 @@ async function convertSlide(
|
|
|
136
213
|
out.push({ type: 'list', ordered: false, children: items });
|
|
137
214
|
}
|
|
138
215
|
|
|
216
|
+
// Embedded pictures land after the bullet list and before any tables.
|
|
217
|
+
if (ctx.extractImages) {
|
|
218
|
+
const images = await extractSlideImages(doc, path, ctx);
|
|
219
|
+
for (const image of images) {
|
|
220
|
+
out.push({ type: 'paragraph', children: [image] });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
139
224
|
const tbls = doc.getElementsByTagNameNS(NS_DRAWINGML, 'tbl');
|
|
140
225
|
for (let t = 0; t < tbls.length; t++) out.push(tableToMarkdown(tbls[t]!));
|
|
141
226
|
|
|
142
227
|
return out;
|
|
143
228
|
}
|
|
144
229
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
): Promise<MarkdownDocument> {
|
|
149
|
-
const
|
|
230
|
+
async function importDocument(
|
|
231
|
+
pkg: OoxmlPackage,
|
|
232
|
+
options: PptxImportOptions,
|
|
233
|
+
): Promise<{ doc: MarkdownDocument; ctx: ImportContext }> {
|
|
234
|
+
const ctx: ImportContext = {
|
|
235
|
+
pkg,
|
|
236
|
+
extractImages: options.extractImages ?? false,
|
|
237
|
+
extractedImages: new Map(),
|
|
238
|
+
imageCounter: 0,
|
|
239
|
+
};
|
|
150
240
|
const paths = await orderedSlidePaths(pkg);
|
|
151
241
|
const children: MarkdownBlockNode[] = [];
|
|
152
242
|
for (let i = 0; i < paths.length; i++) {
|
|
153
|
-
children.push(...(await convertSlide(
|
|
243
|
+
children.push(...(await convertSlide(paths[i]!, i, ctx)));
|
|
154
244
|
}
|
|
155
|
-
return { type: 'document', children };
|
|
245
|
+
return { doc: { type: 'document', children }, ctx };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function pptxToMarkdownDoc(
|
|
249
|
+
data: ArrayBuffer | Blob,
|
|
250
|
+
options: PptxImportOptions = {},
|
|
251
|
+
): Promise<MarkdownDocument> {
|
|
252
|
+
const pkg = await openPackage(data);
|
|
253
|
+
const { doc } = await importDocument(pkg, options);
|
|
254
|
+
return doc;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Convert a .pptx file to a ContentContainer with markdown + extracted images.
|
|
259
|
+
*
|
|
260
|
+
* The container holds the primary markdown document plus every embedded slide
|
|
261
|
+
* image under `images/` (e.g. `images/image1.png`). Image extraction is always
|
|
262
|
+
* forced on here so the markdown's image references resolve inside the
|
|
263
|
+
* container. Mirrors `docxToContainer`.
|
|
264
|
+
*/
|
|
265
|
+
export async function pptxToContainer(
|
|
266
|
+
data: ArrayBuffer | Blob,
|
|
267
|
+
options: PptxImportOptions = {},
|
|
268
|
+
): Promise<ContentContainer> {
|
|
269
|
+
const pkg = await openPackage(data);
|
|
270
|
+
const { doc, ctx } = await importDocument(pkg, { ...options, extractImages: true });
|
|
271
|
+
|
|
272
|
+
return buildContainer(stringifyMarkdown(doc), ctx.extractedImages);
|
|
156
273
|
}
|
package/src/pptx/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type { Doc } from '@bendyline/squisq/schemas';
|
|
|
25
25
|
import { type PptxImportOptions, pptxToMarkdownDoc } from './import.js';
|
|
26
26
|
|
|
27
27
|
export type { PptxImportOptions } from './import.js';
|
|
28
|
-
export { pptxToMarkdownDoc } from './import.js';
|
|
28
|
+
export { pptxToMarkdownDoc, pptxToContainer } from './import.js';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Convert a .pptx file to a squisq Doc (via the markdown model).
|