@bendyline/squisq-formats 1.1.0 → 1.2.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.
Files changed (89) hide show
  1. package/README.md +23 -10
  2. package/dist/{chunk-ULLIPBEJ.js → chunk-743COJWQ.js} +6 -2
  3. package/dist/chunk-743COJWQ.js.map +1 -0
  4. package/dist/{chunk-KAK4V57E.js → chunk-BHHEDPRB.js} +16 -112
  5. package/dist/chunk-BHHEDPRB.js.map +1 -0
  6. package/dist/{chunk-MQHCXI56.js → chunk-CLGUOVYR.js} +193 -23
  7. package/dist/chunk-CLGUOVYR.js.map +1 -0
  8. package/dist/chunk-FIFCQN3W.js +630 -0
  9. package/dist/chunk-FIFCQN3W.js.map +1 -0
  10. package/dist/chunk-KJ4NS4DX.js +120 -0
  11. package/dist/chunk-KJ4NS4DX.js.map +1 -0
  12. package/dist/{chunk-TBPD5PCU.js → chunk-S3Y7H2BK.js} +161 -11
  13. package/dist/chunk-S3Y7H2BK.js.map +1 -0
  14. package/dist/docx/export.d.ts +15 -0
  15. package/dist/docx/export.d.ts.map +1 -1
  16. package/dist/docx/export.js +144 -19
  17. package/dist/docx/export.js.map +1 -1
  18. package/dist/docx/import.d.ts +13 -0
  19. package/dist/docx/import.d.ts.map +1 -1
  20. package/dist/docx/import.js +102 -10
  21. package/dist/docx/import.js.map +1 -1
  22. package/dist/docx/index.d.ts +1 -1
  23. package/dist/docx/index.d.ts.map +1 -1
  24. package/dist/docx/index.js +1 -1
  25. package/dist/docx/index.js.map +1 -1
  26. package/dist/html/htmlTemplate.d.ts +6 -0
  27. package/dist/html/htmlTemplate.d.ts.map +1 -1
  28. package/dist/html/htmlTemplate.js +5 -1
  29. package/dist/html/htmlTemplate.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +2 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/ooxml/index.d.ts +1 -1
  34. package/dist/ooxml/index.d.ts.map +1 -1
  35. package/dist/ooxml/index.js +1 -1
  36. package/dist/ooxml/index.js.map +1 -1
  37. package/dist/ooxml/namespaces.d.ts +10 -0
  38. package/dist/ooxml/namespaces.d.ts.map +1 -1
  39. package/dist/ooxml/namespaces.js +10 -0
  40. package/dist/ooxml/namespaces.js.map +1 -1
  41. package/dist/pdf/export.d.ts +6 -0
  42. package/dist/pdf/export.d.ts.map +1 -1
  43. package/dist/pdf/export.js +37 -9
  44. package/dist/pdf/export.js.map +1 -1
  45. package/dist/pdf/import.d.ts +16 -0
  46. package/dist/pdf/import.d.ts.map +1 -1
  47. package/dist/pdf/import.js +177 -0
  48. package/dist/pdf/import.js.map +1 -1
  49. package/dist/pdf/index.d.ts +1 -1
  50. package/dist/pdf/index.d.ts.map +1 -1
  51. package/dist/pdf/index.js +1 -1
  52. package/dist/pdf/index.js.map +1 -1
  53. package/dist/pptx/export.d.ts +75 -0
  54. package/dist/pptx/export.d.ts.map +1 -0
  55. package/dist/pptx/export.js +722 -0
  56. package/dist/pptx/export.js.map +1 -0
  57. package/dist/pptx/index.d.ts +12 -27
  58. package/dist/pptx/index.d.ts.map +1 -1
  59. package/dist/pptx/index.js +12 -22
  60. package/dist/pptx/index.js.map +1 -1
  61. package/dist/pptx/styles.d.ts +54 -0
  62. package/dist/pptx/styles.d.ts.map +1 -0
  63. package/dist/pptx/styles.js +73 -0
  64. package/dist/pptx/styles.js.map +1 -0
  65. package/dist/pptx/templates.d.ts +24 -0
  66. package/dist/pptx/templates.d.ts.map +1 -0
  67. package/dist/pptx/templates.js +158 -0
  68. package/dist/pptx/templates.js.map +1 -0
  69. package/package.json +2 -2
  70. package/src/docx/export.ts +183 -20
  71. package/src/docx/import.ts +127 -10
  72. package/src/docx/index.ts +1 -1
  73. package/src/html/htmlTemplate.ts +13 -0
  74. package/src/index.ts +2 -2
  75. package/src/ooxml/index.ts +7 -0
  76. package/src/ooxml/namespaces.ts +23 -0
  77. package/src/pdf/export.ts +57 -9
  78. package/src/pdf/import.ts +228 -0
  79. package/src/pdf/index.ts +1 -1
  80. package/src/pptx/export.ts +1041 -0
  81. package/src/pptx/index.ts +15 -37
  82. package/src/pptx/styles.ts +96 -0
  83. package/src/pptx/templates.ts +187 -0
  84. package/dist/chunk-532L4D5D.js +0 -21
  85. package/dist/chunk-532L4D5D.js.map +0 -1
  86. package/dist/chunk-KAK4V57E.js.map +0 -1
  87. package/dist/chunk-MQHCXI56.js.map +0 -1
  88. package/dist/chunk-TBPD5PCU.js.map +0 -1
  89. package/dist/chunk-ULLIPBEJ.js.map +0 -1
package/src/pdf/export.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  import { PDFDocument, StandardFonts, rgb, PDFFont, PDFPage } from 'pdf-lib';
23
23
 
24
24
  import type { Doc } from '@bendyline/squisq/schemas';
25
+ import { resolveTheme } from '@bendyline/squisq/schemas';
25
26
  import { docToMarkdown } from '@bendyline/squisq/doc';
26
27
  import type {
27
28
  MarkdownDocument,
@@ -103,6 +104,12 @@ export interface PdfExportOptions {
103
104
  margin?: number;
104
105
  /** Default body font size in points. Default: 11. */
105
106
  defaultFontSize?: number;
107
+ /**
108
+ * Squisq theme ID to apply (e.g., 'documentary', 'cinematic').
109
+ * When set, overrides heading, text, and link colors with the theme palette.
110
+ * Font changes are not supported (pdf-lib uses standard 14 PDF fonts only).
111
+ */
112
+ themeId?: string;
106
113
  }
107
114
 
108
115
  /**
@@ -151,6 +158,12 @@ interface FontSet {
151
158
  monoBold: PDFFont;
152
159
  }
153
160
 
161
+ interface RgbColor {
162
+ r: number;
163
+ g: number;
164
+ b: number;
165
+ }
166
+
154
167
  interface ExportContext {
155
168
  pdfDoc: PDFDocument;
156
169
  fonts: FontSet;
@@ -166,6 +179,12 @@ interface ExportContext {
166
179
  contentWidth: number;
167
180
  /** Bottom margin y position. */
168
181
  bottomY: number;
182
+ /** Resolved colors (may be overridden by theme). */
183
+ colors: {
184
+ text: RgbColor;
185
+ heading: RgbColor;
186
+ link: RgbColor;
187
+ };
169
188
  }
170
189
 
171
190
  async function createExportContext(
@@ -189,6 +208,20 @@ async function createExportContext(
189
208
 
190
209
  const page = pdfDoc.addPage([pageWidth, pageHeight]);
191
210
 
211
+ // Resolve theme colors if themeId is set
212
+ let colorText = COLOR_TEXT;
213
+ let colorHeading = COLOR_HEADING;
214
+ let colorLink = COLOR_LINK;
215
+
216
+ if (options.themeId) {
217
+ const theme = resolveTheme(options.themeId);
218
+ if (theme.colors) {
219
+ colorText = hexToRgb(theme.colors.text) ?? COLOR_TEXT;
220
+ colorHeading = hexToRgb(theme.colors.primary) ?? COLOR_HEADING;
221
+ colorLink = hexToRgb(theme.colors.highlight || theme.colors.secondary) ?? COLOR_LINK;
222
+ }
223
+ }
224
+
192
225
  return {
193
226
  pdfDoc,
194
227
  fonts: { regular, bold, italic, boldItalic, mono, monoBold },
@@ -200,6 +233,7 @@ async function createExportContext(
200
233
  y: pageHeight - margin,
201
234
  contentWidth: pageWidth - 2 * margin,
202
235
  bottomY: margin,
236
+ colors: { text: colorText, heading: colorHeading, link: colorLink },
203
237
  };
204
238
  }
205
239
 
@@ -262,7 +296,7 @@ function flattenInlines(
262
296
  text: (node as MarkdownText).value,
263
297
  font,
264
298
  fontSize: state.code ? CODE_FONT_SIZE : ctx.fontSize,
265
- color: state.code ? COLOR_CODE_TEXT : (state.color ?? COLOR_TEXT),
299
+ color: state.code ? COLOR_CODE_TEXT : (state.color ?? ctx.colors.text),
266
300
  link: state.link,
267
301
  strikethrough: state.strikethrough,
268
302
  });
@@ -307,7 +341,7 @@ function flattenInlines(
307
341
  ...flattenInlines(linkNode.children, ctx, {
308
342
  ...state,
309
343
  link: linkNode.url,
310
- color: COLOR_LINK,
344
+ color: ctx.colors.link,
311
345
  }),
312
346
  );
313
347
  break;
@@ -329,7 +363,7 @@ function flattenInlines(
329
363
  text: '\n',
330
364
  font: ctx.fonts.regular,
331
365
  fontSize: ctx.fontSize,
332
- color: COLOR_TEXT,
366
+ color: ctx.colors.text,
333
367
  });
334
368
  break;
335
369
 
@@ -361,7 +395,7 @@ function flattenInlines(
361
395
  text: `[${ref.identifier}]`,
362
396
  font: ctx.fonts.regular,
363
397
  fontSize: ctx.fontSize * 0.75,
364
- color: COLOR_LINK,
398
+ color: ctx.colors.link,
365
399
  });
366
400
  break;
367
401
  }
@@ -376,7 +410,7 @@ function flattenInlines(
376
410
  text: fallback.value,
377
411
  font: pickFont(ctx, state.bold, state.italic),
378
412
  fontSize: ctx.fontSize,
379
- color: state.color ?? COLOR_TEXT,
413
+ color: state.color ?? ctx.colors.text,
380
414
  });
381
415
  }
382
416
  break;
@@ -601,7 +635,7 @@ function renderHeading(node: MarkdownHeading, ctx: ExportContext, extraIndent: n
601
635
  bold: true,
602
636
  italic: false,
603
637
  code: false,
604
- color: COLOR_HEADING,
638
+ color: ctx.colors.heading,
605
639
  });
606
640
 
607
641
  drawSpans(spans, ctx, w, x0);
@@ -704,7 +738,7 @@ function renderListItem(
704
738
  y: ctx.y - ctx.fontSize,
705
739
  size: ctx.fontSize,
706
740
  font: bulletFont,
707
- color: rgb(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b),
741
+ color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b),
708
742
  });
709
743
 
710
744
  const textIndent = indent + bulletWidth + 4;
@@ -969,7 +1003,7 @@ function renderFootnoteDefinition(
969
1003
  y: ctx.y - ctx.fontSize * 0.75,
970
1004
  size: ctx.fontSize * 0.75,
971
1005
  font: ctx.fonts.bold,
972
- color: rgb(COLOR_LINK.r, COLOR_LINK.g, COLOR_LINK.b),
1006
+ color: rgb(ctx.colors.link.r, ctx.colors.link.g, ctx.colors.link.b),
973
1007
  });
974
1008
 
975
1009
  // Render children indented
@@ -1022,8 +1056,22 @@ function renderFallbackBlock(
1022
1056
  y: ctx.y - ctx.fontSize,
1023
1057
  size: ctx.fontSize,
1024
1058
  font: ctx.fonts.regular,
1025
- color: rgb(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b),
1059
+ color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b),
1026
1060
  });
1027
1061
  ctx.y -= lineH + PARAGRAPH_SPACING;
1028
1062
  }
1029
1063
  }
1064
+
1065
+ // ============================================
1066
+ // Color Helpers
1067
+ // ============================================
1068
+
1069
+ function hexToRgb(hex: string): RgbColor | undefined {
1070
+ const h = hex.startsWith('#') ? hex.slice(1) : hex;
1071
+ if (h.length !== 6) return undefined;
1072
+ const r = parseInt(h.slice(0, 2), 16);
1073
+ const g = parseInt(h.slice(2, 4), 16);
1074
+ const b = parseInt(h.slice(4, 6), 16);
1075
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return undefined;
1076
+ return { r: r / 255, g: g / 255, b: b / 255 };
1077
+ }
package/src/pdf/import.ts CHANGED
@@ -40,8 +40,12 @@ import type {
40
40
  MarkdownStrong,
41
41
  MarkdownInlineCode,
42
42
  MarkdownLink,
43
+ MarkdownImage,
43
44
  } from '@bendyline/squisq/markdown';
44
45
 
46
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
47
+ import type { ContentContainer } from '@bendyline/squisq/storage';
48
+
45
49
  import {
46
50
  DEFAULT_FONT_SIZE,
47
51
  IMPORT_HEADING_MIN_SIZE,
@@ -127,6 +131,230 @@ export async function pdfToDoc(
127
131
  return markdownToDoc(markdownDoc);
128
132
  }
129
133
 
134
+ /**
135
+ * Convert a PDF file to a ContentContainer with markdown + extracted images.
136
+ *
137
+ * The container will contain:
138
+ * - The primary markdown document (index.md)
139
+ * - Any embedded images under images/ (e.g., images/image1.png)
140
+ *
141
+ * Image extraction uses pdfjs-dist's operator list API and requires a browser
142
+ * environment (canvas is used to encode pixel data to PNG).
143
+ *
144
+ * @param data - The raw PDF file as ArrayBuffer, Uint8Array, or Blob
145
+ * @param options - Import options
146
+ * @returns A ContentContainer with the document and its media
147
+ */
148
+ export async function pdfToContainer(
149
+ data: ArrayBuffer | Uint8Array | Blob,
150
+ options: PdfImportOptions = {},
151
+ ): Promise<ContentContainer> {
152
+ const bytes =
153
+ data instanceof Blob
154
+ ? new Uint8Array(await data.arrayBuffer())
155
+ : data instanceof ArrayBuffer
156
+ ? new Uint8Array(data)
157
+ : data;
158
+
159
+ const textLines = await extractTextLines(bytes);
160
+ const images = await extractImages(bytes);
161
+
162
+ const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
163
+
164
+ // If we have images, insert image references into the block list
165
+ let blocks = classifyLines(textLines, bodySize, options);
166
+ if (images.length > 0) {
167
+ blocks = insertImageBlocks(blocks, images);
168
+ }
169
+
170
+ const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
171
+
172
+ const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
173
+ const markdown = stringifyMarkdown(markdownDoc);
174
+
175
+ const container = new MemoryContentContainer();
176
+ await container.writeDocument(markdown);
177
+
178
+ for (const img of images) {
179
+ await container.writeFile(img.path, new Uint8Array(img.data), 'image/png');
180
+ }
181
+
182
+ return container;
183
+ }
184
+
185
+ /** Extracted image with position info for placement. */
186
+ interface ExtractedImage {
187
+ path: string;
188
+ data: ArrayBuffer;
189
+ page: number;
190
+ y: number;
191
+ }
192
+
193
+ /**
194
+ * Extract embedded images from a PDF using pdfjs-dist operator list API.
195
+ * Requires browser canvas for PNG encoding.
196
+ */
197
+ async function extractImages(data: Uint8Array): Promise<ExtractedImage[]> {
198
+ // Canvas is required for PNG encoding — skip in non-browser environments
199
+ if (typeof document === 'undefined') return [];
200
+
201
+ let pdfjsLib: PdfjsLib & { OPS?: Record<string, number> };
202
+ try {
203
+ pdfjsLib = (await import('pdfjs-dist/legacy/build/pdf.mjs')) as unknown as PdfjsLib & {
204
+ OPS?: Record<string, number>;
205
+ };
206
+ } catch {
207
+ pdfjsLib = (await import('pdfjs-dist')) as unknown as PdfjsLib & {
208
+ OPS?: Record<string, number>;
209
+ };
210
+ }
211
+
212
+ await applyWorkerConfig(pdfjsLib);
213
+
214
+ const OPS_paintImageXObject = pdfjsLib.OPS?.paintImageXObject ?? 85;
215
+
216
+ const loadingTask = pdfjsLib.getDocument({
217
+ data,
218
+ isEvalSupported: false,
219
+ useSystemFonts: true,
220
+ });
221
+
222
+ const pdf = await loadingTask.promise;
223
+ const images: ExtractedImage[] = [];
224
+ let counter = 0;
225
+
226
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
227
+ const page = (await pdf.getPage(pageNum)) as PdfjsPageFull;
228
+ if (!page.getOperatorList) continue;
229
+
230
+ const opList = await page.getOperatorList();
231
+ const seen = new Set<string>();
232
+
233
+ for (let i = 0; i < opList.fnArray.length; i++) {
234
+ if (opList.fnArray[i] !== OPS_paintImageXObject) continue;
235
+
236
+ const imgName = opList.argsArray[i]?.[0];
237
+ if (!imgName || typeof imgName !== 'string' || seen.has(imgName)) continue;
238
+ seen.add(imgName);
239
+
240
+ try {
241
+ const imgData = page.objs?.get(imgName) as PdfjsImageData | null;
242
+ if (!imgData?.data || !imgData.width || !imgData.height) continue;
243
+
244
+ const pngData = imageDataToPng(imgData);
245
+ if (!pngData) continue;
246
+
247
+ counter++;
248
+ images.push({
249
+ path: `images/image${counter}.png`,
250
+ data: pngData,
251
+ page: pageNum - 1,
252
+ y: 0,
253
+ });
254
+ } catch {
255
+ // Skip images that fail to extract
256
+ }
257
+ }
258
+ }
259
+
260
+ return images;
261
+ }
262
+
263
+ /** Minimal pdfjs image data shape. */
264
+ interface PdfjsImageData {
265
+ width: number;
266
+ height: number;
267
+ data: Uint8ClampedArray;
268
+ kind?: number;
269
+ }
270
+
271
+ /** Extended PdfjsPage with operator list and objs access. */
272
+ interface PdfjsPageFull extends PdfjsPage {
273
+ getOperatorList(): Promise<{ fnArray: number[]; argsArray: unknown[][] }>;
274
+ objs?: { get(name: string): unknown; has?(name: string): boolean };
275
+ }
276
+
277
+ /** Encode pdfjs image data to PNG using a canvas element. */
278
+ function imageDataToPng(img: PdfjsImageData): ArrayBuffer | null {
279
+ try {
280
+ const canvas = document.createElement('canvas');
281
+ canvas.width = img.width;
282
+ canvas.height = img.height;
283
+ const ctx = canvas.getContext('2d');
284
+ if (!ctx) return null;
285
+
286
+ // pdfjs kind=1 is GRAYSCALE, kind=2 is RGB, kind=3 is RGBA
287
+ let imageData: ImageData;
288
+ if (img.kind === 3 || img.data.length === img.width * img.height * 4) {
289
+ // RGBA — use directly
290
+ imageData = new ImageData(new Uint8ClampedArray(img.data), img.width, img.height);
291
+ } else if (img.kind === 2 || img.data.length === img.width * img.height * 3) {
292
+ // RGB — expand to RGBA
293
+ const rgba = new Uint8ClampedArray(img.width * img.height * 4);
294
+ for (let j = 0, k = 0; j < img.data.length; j += 3, k += 4) {
295
+ rgba[k] = img.data[j];
296
+ rgba[k + 1] = img.data[j + 1];
297
+ rgba[k + 2] = img.data[j + 2];
298
+ rgba[k + 3] = 255;
299
+ }
300
+ imageData = new ImageData(rgba, img.width, img.height);
301
+ } else if (img.kind === 1 || img.data.length === img.width * img.height) {
302
+ // Grayscale — expand to RGBA
303
+ const rgba = new Uint8ClampedArray(img.width * img.height * 4);
304
+ for (let j = 0, k = 0; j < img.data.length; j++, k += 4) {
305
+ rgba[k] = img.data[j];
306
+ rgba[k + 1] = img.data[j];
307
+ rgba[k + 2] = img.data[j];
308
+ rgba[k + 3] = 255;
309
+ }
310
+ imageData = new ImageData(rgba, img.width, img.height);
311
+ } else {
312
+ return null;
313
+ }
314
+
315
+ ctx.putImageData(imageData, 0, 0);
316
+
317
+ // Convert canvas to PNG ArrayBuffer
318
+ const dataUrl = canvas.toDataURL('image/png');
319
+ const base64 = dataUrl.split(',')[1];
320
+ const binaryStr = atob(base64);
321
+ const bytes = new Uint8Array(binaryStr.length);
322
+ for (let i = 0; i < binaryStr.length; i++) {
323
+ bytes[i] = binaryStr.charCodeAt(i);
324
+ }
325
+ return bytes.buffer;
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Insert image reference blocks between text blocks.
333
+ * Places each image after the last text block on the same page (or at the end).
334
+ */
335
+ function insertImageBlocks(
336
+ blocks: MarkdownBlockNode[],
337
+ images: ExtractedImage[],
338
+ ): MarkdownBlockNode[] {
339
+ if (images.length === 0) return blocks;
340
+
341
+ // Simple strategy: append all images at the end as paragraphs
342
+ const result = [...blocks];
343
+ for (const img of images) {
344
+ const imgNode: MarkdownImage = {
345
+ type: 'image',
346
+ url: img.path,
347
+ alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
348
+ };
349
+ const para: MarkdownParagraph = {
350
+ type: 'paragraph',
351
+ children: [imgNode],
352
+ };
353
+ result.push(para);
354
+ }
355
+ return result;
356
+ }
357
+
130
358
  // ============================================
131
359
  // Internal Types
132
360
  // ============================================
package/src/pdf/index.ts CHANGED
@@ -25,5 +25,5 @@ export { markdownDocToPdf, docToPdf } from './export.js';
25
25
  export type { PdfExportOptions } from './export.js';
26
26
 
27
27
  // Import
28
- export { pdfToMarkdownDoc, pdfToDoc, configurePdfWorker } from './import.js';
28
+ export { pdfToMarkdownDoc, pdfToDoc, pdfToContainer, configurePdfWorker } from './import.js';
29
29
  export type { PdfImportOptions } from './import.js';