feat: 实现 DOCX 媒体预处理与跨端 PNG 捕获

This commit is contained in:
SkyJourney
2026-07-30 13:21:18 +08:00
parent 7831bc23f0
commit fe5d67fb6d
20 changed files with 1294 additions and 16 deletions
+3 -2
View File
@@ -48,8 +48,9 @@ const payload = createPagedDocumentPayload({
```
DOCX 协议固定 Pandoc `3.9.0.2`,并统一定义跨 HTTP/IPC 使用的请求、
capability、错误码、结果、诊断耗时类型。该包只描述数据协议,不启动
Pandoc 或读写临时文件
capability、错误码、结果、诊断耗时、媒体捕获计划和 PNG 资源清单。
媒体协议固定 300 DPI 目标倍率,并限制边长、总像素、单文件大小和总量
该包只描述数据协议,不启动 Pandoc、浏览器或读写临时文件。
`classifyDocumentLink()` 只负责稳定分类,不执行平台动作。Web 根据分类
处理锚点和网络链接;Desktop 决定是否调用浏览器、系统程序或打开新的
+69
View File
@@ -8,6 +8,14 @@ export const DOCX_FILE_EXTENSION = ".docx";
export const MAXIMUM_DOCX_MARKDOWN_LENGTH = 1_500_000;
export const MAXIMUM_DOCX_FILE_NAME_LENGTH = 500;
export const MAXIMUM_DOCX_RESOURCE_COUNT = 50;
export const DOCX_MEDIA_RASTER_DPI = 300;
export const DOCX_MEDIA_CSS_DPI = 96;
export const DOCX_MEDIA_RASTER_SCALE =
DOCX_MEDIA_RASTER_DPI / DOCX_MEDIA_CSS_DPI;
export const MAXIMUM_DOCX_MEDIA_EDGE_PIXELS = 4_096;
export const MAXIMUM_DOCX_MEDIA_PIXELS = 16_000_000;
export const MAXIMUM_DOCX_MEDIA_BYTES = 8 * 1024 * 1024;
export const MAXIMUM_DOCX_TOTAL_MEDIA_BYTES = 30 * 1024 * 1024;
export const documentExportFormatSchema = z.enum(["pdf", "docx"]);
export type DocumentExportFormat = z.infer<
@@ -79,6 +87,67 @@ export const docxCapabilitySchema = z.discriminatedUnion("status", [
export type DocxCapability = z.infer<typeof docxCapabilitySchema>;
export const docxMediaKindSchema = z.enum([
"image",
"mermaid",
"echarts"
]);
export type DocxMediaKind = z.infer<typeof docxMediaKindSchema>;
export const docxMediaCaptureTargetSchema = z.object({
id: z.string().regex(/^docx-media-\d+$/u),
kind: docxMediaKindSchema,
ordinal: z.number().int().min(1).max(MAXIMUM_DOCX_RESOURCE_COUNT),
kindOrdinal: z
.number()
.int()
.min(1)
.max(MAXIMUM_DOCX_RESOURCE_COUNT),
altText: z.string().max(1_000),
caption: z.string().max(1_000).optional(),
displayWidthPx: z.number().positive().max(10_000),
displayHeightPx: z.number().positive().max(10_000),
captureX: z.number().int().nonnegative().max(100_000),
captureY: z.number().int().nonnegative().max(100_000),
captureWidthPx: z.number().int().positive().max(10_000),
captureHeightPx: z.number().int().positive().max(10_000),
rasterScale: z.number().positive().max(DOCX_MEDIA_RASTER_SCALE)
});
export type DocxMediaCaptureTarget = z.infer<
typeof docxMediaCaptureTargetSchema
>;
export const docxMediaCapturePlanSchema = z.object({
targets: z
.array(docxMediaCaptureTargetSchema)
.max(MAXIMUM_DOCX_RESOURCE_COUNT),
echartsErrors: z.array(z.string().max(1_000)),
mermaidErrors: z.array(z.string().max(1_000))
});
export type DocxMediaCapturePlan = z.infer<
typeof docxMediaCapturePlanSchema
>;
export interface DocxPngMediaResource
extends DocxMediaCaptureTarget {
fileName: string;
contentType: "image/png";
content: Uint8Array;
pixelWidth: number;
pixelHeight: number;
}
export interface PreparedDocxMedia {
resources: DocxPngMediaResource[];
echartsErrors: string[];
mermaidErrors: string[];
warnings: string[];
totalBytes: number;
}
export const docxExportErrorCodeSchema = z.enum([
"INVALID_DOCX_REQUEST",
"INVALID_EXPORT_CONFIG",
+53
View File
@@ -1,11 +1,14 @@
import { describe, expect, it } from "vitest";
import {
DOCX_MIME_TYPE,
DOCX_MEDIA_RASTER_SCALE,
DOCX_PANDOC_VERSION,
MAXIMUM_DOCX_MEDIA_EDGE_PIXELS,
MAXIMUM_DOCX_MARKDOWN_LENGTH,
createDocxFileName,
defaultExportConfig,
docxCapabilitySchema,
docxMediaCapturePlanSchema,
docxExportErrorResponseSchema,
docxExportRequestSchema
} from "../src/index.js";
@@ -111,4 +114,54 @@ describe("DOCX 共享协议", () => {
retryable: false
});
});
it("校验 DOCX PNG 媒体捕获计划", () => {
expect(DOCX_MEDIA_RASTER_SCALE).toBe(3.125);
expect(MAXIMUM_DOCX_MEDIA_EDGE_PIXELS).toBe(4096);
expect(
docxMediaCapturePlanSchema.parse({
targets: [
{
id: "docx-media-1",
kind: "mermaid",
ordinal: 1,
kindOrdinal: 1,
altText: "流程图",
caption: "处理流程",
displayWidthPx: 640,
displayHeightPx: 320,
captureX: 20,
captureY: 120,
captureWidthPx: 640,
captureHeightPx: 320,
rasterScale: 3.125
}
],
echartsErrors: [],
mermaidErrors: []
}).targets
).toHaveLength(1);
expect(
docxMediaCapturePlanSchema.safeParse({
targets: [
{
id: "../越界",
kind: "image",
ordinal: 1,
kindOrdinal: 1,
altText: "",
displayWidthPx: 100,
displayHeightPx: 100,
captureX: 0,
captureY: 0,
captureWidthPx: 100,
captureHeightPx: 100,
rasterScale: 3.125
}
],
echartsErrors: [],
mermaidErrors: []
}).success
).toBe(false);
});
});