reamkit 1.0.1 → 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/core/style-cascade/resolver.d.ts +1 -1
- package/dist/esm/core/style-cascade/resolver.js +47 -41
- package/dist/esm/excel/print-model.js +21 -3
- 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 +2 -2
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") {
|
|
@@ -3,5 +3,5 @@ import { ResolvedParagraphProperties, ResolvedRunProperties } from './types.js';
|
|
|
3
3
|
export declare const EMPTY_STYLE_SHEET: StyleSheet;
|
|
4
4
|
export declare function resolveRunProperties(runDirect: RunProperties, paragraphDirect: ParagraphProperties, sheet: StyleSheet): ResolvedRunProperties;
|
|
5
5
|
export declare function resolveParagraphProperties(paragraphDirect: ParagraphProperties, sheet: StyleSheet): ResolvedParagraphProperties;
|
|
6
|
-
export declare function resolveBodyStyles(body: ReadonlyArray<BodyElement>, sheet: StyleSheet):
|
|
6
|
+
export declare function resolveBodyStyles(body: ReadonlyArray<BodyElement>, sheet: StyleSheet): ReadonlyArray<BodyElement>;
|
|
7
7
|
export declare function resolveHeadersFootersStyles(hf: ReadonlyMap<string, ReadonlyArray<BodyElement>>, sheet: StyleSheet): ReadonlyMap<string, ReadonlyArray<BodyElement>>;
|
|
@@ -35,7 +35,20 @@ function computeRunProperties(runDirect, paragraphDirect, sheet) {
|
|
|
35
35
|
}
|
|
36
36
|
return mergeRun(acc, runDirect);
|
|
37
37
|
}
|
|
38
|
+
var paragraphCascadeCache = /* @__PURE__ */ new WeakMap();
|
|
38
39
|
function resolveParagraphProperties(paragraphDirect, sheet) {
|
|
40
|
+
let bySheet = paragraphCascadeCache.get(sheet);
|
|
41
|
+
if (!bySheet) {
|
|
42
|
+
bySheet = /* @__PURE__ */ new WeakMap();
|
|
43
|
+
paragraphCascadeCache.set(sheet, bySheet);
|
|
44
|
+
}
|
|
45
|
+
const hit = bySheet.get(paragraphDirect);
|
|
46
|
+
if (hit) return hit;
|
|
47
|
+
const resolved = computeParagraphProperties(paragraphDirect, sheet);
|
|
48
|
+
bySheet.set(paragraphDirect, resolved);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
function computeParagraphProperties(paragraphDirect, sheet) {
|
|
39
52
|
let acc = mergePar(DEFAULT_RESOLVED_PARAGRAPH, sheet.defaultParagraphProperties);
|
|
40
53
|
if (paragraphDirect.styleId) {
|
|
41
54
|
const chain = resolveStyleChain(paragraphDirect.styleId, sheet);
|
|
@@ -117,52 +130,45 @@ function copyDefined(base, override) {
|
|
|
117
130
|
}
|
|
118
131
|
return out;
|
|
119
132
|
}
|
|
133
|
+
function primeResolvedFixpoint(run, para) {
|
|
134
|
+
let bySheet = runCascadeCache.get(EMPTY_STYLE_SHEET);
|
|
135
|
+
if (!bySheet) {
|
|
136
|
+
bySheet = /* @__PURE__ */ new WeakMap();
|
|
137
|
+
runCascadeCache.set(EMPTY_STYLE_SHEET, bySheet);
|
|
138
|
+
}
|
|
139
|
+
let byRun = bySheet.get(run);
|
|
140
|
+
if (!byRun) {
|
|
141
|
+
byRun = /* @__PURE__ */ new WeakMap();
|
|
142
|
+
bySheet.set(run, byRun);
|
|
143
|
+
}
|
|
144
|
+
if (!byRun.has(para)) byRun.set(para, run);
|
|
145
|
+
}
|
|
146
|
+
function primeParagraphFixpoint(para) {
|
|
147
|
+
let bySheet = paragraphCascadeCache.get(EMPTY_STYLE_SHEET);
|
|
148
|
+
if (!bySheet) {
|
|
149
|
+
bySheet = /* @__PURE__ */ new WeakMap();
|
|
150
|
+
paragraphCascadeCache.set(EMPTY_STYLE_SHEET, bySheet);
|
|
151
|
+
}
|
|
152
|
+
if (!bySheet.has(para)) bySheet.set(para, para);
|
|
153
|
+
}
|
|
120
154
|
function resolveBodyStyles(body, sheet) {
|
|
121
|
-
const visitParagraph = (p) =>
|
|
122
|
-
|
|
123
|
-
properties
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}))
|
|
128
|
-
});
|
|
155
|
+
const visitParagraph = (p) => {
|
|
156
|
+
for (const r of p.runs) r.properties = resolveRunProperties(r.properties, p.properties, sheet);
|
|
157
|
+
p.properties = resolveParagraphProperties(p.properties, sheet);
|
|
158
|
+
primeParagraphFixpoint(p.properties);
|
|
159
|
+
for (const r of p.runs) primeResolvedFixpoint(r.properties, p.properties);
|
|
160
|
+
};
|
|
129
161
|
const visit = (el) => {
|
|
130
|
-
if (el.kind === "paragraph")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
if (el.kind === "table") return {
|
|
135
|
-
kind: "table",
|
|
136
|
-
table: {
|
|
137
|
-
...el.table,
|
|
138
|
-
rows: el.table.rows.map((row) => ({
|
|
139
|
-
...row,
|
|
140
|
-
cells: row.cells.map((cell) => ({
|
|
141
|
-
...cell,
|
|
142
|
-
content: cell.content.map(visit)
|
|
143
|
-
}))
|
|
144
|
-
}))
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
if (el.kind === "shape" && el.shape.text) return {
|
|
148
|
-
kind: "shape",
|
|
149
|
-
shape: {
|
|
150
|
-
...el.shape,
|
|
151
|
-
text: {
|
|
152
|
-
...el.shape.text,
|
|
153
|
-
content: el.shape.text.content.map(visit)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
return el;
|
|
162
|
+
if (el.kind === "paragraph") visitParagraph(el.paragraph);
|
|
163
|
+
else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) for (const child of cell.content) visit(child);
|
|
164
|
+
else if (el.kind === "shape" && el.shape.text) for (const child of el.shape.text.content) visit(child);
|
|
158
165
|
};
|
|
159
|
-
|
|
166
|
+
for (const el of body) visit(el);
|
|
167
|
+
return body;
|
|
160
168
|
}
|
|
161
169
|
function resolveHeadersFootersStyles(hf, sheet) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
for (const [key, value] of hf) out.set(key, resolveBodyStyles(value, sheet));
|
|
165
|
-
return out;
|
|
170
|
+
for (const value of hf.values()) resolveBodyStyles(value, sheet);
|
|
171
|
+
return hf;
|
|
166
172
|
}
|
|
167
173
|
//#endregion
|
|
168
174
|
export { EMPTY_STYLE_SHEET, resolveBodyStyles, resolveHeadersFootersStyles, resolveParagraphProperties, resolveRunProperties };
|
|
@@ -169,6 +169,25 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
169
169
|
const scaled = printScale < .999;
|
|
170
170
|
const breakRows = new Set(worksheet.rowBreaks ?? []);
|
|
171
171
|
let textBudget = MAX_SHEET_TEXT_CHARS;
|
|
172
|
+
const runPropsByXf = /* @__PURE__ */ new Map();
|
|
173
|
+
const cellRunProps = (xf) => {
|
|
174
|
+
let props = runPropsByXf.get(xf);
|
|
175
|
+
if (props === void 0) {
|
|
176
|
+
const base = xf ? runPropsFromXf(xf, styles) : {};
|
|
177
|
+
props = scaled ? scaleRunFont(base, printScale) : base;
|
|
178
|
+
runPropsByXf.set(xf, props);
|
|
179
|
+
}
|
|
180
|
+
return props;
|
|
181
|
+
};
|
|
182
|
+
const paraPropsByAlignment = /* @__PURE__ */ new Map();
|
|
183
|
+
const cellParaProps = (alignment) => {
|
|
184
|
+
let props = paraPropsByAlignment.get(alignment);
|
|
185
|
+
if (props === void 0) {
|
|
186
|
+
props = alignment ? { alignment } : {};
|
|
187
|
+
paraPropsByAlignment.set(alignment, props);
|
|
188
|
+
}
|
|
189
|
+
return props;
|
|
190
|
+
};
|
|
172
191
|
const rows = [];
|
|
173
192
|
for (let r = 0; r < rowCount; r++) {
|
|
174
193
|
const absR = r + rowStart;
|
|
@@ -186,8 +205,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
186
205
|
if (text.length > textBudget) text = text.slice(0, Math.max(0, textBudget));
|
|
187
206
|
textBudget -= text.length;
|
|
188
207
|
const xf = ws && ws.styleIndex !== void 0 ? styles.cellXfs[ws.styleIndex] : void 0;
|
|
189
|
-
const
|
|
190
|
-
const runProps = scaled ? scaleRunFont(baseRunProps, printScale) : baseRunProps;
|
|
208
|
+
const runProps = cellRunProps(xf);
|
|
191
209
|
const alignment = xf ? alignmentFromXf(xf) : void 0;
|
|
192
210
|
const shading = xf ? shadingFromXf(xf, styles) : void 0;
|
|
193
211
|
const borders = xf ? bordersFromXf(xf, styles) : void 0;
|
|
@@ -210,7 +228,7 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
210
228
|
...shading ? { shading } : {},
|
|
211
229
|
...borders ? { borders } : {}
|
|
212
230
|
};
|
|
213
|
-
const paragraphProps = alignment
|
|
231
|
+
const paragraphProps = cellParaProps(alignment);
|
|
214
232
|
cells.push({
|
|
215
233
|
properties,
|
|
216
234
|
content: [{
|
|
@@ -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,9 +1,9 @@
|
|
|
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
|
-
"author": "Alex Krassavin",
|
|
6
|
+
"author": "Alex Krassavin <info@reamkit.dev>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/alex-krassavin/reamkit.git"
|