@uurtech/jdf 0.2.4 → 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 +46 -1
- 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
|
@@ -455,6 +455,7 @@ function buildItems(parent: HTMLElement, items: ListItem[], def: "ordered" | "un
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
// ── shape ───────────────────────────────────────────────────────────────────
|
|
458
|
+
let gradientSeq = 0;
|
|
458
459
|
function renderShape(el: ShapeElement): HTMLElement {
|
|
459
460
|
const wrap = document.createElement("div");
|
|
460
461
|
wrap.className = "jdfjs-shape";
|
|
@@ -534,7 +535,35 @@ function renderShape(el: ShapeElement): HTMLElement {
|
|
|
534
535
|
}
|
|
535
536
|
}
|
|
536
537
|
if (shapeNode) {
|
|
537
|
-
|
|
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);
|
|
538
567
|
shapeNode.setAttribute("stroke", strokeColor);
|
|
539
568
|
if (strokeWidth) shapeNode.setAttribute("stroke-width", String(strokeWidth));
|
|
540
569
|
svg.appendChild(shapeNode);
|
|
@@ -681,10 +710,23 @@ function commitFormChange(ctx: RenderContext, field: string, value: unknown) {
|
|
|
681
710
|
ctx.onFormChange(ctx.path, field, value);
|
|
682
711
|
}
|
|
683
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
|
+
|
|
684
725
|
function renderFormInput(el: FormInputElement, ctx: RenderContext): HTMLElement {
|
|
685
726
|
const wrap = document.createElement("div");
|
|
686
727
|
wrap.className = "jdfjs-form-field jdfjs-form-input";
|
|
687
728
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
729
|
+
compactField(wrap, el);
|
|
688
730
|
const lab = makeLabel(el.label);
|
|
689
731
|
if (lab) wrap.appendChild(lab);
|
|
690
732
|
const input = document.createElement("input");
|
|
@@ -706,6 +748,7 @@ function renderFormTextarea(el: FormTextareaElement, ctx: RenderContext): HTMLEl
|
|
|
706
748
|
const wrap = document.createElement("div");
|
|
707
749
|
wrap.className = "jdfjs-form-field jdfjs-form-textarea";
|
|
708
750
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
751
|
+
compactField(wrap, el);
|
|
709
752
|
const lab = makeLabel(el.label);
|
|
710
753
|
if (lab) wrap.appendChild(lab);
|
|
711
754
|
const ta = document.createElement("textarea");
|
|
@@ -726,6 +769,7 @@ function renderFormCheckbox(el: FormCheckboxElement, ctx: RenderContext): HTMLEl
|
|
|
726
769
|
const wrap = document.createElement("label");
|
|
727
770
|
wrap.className = "jdfjs-form-field jdfjs-form-checkbox";
|
|
728
771
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
772
|
+
compactField(wrap, el);
|
|
729
773
|
const cb = document.createElement("input");
|
|
730
774
|
cb.type = "checkbox";
|
|
731
775
|
cb.name = el.name;
|
|
@@ -747,6 +791,7 @@ function renderFormSelect(el: FormSelectElement, ctx: RenderContext): HTMLElemen
|
|
|
747
791
|
const wrap = document.createElement("div");
|
|
748
792
|
wrap.className = "jdfjs-form-field jdfjs-form-select";
|
|
749
793
|
applyStyle(wrap, resolveStyle(el.style, ctx.styles));
|
|
794
|
+
compactField(wrap, el);
|
|
750
795
|
const lab = makeLabel(el.label);
|
|
751
796
|
if (lab) wrap.appendChild(lab);
|
|
752
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
|
+
}
|