350 lines
10 KiB
TypeScript
350 lines
10 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { describe, it } from "node:test"
|
|
import { computeGraphicLayoutStyle, resizeWithHandle } from "./docs-graphic-layout.ts"
|
|
import { normalizeImportedGraphics } from "./docs-graphic-import.ts"
|
|
import {
|
|
DOCS_GRAPHIC_DEFAULTS,
|
|
parseGraphicAttrs,
|
|
computeCropApplyPatch,
|
|
computeCropDisplayGeometry,
|
|
computeCropImageStyle,
|
|
computeCropReeditInitialRegion,
|
|
computeImageContentRect,
|
|
computeImageFitStyle,
|
|
layoutFitForImage,
|
|
usesCropImageFit,
|
|
resizeCropRegion,
|
|
} from "./docs-graphic-types.ts"
|
|
|
|
describe("docs-graphic", () => {
|
|
it("computeGraphicLayoutStyle floats the wrapper so text wraps", () => {
|
|
const layout = computeGraphicLayoutStyle({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
graphicType: "image",
|
|
wrap: "square",
|
|
floatSide: "left",
|
|
})
|
|
assert.equal(layout.wrapper.float, "left")
|
|
// Wrap gap sits on the text side of the float.
|
|
assert.ok(typeof layout.wrapper.marginInlineEnd === "string")
|
|
})
|
|
|
|
it("normalizeImportedGraphics preserves draw scene JSON", () => {
|
|
const scene = '{"type":"excalidraw","elements":[{"id":"a","type":"rectangle"}]}'
|
|
const normalized = normalizeImportedGraphics({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "docsGraphic",
|
|
attrs: {
|
|
graphicType: "draw",
|
|
drawScene: scene,
|
|
src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E",
|
|
width: 320,
|
|
height: 240,
|
|
},
|
|
},
|
|
],
|
|
})
|
|
const node = (normalized.content as Array<Record<string, unknown>>)[0]
|
|
assert.equal((node.attrs as { drawScene?: string }).drawScene, scene)
|
|
})
|
|
|
|
it("parseGraphicAttrs preserves draw scene JSON", () => {
|
|
const attrs = parseGraphicAttrs({
|
|
graphicType: "draw",
|
|
drawScene: '{"type":"excalidraw","elements":[]}',
|
|
src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E",
|
|
width: 320,
|
|
height: 240,
|
|
})
|
|
assert.equal(attrs.graphicType, "draw")
|
|
assert.equal(attrs.drawScene, '{"type":"excalidraw","elements":[]}')
|
|
assert.ok(attrs.src?.startsWith("data:image/svg+xml"))
|
|
})
|
|
|
|
it("computeGraphicLayoutStyle applies absolute placement", () => {
|
|
const layout = computeGraphicLayoutStyle({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
graphicType: "shape",
|
|
placement: "absolute",
|
|
x: 40,
|
|
y: 20,
|
|
})
|
|
assert.equal(layout.inner.position, "absolute")
|
|
assert.equal(layout.inner.left, 40)
|
|
assert.equal(layout.inner.top, 20)
|
|
})
|
|
|
|
it("resizeWithHandle respects minimum size", () => {
|
|
const next = resizeWithHandle("se", 120, 80, -200, -200)
|
|
assert.equal(next.width, 24)
|
|
assert.equal(next.height, 24)
|
|
})
|
|
|
|
it("resizeWithHandle respects aspect lock", () => {
|
|
const next = resizeWithHandle("se", 200, 100, 100, 50, 24, true)
|
|
assert.equal(next.width, 300)
|
|
assert.equal(next.height, 150)
|
|
})
|
|
|
|
it("normalizeImportedGraphics upgrades standalone image paragraph", () => {
|
|
const result = normalizeImportedGraphics({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "paragraph",
|
|
content: [{ type: "image", attrs: { src: "data:image/png;base64,abc", width: 200 } }],
|
|
},
|
|
],
|
|
})
|
|
const first = (result.content as Array<Record<string, unknown>>)[0]
|
|
assert.equal(first.type, "docsGraphic")
|
|
assert.equal((first.attrs as { graphicType?: string }).graphicType, "image")
|
|
})
|
|
|
|
it("computeGraphicLayoutStyle applies wrap margin from mm", () => {
|
|
const layout = computeGraphicLayoutStyle({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
graphicType: "image",
|
|
wrap: "square",
|
|
floatSide: "left",
|
|
wrapMarginMm: 6,
|
|
})
|
|
assert.ok(typeof layout.wrapper.marginInlineEnd === "string")
|
|
assert.notEqual(layout.wrapper.marginInlineEnd, "12px")
|
|
})
|
|
|
|
it("computeGraphicLayoutStyle centers float side", () => {
|
|
const layout = computeGraphicLayoutStyle({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
wrap: "square",
|
|
floatSide: "center",
|
|
})
|
|
assert.equal(layout.wrapper.float, "none")
|
|
assert.equal(layout.wrapper.marginInline, "auto")
|
|
})
|
|
|
|
it("computeGraphicLayoutStyle places fixed graphics with page coords", () => {
|
|
const layout = computeGraphicLayoutStyle(
|
|
{
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
positionMode: "fixed-on-page" as const,
|
|
placement: "absolute" as const,
|
|
pageIndex: 1,
|
|
pageX: 30,
|
|
pageY: 40,
|
|
},
|
|
{ pageHeight: 1000 }
|
|
)
|
|
assert.equal(layout.usePageLayer, true)
|
|
assert.equal(layout.inner.left, 30)
|
|
assert.ok(typeof layout.inner.top === "number" && layout.inner.top > 1000)
|
|
})
|
|
|
|
it("parseGraphicAttrs forces fixed-on-page for absolute placement", () => {
|
|
const attrs = parseGraphicAttrs({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
placement: "absolute",
|
|
positionMode: "move-with-text",
|
|
})
|
|
assert.equal(attrs.positionMode, "fixed-on-page")
|
|
})
|
|
|
|
it("parseGraphicAttrs migrates legacy absolute to fixed-on-page", () => {
|
|
const attrs = parseGraphicAttrs({
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
placement: "absolute",
|
|
wrap: "behind",
|
|
x: 20,
|
|
y: 30,
|
|
})
|
|
assert.equal(attrs.positionMode, "fixed-on-page")
|
|
assert.equal(attrs.pageX, 20)
|
|
assert.equal(attrs.pageY, 30)
|
|
})
|
|
|
|
it("normalizeImportedGraphics maps docx wrap aliases", () => {
|
|
const result = normalizeImportedGraphics({
|
|
type: "doc",
|
|
content: [
|
|
{
|
|
type: "docsGraphic",
|
|
attrs: {
|
|
graphicType: "image",
|
|
src: "https://example.com/a.png",
|
|
wrap: "topAndBottom",
|
|
placement: "anchored",
|
|
},
|
|
},
|
|
],
|
|
})
|
|
const attrs = (result.content as Array<Record<string, unknown>>)[0].attrs as {
|
|
wrap?: string
|
|
placement?: string
|
|
}
|
|
assert.equal(attrs.wrap, "top-bottom")
|
|
assert.equal(attrs.placement, "absolute")
|
|
})
|
|
|
|
it("computeImageContentRect letterboxes wide images", () => {
|
|
const rect = computeImageContentRect(200, 100, 400, 200)
|
|
assert.equal(rect.width, 200)
|
|
assert.equal(rect.height, 100)
|
|
assert.equal(rect.top, 0)
|
|
})
|
|
|
|
it("computeImageContentRect letterboxes tall images", () => {
|
|
const rect = computeImageContentRect(200, 100, 200, 400)
|
|
assert.equal(rect.height, 100)
|
|
assert.equal(rect.width, 50)
|
|
assert.equal(rect.left, 75)
|
|
})
|
|
|
|
it("computeImageContentRect cover fills the frame", () => {
|
|
const rect = computeImageContentRect(200, 100, 200, 400, "cover")
|
|
assert.equal(rect.width, 200)
|
|
assert.equal(rect.height, 400)
|
|
assert.equal(rect.top, -150)
|
|
})
|
|
|
|
it("computeImageContentRect cover anchor shifts focal point", () => {
|
|
const centered = computeImageContentRect(200, 100, 400, 200, "cover", 0.5, 0.5)
|
|
const left = computeImageContentRect(200, 100, 400, 200, "cover", 0, 0.5)
|
|
assert.ok(left.left > centered.left)
|
|
})
|
|
|
|
it("parseGraphicAttrs accepts crop imageFit", () => {
|
|
const attrs = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, imageFit: "crop" })
|
|
assert.equal(attrs.imageFit, "crop")
|
|
assert.equal(usesCropImageFit(attrs.imageFit), true)
|
|
assert.equal(layoutFitForImage(attrs.imageFit), "contain")
|
|
})
|
|
|
|
it("computeImageFitStyle maps anchors to object-position", () => {
|
|
const style = computeImageFitStyle({
|
|
imageFit: "cover",
|
|
imageFitAnchorH: 0,
|
|
imageFitAnchorV: 1,
|
|
})
|
|
assert.equal(style.objectFit, "cover")
|
|
assert.equal(style.objectPosition, "0% 100%")
|
|
})
|
|
|
|
it("resizeCropRegion shrinks from east without moving origin", () => {
|
|
const next = resizeCropRegion("e", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, -0.2, 0)
|
|
assert.equal(next.cropX, 0.1)
|
|
assert.equal(next.cropWidth, 0.6)
|
|
})
|
|
|
|
it("resizeCropRegion shrinks from west and shifts cropX", () => {
|
|
const next = resizeCropRegion("w", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, 0.2, 0)
|
|
assert.equal(next.cropX, 0.3)
|
|
assert.equal(next.cropWidth, 0.6)
|
|
})
|
|
|
|
it("computeCropImageStyle maps source crop 1:1 to frame", () => {
|
|
const style = computeCropImageStyle(
|
|
{
|
|
cropX: 0,
|
|
cropY: 0,
|
|
cropWidth: 0.5,
|
|
cropHeight: 1,
|
|
cropShape: "rect",
|
|
},
|
|
100,
|
|
100,
|
|
400,
|
|
200
|
|
)
|
|
assert.equal(style.img.width, 200)
|
|
assert.equal(style.img.height, 100)
|
|
assert.equal(Math.abs(style.img.left as number), 0)
|
|
assert.equal(style.img.objectFit, undefined)
|
|
})
|
|
|
|
it("computeCropImageStyle offsets the image by the crop origin", () => {
|
|
const style = computeCropImageStyle(
|
|
{
|
|
cropX: 0.25,
|
|
cropY: 0.1,
|
|
cropWidth: 0.5,
|
|
cropHeight: 0.5,
|
|
cropShape: "rect",
|
|
},
|
|
100,
|
|
100,
|
|
400,
|
|
400
|
|
)
|
|
assert.equal(style.img.position, "absolute")
|
|
assert.equal(style.img.left, -50)
|
|
assert.equal(style.img.top, -20)
|
|
})
|
|
|
|
it("computeCropDisplayGeometry re-edit uses full image as crop window", () => {
|
|
const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 }
|
|
const initial = computeCropReeditInitialRegion(baseSource, 100, 100, 400, 200)
|
|
const geometry = computeCropDisplayGeometry(
|
|
{ ...DOCS_GRAPHIC_DEFAULTS, ...initial, imageFit: "crop" },
|
|
100,
|
|
100,
|
|
400,
|
|
200,
|
|
baseSource
|
|
)
|
|
assert.equal(geometry.windowRect.width, 200)
|
|
assert.equal(geometry.windowRect.height, 100)
|
|
assert.equal(geometry.cropRect.width, 100)
|
|
assert.equal(geometry.imageRect.width, 200)
|
|
assert.equal(initial.cropWidth, 0.5)
|
|
})
|
|
|
|
it("computeCropApplyPatch re-edit can expand crop to full source", () => {
|
|
const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 }
|
|
const patch = computeCropApplyPatch(
|
|
{
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
cropX: 0,
|
|
cropY: 0,
|
|
cropWidth: 1,
|
|
cropHeight: 1,
|
|
imageFit: "crop",
|
|
width: 100,
|
|
height: 100,
|
|
},
|
|
0,
|
|
0,
|
|
400,
|
|
200,
|
|
baseSource
|
|
)
|
|
assert.equal(patch.cropWidth, 1)
|
|
assert.equal(patch.width, 200)
|
|
assert.equal(patch.height, 100)
|
|
})
|
|
|
|
it("computeCropApplyPatch resizes frame to cropped section", () => {
|
|
const patch = computeCropApplyPatch(
|
|
{
|
|
...DOCS_GRAPHIC_DEFAULTS,
|
|
cropX: 0,
|
|
cropY: 0,
|
|
cropWidth: 0.5,
|
|
cropHeight: 1,
|
|
imageFit: "contain",
|
|
width: 200,
|
|
height: 100,
|
|
},
|
|
0,
|
|
0,
|
|
400,
|
|
200
|
|
)
|
|
assert.equal(patch.width, 100)
|
|
assert.equal(patch.height, 100)
|
|
assert.equal(patch.cropWidth, 0.5)
|
|
assert.equal(patch.imageFit, "crop")
|
|
})
|
|
})
|