@docen/core 0.5.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Demo Macro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @docen/core
2
+
3
+ ![npm version](https://img.shields.io/npm/v/@docen/core)
4
+ ![npm downloads](https://img.shields.io/npm/dw/@docen/core)
5
+ ![npm license](https://img.shields.io/npm/l/@docen/core)
6
+
7
+ > Pure rendering layer mapping OOXML drawing data (image / shape / chart / smartart) to LeaferJS / ECharts elements, shared across the docx (Tiptap), pptx (LeaferJS), and xlsx (RevoGrid) editors.
8
+
9
+ > Consumed by [`@docen/editor`](../editor/README.md), which mounts these elements in `<docen-image>` web components with selection, resize, and rotate UX.
10
+
11
+ ## Features
12
+
13
+ - 🖼️ **Image** — `renderImage` / `parseImage` map OOXML `<pic:pic>` (blip + transform + srcRect crop + outline) to LeaferJS `Image` elements
14
+ - 📐 **Geometry** — EMU ↔ px, srcRect → clip, transform math as stateless functions (reuse `@office-open/core`'s `convertEmuToPixels`)
15
+ - 🎨 **Style** — `renderFill` / `renderOutline` map OOXML `FillOptions` / `OutlineOptions` to LeaferJS paints
16
+ - 📤 **Export** — `exportImage` / `exportCanvas` wrap LeaferJS export (canvas → png/jpg base64) for DOCX `<a:blip>` round-trip
17
+ - 🧩 **Headless-ready** — DOM-free functions run identically in the browser and in Node (`@leafer-ui/node`) for SSR / thumbnails
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # Install with pnpm
23
+ $ pnpm add @docen/core
24
+
25
+ # Install with npm
26
+ $ npm install @docen/core
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { renderImage, parseImage } from "@docen/core/image";
33
+ import { exportImage } from "@docen/core/export";
34
+ import { App, Image } from "leafer-ui";
35
+
36
+ // Render an OOXML image to LeaferJS
37
+ const options = renderImage({
38
+ src: "data:image/png;base64,...",
39
+ width: 400,
40
+ height: 300,
41
+ rotation: 15,
42
+ crop: { left: 10000, top: 5000 }, // permyriad (0–100000 per side)
43
+ });
44
+
45
+ const app = new App({ view, editor: {} });
46
+ const image = new Image(options);
47
+ app.add(image);
48
+
49
+ // Read back the edited geometry after a user resize/rotate
50
+ const { width, height, rotation } = parseImage(image);
51
+
52
+ // Export the canvas to a base64 data URL for DOCX round-trip
53
+ const dataUrl = await exportImage(app, "png");
54
+ ```
55
+
56
+ ## Architecture
57
+
58
+ ```
59
+ @office-open/core OOXML data model (read / write .docx .pptx .xlsx)
60
+
61
+ @docen/core OOXML data ↔ visual elements (this package)
62
+
63
+ @docen/editor UI components + NodeViews + interaction
64
+ ```
65
+
66
+ `@docen/core` is the visual counterpart of `@office-open/core`'s data model. It owns the `render*` (data → element) and `parse*` (element → data) mappings plus geometry math and PNG/SVG export. It deliberately owns **no editing semantics**.
67
+
68
+ ## License
69
+
70
+ - [MIT](LICENSE) &copy; [Demo Macro](https://www.demomacro.com/)
@@ -0,0 +1,31 @@
1
+ import { IUI } from "leafer-ui";
2
+
3
+ //#region src/export.d.ts
4
+ /** Format accepted by {@link exportImage} / {@link exportCanvas}. */
5
+ type ExportFormat = "png" | "jpg" | "jpeg";
6
+ /** Options forwarded to LeaferJS `element.export()`. */
7
+ interface ExportRequestOptions {
8
+ /** Pixel ratio for high-DPI export (e.g. 2 for retina). Default 1. */
9
+ pixelRatio?: number;
10
+ /** JPEG quality 0–1 (ignored for png). Default 0.92. */
11
+ quality?: number;
12
+ }
13
+ /**
14
+ * Export a LeaferJS element to a base64 data URL.
15
+ *
16
+ * The element must come from a LeaferJS app that has loaded `@leafer-in/export`
17
+ * (the `@docen/editor` image/shape/chart editor components set this up). Returns
18
+ * a `data:image/<format>;base64,…` string ready to drop into an `<img src>` or
19
+ * an OOXML `<a:blip>` payload.
20
+ */
21
+ declare const exportImage: (element: IUI, format?: ExportFormat, options?: ExportRequestOptions) => Promise<string>;
22
+ /**
23
+ * Export a LeaferJS element synchronously.
24
+ *
25
+ * Only works when all async-loaded images in the element have finished decoding
26
+ * (the caller's responsibility). Prefer {@link exportImage} unless you are on a
27
+ * hot path that has already awaited image loads.
28
+ */
29
+ declare const exportCanvas: (element: IUI, format?: ExportFormat, options?: ExportRequestOptions) => string;
30
+ //#endregion
31
+ export { ExportFormat, ExportRequestOptions, exportCanvas, exportImage };
@@ -0,0 +1,38 @@
1
+ //#region src/export.ts
2
+ /** Build the LeaferJS `IExportOptions` from our simpler {@link
3
+ * ExportRequestOptions}. */
4
+ const toExportOptions = (options) => ({
5
+ pixelRatio: options.pixelRatio ?? 1,
6
+ quality: options.quality ?? .92
7
+ });
8
+ /** Coerce a LeaferJS export result's `data` field to a base64 data URL string.
9
+ * Returns "" when the plugin returned a non-string (canvas / blob) — callers
10
+ * should then use the element's `export(filename)` form for file output. */
11
+ const toDataUrl = (data) => typeof data === "string" ? data : "";
12
+ /**
13
+ * Export a LeaferJS element to a base64 data URL.
14
+ *
15
+ * The element must come from a LeaferJS app that has loaded `@leafer-in/export`
16
+ * (the `@docen/editor` image/shape/chart editor components set this up). Returns
17
+ * a `data:image/<format>;base64,…` string ready to drop into an `<img src>` or
18
+ * an OOXML `<a:blip>` payload.
19
+ */
20
+ const exportImage = async (element, format = "png", options = {}) => {
21
+ const fmt = format === "jpeg" ? "jpg" : format;
22
+ const result = await element.export(fmt, toExportOptions(options));
23
+ return toDataUrl(result.data);
24
+ };
25
+ /**
26
+ * Export a LeaferJS element synchronously.
27
+ *
28
+ * Only works when all async-loaded images in the element have finished decoding
29
+ * (the caller's responsibility). Prefer {@link exportImage} unless you are on a
30
+ * hot path that has already awaited image loads.
31
+ */
32
+ const exportCanvas = (element, format = "png", options = {}) => {
33
+ const fmt = format === "jpeg" ? "jpg" : format;
34
+ const result = element.syncExport(fmt, toExportOptions(options));
35
+ return toDataUrl(result.data);
36
+ };
37
+ //#endregion
38
+ export { exportCanvas, exportImage };
@@ -0,0 +1,53 @@
1
+ //#region src/geometry.d.ts
2
+ /**
3
+ * @docen/core/geometry — pure geometry math for OOXML drawings.
4
+ *
5
+ * All functions are stateless and carry no DOM/LeaferJS dependency, so they run
6
+ * identically in the browser and in Node (SSR / headless export). EMU/px
7
+ * conversion delegates to `@office-open/core/util` so there is one source of
8
+ * truth for the EMU-per-pixel constant.
9
+ *
10
+ * @module
11
+ */
12
+ /** Per-axis visible fraction produced by {@link cropFractions}. */
13
+ interface CropFractions {
14
+ /** Visible width fraction (0–1); 1 when horizontally uncropped. */
15
+ visibleW: number;
16
+ /** Visible height fraction (0–1); 1 when vertically uncropped. */
17
+ visibleH: number;
18
+ /** Left offset fraction (0–1) of the cropped-out region. */
19
+ left: number;
20
+ /** Top offset fraction (0–1) of the cropped-out region. */
21
+ top: number;
22
+ }
23
+ /** Convert OOXML permyriad (0–100000) to a 0–1 fraction. */
24
+ declare const permyriadToFraction: (value: number | undefined) => number;
25
+ /** True when any side of an OOXML srcRect is non-zero. */
26
+ declare const hasCrop: (crop: {
27
+ left?: number;
28
+ top?: number;
29
+ right?: number;
30
+ bottom?: number;
31
+ } | null | undefined) => boolean;
32
+ /**
33
+ * Resolve an OOXML srcRect (permyriad per side) into per-axis visible/offset
34
+ * fractions.
35
+ *
36
+ * Mirrors the math in `@docen/docx` `renderCropAttrs` so the canvas crop box
37
+ * matches the HTML crop box byte-for-byte. Returns `{ visibleW:1, visibleH:1,
38
+ * left:0, top:0 }` when nothing is cropped.
39
+ */
40
+ declare const cropFractions: (crop: {
41
+ left?: number;
42
+ top?: number;
43
+ right?: number;
44
+ bottom?: number;
45
+ } | null | undefined) => CropFractions;
46
+ /** Convert an EMU value to CSS pixels (rounded for stable layout). */
47
+ declare const emuToPx: (emu: number | undefined) => number;
48
+ /** Convert CSS pixels to EMU (for writing geometry back to OOXML). */
49
+ declare const pxToEmu: (px: number) => number;
50
+ /** Clamp a dimension to a positive, finite number, falling back to `fallback`. */
51
+ declare const clampDimension: (value: number | null | undefined, fallback: number) => number;
52
+ //#endregion
53
+ export { CropFractions, clampDimension, cropFractions, emuToPx, hasCrop, permyriadToFraction, pxToEmu };
@@ -0,0 +1,50 @@
1
+ import { convertEmuToPixels, convertToEmu } from "@office-open/core/util";
2
+ //#region src/geometry.ts
3
+ /**
4
+ * @docen/core/geometry — pure geometry math for OOXML drawings.
5
+ *
6
+ * All functions are stateless and carry no DOM/LeaferJS dependency, so they run
7
+ * identically in the browser and in Node (SSR / headless export). EMU/px
8
+ * conversion delegates to `@office-open/core/util` so there is one source of
9
+ * truth for the EMU-per-pixel constant.
10
+ *
11
+ * @module
12
+ */
13
+ /** Convert OOXML permyriad (0–100000) to a 0–1 fraction. */
14
+ const permyriadToFraction = (value) => (value ?? 0) / 1e5;
15
+ /** True when any side of an OOXML srcRect is non-zero. */
16
+ const hasCrop = (crop) => Boolean(crop && (crop.left || crop.top || crop.right || crop.bottom));
17
+ /**
18
+ * Resolve an OOXML srcRect (permyriad per side) into per-axis visible/offset
19
+ * fractions.
20
+ *
21
+ * Mirrors the math in `@docen/docx` `renderCropAttrs` so the canvas crop box
22
+ * matches the HTML crop box byte-for-byte. Returns `{ visibleW:1, visibleH:1,
23
+ * left:0, top:0 }` when nothing is cropped.
24
+ */
25
+ const cropFractions = (crop) => {
26
+ if (!crop) return {
27
+ visibleW: 1,
28
+ visibleH: 1,
29
+ left: 0,
30
+ top: 0
31
+ };
32
+ const left = permyriadToFraction(crop.left);
33
+ const top = permyriadToFraction(crop.top);
34
+ const right = permyriadToFraction(crop.right);
35
+ const bottom = permyriadToFraction(crop.bottom);
36
+ return {
37
+ visibleW: Math.max(0, 1 - left - right),
38
+ visibleH: Math.max(0, 1 - top - bottom),
39
+ left,
40
+ top
41
+ };
42
+ };
43
+ /** Convert an EMU value to CSS pixels (rounded for stable layout). */
44
+ const emuToPx = (emu) => Math.round(convertEmuToPixels(emu ?? 0));
45
+ /** Convert CSS pixels to EMU (for writing geometry back to OOXML). */
46
+ const pxToEmu = (px) => convertToEmu(px);
47
+ /** Clamp a dimension to a positive, finite number, falling back to `fallback`. */
48
+ const clampDimension = (value, fallback) => typeof value === "number" && value > 0 && Number.isFinite(value) ? value : fallback;
49
+ //#endregion
50
+ export { clampDimension, cropFractions, emuToPx, hasCrop, permyriadToFraction, pxToEmu };
@@ -0,0 +1,73 @@
1
+ import { IImageInputData, IUI } from "leafer-ui";
2
+ import { OutlineOptions, SourceRectangleOptions } from "@office-open/core/drawingml";
3
+
4
+ //#region src/image.d.ts
5
+ /** Minimal subset of `ImageAttrs` (from `@docen/docx`) that the renderer needs.
6
+ * Declared locally so @docen/core has no hard dependency on the docx engine —
7
+ * pptx/xlsx image data can conform to the same shape. The `crop`/`outline`
8
+ * fields reuse `@office-open/core`'s native types (`SourceRectangleOptions` /
9
+ * `OutlineOptions`) instead of redefining them. */
10
+ interface RenderImageInput {
11
+ /** Image source as a URL or `data:` URL. */
12
+ src: string;
13
+ /** X position in CSS pixels (defaults to 0). */
14
+ x?: number | null;
15
+ /** Y position in CSS pixels (defaults to 0). */
16
+ y?: number | null;
17
+ /** Display width in CSS pixels (OOXML extent already converted). */
18
+ width: number | null | undefined;
19
+ /** Display height in CSS pixels (OOXML extent already converted). */
20
+ height: number | null | undefined;
21
+ /** Clockwise rotation in degrees (OOXML `transformation.rotation`). */
22
+ rotation?: number | null;
23
+ /** OOXML srcRect crop (`a:srcRect`), permyriad per side. */
24
+ crop?: SourceRectangleOptions | null;
25
+ /** OOXML outline (`a:ln`). */
26
+ outline?: OutlineOptions | null;
27
+ }
28
+ /** Geometric fields the editor reads back after a user move/resize/rotate. */
29
+ interface ParsedImageOutput {
30
+ /** X position in CSS pixels (LeaferJS local element.x). */
31
+ x: number;
32
+ /** Y position in CSS pixels (LeaferJS local element.y). */
33
+ y: number;
34
+ /** New display width in CSS pixels. */
35
+ width: number;
36
+ /** New display height in CSS pixels. */
37
+ height: number;
38
+ /** New clockwise rotation in degrees, normalized to [0, 360). */
39
+ rotation: number;
40
+ }
41
+ /**
42
+ * Build the LeaferJS `Image` element options (`IImageInputData`) for an OOXML
43
+ * image.
44
+ *
45
+ * Width / height default to 400×300 when absent (the same placeholder ratio the
46
+ * engine's `renderHTML` uses). Rotation is normalized to [0, 360). The OOXML
47
+ * outline is mapped to LeaferJS `stroke` / `strokeWidth` / `dashPattern` via
48
+ * {@link renderOutline}. srcRect crop is NOT included here (LeaferJS's Image
49
+ * ignores an unknown `crop` key) — the editor component reads `input.crop`
50
+ * and wraps the image in a Box{overflow:'hide'} sized via {@link renderCropBox}.
51
+ */
52
+ declare const renderImage: (input: RenderImageInput) => IImageInputData;
53
+ /** Read the user-moved/resized geometry of a LeaferJS element back into the
54
+ * OOXML data shape. Use this in the editor's transform-change handler.
55
+ *
56
+ * Reads LOCAL geometry (element.x/y/width/height) rather than `boxBounds`:
57
+ * boxBounds is the world-space axis-aligned bounding box, which swaps/enlarges
58
+ * under rotation — reading it after rotate() would bleed the rotated AABB back
59
+ * into width/height and corrupt the exported extent. Local width/height are
60
+ * unchanged by rotation (only `rotation` changes), so a pure rotate() round-
61
+ * trips without dimension drift (edit == render == export). */
62
+ declare const parseImage: (element: IUI) => ParsedImageOutput;
63
+ /** Compute the LeaferJS inner-image size + offset that realize a srcRect crop
64
+ * inside an outer box of the given width/height. Used by the editor component
65
+ * to lay out a crop frame (mirrors `@docen/docx` `renderCropAttrs` math). */
66
+ declare const renderCropBox: (crop: SourceRectangleOptions, width: number, height: number) => {
67
+ innerWidth: number;
68
+ innerHeight: number;
69
+ offsetX: number;
70
+ offsetY: number;
71
+ };
72
+ //#endregion
73
+ export { ParsedImageOutput, RenderImageInput, parseImage, renderCropBox, renderImage };
package/dist/image.mjs ADDED
@@ -0,0 +1,76 @@
1
+ import { clampDimension, cropFractions } from "./geometry.mjs";
2
+ import { renderOutline } from "./style.mjs";
3
+ //#region src/image.ts
4
+ /** Default display size when width/height are missing or invalid (matches the
5
+ * `@docen/docx` image renderHTML placeholder ratio 4:3). */
6
+ const DEFAULT_WIDTH = 400;
7
+ const DEFAULT_HEIGHT = 300;
8
+ /** Normalize a rotation in degrees to [0, 360). */
9
+ const normalizeRotation = (deg) => {
10
+ if (!deg || !Number.isFinite(deg)) return 0;
11
+ return (deg % 360 + 360) % 360;
12
+ };
13
+ /**
14
+ * Build the LeaferJS `Image` element options (`IImageInputData`) for an OOXML
15
+ * image.
16
+ *
17
+ * Width / height default to 400×300 when absent (the same placeholder ratio the
18
+ * engine's `renderHTML` uses). Rotation is normalized to [0, 360). The OOXML
19
+ * outline is mapped to LeaferJS `stroke` / `strokeWidth` / `dashPattern` via
20
+ * {@link renderOutline}. srcRect crop is NOT included here (LeaferJS's Image
21
+ * ignores an unknown `crop` key) — the editor component reads `input.crop`
22
+ * and wraps the image in a Box{overflow:'hide'} sized via {@link renderCropBox}.
23
+ */
24
+ const renderImage = (input) => {
25
+ const width = clampDimension(input.width, DEFAULT_WIDTH);
26
+ const height = clampDimension(input.height, DEFAULT_HEIGHT);
27
+ const rotation = normalizeRotation(input.rotation);
28
+ const outline = renderOutline(input.outline);
29
+ const options = {
30
+ url: input.src,
31
+ x: input.x ?? 0,
32
+ y: input.y ?? 0,
33
+ width,
34
+ height,
35
+ rotation,
36
+ editable: true
37
+ };
38
+ if (outline) {
39
+ options.stroke = outline.stroke;
40
+ options.strokeWidth = outline.strokeWidth;
41
+ options.dashPattern = outline.dashPattern;
42
+ }
43
+ return options;
44
+ };
45
+ /** Read the user-moved/resized geometry of a LeaferJS element back into the
46
+ * OOXML data shape. Use this in the editor's transform-change handler.
47
+ *
48
+ * Reads LOCAL geometry (element.x/y/width/height) rather than `boxBounds`:
49
+ * boxBounds is the world-space axis-aligned bounding box, which swaps/enlarges
50
+ * under rotation — reading it after rotate() would bleed the rotated AABB back
51
+ * into width/height and corrupt the exported extent. Local width/height are
52
+ * unchanged by rotation (only `rotation` changes), so a pure rotate() round-
53
+ * trips without dimension drift (edit == render == export). */
54
+ const parseImage = (element) => ({
55
+ x: Math.round(element.x ?? 0),
56
+ y: Math.round(element.y ?? 0),
57
+ width: Math.round(element.width ?? 0),
58
+ height: Math.round(element.height ?? 0),
59
+ rotation: normalizeRotation(element.rotation ?? 0)
60
+ });
61
+ /** Compute the LeaferJS inner-image size + offset that realize a srcRect crop
62
+ * inside an outer box of the given width/height. Used by the editor component
63
+ * to lay out a crop frame (mirrors `@docen/docx` `renderCropAttrs` math). */
64
+ const renderCropBox = (crop, width, height) => {
65
+ const { visibleW, visibleH, left, top } = cropFractions(crop);
66
+ const innerWidth = visibleW > 0 ? width / visibleW : width;
67
+ const innerHeight = visibleH > 0 ? height / visibleH : height;
68
+ return {
69
+ innerWidth,
70
+ innerHeight,
71
+ offsetX: -(left * innerWidth),
72
+ offsetY: -(top * innerHeight)
73
+ };
74
+ };
75
+ //#endregion
76
+ export { parseImage, renderCropBox, renderImage };
@@ -0,0 +1,5 @@
1
+ import { ExportFormat, ExportRequestOptions, exportCanvas, exportImage } from "./export.mjs";
2
+ import { CropFractions, clampDimension, cropFractions, emuToPx, hasCrop, permyriadToFraction, pxToEmu } from "./geometry.mjs";
3
+ import { ParsedImageOutput, RenderImageInput, parseImage, renderCropBox, renderImage } from "./image.mjs";
4
+ import { LeaferStroke, normalizeColorToHex, renderFill, renderOutline } from "./style.mjs";
5
+ export { CropFractions, ExportFormat, ExportRequestOptions, LeaferStroke, ParsedImageOutput, RenderImageInput, clampDimension, cropFractions, emuToPx, exportCanvas, exportImage, hasCrop, normalizeColorToHex, parseImage, permyriadToFraction, pxToEmu, renderCropBox, renderFill, renderImage, renderOutline };
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { clampDimension, cropFractions, emuToPx, hasCrop, permyriadToFraction, pxToEmu } from "./geometry.mjs";
2
+ import { normalizeColorToHex, renderFill, renderOutline } from "./style.mjs";
3
+ import { parseImage, renderCropBox, renderImage } from "./image.mjs";
4
+ import { exportCanvas, exportImage } from "./export.mjs";
5
+ export { clampDimension, cropFractions, emuToPx, exportCanvas, exportImage, hasCrop, normalizeColorToHex, parseImage, permyriadToFraction, pxToEmu, renderCropBox, renderFill, renderImage, renderOutline };
@@ -0,0 +1,30 @@
1
+ import { FillOptions, OutlineOptions } from "@office-open/core/drawingml";
2
+
3
+ //#region src/style.d.ts
4
+ /** Normalize any OOXML color value (bare hex, #hex, named, or { val }) to #RRGGBB.
5
+ * Returns undefined for "auto" (no CSS/LeaferJS equivalent) and unrecognized input. */
6
+ declare const normalizeColorToHex: (color: unknown) => string | undefined;
7
+ /** OOXML fill → LeaferJS `fill` paint value (a hex color string, or undefined).
8
+ * Solid fills only at this stage; none/gradient/blip/pattern return undefined. */
9
+ declare const renderFill: (fill: FillOptions | null | undefined) => string | undefined;
10
+ /** LeaferJS stroke properties (`IStrokeStyle` + `stroke` color), emitted by
11
+ * {@link renderOutline}. These map directly onto `IUIBaseInputData` stroke
12
+ * fields — no custom wrapper type. */
13
+ interface LeaferStroke {
14
+ /** Hex color (#RRGGBB), defaulting to black when the OOXML color is "auto". */
15
+ stroke: string;
16
+ /** Stroke width in pixels. */
17
+ strokeWidth: number;
18
+ /** Dash pattern as a number array (Canvas `setLineDash` contract). Undefined
19
+ * means a solid line (LeaferJS default) — never set it to a string, since
20
+ * `ctx.setLineDash` rejects non-numeric input. */
21
+ dashPattern?: number[];
22
+ }
23
+ /** OOXML outline → LeaferJS stroke properties. `noFill` and absent outlines
24
+ * return undefined. EMU widths are converted to px via the standard 9525
25
+ * EMU/px ratio. The dash preset is collapsed to a `number[]` (Canvas
26
+ * `setLineDash` contract) for the common OOXML dot/dash presets; full
27
+ * PresetDash → dash-array mapping is stage 2. */
28
+ declare const renderOutline: (outline: OutlineOptions | null | undefined) => LeaferStroke | undefined;
29
+ //#endregion
30
+ export { LeaferStroke, normalizeColorToHex, renderFill, renderOutline };
package/dist/style.mjs ADDED
@@ -0,0 +1,88 @@
1
+ import { convertEmuToPixels } from "@office-open/core/util";
2
+ //#region src/style.ts
3
+ /** A small CSS-named-color table so OOXML bare hex / named colors resolve to
4
+ * #RRGGBB. Mirrors `@docen/docx` `normalizeColorToHex` — kept local so
5
+ * @docen/core has no dependency on the docx engine. */
6
+ const CSS_COLORS = {
7
+ black: "#000000",
8
+ white: "#FFFFFF",
9
+ red: "#FF0000",
10
+ green: "#008000",
11
+ blue: "#0000FF",
12
+ yellow: "#FFFF00",
13
+ cyan: "#00FFFF",
14
+ magenta: "#FF00FF",
15
+ gray: "#808080",
16
+ grey: "#808080",
17
+ silver: "#C0C0C0",
18
+ maroon: "#800000",
19
+ olive: "#808000",
20
+ purple: "#800080",
21
+ teal: "#008080",
22
+ navy: "#000080",
23
+ orange: "#FFA500",
24
+ pink: "#FFC0CB",
25
+ brown: "#A52A2A",
26
+ lime: "#00FF00",
27
+ gold: "#FFD700",
28
+ aqua: "#00FFFF",
29
+ fuchsia: "#FF00FF",
30
+ indigo: "#4B0082",
31
+ violet: "#EE82EE",
32
+ coral: "#FF7F50",
33
+ salmon: "#FA8072",
34
+ tomato: "#FF6347"
35
+ };
36
+ /** Normalize any OOXML color value (bare hex, #hex, named, or { val }) to #RRGGBB.
37
+ * Returns undefined for "auto" (no CSS/LeaferJS equivalent) and unrecognized input. */
38
+ const normalizeColorToHex = (color) => {
39
+ if (!color) return void 0;
40
+ if (typeof color === "object") {
41
+ const { val } = color;
42
+ return val ? normalizeColorToHex(val) : void 0;
43
+ }
44
+ if (typeof color !== "string") return void 0;
45
+ if (color === "auto") return void 0;
46
+ if (color.startsWith("#")) return color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`.toUpperCase() : color.toUpperCase();
47
+ if (/^[0-9A-Fa-f]{6}$/.test(color)) return `#${color.toUpperCase()}`;
48
+ if (/^[0-9A-Fa-f]{3}$/.test(color)) return `#${color[0]}${color[0]}${color[1]}${color[1]}${color[2]}${color[2]}`.toUpperCase();
49
+ return CSS_COLORS[color.toLowerCase()] ?? void 0;
50
+ };
51
+ /** Extract a hex color from a SolidFillOptions union member (rgb/scheme/hsl/…). */
52
+ const solidColorValue = (color) => {
53
+ if (!color) return void 0;
54
+ if (typeof color === "string") return color;
55
+ const obj = color;
56
+ return obj.value ?? obj.val;
57
+ };
58
+ /** OOXML fill → LeaferJS `fill` paint value (a hex color string, or undefined).
59
+ * Solid fills only at this stage; none/gradient/blip/pattern return undefined. */
60
+ const renderFill = (fill) => {
61
+ if (!fill) return void 0;
62
+ if (typeof fill === "string") return normalizeColorToHex(fill);
63
+ if (fill.type !== "solid") return void 0;
64
+ const color = typeof fill.color === "string" ? fill.color : solidColorValue(fill.color);
65
+ return normalizeColorToHex(color);
66
+ };
67
+ /** OOXML outline → LeaferJS stroke properties. `noFill` and absent outlines
68
+ * return undefined. EMU widths are converted to px via the standard 9525
69
+ * EMU/px ratio. The dash preset is collapsed to a `number[]` (Canvas
70
+ * `setLineDash` contract) for the common OOXML dot/dash presets; full
71
+ * PresetDash → dash-array mapping is stage 2. */
72
+ const renderOutline = (outline) => {
73
+ if (!outline || outline.type === "noFill") return void 0;
74
+ const stroke = normalizeColorToHex(solidColorValue(outline.color)) ?? "#000000";
75
+ const emu = typeof outline.width === "number" ? outline.width : 9525;
76
+ const strokeWidth = Math.max(.5, convertEmuToPixels(emu));
77
+ const dash = outline.dash;
78
+ let dashPattern;
79
+ if (dash === "sysDot" || dash === "dot") dashPattern = [2, 4];
80
+ else if (dash === "sysDash" || dash === "dash" || dash === "sysDashDot") dashPattern = [8, 4];
81
+ return {
82
+ stroke,
83
+ strokeWidth,
84
+ dashPattern
85
+ };
86
+ };
87
+ //#endregion
88
+ export { normalizeColorToHex, renderFill, renderOutline };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@docen/core",
3
+ "version": "0.5.2",
4
+ "description": "Pure rendering layer mapping OOXML drawing data (image/shape/chart/smartart) to LeaferJS/ECharts elements, shared across docx/pptx/xlsx editors",
5
+ "keywords": [
6
+ "canvas",
7
+ "chart",
8
+ "docen",
9
+ "drawingml",
10
+ "echarts",
11
+ "graphics",
12
+ "image",
13
+ "leaferjs",
14
+ "office",
15
+ "ooxml",
16
+ "render",
17
+ "shape",
18
+ "smartart"
19
+ ],
20
+ "homepage": "https://github.com/DemoMacro/docen#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/DemoMacro/docen/issues"
23
+ },
24
+ "license": "MIT",
25
+ "author": {
26
+ "name": "Demo Macro",
27
+ "email": "abc@imst.xyz",
28
+ "url": "https://www.demomacro.com/"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/DemoMacro/docen.git"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "type": "module",
38
+ "main": "dist/index.mjs",
39
+ "types": "dist/index.d.mts",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.mts",
43
+ "import": "./dist/index.mjs"
44
+ },
45
+ "./image": {
46
+ "types": "./dist/image.d.mts",
47
+ "import": "./dist/image.mjs"
48
+ },
49
+ "./geometry": {
50
+ "types": "./dist/geometry.d.mts",
51
+ "import": "./dist/geometry.mjs"
52
+ },
53
+ "./style": {
54
+ "types": "./dist/style.d.mts",
55
+ "import": "./dist/style.mjs"
56
+ },
57
+ "./export": {
58
+ "types": "./dist/export.d.mts",
59
+ "import": "./dist/export.mjs"
60
+ }
61
+ },
62
+ "dependencies": {
63
+ "@office-open/core": "0.10.13",
64
+ "leafer-ui": "2.2.0"
65
+ },
66
+ "scripts": {
67
+ "dev": "basis build --stub",
68
+ "build": "vp pack"
69
+ }
70
+ }