import { DOCX_MEDIA_RASTER_SCALE, MAXIMUM_DOCX_MEDIA_EDGE_PIXELS, MAXIMUM_DOCX_MEDIA_PIXELS, MAXIMUM_DOCX_RESOURCE_COUNT, type DocxMediaCapturePlan, type DocxMediaCaptureTarget, type DocxMediaAlignment, type DocxMediaKind, type PagedDocumentPayload } from "@md-to-pdf/core"; import { PagedDocumentRuntime } from "./paged-document-runtime.js"; export interface DocxMediaRenderDimensions { contentWidthPx: number; contentHeightPx: number; } declare global { interface Window { __mdToPdfRenderDocxMedia?: ( payload: PagedDocumentPayload, dimensions: DocxMediaRenderDimensions ) => Promise; } } function finitePositive(value: number, fallback: number) { return Number.isFinite(value) && value > 0 ? value : fallback; } function getMediaKind(element: Element): DocxMediaKind { if (element.matches("img.md-document-image")) { return "image"; } if (element.closest(".md-echarts")) { return "echarts"; } return "mermaid"; } function getCaption(element: Element) { return ( element .closest("figure") ?.querySelector("figcaption") ?.textContent?.trim() || undefined ); } function getAltText( element: Element, kind: DocxMediaKind, kindOrdinal: number ) { if (element instanceof HTMLImageElement) { return element.alt.trim() || `图片 ${kindOrdinal}`; } const labelled = element.getAttribute("aria-label")?.trim() || element .closest("[aria-label]") ?.getAttribute("aria-label") ?.trim(); if (labelled) { return labelled; } return kind === "echarts" ? `ECharts 图表 ${kindOrdinal}` : `Mermaid 图表 ${kindOrdinal}`; } function fitElementToContent( element: HTMLElement | SVGSVGElement, dimensions: DocxMediaRenderDimensions ) { const initial = element.getBoundingClientRect(); const width = finitePositive(initial.width, dimensions.contentWidthPx); const height = finitePositive( initial.height, Math.min(dimensions.contentHeightPx, width * 0.75) ); const fitScale = Math.min( 1, dimensions.contentWidthPx / width, dimensions.contentHeightPx / height ); if (fitScale < 1) { element.style.width = `${width * fitScale}px`; element.style.height = `${height * fitScale}px`; element.style.maxWidth = "none"; element.style.maxHeight = "none"; } return element.getBoundingClientRect(); } function getMediaAlignment( article: HTMLElement, rect: DOMRect ): DocxMediaAlignment { const articleRect = article.getBoundingClientRect(); const articleWidth = finitePositive(articleRect.width, rect.width); if (rect.width >= articleWidth - 2) { return "center"; } const leftScore = Math.abs(rect.left - articleRect.left); const centerScore = Math.abs( (rect.left + rect.right) / 2 - (articleRect.left + articleRect.right) / 2 ); const rightScore = Math.abs(articleRect.right - rect.right); const minimumScore = Math.min( leftScore, centerScore, rightScore ); if (centerScore <= minimumScore + 2) { return "center"; } return leftScore <= rightScore ? "left" : "right"; } function getRasterScale(width: number, height: number) { return Math.min( DOCX_MEDIA_RASTER_SCALE, MAXIMUM_DOCX_MEDIA_EDGE_PIXELS / width, MAXIMUM_DOCX_MEDIA_EDGE_PIXELS / height, Math.sqrt(MAXIMUM_DOCX_MEDIA_PIXELS / (width * height)) ); } function createGeometryCss(dimensions: DocxMediaRenderDimensions) { return ` #preview-root { width: ${dimensions.contentWidthPx}px !important; padding: 0 !important; } #write { width: ${dimensions.contentWidthPx}px !important; padding: 0 !important; box-shadow: none !important; } `; } export function collectDocxMediaCaptureTargets( root: ParentNode, dimensions: DocxMediaRenderDimensions ) { const article = root.querySelector("#write"); if (!article) { throw new Error("DOCX 媒体舞台缺少 #write 文档容器"); } const candidates = Array.from( article.querySelectorAll( [ "img.md-document-image", ".mermaid:not(.mermaid-error) svg", ".md-echarts:not(.md-echarts-error) .md-echarts-host svg" ].join(",") ) ); if (candidates.length > MAXIMUM_DOCX_RESOURCE_COUNT) { throw new Error( `DOCX 媒体数量不能超过 ${MAXIMUM_DOCX_RESOURCE_COUNT}` ); } const kindOrdinals: Record = { image: 0, mermaid: 0, echarts: 0 }; return candidates.map((element, index): DocxMediaCaptureTarget => { const kind = getMediaKind(element); kindOrdinals[kind] += 1; const kindOrdinal = kindOrdinals[kind]; const rect = fitElementToContent(element, dimensions); const width = finitePositive(rect.width, 1); const height = finitePositive(rect.height, 1); const captureX = Math.max( 0, Math.floor(rect.left + window.scrollX) ); const captureY = Math.max( 0, Math.floor(rect.top + window.scrollY) ); const captureWidth = Math.max( 1, Math.ceil(rect.right + window.scrollX) - captureX ); const captureHeight = Math.max( 1, Math.ceil(rect.bottom + window.scrollY) - captureY ); const id = `docx-media-${index + 1}`; element.dataset.docxMediaId = id; return { id, kind, ordinal: index + 1, kindOrdinal, altText: getAltText(element, kind, kindOrdinal), ...(getCaption(element) ? { caption: getCaption(element) } : {}), alignment: getMediaAlignment(article, rect), displayWidthPx: width, displayHeightPx: height, captureX, captureY, captureWidthPx: captureWidth, captureHeightPx: captureHeight, rasterScale: getRasterScale(captureWidth, captureHeight) }; }); } export async function renderDocxMediaCapturePlan( runtime: PagedDocumentRuntime, root: HTMLElement, payload: PagedDocumentPayload, dimensions: DocxMediaRenderDimensions ): Promise { const renderResult = await runtime.renderContinuous(payload, { geometryCss: createGeometryCss(dimensions) }); if (!renderResult) { throw new Error("DOCX 媒体渲染已取消"); } return { targets: collectDocxMediaCaptureTargets(root, dimensions), echartsErrors: renderResult.echartsErrors, mermaidErrors: renderResult.mermaidErrors }; }