@polotno/pdf-export 0.1.28 → 0.1.30

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/lib/svg-render.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as svg from './svg.js';
2
+ import { Util } from 'konva/lib/Util.js';
2
3
  export async function renderSVG(doc, element, cache = null) {
3
- const svgStr = await svg.urlToString(element.src, cache);
4
- const src = svg.replaceColors(svgStr, new Map(Object.entries(element.colorsReplace)));
5
- const str = await svg.urlToString(src, cache);
4
+ const str = await svg.urlToString(element.src, cache);
5
+ const replaceEntries = Object.entries(element.colorsReplace || {});
6
6
  doc.addSVG(str, 0, 0, {
7
7
  // Use 'none' to allow stretching to exact dimensions, 'xMinYMin meet' to preserve aspect ratio
8
8
  preserveAspectRatio: 'none',
@@ -13,8 +13,51 @@ export async function renderSVG(doc, element, cache = null) {
13
13
  if (!colors) {
14
14
  return colors;
15
15
  }
16
- const [color, opacity] = colors;
17
- return [color, opacity * element.opacity];
16
+ const [rgb, opacity] = colors;
17
+ let colorString = null;
18
+ if (Array.isArray(rgb) && rgb.length === 3) {
19
+ colorString = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
20
+ }
21
+ else if (typeof rgb === 'string') {
22
+ colorString = rgb;
23
+ }
24
+ let nextColorString = colorString;
25
+ if (replaceEntries.length && colorString) {
26
+ for (const [from, to] of replaceEntries) {
27
+ if (svg.sameColors(from, colorString)) {
28
+ nextColorString = to;
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ let nextColorArray = rgb;
34
+ let finalOpacity = opacity * element.opacity;
35
+ if (nextColorString != null) {
36
+ const rgbaObject = Util.colorToRGBA(nextColorString);
37
+ if (rgbaObject) {
38
+ nextColorArray = [
39
+ Math.round(rgbaObject.r),
40
+ Math.round(rgbaObject.g),
41
+ Math.round(rgbaObject.b),
42
+ ];
43
+ // Handle alpha channel from the color string
44
+ if (rgbaObject.a !== undefined) {
45
+ finalOpacity = rgbaObject.a * opacity * element.opacity;
46
+ }
47
+ }
48
+ }
49
+ if (!Array.isArray(nextColorArray) &&
50
+ typeof nextColorArray === 'string') {
51
+ const rgbObject = Util.getRGB(nextColorArray);
52
+ if (rgbObject) {
53
+ nextColorArray = [
54
+ Math.round(rgbObject.r),
55
+ Math.round(rgbObject.g),
56
+ Math.round(rgbObject.b),
57
+ ];
58
+ }
59
+ }
60
+ return [nextColorArray, finalOpacity];
18
61
  },
19
62
  });
20
63
  }
package/lib/svg.d.ts CHANGED
@@ -8,4 +8,5 @@ export declare function getSvgSize(url: string): Promise<{
8
8
  height: number;
9
9
  }>;
10
10
  export declare function fixSize(svgString: string): string;
11
+ export declare const sameColors: (color1: any, color2: any) => boolean;
11
12
  export declare function replaceColors(svgString: string, replaceMap: Map<string, string>): string;
package/lib/svg.js CHANGED
@@ -175,7 +175,7 @@ export function fixSize(svgString) {
175
175
  const str = xmlSerializer.serializeToString(doc);
176
176
  return str;
177
177
  }
178
- const sameColors = (color1, color2) => {
178
+ export const sameColors = (color1, color2) => {
179
179
  if (!color1 || !color2) {
180
180
  return false;
181
181
  }
package/lib/text.d.ts CHANGED
@@ -36,10 +36,14 @@ export interface TextSegment {
36
36
  * while preserving inline formatting tags.
37
37
  */
38
38
  declare function normalizeRichText(text: string): string;
39
+ /**
40
+ * Parse HTML text into styled segments
41
+ */
42
+ declare function parseHTMLToSegments(html: string, baseElement: TextElement): TextSegment[];
39
43
  export declare function getGoogleFontPath(fontFamily: string, fontWeight?: string, italic?: boolean): Promise<string>;
40
44
  export declare function loadFontIfNeeded(doc: any, element: TextElement, fonts: Record<string, boolean>): Promise<string>;
41
45
  /**
42
46
  * Main text rendering function
43
47
  */
44
48
  export declare function renderText(doc: PDFKit.PDFDocument, element: TextElement, fonts: Record<string, boolean>, attrs?: RenderAttrs): Promise<void>;
45
- export { normalizeRichText as __normalizeRichTextForTests };
49
+ export { normalizeRichText as __normalizeRichTextForTests, parseHTMLToSegments as __parseHTMLToSegmentsForTests, };
package/lib/text.js CHANGED
@@ -2,6 +2,14 @@ import { parseColor, srcToBuffer } from './utils.js';
2
2
  import getUrls from 'get-urls';
3
3
  import fetch from 'node-fetch';
4
4
  import { stripHtml } from 'string-strip-html';
5
+ import { decode as decodeEntities } from 'html-entities';
6
+ function decodeHtmlEntities(text) {
7
+ if (!text) {
8
+ return text;
9
+ }
10
+ const decoded = decodeEntities(text);
11
+ return decoded.replace(/\t/g, ' ');
12
+ }
5
13
  /**
6
14
  * Check if text contains HTML tags
7
15
  */
@@ -18,6 +26,8 @@ function normalizeRichText(text) {
18
26
  return text;
19
27
  }
20
28
  let normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
29
+ // Normalize tab characters into 8 spaces
30
+ normalized = normalized.replace(/\t/g, ' '.repeat(8));
21
31
  // Convert explicit HTML break tags into newline characters
22
32
  normalized = normalized.replace(/<br\s*\/?>/gi, '\n');
23
33
  // Treat paragraph boundaries as newlines and drop opening tags
@@ -45,7 +55,7 @@ function parseHTMLToSegments(html, baseElement) {
45
55
  while ((match = regex.exec(html)) !== null) {
46
56
  if (match[4]) {
47
57
  // Text content
48
- const text = match[4];
58
+ const text = decodeHtmlEntities(match[4]);
49
59
  // Calculate current styles from tag stack
50
60
  let bold = false;
51
61
  let italic = false;
@@ -179,9 +189,11 @@ function tokenizeHTML(html) {
179
189
  while ((match = regex.exec(html)) !== null) {
180
190
  if (match[4]) {
181
191
  // Text content
192
+ const decodedContent = decodeHtmlEntities(match[4]);
182
193
  tokens.push({
183
194
  type: 'text',
184
195
  content: match[4],
196
+ decodedContent,
185
197
  });
186
198
  }
187
199
  else {
@@ -541,7 +553,7 @@ function splitTextIntoLines(doc, element, props) {
541
553
  // Extract plain text for width calculation
542
554
  const plainText = tokens
543
555
  .filter((t) => t.type === 'text')
544
- .map((t) => t.content)
556
+ .map((t) => t.decodedContent ?? decodeHtmlEntities(t.content))
545
557
  .join('');
546
558
  const baseMeta = paragraph.listMeta
547
559
  ? createListLineMeta(doc, element, props, paragraph.listMeta)
@@ -565,7 +577,7 @@ function splitTextIntoLines(doc, element, props) {
565
577
  }
566
578
  else {
567
579
  // Need to split paragraph into multiple lines
568
- let currentLine = '';
580
+ let currentLineDecoded = '';
569
581
  let currentWidth = 0;
570
582
  let currentTokens = [];
571
583
  let openTags = [];
@@ -575,34 +587,32 @@ function splitTextIntoLines(doc, element, props) {
575
587
  continue;
576
588
  }
577
589
  // Text token - split by words
578
- const textWords = token.content.split(' ');
579
- for (let i = 0; i < textWords.length; i++) {
580
- const word = textWords[i];
581
- const testLine = currentLine
582
- ? `${currentLine}${i > 0 ? ' ' : ''}${word}`
583
- : word;
584
- const testWidth = doc.widthOfString(testLine, props);
590
+ const rawWords = token.content.split(' ');
591
+ const decodedWords = (token.decodedContent ?? decodeHtmlEntities(token.content)).split(' ');
592
+ for (let i = 0; i < rawWords.length; i++) {
593
+ const rawWord = rawWords[i];
594
+ const decodedWord = decodedWords[i] ?? decodeHtmlEntities(rawWord);
595
+ const separator = i > 0 ? ' ' : '';
596
+ const hasCurrentLine = currentLineDecoded.length > 0;
597
+ const testLineDecoded = hasCurrentLine
598
+ ? `${currentLineDecoded}${separator}${decodedWord}`
599
+ : decodedWord;
600
+ const testWidth = doc.widthOfString(testLineDecoded, props);
585
601
  if (testWidth <= availableWidth) {
586
- currentLine = testLine;
602
+ currentLineDecoded = testLineDecoded;
587
603
  currentWidth = testWidth;
588
604
  // Add text token (with space if not first word in token)
589
- if (i > 0 || currentTokens.length > 0) {
590
- let content = (i > 0 ? ' ' : '') + word;
591
- currentTokens.push({
592
- type: 'text',
593
- content: content,
594
- });
595
- }
596
- else {
597
- currentTokens.push({
598
- type: 'text',
599
- content: word,
600
- });
601
- }
605
+ const rawContent = separator.length > 0 ? `${separator}${rawWord}` : rawWord;
606
+ const decodedContent = separator.length > 0 ? `${separator}${decodedWord}` : decodedWord;
607
+ currentTokens.push({
608
+ type: 'text',
609
+ content: rawContent,
610
+ decodedContent,
611
+ });
602
612
  }
603
613
  else {
604
614
  // Line is too long, save current line and start new one
605
- if (currentLine) {
615
+ if (currentLineDecoded.length > 0) {
606
616
  const result = tokensToHTML(currentTokens, openTags);
607
617
  const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
608
618
  lines.push({
@@ -615,17 +625,18 @@ function splitTextIntoLines(doc, element, props) {
615
625
  currentTokens = [];
616
626
  showMarkerForLine = false;
617
627
  }
618
- currentLine = word;
619
- currentWidth = doc.widthOfString(word, props);
628
+ currentLineDecoded = decodedWord;
629
+ currentWidth = doc.widthOfString(decodedWord, props);
620
630
  currentTokens.push({
621
631
  type: 'text',
622
- content: word,
632
+ content: rawWord,
633
+ decodedContent: decodedWord,
623
634
  });
624
635
  }
625
636
  }
626
637
  }
627
638
  // Add the last line
628
- if (currentLine) {
639
+ if (currentLineDecoded.length > 0) {
629
640
  const result = tokensToHTML(currentTokens, openTags);
630
641
  const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
631
642
  lines.push({
@@ -1036,4 +1047,4 @@ export async function renderText(doc, element, fonts, attrs = {}) {
1036
1047
  }
1037
1048
  }
1038
1049
  // Internal exports for testing
1039
- export { normalizeRichText as __normalizeRichTextForTests };
1050
+ export { normalizeRichText as __normalizeRichTextForTests, parseHTMLToSegments as __parseHTMLToSegmentsForTests, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polotno/pdf-export",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Convert Polotno JSON into vector PDF",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",