@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 +48 -5
- package/lib/svg.d.ts +1 -0
- package/lib/svg.js +1 -1
- package/lib/text.d.ts +5 -1
- package/lib/text.js +41 -30
- package/package.json +1 -1
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
|
|
4
|
-
const
|
|
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 [
|
|
17
|
-
|
|
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
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
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
|
-
|
|
602
|
+
currentLineDecoded = testLineDecoded;
|
|
587
603
|
currentWidth = testWidth;
|
|
588
604
|
// Add text token (with space if not first word in token)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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 (
|
|
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
|
-
|
|
619
|
-
currentWidth = doc.widthOfString(
|
|
628
|
+
currentLineDecoded = decodedWord;
|
|
629
|
+
currentWidth = doc.widthOfString(decodedWord, props);
|
|
620
630
|
currentTokens.push({
|
|
621
631
|
type: 'text',
|
|
622
|
-
content:
|
|
632
|
+
content: rawWord,
|
|
633
|
+
decodedContent: decodedWord,
|
|
623
634
|
});
|
|
624
635
|
}
|
|
625
636
|
}
|
|
626
637
|
}
|
|
627
638
|
// Add the last line
|
|
628
|
-
if (
|
|
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, };
|