release: 发布 v0.6.1 DOCX 视觉一致性修复

新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。

新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。

重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。

源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。

验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
This commit is contained in:
SkyJourney
2026-08-04 10:30:44 +08:00
parent b275c671fc
commit 2c5c1bd317
84 changed files with 4404 additions and 344 deletions
+183 -8
View File
@@ -16,6 +16,14 @@ interface DecodedRaster {
rgba: Uint8ClampedArray;
}
interface RgbColor {
red: number;
green: number;
blue: number;
}
const RASTER_MASK_BORDER_PX = 2;
function sha256(bytes: Uint8Array): string {
return createHash("sha256").update(bytes).digest("hex");
}
@@ -48,6 +56,143 @@ function luminance(
return (77 * red + 150 * green + 29 * blue) >> 8;
}
function colorDistance(left: RgbColor, right: RgbColor): number {
return Math.sqrt(
((left.red - right.red) ** 2 +
(left.green - right.green) ** 2 +
(left.blue - right.blue) ** 2) /
3,
);
}
function rasterBackgroundColor(
rgba: Uint8ClampedArray,
_width: number,
_height: number,
): RgbColor {
const buckets = new Map<
number,
{ count: number; red: number; green: number; blue: number }
>();
let dominant:
| { count: number; red: number; green: number; blue: number }
| undefined;
for (let offset = 0; offset < rgba.length; offset += 4) {
const red = rgba[offset] ?? 255;
const green = rgba[offset + 1] ?? 255;
const blue = rgba[offset + 2] ?? 255;
const bucketKey = (red >> 3) << 10 | (green >> 3) << 5 | (blue >> 3);
const bucket = buckets.get(bucketKey) ?? {
count: 0,
red: 0,
green: 0,
blue: 0,
};
bucket.count += 1;
bucket.red += red;
bucket.green += green;
bucket.blue += blue;
buckets.set(bucketKey, bucket);
if (!dominant || bucket.count > dominant.count) {
dominant = bucket;
}
}
return !dominant
? { red: 255, green: 255, blue: 255 }
: {
red: dominant.red / dominant.count,
green: dominant.green / dominant.count,
blue: dominant.blue / dominant.count,
};
}
function rasterForegroundColor(
decoded: DecodedRaster,
): RgbColor {
const background = rasterBackgroundColor(
decoded.rgba,
decoded.width,
decoded.height,
);
const buckets = new Map<
number,
{ count: number; red: number; green: number; blue: number }
>();
for (let offset = 0; offset < decoded.rgba.length; offset += 4) {
const color = {
red: decoded.rgba[offset] ?? 255,
green: decoded.rgba[offset + 1] ?? 255,
blue: decoded.rgba[offset + 2] ?? 255,
};
const distance = colorDistance(color, background);
if (distance < 48) {
continue;
}
const bucketKey = (color.red >> 3) << 10 |
(color.green >> 3) << 5 |
(color.blue >> 3);
const bucket = buckets.get(bucketKey) ?? {
count: 0,
red: 0,
green: 0,
blue: 0,
};
bucket.count += 1;
bucket.red += color.red;
bucket.green += color.green;
bucket.blue += color.blue;
buckets.set(bucketKey, bucket);
}
let dominant:
| { count: number; red: number; green: number; blue: number }
| undefined;
let dominantDistance = -1;
for (const bucket of buckets.values()) {
const averageColor = {
red: bucket.red / bucket.count,
green: bucket.green / bucket.count,
blue: bucket.blue / bucket.count,
};
const distance = colorDistance(averageColor, background);
if (
distance > dominantDistance + 1 ||
(Math.abs(distance - dominantDistance) <= 1 &&
bucket.count > (dominant?.count ?? 0))
) {
dominant = bucket;
dominantDistance = distance;
}
}
return !dominant
? background
: {
red: dominant.red / dominant.count,
green: dominant.green / dominant.count,
blue: dominant.blue / dominant.count,
};
}
function clearMaskBorder(
mask: Uint8Array,
width: number,
height: number,
borderPx = RASTER_MASK_BORDER_PX,
) {
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
if (
x < borderPx ||
y < borderPx ||
x >= width - borderPx ||
y >= height - borderPx
) {
mask[y * width + x] = 0;
}
}
}
return mask;
}
function calculateBinaryIou(
left: Uint8Array,
right: Uint8Array,
@@ -292,6 +437,16 @@ export async function comparePageRasters(
const pixelDifference = new Uint8Array(pixelCount);
const baselineInk = new Uint8Array(pixelCount);
const candidateInk = new Uint8Array(pixelCount);
const baselineBackground = rasterBackgroundColor(
baselineDecoded.rgba,
width,
height,
);
const candidateBackground = rasterBackgroundColor(
candidateDecoded.rgba,
width,
height,
);
let absoluteError = 0;
let changedPixels = 0;
@@ -323,25 +478,43 @@ export async function comparePageRasters(
const candidateValue = luminance(candidateDecoded.rgba, offset);
baselineGray[pixelIndex] = baselineValue;
candidateGray[pixelIndex] = candidateValue;
if (baselineValue < 245) {
const baselineColor = {
red: baselineDecoded.rgba[offset] ?? 255,
green: baselineDecoded.rgba[offset + 1] ?? 255,
blue: baselineDecoded.rgba[offset + 2] ?? 255,
};
const candidateColor = {
red: candidateDecoded.rgba[offset] ?? 255,
green: candidateDecoded.rgba[offset + 1] ?? 255,
blue: candidateDecoded.rgba[offset + 2] ?? 255,
};
if (colorDistance(baselineColor, baselineBackground) >= 16) {
baselineInk[pixelIndex] = 1;
}
if (candidateValue < 245) {
if (colorDistance(candidateColor, candidateBackground) >= 16) {
candidateInk[pixelIndex] = 1;
}
}
const baselineEdges = createEdgeMask(
baselineGray,
clearMaskBorder(baselineInk, width, height);
clearMaskBorder(candidateInk, width, height);
const baselineEdges = clearMaskBorder(
createEdgeMask(baselineGray, width, height, 40),
width,
height,
40,
);
const candidateEdges = createEdgeMask(
candidateGray,
const candidateEdges = clearMaskBorder(
createEdgeMask(candidateGray, width, height, 40),
width,
height,
40,
);
const foregroundColorDelta = colorDistance(
rasterForegroundColor(baselineDecoded),
rasterForegroundColor(candidateDecoded),
);
const backgroundColorDelta = colorDistance(
baselineBackground,
candidateBackground,
);
const metrics: PdfRasterDiffMetrics = {
baselineWidthPx: baseline.widthPx,
@@ -371,6 +544,8 @@ export async function comparePageRasters(
height,
spatialTolerancePx,
),
backgroundColorDelta,
foregroundColorDelta,
};
return {
metrics,