@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 +21 -0
- package/README.md +70 -0
- package/dist/export.d.mts +31 -0
- package/dist/export.mjs +38 -0
- package/dist/geometry.d.mts +53 -0
- package/dist/geometry.mjs +50 -0
- package/dist/image.d.mts +73 -0
- package/dist/image.mjs +76 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/style.d.mts +30 -0
- package/dist/style.mjs +88 -0
- package/package.json +70 -0
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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) © [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 };
|
package/dist/export.mjs
ADDED
|
@@ -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 };
|
package/dist/image.d.mts
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/style.d.mts
ADDED
|
@@ -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
|
+
}
|