feat: 实现 DOCX 媒体预处理与跨端 PNG 捕获
This commit is contained in:
@@ -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 决定是否调用浏览器、系统程序或打开新的
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user