@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uurtech/jdf",
3
- "version": "0.2.3",
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;
@@ -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
- applyStyle(wrap, resolveStyle(el.style, ctx.styles));
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 = "8px 12px";
369
+ th.style.padding = cellPad;
363
370
  th.style.fontWeight = "600";
364
- th.style.background = "#f8fafc";
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 = "8px 12px";
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
- shapeNode.setAttribute("fill", fill);
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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
+ }