reamkit 1.0.2 → 1.1.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 +3 -2
- package/dist/esm/core/bytes.d.ts +1 -0
- package/dist/esm/core/bytes.js +6 -1
- package/dist/esm/core/converter/facade.d.ts +2 -2
- package/dist/esm/core/converter/facade.js +12 -0
- package/dist/esm/core/converter/ream.d.ts +1 -1
- package/dist/esm/core/converter/ream.js +10 -0
- package/dist/esm/html/html-writer.d.ts +4 -0
- package/dist/esm/html/html-writer.js +292 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/svg/svg-writer.js +1 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ const bytes = new Uint8Array(await file.arrayBuffer());
|
|
|
33
33
|
const doc = Ream.parse(bytes); // docx or xlsx — sniffed
|
|
34
34
|
const pdf = await doc.convert('pdf'); // async — fetches a font if needed
|
|
35
35
|
const svg = await doc.convert('svg'); // same parse, different target
|
|
36
|
+
const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
|
|
36
37
|
|
|
37
38
|
// Hand the bytes to the browser: preview, download, upload, …
|
|
38
39
|
const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }));
|
|
@@ -147,7 +148,7 @@ automatically from the document's `docProps/core.xml`), `attachments`
|
|
|
147
148
|
|
|
148
149
|
### Lower-level APIs
|
|
149
150
|
|
|
150
|
-
- `docxReader` / `xlsxReader`, `svgWriter` — the `@experimental` reader/writer
|
|
151
|
+
- `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter` — the `@experimental` reader/writer
|
|
151
152
|
interfaces of the interlayer, for building custom pipelines (and keeping
|
|
152
153
|
unused formats out of your bundle); `layoutStyledDocument` produces the
|
|
153
154
|
frozen page model (`PageItem` pages in a top-left `Pt` frame) they consume.
|
|
@@ -163,7 +164,7 @@ repeated titles, page breaks), DrawingML shapes and charts, OMML math,
|
|
|
163
164
|
Type0+CIDFontType2 embedding with subsetting, Knuth-Plass line breaking,
|
|
164
165
|
Liang hyphenation, OpenType ligatures/kerning + Arabic cursive joining,
|
|
165
166
|
BiDi (UAX #9), tagged PDF, PDF/A-1/2/3 (a/b/u), digital signatures
|
|
166
|
-
(PKCS#7/ECDSA/PAdES/RFC 3161), SVG page preview.
|
|
167
|
+
(PKCS#7/ECDSA/PAdES/RFC 3161), SVG page preview, flowed HTML export.
|
|
167
168
|
|
|
168
169
|
See `handoff.md` for the full feature matrix and known limitations.
|
|
169
170
|
|
package/dist/esm/core/bytes.d.ts
CHANGED
package/dist/esm/core/bytes.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//#region src/core/bytes.ts
|
|
2
|
+
function toBase64(bytes) {
|
|
3
|
+
let bin = "";
|
|
4
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
5
|
+
return btoa(bin);
|
|
6
|
+
}
|
|
2
7
|
function bytesInclude(haystack, needle) {
|
|
3
8
|
const n = new TextEncoder().encode(needle);
|
|
4
9
|
outer: for (let i = 0; i + n.length <= haystack.length; i++) {
|
|
@@ -8,4 +13,4 @@ function bytesInclude(haystack, needle) {
|
|
|
8
13
|
return false;
|
|
9
14
|
}
|
|
10
15
|
//#endregion
|
|
11
|
-
export { bytesInclude };
|
|
16
|
+
export { bytesInclude, toBase64 };
|
|
@@ -5,8 +5,8 @@ import { FontProvider } from '../fonts/provider.js';
|
|
|
5
5
|
import { Loss, LossReport } from '../ir/index.js';
|
|
6
6
|
import { FontBytesByVariant } from '../font/index.js';
|
|
7
7
|
export interface ConvertOptions extends ConvertDocxOptions {
|
|
8
|
-
/** Target
|
|
9
|
-
readonly to?: 'pdf' | 'svg';
|
|
8
|
+
/** Target: 'pdf' (default), 'svg' (page-stack preview) or 'html' (flowed). */
|
|
9
|
+
readonly to?: 'pdf' | 'svg' | 'html';
|
|
10
10
|
/**
|
|
11
11
|
* Strict mode (handoff v1 §5): throw ConversionLossError on the first
|
|
12
12
|
* recorded loss instead of returning it in the report.
|
|
@@ -6,6 +6,7 @@ import { docxReader } from "../../word/docx-reader.js";
|
|
|
6
6
|
import { xlsxReader } from "../../excel/xlsx-reader.js";
|
|
7
7
|
import { chainProviders } from "../fonts/provider.js";
|
|
8
8
|
import { flowRenderOptions } from "./project.js";
|
|
9
|
+
import { writeHtml } from "../../html/html-writer.js";
|
|
9
10
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
10
11
|
import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
|
|
11
12
|
import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
|
|
@@ -19,6 +20,17 @@ function createConverter(opts = {}) {
|
|
|
19
20
|
const reader = detect(bytes);
|
|
20
21
|
if (!reader) throw new Error("Unrecognized input format (no registered reader sniffs these bytes)");
|
|
21
22
|
const losses = [];
|
|
23
|
+
if (to === "html") {
|
|
24
|
+
const { doc: flow, losses: readLosses } = reader.read(bytes);
|
|
25
|
+
losses.push(...readLosses);
|
|
26
|
+
const html = writeHtml(flow);
|
|
27
|
+
losses.push(...html.losses);
|
|
28
|
+
if (strict && losses.length > 0) throw new ConversionLossError(losses[0]);
|
|
29
|
+
return {
|
|
30
|
+
bytes: html.bytes,
|
|
31
|
+
losses
|
|
32
|
+
};
|
|
33
|
+
}
|
|
22
34
|
let conv = rest;
|
|
23
35
|
if (fontProviders && fontProviders.length > 0 && !rest.fonts && !rest.fontBytes) {
|
|
24
36
|
const { fonts, loss } = await resolveFontsViaChain(fontProviders);
|
|
@@ -6,7 +6,7 @@ import { Loss } from '../ir/index.js';
|
|
|
6
6
|
import { DocumentReader } from '../ir/adapters.js';
|
|
7
7
|
import { FlowDoc } from '../ir/flow.js';
|
|
8
8
|
import { SignatureOptions, StyledRenderOptions } from '../../pdf/index.js';
|
|
9
|
-
export type ReamTarget = 'pdf' | 'svg';
|
|
9
|
+
export type ReamTarget = 'pdf' | 'svg' | 'html';
|
|
10
10
|
export interface ReamParseOptions {
|
|
11
11
|
readonly readers?: ReadonlyArray<DocumentReader>;
|
|
12
12
|
}
|
|
@@ -6,6 +6,7 @@ import { signPdf } from "../../pdf/signature.js";
|
|
|
6
6
|
import { renderStyledPdf } from "../../pdf/styled-page-renderer.js";
|
|
7
7
|
import "../../pdf/index.js";
|
|
8
8
|
import { flowRenderOptions } from "./project.js";
|
|
9
|
+
import { writeHtml } from "../../html/html-writer.js";
|
|
9
10
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
10
11
|
import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
|
|
11
12
|
import { DEFAULT_READERS, resolveFontsViaChain } from "./facade.js";
|
|
@@ -36,6 +37,15 @@ var Ream = class Ream {
|
|
|
36
37
|
}
|
|
37
38
|
async convertWithReport(to, options = {}) {
|
|
38
39
|
const losses = [...this.losses];
|
|
40
|
+
if (to === "html") {
|
|
41
|
+
const html = writeHtml(this.flow);
|
|
42
|
+
losses.push(...html.losses);
|
|
43
|
+
this.enforceStrict(options, losses);
|
|
44
|
+
return {
|
|
45
|
+
bytes: html.bytes,
|
|
46
|
+
losses
|
|
47
|
+
};
|
|
48
|
+
}
|
|
39
49
|
const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
|
|
40
50
|
const registry = FontRegistry.fromBytes(fonts);
|
|
41
51
|
if (to === "svg") {
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { FEATURES } from "../core/ir/features.js";
|
|
2
|
+
import { detectImageFormat } from "../core/images.js";
|
|
3
|
+
import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
|
|
4
|
+
import "../core/style-cascade/index.js";
|
|
5
|
+
import { toBase64 } from "../core/bytes.js";
|
|
6
|
+
//#region src/html/html-writer.ts
|
|
7
|
+
function writeHtml(flow) {
|
|
8
|
+
const losses = [];
|
|
9
|
+
const out = [];
|
|
10
|
+
const ctx = {
|
|
11
|
+
resources: flow.resources,
|
|
12
|
+
losses
|
|
13
|
+
};
|
|
14
|
+
const lang = flow.language ?? "en-US";
|
|
15
|
+
const title = flow.info?.title ?? "Document";
|
|
16
|
+
out.push("<!DOCTYPE html>");
|
|
17
|
+
out.push(`<html lang="${escapeAttr(lang)}">`);
|
|
18
|
+
out.push("<head>");
|
|
19
|
+
out.push("<meta charset=\"utf-8\"/>");
|
|
20
|
+
out.push("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>");
|
|
21
|
+
out.push(`<title>${escapeText(title)}</title>`);
|
|
22
|
+
out.push(`<style>${BASE_CSS}</style>`);
|
|
23
|
+
out.push("</head>");
|
|
24
|
+
out.push("<body>");
|
|
25
|
+
out.push(`<article style="max-width: ${fmt(contentWidthPt(flow))}pt">`);
|
|
26
|
+
if (flow.headersFooters && flow.headersFooters.size > 0) losses.push({
|
|
27
|
+
severity: "dropped",
|
|
28
|
+
feature: FEATURES.headersFooters,
|
|
29
|
+
detail: "headers/footers have no fixed pages to band onto in flowed HTML"
|
|
30
|
+
});
|
|
31
|
+
for (const el of flow.body) emitBlock(out, el, ctx);
|
|
32
|
+
out.push("</article>");
|
|
33
|
+
out.push("</body>");
|
|
34
|
+
out.push("</html>");
|
|
35
|
+
return {
|
|
36
|
+
bytes: new TextEncoder().encode(out.join("\n")),
|
|
37
|
+
losses
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var htmlWriter = {
|
|
41
|
+
id: "html",
|
|
42
|
+
consumes: "flow",
|
|
43
|
+
supports: new Set([
|
|
44
|
+
FEATURES.text,
|
|
45
|
+
FEATURES.tables,
|
|
46
|
+
FEATURES.tablesNested,
|
|
47
|
+
FEATURES.lists,
|
|
48
|
+
FEATURES.images,
|
|
49
|
+
FEATURES.rtl,
|
|
50
|
+
FEATURES.trackedChanges
|
|
51
|
+
]),
|
|
52
|
+
write: (doc) => writeHtml(doc)
|
|
53
|
+
};
|
|
54
|
+
var BASE_CSS = [
|
|
55
|
+
"body{margin:24pt;color:#000;background:#fff}",
|
|
56
|
+
"article{margin:0 auto}",
|
|
57
|
+
"p,h1,h2,h3,h4,h5,h6,figure{margin:0}",
|
|
58
|
+
"table{border-collapse:collapse}",
|
|
59
|
+
"td,th{vertical-align:top}",
|
|
60
|
+
"th{text-align:inherit}",
|
|
61
|
+
".tab{display:inline-block;min-width:18pt}",
|
|
62
|
+
"img{vertical-align:baseline}"
|
|
63
|
+
].join("");
|
|
64
|
+
function emitBlock(out, el, ctx) {
|
|
65
|
+
if (el.kind === "paragraph") emitParagraph(out, el.paragraph, ctx);
|
|
66
|
+
else if (el.kind === "table") emitTable(out, el.table, ctx);
|
|
67
|
+
else if (el.kind === "image") emitImageBlock(out, el.image, ctx);
|
|
68
|
+
else if (el.kind === "chart") ctx.losses.push({
|
|
69
|
+
severity: "dropped",
|
|
70
|
+
feature: FEATURES.charts,
|
|
71
|
+
detail: "chart not rendered by the HTML writer (v0)"
|
|
72
|
+
});
|
|
73
|
+
else {
|
|
74
|
+
ctx.losses.push({
|
|
75
|
+
severity: "dropped",
|
|
76
|
+
feature: FEATURES.shapes,
|
|
77
|
+
detail: "shape geometry not rendered by the HTML writer (v0)"
|
|
78
|
+
});
|
|
79
|
+
if (el.shape.text) {
|
|
80
|
+
out.push("<div>");
|
|
81
|
+
for (const child of el.shape.text.content) emitBlock(out, child, ctx);
|
|
82
|
+
out.push("</div>");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function emitParagraph(out, p, ctx) {
|
|
87
|
+
const resolved = resolveParagraphProperties(p.properties, EMPTY_STYLE_SHEET);
|
|
88
|
+
const lvl = resolved.outlineLevel;
|
|
89
|
+
const tag = lvl !== void 0 && lvl >= 0 && lvl <= 8 ? `h${Math.min(lvl, 5) + 1}` : "p";
|
|
90
|
+
const style = paragraphCss(resolved);
|
|
91
|
+
const open = `<${tag}${resolved.bidi ? " dir=\"rtl\"" : ""}${style ? ` style="${style}"` : ""}>`;
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const run of p.runs) parts.push(runHtml(run, p, ctx));
|
|
94
|
+
out.push(`${open}${parts.join("") || " "}</${tag}>`);
|
|
95
|
+
}
|
|
96
|
+
function paragraphCss(r) {
|
|
97
|
+
const css = [];
|
|
98
|
+
if (r.alignment === "center" || r.alignment === "right") css.push(`text-align:${r.alignment}`);
|
|
99
|
+
else if (r.alignment === "both" || r.alignment === "distribute") css.push("text-align:justify");
|
|
100
|
+
if (r.spacingBefore > 0) css.push(`margin-top:${fmt(r.spacingBefore)}pt`);
|
|
101
|
+
if (r.spacingAfter > 0) css.push(`margin-bottom:${fmt(r.spacingAfter)}pt`);
|
|
102
|
+
if (r.indentLeft !== 0) css.push(`margin-left:${fmt(r.indentLeft)}pt`);
|
|
103
|
+
if (r.indentRight !== 0) css.push(`margin-right:${fmt(r.indentRight)}pt`);
|
|
104
|
+
if (r.indentFirstLine !== 0) css.push(`text-indent:${fmt(r.indentFirstLine)}pt`);
|
|
105
|
+
if (r.spacingLine > 0) if (r.spacingLineRule === "auto") css.push(`line-height:${fmt(r.spacingLine / 12)}`);
|
|
106
|
+
else css.push(`line-height:${fmt(r.spacingLine)}pt`);
|
|
107
|
+
if (r.pageBreakBefore) css.push("break-before:page");
|
|
108
|
+
return css.join(";");
|
|
109
|
+
}
|
|
110
|
+
function runHtml(run, p, ctx) {
|
|
111
|
+
if (run.math !== void 0) {
|
|
112
|
+
ctx.losses.push({
|
|
113
|
+
severity: "dropped",
|
|
114
|
+
feature: FEATURES.math,
|
|
115
|
+
detail: "inline math not rendered by the HTML writer (v0)"
|
|
116
|
+
});
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
if (run.inlineImage) {
|
|
120
|
+
const img = run.inlineImage;
|
|
121
|
+
const src = dataUri(img.resource, ctx.resources);
|
|
122
|
+
if (!src) return "";
|
|
123
|
+
return `<img src="${src}" style="width:${fmt(img.width)}pt;height:${fmt(img.height)}pt" alt=""/>`;
|
|
124
|
+
}
|
|
125
|
+
if (run.text.length === 0) return "";
|
|
126
|
+
const resolved = resolveRunProperties(run.properties, p.properties, EMPTY_STYLE_SHEET);
|
|
127
|
+
const style = runCss(resolved);
|
|
128
|
+
let html = `<span${resolved.rtl ? " dir=\"rtl\"" : ""}${style ? ` style="${style}"` : ""}>${textHtml(run.text)}</span>`;
|
|
129
|
+
if (resolved.verticalAlign === "superscript") html = `<sup>${html}</sup>`;
|
|
130
|
+
else if (resolved.verticalAlign === "subscript") html = `<sub>${html}</sub>`;
|
|
131
|
+
return html;
|
|
132
|
+
}
|
|
133
|
+
function runCss(r) {
|
|
134
|
+
const css = [];
|
|
135
|
+
const family = r.fontFamily.ascii;
|
|
136
|
+
if (family) css.push(`font-family:${JSON.stringify(family)}`);
|
|
137
|
+
css.push(`font-size:${fmt(r.fontSizePt)}pt`);
|
|
138
|
+
if (r.bold) css.push("font-weight:700");
|
|
139
|
+
if (r.italic) css.push("font-style:italic");
|
|
140
|
+
const deco = [];
|
|
141
|
+
const underlined = r.underline !== "none";
|
|
142
|
+
if (underlined) deco.push("underline");
|
|
143
|
+
if (r.strike) deco.push("line-through");
|
|
144
|
+
if (deco.length > 0) {
|
|
145
|
+
css.push(`text-decoration-line:${deco.join(" ")}`);
|
|
146
|
+
const style = underlined ? decorationStyle(r.underline) : void 0;
|
|
147
|
+
if (style) css.push(`text-decoration-style:${style}`);
|
|
148
|
+
}
|
|
149
|
+
if (r.colorHex !== "000000") css.push(`color:#${r.colorHex}`);
|
|
150
|
+
return css.join(";");
|
|
151
|
+
}
|
|
152
|
+
function decorationStyle(u) {
|
|
153
|
+
switch (u) {
|
|
154
|
+
case "double": return "double";
|
|
155
|
+
case "dotted":
|
|
156
|
+
case "dottedHeavy": return "dotted";
|
|
157
|
+
case "dash":
|
|
158
|
+
case "dashHeavy": return "dashed";
|
|
159
|
+
case "wave": return "wavy";
|
|
160
|
+
default: return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function textHtml(text) {
|
|
164
|
+
return escapeText(text).replaceAll(" ", "<span class=\"tab\"></span>").replaceAll("\n", "<br/>");
|
|
165
|
+
}
|
|
166
|
+
function emitTable(out, table, ctx) {
|
|
167
|
+
const css = [];
|
|
168
|
+
if (table.properties.widthPt !== void 0) css.push(`width:${fmt(table.properties.widthPt)}pt`);
|
|
169
|
+
else if (table.properties.widthFraction !== void 0) css.push(`width:${fmt(table.properties.widthFraction * 100)}%`);
|
|
170
|
+
if (table.properties.alignment === "center") css.push("margin-left:auto;margin-right:auto");
|
|
171
|
+
else if (table.properties.alignment === "right") css.push("margin-left:auto");
|
|
172
|
+
out.push(`<table${css.length > 0 ? ` style="${css.join(";")}"` : ""}>`);
|
|
173
|
+
if (table.grid.length > 0) {
|
|
174
|
+
out.push("<colgroup>");
|
|
175
|
+
for (const w of table.grid) out.push(`<col style="width:${fmt(w)}pt"/>`);
|
|
176
|
+
out.push("</colgroup>");
|
|
177
|
+
}
|
|
178
|
+
const colStarts = table.rows.map((row) => {
|
|
179
|
+
let col = 0;
|
|
180
|
+
return row.cells.map((cell) => {
|
|
181
|
+
const start = col;
|
|
182
|
+
col += cell.properties.colSpan ?? 1;
|
|
183
|
+
return start;
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
for (let ri = 0; ri < table.rows.length; ri++) {
|
|
187
|
+
const row = table.rows[ri];
|
|
188
|
+
out.push("<tr>");
|
|
189
|
+
for (let ci = 0; ci < row.cells.length; ci++) {
|
|
190
|
+
const cell = row.cells[ci];
|
|
191
|
+
const merge = cell.properties.merge;
|
|
192
|
+
if (merge === "middle" || merge === "end") continue;
|
|
193
|
+
const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, colStarts[ri][ci]) : 1;
|
|
194
|
+
emitCell(out, cell, table, ctx, {
|
|
195
|
+
isHeader: row.properties.isHeader === true,
|
|
196
|
+
firstRow: ri === 0,
|
|
197
|
+
lastRow: ri === table.rows.length - 1,
|
|
198
|
+
firstCol: colStarts[ri][ci] === 0,
|
|
199
|
+
lastCol: colStarts[ri][ci] + (cell.properties.colSpan ?? 1) >= table.grid.length,
|
|
200
|
+
rowSpan
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
out.push("</tr>");
|
|
204
|
+
}
|
|
205
|
+
out.push("</table>");
|
|
206
|
+
}
|
|
207
|
+
function mergeRowSpan(table, colStarts, startRow, colStart) {
|
|
208
|
+
let span = 1;
|
|
209
|
+
for (let r = startRow + 1; r < table.rows.length; r++) {
|
|
210
|
+
const row = table.rows[r];
|
|
211
|
+
const idx = colStarts[r].findIndex((c) => c === colStart);
|
|
212
|
+
const merge = (idx >= 0 ? row.cells[idx] : void 0)?.properties.merge;
|
|
213
|
+
if (merge !== "middle" && merge !== "end") break;
|
|
214
|
+
span++;
|
|
215
|
+
if (merge === "end") break;
|
|
216
|
+
}
|
|
217
|
+
return span;
|
|
218
|
+
}
|
|
219
|
+
function emitCell(out, cell, table, ctx, pos) {
|
|
220
|
+
const tag = pos.isHeader ? "th" : "td";
|
|
221
|
+
const attrs = [];
|
|
222
|
+
const colSpan = cell.properties.colSpan ?? 1;
|
|
223
|
+
if (colSpan > 1) attrs.push(`colspan="${colSpan}"`);
|
|
224
|
+
if (pos.rowSpan > 1) attrs.push(`rowspan="${pos.rowSpan}"`);
|
|
225
|
+
const css = [];
|
|
226
|
+
const t = table.properties.borders;
|
|
227
|
+
const c = cell.properties.borders;
|
|
228
|
+
pushBorder(css, "top", c?.top ?? (pos.firstRow ? t?.top : t?.insideH));
|
|
229
|
+
pushBorder(css, "bottom", c?.bottom ?? (pos.lastRow ? t?.bottom : t?.insideH));
|
|
230
|
+
pushBorder(css, "left", c?.left ?? (pos.firstCol ? t?.left : t?.insideV));
|
|
231
|
+
pushBorder(css, "right", c?.right ?? (pos.lastCol ? t?.right : t?.insideV));
|
|
232
|
+
if (cell.properties.shading) css.push(`background-color:#${cell.properties.shading.colorHex}`);
|
|
233
|
+
const margins = cell.properties.margins ?? table.properties.defaultCellMargins;
|
|
234
|
+
const padTop = margins?.top ?? 0;
|
|
235
|
+
const padRight = margins?.right ?? 5.4;
|
|
236
|
+
const padBottom = margins?.bottom ?? 0;
|
|
237
|
+
const padLeft = margins?.left ?? 5.4;
|
|
238
|
+
css.push(`padding:${fmt(padTop)}pt ${fmt(padRight)}pt ${fmt(padBottom)}pt ${fmt(padLeft)}pt`);
|
|
239
|
+
out.push(`<${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""} style="${css.join(";")}">`);
|
|
240
|
+
for (const child of cell.content) emitBlock(out, child, ctx);
|
|
241
|
+
out.push(`</${tag}>`);
|
|
242
|
+
}
|
|
243
|
+
function pushBorder(css, side, border) {
|
|
244
|
+
if (!border || border.style === "none") return;
|
|
245
|
+
const width = border.width ?? .5;
|
|
246
|
+
const style = border.style === "double" ? "double" : border.style === "dotted" ? "dotted" : border.style === "dashed" ? "dashed" : "solid";
|
|
247
|
+
css.push(`border-${side}:${fmt(width)}pt ${style} #${border.colorHex ?? "000000"}`);
|
|
248
|
+
}
|
|
249
|
+
function emitImageBlock(out, image, ctx) {
|
|
250
|
+
const src = dataUri(image.resource, ctx.resources);
|
|
251
|
+
if (!src) return;
|
|
252
|
+
const pp = image.paragraphProperties;
|
|
253
|
+
const align = pp.alignment === "center" || pp.alignment === "right" ? pp.alignment : void 0;
|
|
254
|
+
const css = [];
|
|
255
|
+
if (align) css.push(`text-align:${align}`);
|
|
256
|
+
if (pp.spacingBefore !== void 0 && pp.spacingBefore > 0) css.push(`margin-top:${fmt(pp.spacingBefore)}pt`);
|
|
257
|
+
if (pp.spacingAfter !== void 0 && pp.spacingAfter > 0) css.push(`margin-bottom:${fmt(pp.spacingAfter)}pt`);
|
|
258
|
+
const alt = image.altText ?? "";
|
|
259
|
+
out.push(`<figure${css.length > 0 ? ` style="${css.join(";")}"` : ""}><img src="${src}" style="width:${fmt(image.width)}pt;height:${fmt(image.height)}pt" alt="${escapeAttr(alt)}"/></figure>`);
|
|
260
|
+
}
|
|
261
|
+
var MIME_BY_FORMAT = {
|
|
262
|
+
jpeg: "image/jpeg",
|
|
263
|
+
png: "image/png",
|
|
264
|
+
jpeg2000: "image/jp2"
|
|
265
|
+
};
|
|
266
|
+
function dataUri(resource, store) {
|
|
267
|
+
if (resource === void 0) return void 0;
|
|
268
|
+
const bytes = store.get(resource);
|
|
269
|
+
if (!bytes) return void 0;
|
|
270
|
+
const format = detectImageFormat(bytes);
|
|
271
|
+
if (!format) return void 0;
|
|
272
|
+
return `data:${MIME_BY_FORMAT[format]};base64,${toBase64(bytes)}`;
|
|
273
|
+
}
|
|
274
|
+
function contentWidthPt(flow) {
|
|
275
|
+
const props = flow.sections[0]?.properties ?? flow.section;
|
|
276
|
+
const pageWidth = props?.pageSize?.width ?? 595;
|
|
277
|
+
const left = props?.margins?.left ?? 72;
|
|
278
|
+
const right = props?.margins?.right ?? 72;
|
|
279
|
+
return Math.max(72, pageWidth - left - right);
|
|
280
|
+
}
|
|
281
|
+
function escapeText(s) {
|
|
282
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
283
|
+
}
|
|
284
|
+
function escapeAttr(s) {
|
|
285
|
+
return escapeText(s).replaceAll("\"", """);
|
|
286
|
+
}
|
|
287
|
+
function fmt(n) {
|
|
288
|
+
const r = Math.round(n * 100) / 100;
|
|
289
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
export { htmlWriter, writeHtml };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -23,5 +23,6 @@ export { chainProviders, callerFontProvider, embeddedDocFontProvider, remoteFont
|
|
|
23
23
|
export type { FontProvider, FontRequest, FontAnswer } from './core/fonts/provider.js';
|
|
24
24
|
export { svgWriter, writeSvg } from './svg/svg-writer.js';
|
|
25
25
|
export type { SvgWriteOptions } from './svg/svg-writer.js';
|
|
26
|
+
export { htmlWriter, writeHtml } from './html/html-writer.js';
|
|
26
27
|
export { layoutStyledDocument } from './layout/styled-layout.js';
|
|
27
28
|
export type { LaidOutDocument } from './layout/page-doc.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -15,7 +15,8 @@ import { createLanguageHyphenator, getHyphenator } from "./core/hyphenation/inde
|
|
|
15
15
|
import { docxReader, readDocx } from "./word/docx-reader.js";
|
|
16
16
|
import { readXlsx, xlsxReader } from "./excel/xlsx-reader.js";
|
|
17
17
|
import { NO_FONT, callerFontProvider, chainProviders, embeddedDocFontProvider, isEmbeddingRestricted, localFontProvider, readOs2FsType, remoteFontProvider } from "./core/fonts/provider.js";
|
|
18
|
+
import { htmlWriter, writeHtml } from "./html/html-writer.js";
|
|
18
19
|
import { svgWriter, writeSvg } from "./svg/svg-writer.js";
|
|
19
20
|
import { createConverter } from "./core/converter/facade.js";
|
|
20
21
|
import { Ream } from "./core/converter/ream.js";
|
|
21
|
-
export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeSvg, xlsxReader };
|
|
22
|
+
export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeHtml, writeSvg, xlsxReader };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FEATURES } from "../core/ir/features.js";
|
|
2
2
|
import { paintPlan } from "../layout/page-doc.js";
|
|
3
|
+
import { toBase64 } from "../core/bytes.js";
|
|
3
4
|
//#region src/svg/svg-writer.ts
|
|
4
5
|
var PAGE_GAP = 12;
|
|
5
6
|
function writeSvg(laid, opts = {}) {
|
|
@@ -121,11 +122,6 @@ function imageHref(resourceName, laid) {
|
|
|
121
122
|
return `data:${res.prepared.mimeType};base64,${toBase64(bytes)}`;
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
function toBase64(bytes) {
|
|
125
|
-
let bin = "";
|
|
126
|
-
for (const b of bytes) bin += String.fromCharCode(b);
|
|
127
|
-
return btoa(bin);
|
|
128
|
-
}
|
|
129
125
|
function escapeXml(s) {
|
|
130
126
|
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
131
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reamkit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Ream — DOCX/XLSX to PDF (and SVG), built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Krassavin <info@reamkit.dev>",
|