omni-viewer-core 0.2.0 → 0.3.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.
@@ -11,5 +11,6 @@ export interface PptxParserDeps {
11
11
  openZip(data: Uint8Array, options?: ParseOptions): Promise<PptxZipReader>;
12
12
  }
13
13
  export { openPptxZip } from './zip-reader.js';
14
- export { parsePptxVscode } from './vscode.js';
14
+ import { parsePptxVscode } from './vscode.js';
15
+ export { parsePptxVscode };
15
16
  export declare function parsePptx(input: Uint8Array, deps: PptxParserDeps, options?: ParseOptions): Promise<ParseOutcome<SlideDeck>>;
@@ -2,7 +2,8 @@
2
2
  import { DEFAULT_LIMITS, LimitTracker } from '../types.js';
3
3
  export * from '../slide-model.js';
4
4
  export { openPptxZip } from './zip-reader.js';
5
- export { parsePptxVscode } from './vscode.js';
5
+ import { parsePptxVscode } from './vscode.js';
6
+ export { parsePptxVscode };
6
7
  const EMU_PER_PX = 9525;
7
8
  const DEFAULT_SIZE = { widthPx: 1280, heightPx: 720 };
8
9
  const attr = (tag, name) => new RegExp(`(?:^|\\s)${name}="([^"]*)"`).exec(tag)?.[1];
@@ -54,7 +55,7 @@ async function parseSlide(zip, path, n, size) { const xml = await zip.text(path)
54
55
  else
55
56
  elements.push({ ...common, type: 'shape', presetGeom: geom, fillColor: color(block, 'transparent'), borderColor: color(/<a:ln\b[\s\S]*?<\/a:ln>/.exec(block)?.[0] ?? '') });
56
57
  } return { slideNumber: n, ...size, backgroundColor: color(/<p:bg\b[\s\S]*?<\/p:bg>/.exec(xml)?.[0] ?? '', '#ffffff') ?? '#ffffff', elements }; }
57
- export async function parsePptx(input, deps, options = {}) { const started = Date.now(), diagnostics = []; if (input.byteLength > (options.limits?.maxInputBytes ?? 50 * 1024 * 1024))
58
+ async function parsePptxLegacy(input, deps, options = {}) { const started = Date.now(), diagnostics = []; if (input.byteLength > (options.limits?.maxInputBytes ?? 50 * 1024 * 1024))
58
59
  return { result: { status: 'failed', failure: { code: 'limit-exceeded', retryable: false, messageKey: 'diag.limit-exceeded.input' }, diagnostics }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } }; let zip; try {
59
60
  zip = await deps.openZip(input, options);
60
61
  const presentation = await zip.text('ppt/presentation.xml');
@@ -86,3 +87,5 @@ catch {
86
87
  finally {
87
88
  await zip?.close?.();
88
89
  } }
90
+ export async function parsePptx(input, deps, options = {}) { const full = await parsePptxVscode(input, options); if (full.result.status !== 'failed' || full.result.failure.code !== 'invalid-format')
91
+ return full; return parsePptxLegacy(input, deps, options); }
@@ -1 +1 @@
1
- :host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}
1
+ :host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}
@@ -2,7 +2,7 @@ import { type PdfAnnotation, type PdfViewState } from './controller.js';
2
2
  /** Type-only reference — erased at compile time. */
3
3
  export type PdfLibModule = typeof import('pdf-lib');
4
4
  export declare const SIDECAR_LAYER_NAME = "omni-viewer-layer.json";
5
- export declare const SIDECAR_ORIGINAL_NAME = "omni-viewer-original.pdf";
5
+ export declare const SIDECAR_BASE_NAME = "omni-viewer-base.pdf";
6
6
  /** Result of parsing a sidecar's layer JSON. */
7
7
  export interface ParsedLayer {
8
8
  pageOrder: number[];
@@ -14,9 +14,11 @@ export declare function buildEditedPdf(pdfLib: PdfLibModule, source: Uint8Array,
14
14
  /**
15
15
  * Like {@link buildEditedPdf}, but also embeds the hybrid sidecar so the
16
16
  * omni viewer can rehydrate a removable overlay layer on reopen:
17
- * - the pristine (unflattened) source, and
18
- * - the layer JSON `{ version, pageOrder, annotations }`.
19
- * External readers only ever see the flattened pages.
17
+ * - a re-editable base (kept pages only, signatures flattened in), and
18
+ * - the layer JSON `{ version, pageOrder, annotations }` for text/markup,
19
+ * with page references remapped onto the kept-pages base.
20
+ * Deleted pages and signatures are unrecoverable; external readers only ever
21
+ * see the flattened pages.
20
22
  */
21
23
  export declare function buildSavedPdf(pdfLib: PdfLibModule, pristine: Uint8Array, state: PdfViewState): Promise<Uint8Array>;
22
24
  /** Parse and validate a sidecar layer JSON string; null if unusable. */
@@ -5,13 +5,16 @@
5
5
  // imports). Callers inject the loaded module; `viewers/pdf/self-loading`
6
6
  // provides the dynamic-import loader for platforms whose bundler resolves it.
7
7
  import { mergeHighlightRects } from './controller.js';
8
- // Hybrid sidecar: saved PDFs keep flattened (portable) pages while embedding
9
- // the pristine source + a layer JSON, so the omni viewer can restore a
10
- // removable overlay layer on reopen. External readers only see the flat pages.
8
+ // Hybrid sidecar: saved PDFs keep flattened (portable) pages for external
9
+ // readers while embedding a re-editable base + a layer JSON, so the omni
10
+ // viewer can restore a removable overlay layer on reopen. The embedded base
11
+ // holds only the kept pages (deleted pages are gone for good) with signatures
12
+ // permanently flattened in; text/markup stay out so they remain editable.
13
+ // External readers only ever see the flat pages.
11
14
  export const SIDECAR_LAYER_NAME = 'omni-viewer-layer.json';
12
- export const SIDECAR_ORIGINAL_NAME = 'omni-viewer-original.pdf';
15
+ export const SIDECAR_BASE_NAME = 'omni-viewer-base.pdf';
13
16
  /** Bumped when the sidecar shape changes; older/newer versions are ignored. */
14
- const SIDECAR_VERSION = 1;
17
+ const SIDECAR_VERSION = 2;
15
18
  /** Defensive ceilings — the sidecar is untrusted input on reopen. */
16
19
  const MAX_ANNOTATIONS = 2000;
17
20
  const MAX_TEXT_LENGTH = 10_000;
@@ -104,16 +107,39 @@ async function stamp(pdfLib, page, annotation) {
104
107
  height: annotation.height
105
108
  });
106
109
  }
107
- /** Reorder/delete pages and stamp the overlays into a fresh (unsaved) doc. */
108
- async function flattenInto(pdfLib, source, state) {
110
+ /** Copy the kept pages (in display order) from `source` into a fresh doc. */
111
+ async function copyKeptPages(pdfLib, source, pageOrder) {
109
112
  const original = await pdfLib.PDFDocument.load(source);
110
113
  const output = await pdfLib.PDFDocument.create();
111
- const indices = state.pageOrder
114
+ const indices = pageOrder
112
115
  .map((page) => page - 1)
113
116
  .filter((page) => page >= 0 && page < original.getPageCount());
114
117
  const copied = await output.copyPages(original, indices);
115
118
  copied.forEach((page) => output.addPage(page));
119
+ return output;
120
+ }
121
+ /** Reorder/delete pages and stamp the overlays into a fresh (unsaved) doc. */
122
+ async function flattenInto(pdfLib, source, state) {
123
+ const output = await copyKeptPages(pdfLib, source, state.pageOrder);
124
+ for (const annotation of state.annotations) {
125
+ const outputIndex = state.pageOrder.indexOf(annotation.page);
126
+ const page = output.getPages()[outputIndex];
127
+ if (page)
128
+ await stamp(pdfLib, page, annotation);
129
+ }
130
+ return output;
131
+ }
132
+ /**
133
+ * The re-editable base embedded in a saved PDF: the kept pages (unflattened)
134
+ * with ONLY signatures permanently stamped in. Text/markup are deliberately
135
+ * left out so the omni viewer can restore them as a removable layer, while
136
+ * signatures — and any pages the user deleted — cannot be recovered.
137
+ */
138
+ async function buildEditableBase(pdfLib, pristine, state) {
139
+ const output = await copyKeptPages(pdfLib, pristine, state.pageOrder);
116
140
  for (const annotation of state.annotations) {
141
+ if (annotation.kind !== 'signature')
142
+ continue;
117
143
  const outputIndex = state.pageOrder.indexOf(annotation.page);
118
144
  const page = output.getPages()[outputIndex];
119
145
  if (page)
@@ -130,21 +156,29 @@ export async function buildEditedPdf(pdfLib, source, state) {
130
156
  /**
131
157
  * Like {@link buildEditedPdf}, but also embeds the hybrid sidecar so the
132
158
  * omni viewer can rehydrate a removable overlay layer on reopen:
133
- * - the pristine (unflattened) source, and
134
- * - the layer JSON `{ version, pageOrder, annotations }`.
135
- * External readers only ever see the flattened pages.
159
+ * - a re-editable base (kept pages only, signatures flattened in), and
160
+ * - the layer JSON `{ version, pageOrder, annotations }` for text/markup,
161
+ * with page references remapped onto the kept-pages base.
162
+ * Deleted pages and signatures are unrecoverable; external readers only ever
163
+ * see the flattened pages.
136
164
  */
137
165
  export async function buildSavedPdf(pdfLib, pristine, state) {
138
166
  const output = await flattenInto(pdfLib, pristine, state);
167
+ const base = await buildEditableBase(pdfLib, pristine, state);
168
+ // The base is already in display order, so its pages number 1..keptCount.
169
+ // Remap each overlay's page reference (an original page number) onto that.
170
+ const remap = new Map(state.pageOrder.map((page, index) => [page, index + 1]));
139
171
  const layer = JSON.stringify({
140
172
  version: SIDECAR_VERSION,
141
- pageOrder: state.pageOrder,
173
+ pageOrder: base.getPages().map((_, index) => index + 1),
142
174
  annotations: state.annotations
175
+ .filter((annotation) => annotation.kind !== 'signature')
176
+ .map((annotation) => ({ ...annotation, page: remap.get(annotation.page) ?? annotation.page }))
143
177
  });
144
178
  await output.attach(new TextEncoder().encode(layer), SIDECAR_LAYER_NAME, {
145
179
  mimeType: 'application/json'
146
180
  });
147
- await output.attach(pristine, SIDECAR_ORIGINAL_NAME, {
181
+ await output.attach(await base.save(), SIDECAR_BASE_NAME, {
148
182
  mimeType: 'application/pdf'
149
183
  });
150
184
  return output.save();
@@ -11,7 +11,7 @@
11
11
  // annotations use an inline input overlay instead.
12
12
  import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
13
13
  import { createPdfController, mergeHighlightRects, PDF_ZOOM_LEVELS } from './controller.js';
14
- import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_ORIGINAL_NAME } from './editing.js';
14
+ import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_BASE_NAME } from './editing.js';
15
15
  import { pdfViewerCss } from './styles.js';
16
16
  export { createPdfController, PDF_ZOOM_LEVELS } from './controller.js';
17
17
  export { pdfViewerCss } from './styles.js';
@@ -176,7 +176,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
176
176
  }
177
177
  }
178
178
  /**
179
- * Read the hybrid sidecar (pristine source + layer JSON) from a document's
179
+ * Read the hybrid sidecar (re-editable base + layer JSON) from a document's
180
180
  * attachments. Returns null unless BOTH parts are present and the layer
181
181
  * parses — a lone layer over already-flattened pages would double up.
182
182
  */
@@ -185,19 +185,19 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
185
185
  const attachments = await d.getAttachments?.();
186
186
  if (!attachments)
187
187
  return null;
188
- let pristine;
188
+ let base;
189
189
  let layerText;
190
190
  for (const entry of Object.values(attachments)) {
191
- if (entry.filename === SIDECAR_ORIGINAL_NAME)
192
- pristine = entry.content;
191
+ if (entry.filename === SIDECAR_BASE_NAME)
192
+ base = entry.content;
193
193
  else if (entry.filename === SIDECAR_LAYER_NAME) {
194
194
  layerText = new TextDecoder().decode(entry.content);
195
195
  }
196
196
  }
197
- if (!pristine || layerText === undefined)
197
+ if (!base || layerText === undefined)
198
198
  return null;
199
199
  const layer = parseLayer(layerText);
200
- return layer ? { pristine, layer } : null;
200
+ return layer ? { base, layer } : null;
201
201
  }
202
202
  catch {
203
203
  return null;
@@ -1374,12 +1374,13 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
1374
1374
  }
1375
1375
  throwIfAborted();
1376
1376
  // Hybrid rehydration: if this file was saved by us, its visible pages are
1377
- // flattened but it carries the pristine source + layer JSON. Render/edit
1378
- // from the pristine base and restore the overlays as a removable layer.
1377
+ // flattened but it carries a re-editable base (kept pages, signatures baked
1378
+ // in) + layer JSON. Render/edit from that base and restore the text/markup
1379
+ // overlays as a removable layer.
1379
1380
  const sidecar = await readSidecar(doc);
1380
1381
  if (sidecar) {
1381
1382
  await doc.destroy();
1382
- sourceBytes = sidecar.pristine;
1383
+ sourceBytes = sidecar.base;
1383
1384
  doc = await loadDocument(sourceBytes, password);
1384
1385
  throwIfAborted();
1385
1386
  adoptController(createPdfController(doc.numPages, sidecar.layer));
@@ -9,7 +9,7 @@ export function computeChartBounds(data) { const explicitMin = data.valueAxis?.m
9
9
  const positive = data.categories.map((_, i) => data.series.reduce((sum, s) => sum + Math.max(0, s.values[i] ?? 0), 0)), negative = data.categories.map((_, i) => data.series.reduce((sum, s) => sum + Math.min(0, s.values[i] ?? 0), 0));
10
10
  const min = finite(explicitMin) ? explicitMin : Math.min(0, ...negative), max = finite(explicitMax) ? explicitMax : Math.max(1, ...positive);
11
11
  return min < max ? { min, max } : { min, max: min + 1 };
12
- } const values = data.series.flatMap(s => s.values).filter(finite); let min = finite(explicitMin) ? explicitMin : Math.min(0, ...values), max = finite(explicitMax) ? explicitMax : Math.max(1, ...values); if (min >= max) {
12
+ } const values = data.series.flatMap(s => s.values).filter(finite); let min = finite(explicitMin) ? explicitMin : Math.min(0, ...values), max = finite(explicitMax) ? explicitMax : values.length ? Math.max(...values) : 1; if (min >= max) {
13
13
  const pad = Math.max(1, Math.abs(max || 1) * .1);
14
14
  min -= pad;
15
15
  max += pad;
@@ -26,6 +26,12 @@ function legend(ctx, w, h, data, line) { const style = data.legend ?? {}, font =
26
26
  ctx.moveTo(x, h - 11);
27
27
  ctx.lineTo(x + swatch, h - 11);
28
28
  ctx.stroke();
29
+ ctx.fillStyle = '#fff';
30
+ ctx.beginPath();
31
+ ctx.arc(x + swatch / 2, h - 11, 3.5, 0, Math.PI * 2);
32
+ ctx.fill();
33
+ ctx.strokeStyle = item.color;
34
+ ctx.stroke();
29
35
  }
30
36
  else {
31
37
  ctx.fillStyle = item.color;
@@ -36,7 +42,11 @@ function legend(ctx, w, h, data, line) { const style = data.legend ?? {}, font =
36
42
  ctx.fillText(item.label, x, h - 7);
37
43
  x += item.label.length * font * .55 + gap;
38
44
  } }
39
- function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 20, bottom: 88 }, pw = w - m.left - m.right, ph = h - m.top - m.bottom, axis = data.valueAxis ?? {}, format = axis.numFmt ?? (data.kind === 'stackedColumn' ? '$#,##0.0' : '0.0'), step = finite(axis.majorUnit) && axis.majorUnit > 0 ? axis.majorUnit : niceStep(bounds.max - bounds.min), y = (value) => m.top + (bounds.max - value) / (bounds.max - bounds.min) * ph; ctx.strokeStyle = axis.gridColor ?? 'rgba(0,0,0,0.22)'; ctx.lineWidth = 1; for (let tick = Math.floor(bounds.min / step) * step, count = 0; tick <= bounds.max + step * .5 && count++ < 250; tick += step) {
45
+ function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 20, bottom: 88 }, pw = w - m.left - m.right, ph = h - m.top - m.bottom, axis = data.valueAxis ?? {}, format = axis.numFmt ?? (data.kind === 'stackedColumn' ? '$#,##0.0' : '0.0'), step = finite(axis.majorUnit) && axis.majorUnit > 0 ? axis.majorUnit : niceStep(bounds.max - bounds.min), y = (value) => m.top + (bounds.max - value) / (bounds.max - bounds.min) * ph; if (data.kind === 'stackedColumn') {
46
+ ctx.strokeStyle = axis.lineColor ?? '#4f81bd';
47
+ ctx.lineWidth = 1;
48
+ ctx.strokeRect(m.left, m.top, pw, ph);
49
+ } ctx.strokeStyle = axis.gridColor ?? (data.kind === 'line' ? 'rgba(0,0,0,0.18)' : 'rgba(0,0,0,0.22)'); ctx.lineWidth = 1; for (let tick = Math.floor(bounds.min / step) * step, count = 0; tick <= bounds.max + step * .5 && count++ < 250; tick += step) {
40
50
  const py = y(tick);
41
51
  ctx.beginPath();
42
52
  ctx.moveTo(m.left, py);
@@ -47,7 +57,13 @@ function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 2
47
57
  ctx.textAlign = 'right';
48
58
  ctx.textBaseline = 'middle';
49
59
  ctx.fillText(formatChartValue(tick, format), m.left - 12, py);
50
- } const cross = finite(axis.crossesAt) ? axis.crossesAt : 0; ctx.strokeStyle = axis.lineColor ?? '#111111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(m.left, y(cross)); ctx.lineTo(m.left + pw, y(cross)); ctx.stroke(); return { ...m, pw, ph, y, format }; }
60
+ } const cross = finite(axis.crossesAt) ? axis.crossesAt : 0; ctx.strokeStyle = data.kind === 'stackedColumn' ? '#111111' : axis.lineColor ?? '#111111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(m.left, y(cross)); ctx.lineTo(m.left + pw, y(cross)); ctx.stroke(); if (data.kind === 'line') {
61
+ ctx.beginPath();
62
+ ctx.moveTo(m.left, m.top);
63
+ ctx.lineTo(m.left, m.top + ph);
64
+ ctx.lineTo(m.left + pw, m.top + ph);
65
+ ctx.stroke();
66
+ } return { ...m, pw, ph, y, format }; }
51
67
  function label(ctx, series, value, x, y, height, axisFormat) { const style = series.dataLabel ?? {}; if (style.showValue === false || Math.abs(value) < .1 || height <= 12)
52
68
  return; ctx.fillStyle = style.color ?? '#ffffff'; ctx.font = `400 ${Math.max(9, style.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(formatChartValue(value, style.numFmt ?? series.valueFormat ?? axisFormat), x, y); }
53
69
  function stacked(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds), group = a.pw / data.categories.length, gap = finite(data.gapWidth) ? Math.max(.2, Math.min(.9, 1 - data.gapWidth / 300)) : .58, bar = Math.max(8, group * gap), category = data.categoryAxis ?? {}; data.categories.forEach((name, i) => { const cx = a.left + group * i + group / 2; let pos = 0, neg = 0; for (const series of data.series) {
@@ -72,6 +88,9 @@ function line(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds)
72
88
  ctx.arc(px, py, 4.5, 0, Math.PI * 2);
73
89
  ctx.fill();
74
90
  ctx.strokeStyle = series.color || '#4472c4';
91
+ ctx.lineWidth = 2;
92
+ ctx.beginPath();
93
+ ctx.arc(px, py, 4.5, 0, Math.PI * 2);
75
94
  ctx.stroke();
76
95
  const style = series.dataLabel ?? {};
77
96
  if (style.showValue !== false) {
@@ -79,8 +79,9 @@ function imageElement(e) { const box = frame(e, 'image'), img = node('img', 'omn
79
79
  img.src = recolored;
80
80
  }
81
81
  catch { /* keep the original raster when recoloring fails */ } }, { once: true }); box.append(img); return box; }
82
- function tableElement(e) { const box = frame(e, 'table'), table = node('table', 'omni-ppt__table'); (e.tableRows ?? []).forEach((row, rowIndex) => { const tr = node('tr'); row.forEach(value => { const cell = node(rowIndex === 0 ? 'th' : 'td'); cell.textContent = value; tr.append(cell); }); table.append(tr); }); box.append(table); return box; }
82
+ function tableElement(e) { const box = frame(e, 'table-frame'), table = node('table', 'omni-ppt__table'); (e.tableRows ?? []).forEach((row, rowIndex) => { const tr = node('tr'); row.forEach(value => { const cell = node(rowIndex === 0 ? 'th' : 'td'); cell.textContent = value; tr.append(cell); }); table.append(tr); }); box.append(table); return box; }
83
83
  function chartElement(e) { const box = frame(e, 'chart'); if (e.chartData) {
84
+ box.classList.add('omni-ppt__chart--rendered');
84
85
  const canvas = node('canvas');
85
86
  canvas.width = Math.max(300, Math.floor(e.width));
86
87
  canvas.height = Math.max(180, Math.floor(e.height));
@@ -90,7 +91,10 @@ function chartElement(e) { const box = frame(e, 'chart'); if (e.chartData) {
90
91
  queueMicrotask(() => drawChart(canvas, e.chartData));
91
92
  }
92
93
  else {
93
- box.textContent = e.chartTitle ?? (e.chartKind === 'smartart' ? 'SmartArt' : 'Chart');
94
+ const title = node('div', 'omni-ppt__chart-title'), subtitle = node('div', 'omni-ppt__chart-subtitle');
95
+ title.textContent = e.chartTitle ?? (e.chartKind === 'smartart' ? 'SmartArt' : 'Chart');
96
+ subtitle.textContent = e.chartKind === 'smartart' ? 'SmartArt placeholder' : 'Chart placeholder';
97
+ box.append(title, subtitle);
94
98
  } return box; }
95
99
  export function presetGeomToSvgPath(preset, w, h) { if (!preset || w <= 0 || h <= 0)
96
100
  return; switch (preset) {
@@ -120,11 +124,25 @@ export function presetGeomToSvgPath(preset, w, h) { if (!preset || w <= 0 || h <
120
124
  case 'rect': return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
121
125
  default: return;
122
126
  } }
123
- function marker(defs, id, type, color) { const value = document.createElementNS(SVG_NS, 'marker'); value.id = id; value.setAttribute('markerWidth', '8'); value.setAttribute('markerHeight', '8'); value.setAttribute('refX', type === 'start' ? '0' : '8'); value.setAttribute('refY', '4'); value.setAttribute('orient', 'auto'); value.setAttribute('markerUnits', 'strokeWidth'); const path = document.createElementNS(SVG_NS, 'path'); path.setAttribute('d', type === 'circle' ? 'M 4 1 A 3 3 0 1 0 4 7 A 3 3 0 1 0 4 1' : 'M 0 0 L 8 4 L 0 8 Z'); path.setAttribute('fill', color); value.append(path); defs.append(value); }
124
- function shapeElement(e) { const box = frame(e, 'shape'), w = Math.max(1, Math.abs(e.width)), h = Math.max(1, Math.abs(e.height)), svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('width', String(w)); svg.setAttribute('height', String(h)); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); const stroke = e.borderColor ?? '#000', strokeWidth = String(Math.max(1, e.borderWidthPx ?? 1)); if (e.presetGeom === 'line' || e.presetGeom === 'straightConnector1') {
127
+ function marker(defs, id, type, color) { const value = document.createElementNS(SVG_NS, 'marker'), circle = type === 'circle'; value.id = id; value.setAttribute('markerWidth', circle ? '6' : '8'); value.setAttribute('markerHeight', '6'); value.setAttribute('refX', circle ? '3' : '8'); value.setAttribute('refY', '3'); value.setAttribute('orient', 'auto'); value.setAttribute('markerUnits', 'strokeWidth'); value.setAttribute('overflow', 'visible'); if (circle) {
128
+ const dot = document.createElementNS(SVG_NS, 'circle');
129
+ dot.setAttribute('cx', '3');
130
+ dot.setAttribute('cy', '3');
131
+ dot.setAttribute('r', '2.5');
132
+ dot.setAttribute('fill', color);
133
+ value.append(dot);
134
+ }
135
+ else {
136
+ const path = document.createElementNS(SVG_NS, 'path');
137
+ path.setAttribute('d', 'M 0 0 L 8 3 L 0 6 Z');
138
+ path.setAttribute('fill', color);
139
+ value.append(path);
140
+ } defs.append(value); }
141
+ function shapeElement(e) { const box = frame(e, 'shape'), w = Math.max(1, Math.abs(e.width)), h = Math.max(1, Math.abs(e.height)), svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('width', String(w)); svg.setAttribute('height', String(h)); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); Object.assign(svg.style, { display: 'block', overflow: 'visible' }); const stroke = e.borderColor ?? '#000', strokeWidth = String(Math.max(1, e.borderWidthPx ?? 1)); if (e.presetGeom === 'line' || e.presetGeom === 'straightConnector1') {
125
142
  box.style.transform = e.rotateDeg ? `rotate(${e.rotateDeg}deg)` : '';
143
+ Object.assign(svg.style, { position: 'absolute', left: '0', top: '0' });
126
144
  const defs = document.createElementNS(SVG_NS, 'defs'), suffix = `${e.zIndex}-${Math.round(e.x)}-${Math.round(e.y)}`;
127
- marker(defs, `arrow-${suffix}`, 'end', stroke);
145
+ marker(defs, `arrow-${suffix}`, 'arrow', stroke);
128
146
  marker(defs, `circle-${suffix}`, 'circle', stroke);
129
147
  svg.append(defs);
130
148
  const line = document.createElementNS(SVG_NS, 'line');
@@ -1 +1 @@
1
- export declare const pptViewerCss = ":host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset=\"cover-title\"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset=\"cover-subtitle\"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}";
1
+ export declare const pptViewerCss = ":host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset=\"cover-title\"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset=\"cover-subtitle\"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}";
@@ -1 +1 @@
1
- export const pptViewerCss = `:host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}`;
1
+ export const pptViewerCss = `:host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-viewer-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared parsing + rendering core for Omni Viewer (vscode / chrome / obsidian / web)",
5
5
  "type": "module",
6
6
  "license": "MIT",