@uurtech/jdf 0.2.3 → 0.2.5
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/dist/jdfjs.cjs +5 -4
- package/dist/jdfjs.cjs.map +1 -1
- package/dist/jdfjs.css +16 -0
- package/dist/jdfjs.js +5 -4
- package/dist/jdfjs.js.map +1 -1
- package/package.json +1 -1
- package/src/jdfjs.css +16 -0
- package/src/renderers/element.ts +65 -7
- package/src/viewer.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uurtech/jdf",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Render JDF (JSON Document Format) documents in the browser. Drop-in replacement for embedding PDFs — point at a .jdf file and it appears on the page, fully styled, searchable, with a clickable TOC.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ugur Kazdal <ugur@uurtech.com>",
|
package/src/jdfjs.css
CHANGED
|
@@ -275,6 +275,22 @@ textarea.jdfjs-form-control {
|
|
|
275
275
|
line-height: 1.4;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
|
|
279
|
+
/* Compact fields: PDF-form widgets shorter than 9 mm (see compactField). */
|
|
280
|
+
.jdfjs-form-field.jdfjs-form-compact { gap: 0; justify-content: center; }
|
|
281
|
+
.jdfjs-form-compact .jdfjs-form-label { display: none; }
|
|
282
|
+
.jdfjs-form-compact .jdfjs-form-control {
|
|
283
|
+
height: 100%;
|
|
284
|
+
min-height: 0;
|
|
285
|
+
padding: 0 2px;
|
|
286
|
+
border-radius: 1px;
|
|
287
|
+
border-color: rgba(100, 116, 139, 0.35);
|
|
288
|
+
background: transparent;
|
|
289
|
+
line-height: 1;
|
|
290
|
+
}
|
|
291
|
+
.jdfjs-form-compact textarea.jdfjs-form-control { min-height: 0; resize: none; line-height: 1.1; }
|
|
292
|
+
.jdfjs-form-compact.jdfjs-form-checkbox input[type="checkbox"] { width: 100%; height: 100%; margin: 0; }
|
|
293
|
+
|
|
278
294
|
.jdfjs-form-checkbox {
|
|
279
295
|
flex-direction: row;
|
|
280
296
|
align-items: center;
|
package/src/renderers/element.ts
CHANGED
|
@@ -295,7 +295,13 @@ function colWidthCss(w: string | number | undefined): string | undefined {
|
|
|
295
295
|
function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
|
|
296
296
|
const wrap = document.createElement("div");
|
|
297
297
|
wrap.className = "jdfjs-table-wrap";
|
|
298
|
-
|
|
298
|
+
// `style.fontSize` / `lineHeight` size the cells and `style.padding` is the
|
|
299
|
+
// cell padding (the PDF importer writes compact values so a converted
|
|
300
|
+
// statement occupies the same box as the source rows). Same rules as the
|
|
301
|
+
// reader's TableElement — keep them identical.
|
|
302
|
+
const tableCss = resolveStyle(el.style, ctx.styles);
|
|
303
|
+
const { padding: cellPad = "8px 12px", ...wrapCss } = tableCss;
|
|
304
|
+
applyStyle(wrap, wrapCss);
|
|
299
305
|
wrap.style.overflowX = "auto";
|
|
300
306
|
|
|
301
307
|
const headerCss = (() => {
|
|
@@ -336,7 +342,8 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
|
|
|
336
342
|
table.style.width = "100%";
|
|
337
343
|
table.style.borderCollapse = "collapse";
|
|
338
344
|
table.style.tableLayout = el.columns?.some((c) => c.width != null) ? "fixed" : "auto";
|
|
339
|
-
table.style.fontSize = "14px";
|
|
345
|
+
table.style.fontSize = tableCss["font-size"] || "14px";
|
|
346
|
+
if (tableCss["line-height"]) table.style.lineHeight = tableCss["line-height"];
|
|
340
347
|
if (borders.outer) table.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
|
|
341
348
|
|
|
342
349
|
// Column widths — honour columns[].width via a <colgroup> so both header
|
|
@@ -359,9 +366,11 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
|
|
|
359
366
|
headers.forEach((h, i) => {
|
|
360
367
|
const th = document.createElement("th");
|
|
361
368
|
th.textContent = h;
|
|
362
|
-
th.style.padding =
|
|
369
|
+
th.style.padding = cellPad;
|
|
363
370
|
th.style.fontWeight = "600";
|
|
364
|
-
|
|
371
|
+
// Designed tables get the light header band; a table that brings its own
|
|
372
|
+
// headerStyle (the PDF importer, hand-authored docs) shows exactly that.
|
|
373
|
+
if (!el.headerStyle) th.style.background = "#f8fafc";
|
|
365
374
|
th.style.textAlign = colAlign(i) || "left";
|
|
366
375
|
if (borders.inner) th.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
|
|
367
376
|
tr.appendChild(th);
|
|
@@ -382,13 +391,17 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
|
|
|
382
391
|
const tr = document.createElement("tr");
|
|
383
392
|
applyStyle(tr, rowCss as any);
|
|
384
393
|
if (ri % 2 === 1) applyStyle(tr, altRowCss as any);
|
|
394
|
+
// A spacer row (every cell empty — the PDF importer keeps a statement's
|
|
395
|
+
// blank separator lines) must still be one line tall, so it gets a
|
|
396
|
+
// non-breaking space; an empty cell in a filled row stays empty.
|
|
397
|
+
const blankRow = row.every((c) => cellText(c) === "");
|
|
385
398
|
row.forEach((cell, ci) => {
|
|
386
399
|
const td = document.createElement("td");
|
|
387
|
-
td.textContent = cellText(cell);
|
|
400
|
+
td.textContent = blankRow ? "\u00a0" : cellText(cell);
|
|
388
401
|
const attrs = cellAttrs(cell);
|
|
389
402
|
if (attrs.colspan) td.colSpan = attrs.colspan;
|
|
390
403
|
if (attrs.rowspan) td.rowSpan = attrs.rowspan;
|
|
391
|
-
td.style.padding =
|
|
404
|
+
td.style.padding = cellPad;
|
|
392
405
|
td.style.verticalAlign = "top";
|
|
393
406
|
// Cell-level align wins over the column default.
|
|
394
407
|
td.style.textAlign = cellAlign(cell) || colAlign(ci) || "left";
|
|
@@ -442,6 +455,7 @@ function buildItems(parent: HTMLElement, items: ListItem[], def: "ordered" | "un
|
|
|
442
455
|
}
|
|
443
456
|
|
|
444
457
|
// ── shape ───────────────────────────────────────────────────────────────────
|
|
458
|
+
let gradientSeq = 0;
|
|
445
459
|
function renderShape(el: ShapeElement): HTMLElement {
|
|
446
460
|
const wrap = document.createElement("div");
|
|
447
461
|
wrap.className = "jdfjs-shape";
|
|
@@ -521,7 +535,35 @@ function renderShape(el: ShapeElement): HTMLElement {
|
|
|
521
535
|
}
|
|
522
536
|
}
|
|
523
537
|
if (shapeNode) {
|
|
524
|
-
|
|
538
|
+
// Gradient fill via SVG defs (same geometry the reader uses).
|
|
539
|
+
let paint = fill;
|
|
540
|
+
const g = el.gradient;
|
|
541
|
+
if (g && Array.isArray(g.stops) && g.stops.length >= 2) {
|
|
542
|
+
const id = `jdfjs-grad-${++gradientSeq}`;
|
|
543
|
+
const defs = document.createElementNS(NS_SVG, "defs");
|
|
544
|
+
let gradNode: SVGElement;
|
|
545
|
+
if (g.type === "radial") {
|
|
546
|
+
gradNode = document.createElementNS(NS_SVG, "radialGradient");
|
|
547
|
+
gradNode.setAttribute("cx", "50%"); gradNode.setAttribute("cy", "50%"); gradNode.setAttribute("r", "50%");
|
|
548
|
+
} else {
|
|
549
|
+
gradNode = document.createElementNS(NS_SVG, "linearGradient");
|
|
550
|
+
const a = ((g.angle ?? 180) * Math.PI) / 180;
|
|
551
|
+
const dx = Math.sin(a) / 2, dy = -Math.cos(a) / 2;
|
|
552
|
+
gradNode.setAttribute("x1", String(0.5 - dx)); gradNode.setAttribute("y1", String(0.5 - dy));
|
|
553
|
+
gradNode.setAttribute("x2", String(0.5 + dx)); gradNode.setAttribute("y2", String(0.5 + dy));
|
|
554
|
+
}
|
|
555
|
+
gradNode.setAttribute("id", id);
|
|
556
|
+
for (const st of g.stops) {
|
|
557
|
+
const stop = document.createElementNS(NS_SVG, "stop");
|
|
558
|
+
stop.setAttribute("offset", `${Math.max(0, Math.min(1, st.offset)) * 100}%`);
|
|
559
|
+
stop.setAttribute("stop-color", st.color);
|
|
560
|
+
gradNode.appendChild(stop);
|
|
561
|
+
}
|
|
562
|
+
defs.appendChild(gradNode);
|
|
563
|
+
svg.appendChild(defs);
|
|
564
|
+
paint = `url(#${id})`;
|
|
565
|
+
}
|
|
566
|
+
shapeNode.setAttribute("fill", paint);
|
|
525
567
|
shapeNode.setAttribute("stroke", strokeColor);
|
|
526
568
|
if (strokeWidth) shapeNode.setAttribute("stroke-width", String(strokeWidth));
|
|
527
569
|
svg.appendChild(shapeNode);
|
|
@@ -668,10 +710,23 @@ function commitFormChange(ctx: RenderContext, field: string, value: unknown) {
|
|
|
668
710
|
ctx.onFormChange(ctx.path, field, value);
|
|
669
711
|
}
|
|
670
712
|
|
|
713
|
+
/**
|
|
714
|
+
* A field whose box is shorter than 9 mm (an AcroForm widget imported from a
|
|
715
|
+
* PDF form) renders compact: no label, no padding, font sized to the box, so
|
|
716
|
+
* the control sits inside the form's printed cell instead of covering the
|
|
717
|
+
* labels around it. Same rule in the reader's FormElement.
|
|
718
|
+
*/
|
|
719
|
+
function compactField(wrap: HTMLElement, el: { height?: number }) {
|
|
720
|
+
if (el.height == null || el.height >= 9) return;
|
|
721
|
+
wrap.classList.add("jdfjs-form-compact");
|
|
722
|
+
wrap.style.fontSize = `${Math.max(7, Math.min(12, Math.round(el.height * 3.7795 * 0.62)))}px`;
|
|
723
|
+
}
|
|
724
|
+
|
|
671
725
|
function renderFormInput(el: FormInputElement, ctx: RenderContext): HTMLElement {
|
|
672
726
|
const wrap = document.createElement("div");
|
|
673
727
|
wrap.className = "jdfjs-form-field jdfjs-form-input";
|
|
674
728
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
729
|
+
compactField(wrap, el);
|
|
675
730
|
const lab = makeLabel(el.label);
|
|
676
731
|
if (lab) wrap.appendChild(lab);
|
|
677
732
|
const input = document.createElement("input");
|
|
@@ -693,6 +748,7 @@ function renderFormTextarea(el: FormTextareaElement, ctx: RenderContext): HTMLEl
|
|
|
693
748
|
const wrap = document.createElement("div");
|
|
694
749
|
wrap.className = "jdfjs-form-field jdfjs-form-textarea";
|
|
695
750
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
751
|
+
compactField(wrap, el);
|
|
696
752
|
const lab = makeLabel(el.label);
|
|
697
753
|
if (lab) wrap.appendChild(lab);
|
|
698
754
|
const ta = document.createElement("textarea");
|
|
@@ -713,6 +769,7 @@ function renderFormCheckbox(el: FormCheckboxElement, ctx: RenderContext): HTMLEl
|
|
|
713
769
|
const wrap = document.createElement("label");
|
|
714
770
|
wrap.className = "jdfjs-form-field jdfjs-form-checkbox";
|
|
715
771
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
772
|
+
compactField(wrap, el);
|
|
716
773
|
const cb = document.createElement("input");
|
|
717
774
|
cb.type = "checkbox";
|
|
718
775
|
cb.name = el.name;
|
|
@@ -734,6 +791,7 @@ function renderFormSelect(el: FormSelectElement, ctx: RenderContext): HTMLElemen
|
|
|
734
791
|
const wrap = document.createElement("div");
|
|
735
792
|
wrap.className = "jdfjs-form-field jdfjs-form-select";
|
|
736
793
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
794
|
+
compactField(wrap, el);
|
|
737
795
|
const lab = makeLabel(el.label);
|
|
738
796
|
if (lab) wrap.appendChild(lab);
|
|
739
797
|
const sel = document.createElement("select");
|
package/src/viewer.ts
CHANGED
|
@@ -222,6 +222,7 @@ export class JDFViewer {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
private mount() {
|
|
225
|
+
ensureMetricFonts(this.doc);
|
|
225
226
|
this.container.innerHTML = "";
|
|
226
227
|
this.container.classList.add("jdfjs");
|
|
227
228
|
this.applyDarkMode();
|
|
@@ -756,3 +757,24 @@ function resolveContainer(c: HTMLElement | string): HTMLElement {
|
|
|
756
757
|
function escapeHtml(s: string): string {
|
|
757
758
|
return s.replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m]!));
|
|
758
759
|
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The PDF importer maps fonts to metric-compatible open families (Carlito =
|
|
763
|
+
* Calibri, Arimo = Arial, Tinos = Times New Roman, Cousine = Courier New,
|
|
764
|
+
* Gelasio = Georgia, Caladea = Cambria) so converted lines keep their width.
|
|
765
|
+
* Load them from Google Fonts once, only when a document uses them; offline
|
|
766
|
+
* embeds fall through to the stack's next family.
|
|
767
|
+
*/
|
|
768
|
+
const METRIC_FAMILIES = ["Carlito", "Arimo", "Tinos", "Cousine", "Gelasio", "Caladea", "IBM Plex Sans", "IBM Plex Serif"];
|
|
769
|
+
function ensureMetricFonts(doc: JdfDocument) {
|
|
770
|
+
if (typeof document === "undefined" || document.getElementById("jdfjs-metric-fonts")) return;
|
|
771
|
+
let json: string;
|
|
772
|
+
try { json = JSON.stringify(doc.styles ?? {}) + JSON.stringify(doc.pages ?? []); } catch { return; }
|
|
773
|
+
const used = METRIC_FAMILIES.filter((f) => json.includes(f));
|
|
774
|
+
if (!used.length) return;
|
|
775
|
+
const link = document.createElement("link");
|
|
776
|
+
link.id = "jdfjs-metric-fonts";
|
|
777
|
+
link.rel = "stylesheet";
|
|
778
|
+
link.href = "https://fonts.googleapis.com/css2?" + used.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:ital,wght@0,400;0,700;1,400;1,700`).join("&") + "&display=swap";
|
|
779
|
+
document.head.appendChild(link);
|
|
780
|
+
}
|