svelte-rtf-editor 1.3.0 → 1.4.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.
- package/README.md +75 -2
- package/dist/RtfViewer.svelte +18 -0
- package/dist/components/ImageOverlay.svelte +302 -0
- package/dist/components/ImageOverlay.svelte.d.ts +27 -0
- package/dist/components/InkEditor.svelte +556 -33
- package/dist/components/InkEditor.svelte.d.ts +9 -0
- package/dist/images.d.ts +86 -0
- package/dist/images.js +286 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/rtf-parser.d.ts +2 -1
- package/dist/rtf-parser.js +188 -23
- package/dist/rtf-writer.d.ts +16 -2
- package/dist/rtf-writer.js +302 -27
- package/dist/utils.js +13 -0
- package/package.json +1 -1
|
@@ -3,6 +3,11 @@ interface ChangePayload {
|
|
|
3
3
|
text: string;
|
|
4
4
|
wordCount: number;
|
|
5
5
|
charCount: number;
|
|
6
|
+
/**
|
|
7
|
+
* Approximate size of this document as RTF. Pictures dominate it and are
|
|
8
|
+
* counted exactly; see getRtfSize() for the precise figure.
|
|
9
|
+
*/
|
|
10
|
+
estimatedRtfBytes: number;
|
|
6
11
|
}
|
|
7
12
|
interface Props {
|
|
8
13
|
content?: string;
|
|
@@ -13,6 +18,9 @@ interface Props {
|
|
|
13
18
|
showStatusBar?: boolean;
|
|
14
19
|
minHeight?: string;
|
|
15
20
|
readonly?: boolean;
|
|
21
|
+
maxImageEdge?: number;
|
|
22
|
+
maxImageBytes?: number;
|
|
23
|
+
maxImageDisplayWidth?: number;
|
|
16
24
|
onchange?: (payload: ChangePayload) => void;
|
|
17
25
|
onsave?: (payload: {
|
|
18
26
|
html: string;
|
|
@@ -27,6 +35,7 @@ declare const InkEditor: import("svelte").Component<Props, {
|
|
|
27
35
|
setHTML: (html: string) => void;
|
|
28
36
|
getMarkdown: () => string;
|
|
29
37
|
getRTF: () => string;
|
|
38
|
+
getRtfSize: () => number;
|
|
30
39
|
clear: () => void;
|
|
31
40
|
focus: () => void;
|
|
32
41
|
exportFile: (format?: string) => void;
|
package/dist/images.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image helpers for the editor.
|
|
3
|
+
*
|
|
4
|
+
* Images are stored inline as base64 data URLs so a document is self-contained:
|
|
5
|
+
* it can be auto-saved, exported to RTF (as a \pict group) and re-imported
|
|
6
|
+
* without depending on a server. RTF only carries PNG and JPEG bitmaps, so
|
|
7
|
+
* other formats (GIF, WebP, SVG, …) are re-encoded to PNG on insert.
|
|
8
|
+
*
|
|
9
|
+
* Pictures are also scaled down to a maximum edge length on insert. RTF stores
|
|
10
|
+
* picture data as hexadecimal — two characters per byte — so an embedded image
|
|
11
|
+
* costs twice its file size in the document, which matters when that document
|
|
12
|
+
* has to fit through a transport with a size limit (an HL7 OBX-5 field, say).
|
|
13
|
+
* Resizing an image in the editor only changes its display size; this cap is
|
|
14
|
+
* what actually bounds the payload.
|
|
15
|
+
*/
|
|
16
|
+
/** Longest edge, in px, an inserted picture is scaled down to. */
|
|
17
|
+
export declare const DEFAULT_MAX_IMAGE_EDGE = 1600;
|
|
18
|
+
/**
|
|
19
|
+
* Encoded bytes an inserted picture is kept under. Each byte becomes two hex
|
|
20
|
+
* characters in the RTF, so this default costs about 1 MB of document per image.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_MAX_IMAGE_BYTES: number;
|
|
23
|
+
/**
|
|
24
|
+
* Width, in px, a newly inserted picture is displayed at unless the editor
|
|
25
|
+
* column is narrower. 624px × 15 twips = 9360 twips = 6.5in, the text width of
|
|
26
|
+
* a Letter page with 1in margins — so pictures arrive sized to the page they
|
|
27
|
+
* will be printed or filed on rather than to the width of the browser window.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_MAX_IMAGE_DISPLAY_WIDTH = 624;
|
|
30
|
+
/**
|
|
31
|
+
* Width a picture is first shown at: the smallest of its own size, the editor's
|
|
32
|
+
* column, and the page cap. Limits that are absent or zero are ignored, and an
|
|
33
|
+
* image is never enlarged beyond its natural size.
|
|
34
|
+
*/
|
|
35
|
+
export declare function initialDisplayWidth(naturalWidth: number, columnWidth: number, maxDisplayWidth: number): number;
|
|
36
|
+
export interface ImageLimits {
|
|
37
|
+
/** Longest edge in px; 0 or undefined leaves the dimensions alone. */
|
|
38
|
+
maxEdge?: number;
|
|
39
|
+
/** Encoded byte ceiling; 0 or undefined leaves the payload unbounded. */
|
|
40
|
+
maxBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Size an image is reduced to so neither edge exceeds `maxEdge`, keeping the
|
|
44
|
+
* aspect ratio. Images already within the cap are left alone — never upscaled.
|
|
45
|
+
*/
|
|
46
|
+
export declare function scaledSize(width: number, height: number, maxEdge: number): PixelSize;
|
|
47
|
+
interface PixelSize {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The image format a data URL actually holds, read from its leading bytes.
|
|
53
|
+
* The declared MIME type is not trustworthy: an operating system assigns
|
|
54
|
+
* `File.type` from the file extension, so a WebP saved as `.png` claims to be
|
|
55
|
+
* PNG, and a server can send the wrong `Content-Type`. Writing such bytes into
|
|
56
|
+
* RTF as `\pngblip` produces a picture nothing can decode, so the format is
|
|
57
|
+
* decided here and mislabelled images are pushed through the canvas re-encode.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sniffImageType(dataUrl: string): 'image/png' | 'image/jpeg' | null;
|
|
60
|
+
/** Decoded byte length of a base64 data URL, without decoding it. */
|
|
61
|
+
export declare function dataUrlByteLength(dataUrl: string): number;
|
|
62
|
+
/** Refuse anything that is not plainly an image reference (e.g. javascript:). */
|
|
63
|
+
export declare function isSafeImageUrl(url: string): boolean;
|
|
64
|
+
export declare function readFileAsDataUrl(file: Blob): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* Prepare a data URL for embedding: scale it down to `maxEdge` and re-encode it
|
|
67
|
+
* to PNG when its format cannot be carried by RTF. JPEG sources stay JPEG so
|
|
68
|
+
* photographs do not balloon; everything else becomes PNG. Animated images keep
|
|
69
|
+
* their first frame. Falls back to the original data URL when the runtime
|
|
70
|
+
* cannot rasterise, so nothing is lost if the conversion is unavailable.
|
|
71
|
+
*/
|
|
72
|
+
export declare function toRtfSafeDataUrl(dataUrl: string, limits?: ImageLimits): Promise<string>;
|
|
73
|
+
/** Read an image File into a data URL the RTF writer can embed. */
|
|
74
|
+
export declare function fileToImageSrc(file: File, limits?: ImageLimits): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a URL to an embeddable data URL. Remote images are fetched so they
|
|
77
|
+
* survive export; when the fetch is blocked (CORS, offline) the original URL is
|
|
78
|
+
* kept and the image still displays — it just exports as a text placeholder.
|
|
79
|
+
*/
|
|
80
|
+
export declare function urlToImageSrc(url: string, limits?: ImageLimits): Promise<{
|
|
81
|
+
src: string;
|
|
82
|
+
embedded: boolean;
|
|
83
|
+
}>;
|
|
84
|
+
/** "sunset-over-lake.png" → "sunset-over-lake" — a usable default alt text. */
|
|
85
|
+
export declare function stripExtension(filename: string): string;
|
|
86
|
+
export {};
|
package/dist/images.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image helpers for the editor.
|
|
3
|
+
*
|
|
4
|
+
* Images are stored inline as base64 data URLs so a document is self-contained:
|
|
5
|
+
* it can be auto-saved, exported to RTF (as a \pict group) and re-imported
|
|
6
|
+
* without depending on a server. RTF only carries PNG and JPEG bitmaps, so
|
|
7
|
+
* other formats (GIF, WebP, SVG, …) are re-encoded to PNG on insert.
|
|
8
|
+
*
|
|
9
|
+
* Pictures are also scaled down to a maximum edge length on insert. RTF stores
|
|
10
|
+
* picture data as hexadecimal — two characters per byte — so an embedded image
|
|
11
|
+
* costs twice its file size in the document, which matters when that document
|
|
12
|
+
* has to fit through a transport with a size limit (an HL7 OBX-5 field, say).
|
|
13
|
+
* Resizing an image in the editor only changes its display size; this cap is
|
|
14
|
+
* what actually bounds the payload.
|
|
15
|
+
*/
|
|
16
|
+
/** Longest edge, in px, an inserted picture is scaled down to. */
|
|
17
|
+
export const DEFAULT_MAX_IMAGE_EDGE = 1600;
|
|
18
|
+
/**
|
|
19
|
+
* Encoded bytes an inserted picture is kept under. Each byte becomes two hex
|
|
20
|
+
* characters in the RTF, so this default costs about 1 MB of document per image.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_MAX_IMAGE_BYTES = 512 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* Width, in px, a newly inserted picture is displayed at unless the editor
|
|
25
|
+
* column is narrower. 624px × 15 twips = 9360 twips = 6.5in, the text width of
|
|
26
|
+
* a Letter page with 1in margins — so pictures arrive sized to the page they
|
|
27
|
+
* will be printed or filed on rather than to the width of the browser window.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_MAX_IMAGE_DISPLAY_WIDTH = 624;
|
|
30
|
+
/**
|
|
31
|
+
* Width a picture is first shown at: the smallest of its own size, the editor's
|
|
32
|
+
* column, and the page cap. Limits that are absent or zero are ignored, and an
|
|
33
|
+
* image is never enlarged beyond its natural size.
|
|
34
|
+
*/
|
|
35
|
+
export function initialDisplayWidth(naturalWidth, columnWidth, maxDisplayWidth) {
|
|
36
|
+
const limits = [naturalWidth, columnWidth, maxDisplayWidth].filter((value) => value > 0);
|
|
37
|
+
return limits.length > 0 ? Math.round(Math.min(...limits)) : 0;
|
|
38
|
+
}
|
|
39
|
+
/** Successive reductions tried when a picture will not fit the byte budget. */
|
|
40
|
+
const SIZE_LADDER = [1, 0.8, 0.64, 0.5];
|
|
41
|
+
/** JPEG qualities tried at each size, best first. */
|
|
42
|
+
const QUALITY_LADDER = [0.85, 0.7];
|
|
43
|
+
/**
|
|
44
|
+
* Size an image is reduced to so neither edge exceeds `maxEdge`, keeping the
|
|
45
|
+
* aspect ratio. Images already within the cap are left alone — never upscaled.
|
|
46
|
+
*/
|
|
47
|
+
export function scaledSize(width, height, maxEdge) {
|
|
48
|
+
if (!width || !height || !maxEdge || maxEdge <= 0)
|
|
49
|
+
return { width, height };
|
|
50
|
+
const longest = Math.max(width, height);
|
|
51
|
+
if (longest <= maxEdge)
|
|
52
|
+
return { width, height };
|
|
53
|
+
const scale = maxEdge / longest;
|
|
54
|
+
return {
|
|
55
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
56
|
+
height: Math.max(1, Math.round(height * scale))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
60
|
+
/** Decode the first few bytes of a base64 payload — enough to identify a format. */
|
|
61
|
+
function decodePrefix(base64, byteCount) {
|
|
62
|
+
const bytes = [];
|
|
63
|
+
let buffer = 0;
|
|
64
|
+
let bits = 0;
|
|
65
|
+
for (let i = 0; i < base64.length && bytes.length < byteCount; i++) {
|
|
66
|
+
const value = B64_ALPHABET.indexOf(base64[i]);
|
|
67
|
+
if (value < 0)
|
|
68
|
+
continue;
|
|
69
|
+
buffer = (buffer << 6) | value;
|
|
70
|
+
bits += 6;
|
|
71
|
+
if (bits >= 8) {
|
|
72
|
+
bits -= 8;
|
|
73
|
+
bytes.push((buffer >> bits) & 0xff);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return new Uint8Array(bytes);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The image format a data URL actually holds, read from its leading bytes.
|
|
80
|
+
* The declared MIME type is not trustworthy: an operating system assigns
|
|
81
|
+
* `File.type` from the file extension, so a WebP saved as `.png` claims to be
|
|
82
|
+
* PNG, and a server can send the wrong `Content-Type`. Writing such bytes into
|
|
83
|
+
* RTF as `\pngblip` produces a picture nothing can decode, so the format is
|
|
84
|
+
* decided here and mislabelled images are pushed through the canvas re-encode.
|
|
85
|
+
*/
|
|
86
|
+
export function sniffImageType(dataUrl) {
|
|
87
|
+
const comma = dataUrl.indexOf(',');
|
|
88
|
+
if (comma < 0)
|
|
89
|
+
return null;
|
|
90
|
+
const b = decodePrefix(dataUrl.slice(comma + 1, comma + 33), 8);
|
|
91
|
+
if (b.length >= 8 &&
|
|
92
|
+
b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 &&
|
|
93
|
+
b[4] === 0x0d && b[5] === 0x0a && b[6] === 0x1a && b[7] === 0x0a) {
|
|
94
|
+
return 'image/png';
|
|
95
|
+
}
|
|
96
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
97
|
+
return 'image/jpeg';
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/** Decoded byte length of a base64 data URL, without decoding it. */
|
|
101
|
+
export function dataUrlByteLength(dataUrl) {
|
|
102
|
+
const comma = dataUrl.indexOf(',');
|
|
103
|
+
if (comma < 0)
|
|
104
|
+
return 0;
|
|
105
|
+
const body = dataUrl.slice(comma + 1);
|
|
106
|
+
const padding = body.endsWith('==') ? 2 : body.endsWith('=') ? 1 : 0;
|
|
107
|
+
return Math.max(0, Math.floor((body.length * 3) / 4) - padding);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether any pixel is not fully opaque. Only consulted when a PNG has to be
|
|
111
|
+
* considered for JPEG re-encoding, since that would flatten transparency.
|
|
112
|
+
*/
|
|
113
|
+
function hasTransparency(ctx, width, height) {
|
|
114
|
+
try {
|
|
115
|
+
const { data } = ctx.getImageData(0, 0, width, height);
|
|
116
|
+
for (let i = 3; i < data.length; i += 4) {
|
|
117
|
+
if (data[i] < 255)
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Assume transparency rather than risk flattening it away.
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Whether this runtime can rasterise to a canvas (it cannot under happy-dom/SSR). */
|
|
128
|
+
function canRasterize() {
|
|
129
|
+
if (typeof document === 'undefined')
|
|
130
|
+
return false;
|
|
131
|
+
try {
|
|
132
|
+
const canvas = document.createElement('canvas');
|
|
133
|
+
return typeof canvas.getContext === 'function' && !!canvas.getContext('2d');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Refuse anything that is not plainly an image reference (e.g. javascript:). */
|
|
140
|
+
export function isSafeImageUrl(url) {
|
|
141
|
+
const value = url.trim();
|
|
142
|
+
if (!value)
|
|
143
|
+
return false;
|
|
144
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(value))
|
|
145
|
+
return true;
|
|
146
|
+
try {
|
|
147
|
+
const base = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
|
|
148
|
+
const parsed = new URL(value, base);
|
|
149
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'blob:';
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function readFileAsDataUrl(file) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const reader = new FileReader();
|
|
158
|
+
reader.onload = () => resolve(String(reader.result || ''));
|
|
159
|
+
reader.onerror = () => reject(new Error('Could not read the image file'));
|
|
160
|
+
reader.readAsDataURL(file);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function loadImage(src) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const img = new Image();
|
|
166
|
+
img.onload = () => resolve(img);
|
|
167
|
+
img.onerror = () => reject(new Error('Could not decode the image'));
|
|
168
|
+
img.src = src;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Prepare a data URL for embedding: scale it down to `maxEdge` and re-encode it
|
|
173
|
+
* to PNG when its format cannot be carried by RTF. JPEG sources stay JPEG so
|
|
174
|
+
* photographs do not balloon; everything else becomes PNG. Animated images keep
|
|
175
|
+
* their first frame. Falls back to the original data URL when the runtime
|
|
176
|
+
* cannot rasterise, so nothing is lost if the conversion is unavailable.
|
|
177
|
+
*/
|
|
178
|
+
export async function toRtfSafeDataUrl(dataUrl, limits = {}) {
|
|
179
|
+
const { maxEdge = DEFAULT_MAX_IMAGE_EDGE, maxBytes = DEFAULT_MAX_IMAGE_BYTES } = limits;
|
|
180
|
+
// The bytes decide the format, not the declared type — a mislabelled image
|
|
181
|
+
// must not take the early return that hands the original bytes back.
|
|
182
|
+
const actual = sniffImageType(dataUrl);
|
|
183
|
+
const isJpeg = actual === 'image/jpeg';
|
|
184
|
+
const isNative = actual !== null;
|
|
185
|
+
if (!canRasterize())
|
|
186
|
+
return dataUrl;
|
|
187
|
+
try {
|
|
188
|
+
const img = await loadImage(dataUrl);
|
|
189
|
+
const width = img.naturalWidth || img.width;
|
|
190
|
+
const height = img.naturalHeight || img.height;
|
|
191
|
+
if (!width || !height)
|
|
192
|
+
return dataUrl;
|
|
193
|
+
const target = scaledSize(width, height, maxEdge);
|
|
194
|
+
const fits = (bytes) => !maxBytes || maxBytes <= 0 || bytes <= maxBytes;
|
|
195
|
+
// Already embeddable, within the pixel cap and within budget — leave the
|
|
196
|
+
// original bytes alone rather than losing quality to a pointless re-encode.
|
|
197
|
+
if (isNative &&
|
|
198
|
+
target.width === width &&
|
|
199
|
+
target.height === height &&
|
|
200
|
+
fits(dataUrlByteLength(dataUrl))) {
|
|
201
|
+
return dataUrl;
|
|
202
|
+
}
|
|
203
|
+
const canvas = document.createElement('canvas');
|
|
204
|
+
const ctx = canvas.getContext('2d');
|
|
205
|
+
if (!ctx)
|
|
206
|
+
return dataUrl;
|
|
207
|
+
let opaque = null;
|
|
208
|
+
let smallest = '';
|
|
209
|
+
// Shrink in steps until the encoded picture fits the budget. At each size a
|
|
210
|
+
// PNG source is tried as PNG first — screenshots and diagrams stay sharp and
|
|
211
|
+
// usually compress well — and only falls back to JPEG when PNG is too heavy
|
|
212
|
+
// and the image has no transparency to lose.
|
|
213
|
+
for (const scale of SIZE_LADDER) {
|
|
214
|
+
const w = Math.max(1, Math.round(target.width * scale));
|
|
215
|
+
const h = Math.max(1, Math.round(target.height * scale));
|
|
216
|
+
canvas.width = w;
|
|
217
|
+
canvas.height = h;
|
|
218
|
+
ctx.clearRect(0, 0, w, h);
|
|
219
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
220
|
+
const candidates = [];
|
|
221
|
+
if (!isJpeg)
|
|
222
|
+
candidates.push(canvas.toDataURL('image/png'));
|
|
223
|
+
if (isJpeg || !fits(dataUrlByteLength(candidates[0] ?? ''))) {
|
|
224
|
+
if (opaque === null)
|
|
225
|
+
opaque = !hasTransparency(ctx, w, h);
|
|
226
|
+
if (isJpeg || opaque) {
|
|
227
|
+
for (const quality of QUALITY_LADDER) {
|
|
228
|
+
candidates.push(canvas.toDataURL('image/jpeg', quality));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const candidate of candidates) {
|
|
233
|
+
const bytes = dataUrlByteLength(candidate);
|
|
234
|
+
if (fits(bytes))
|
|
235
|
+
return candidate;
|
|
236
|
+
if (!smallest || bytes < dataUrlByteLength(smallest))
|
|
237
|
+
smallest = candidate;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Nothing fit — hand back the smallest we managed rather than the original.
|
|
241
|
+
return smallest || dataUrl;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return dataUrl;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Read an image File into a data URL the RTF writer can embed. */
|
|
248
|
+
export async function fileToImageSrc(file, limits = {}) {
|
|
249
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
250
|
+
if (!dataUrl.startsWith('data:image/')) {
|
|
251
|
+
throw new Error(`${file.name || 'File'} is not an image`);
|
|
252
|
+
}
|
|
253
|
+
return toRtfSafeDataUrl(dataUrl, limits);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a URL to an embeddable data URL. Remote images are fetched so they
|
|
257
|
+
* survive export; when the fetch is blocked (CORS, offline) the original URL is
|
|
258
|
+
* kept and the image still displays — it just exports as a text placeholder.
|
|
259
|
+
*/
|
|
260
|
+
export async function urlToImageSrc(url, limits = {}) {
|
|
261
|
+
const value = url.trim();
|
|
262
|
+
if (value.startsWith('data:image/')) {
|
|
263
|
+
return { src: await toRtfSafeDataUrl(value, limits), embedded: true };
|
|
264
|
+
}
|
|
265
|
+
// A picture that could not be inlined still displays, but it leaves the
|
|
266
|
+
// document as a text placeholder instead of picture data. The caller is told
|
|
267
|
+
// so it can say as much, rather than letting the image vanish at export.
|
|
268
|
+
try {
|
|
269
|
+
const response = await fetch(value, { mode: 'cors' });
|
|
270
|
+
if (!response.ok)
|
|
271
|
+
return { src: value, embedded: false };
|
|
272
|
+
const blob = await response.blob();
|
|
273
|
+
if (!blob.type.startsWith('image/'))
|
|
274
|
+
return { src: value, embedded: false };
|
|
275
|
+
const dataUrl = await readFileAsDataUrl(blob);
|
|
276
|
+
const src = await toRtfSafeDataUrl(dataUrl, limits);
|
|
277
|
+
return { src, embedded: src.startsWith('data:') };
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return { src: value, embedded: false };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** "sunset-over-lake.png" → "sunset-over-lake" — a usable default alt text. */
|
|
284
|
+
export function stripExtension(filename) {
|
|
285
|
+
return filename.replace(/\.[^.]+$/, '');
|
|
286
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { default as RtfViewer } from './RtfViewer.svelte';
|
|
2
2
|
export { default as InkEditor } from './components/InkEditor.svelte';
|
|
3
3
|
export { rtfToHtml, readRtfFile } from './rtf-parser.js';
|
|
4
|
-
export { htmlToRtf } from './rtf-writer.js';
|
|
4
|
+
export { htmlToRtf, estimateRtfBytes } from './rtf-writer.js';
|
|
5
5
|
export { htmlToMarkdown, downloadFile } from './utils.js';
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,5 @@ export { default as RtfViewer } from './RtfViewer.svelte';
|
|
|
3
3
|
export { default as InkEditor } from './components/InkEditor.svelte';
|
|
4
4
|
// RTF utilities
|
|
5
5
|
export { rtfToHtml, readRtfFile } from './rtf-parser.js';
|
|
6
|
-
export { htmlToRtf } from './rtf-writer.js';
|
|
6
|
+
export { htmlToRtf, estimateRtfBytes } from './rtf-writer.js';
|
|
7
7
|
export { htmlToMarkdown, downloadFile } from './utils.js';
|
package/dist/rtf-parser.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Parses RTF control words and groups into an HTML string.
|
|
5
5
|
* Supports: bold, italic, underline, strikethrough, super/subscript,
|
|
6
6
|
* font size, foreground color (\cf), highlight background color (\highlight),
|
|
7
|
-
* color table, font table, paragraphs, bullet/numbered lists,
|
|
7
|
+
* color table, font table, paragraphs, bullet/numbered lists, tables,
|
|
8
|
+
* embedded pictures (\pict PNG/JPEG blips, with captions),
|
|
8
9
|
* Unicode characters, hex escapes, and nested groups.
|
|
9
10
|
*
|
|
10
11
|
* \cb (character background) is silently ignored on import.
|