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.
- package/dist/parsers/pptx/index.d.ts +2 -1
- package/dist/parsers/pptx/index.js +5 -2
- package/dist/styles/ppt.css +1 -1
- package/dist/viewers/pdf/editing.d.ts +6 -4
- package/dist/viewers/pdf/editing.js +47 -13
- package/dist/viewers/pdf/index.js +11 -10
- package/dist/viewers/ppt/chart.js +22 -3
- package/dist/viewers/ppt/render.js +23 -5
- package/dist/viewers/ppt/styles.d.ts +1 -1
- package/dist/viewers/ppt/styles.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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); }
|
package/dist/styles/ppt.css
CHANGED
|
@@ -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%;
|
|
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
|
|
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
|
-
* -
|
|
18
|
-
* - the layer JSON `{ version, pageOrder, annotations }
|
|
19
|
-
*
|
|
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
|
|
9
|
-
//
|
|
10
|
-
// removable overlay layer on reopen.
|
|
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
|
|
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 =
|
|
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
|
-
/**
|
|
108
|
-
async function
|
|
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 =
|
|
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
|
-
* -
|
|
134
|
-
* - the layer JSON `{ version, pageOrder, annotations }
|
|
135
|
-
*
|
|
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:
|
|
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(
|
|
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,
|
|
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 (
|
|
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
|
|
188
|
+
let base;
|
|
189
189
|
let layerText;
|
|
190
190
|
for (const entry of Object.values(attachments)) {
|
|
191
|
-
if (entry.filename ===
|
|
192
|
-
|
|
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 (!
|
|
197
|
+
if (!base || layerText === undefined)
|
|
198
198
|
return null;
|
|
199
199
|
const layer = parseLayer(layerText);
|
|
200
|
-
return layer ? {
|
|
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
|
|
1378
|
-
// from
|
|
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.
|
|
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(
|
|
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;
|
|
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();
|
|
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
|
-
|
|
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', '
|
|
124
|
-
|
|
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}`, '
|
|
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%;
|
|
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%;
|
|
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}`;
|