762 lines
21 KiB
Markdown
762 lines
21 KiB
Markdown
# Code Review: 模块提取对比分析
|
||
|
||
## 审查摘要
|
||
对比原始文件 `history_pdf_compare.js` 与提取的三个模块,检查功能逻辑、依赖关系和状态管理的一致性。
|
||
|
||
---
|
||
|
||
## 1️⃣ TextFittingAdapter 模块审查
|
||
|
||
### 对应的原始方法
|
||
- `initializeTextFitting()` → `TextFittingAdapter.initialize()`
|
||
- `preprocessGlobalFontSizes()` → `TextFittingAdapter.preprocessGlobalFontSizes()`
|
||
- `drawPlainTextInBox()` → `TextFittingAdapter.drawPlainTextInBox()`
|
||
- `drawPlainTextWithFitting()` → `TextFittingAdapter.drawPlainTextWithFitting()`
|
||
- `wrapText()` → `TextFittingAdapter.wrapText()`
|
||
- `renderFormulasInText()` → `TextFittingAdapter.renderFormulasInText()` (未在提取版本中)
|
||
|
||
### ✅ 保持一致的部分
|
||
|
||
| 特性 | 状态 | 备注 |
|
||
|------|------|------|
|
||
| 初始化逻辑 | ✅ | 完全相同的TextFittingEngine初始化 |
|
||
| 预处理算法 | ✅ | globalFontScale、bbox计算完全一致 |
|
||
| wrapText换行算法 | ✅ | CJK断句、标点符号处理、换行符处理完全相同 |
|
||
| drawPlainTextInBox回退方案 | ✅ | 与原始版本的fallback逻辑一致 |
|
||
| drawPlainTextWithFitting主算法 | ✅ | 二分查找、宽度因子、垂直居中完全一致 |
|
||
| 字号范围计算 | ✅ | minFontSize、maxFontSize计算相同 |
|
||
| CJK判断逻辑 | ✅ | `/[\u4e00-\u9fa5]/` 正则完全一致 |
|
||
|
||
### ⚠️ 需要注意的改变
|
||
|
||
#### 1. 缺失方法:renderFormulasInText()
|
||
**原始代码 (1588-1604行)**:
|
||
```javascript
|
||
renderFormulasInText(text) {
|
||
// 使用缓存避免重复渲染
|
||
if (this._formulaCache.has(text)) {
|
||
return this._formulaCache.get(text);
|
||
}
|
||
|
||
if (typeof window.renderMathInElement === 'function') {
|
||
// KaTeX渲染逻辑
|
||
...
|
||
}
|
||
}
|
||
```
|
||
|
||
**模块版本**:
|
||
```javascript
|
||
renderFormulasInText(text) {
|
||
// 363-404行:完全相同的实现
|
||
}
|
||
```
|
||
|
||
✅ **已正确包含** - 在TextFittingAdapter的363-404行
|
||
|
||
#### 2. 选项配置的改变
|
||
**原始代码处理**:
|
||
```javascript
|
||
// history_pdf_compare.js
|
||
this.textFittingEngine = new TextFittingEngine({
|
||
initialScale: 1.0,
|
||
minScale: 0.3,
|
||
scaleStepHigh: 0.05,
|
||
scaleStepLow: 0.1,
|
||
lineSkipCJK: 1.5,
|
||
lineSkipWestern: 1.3,
|
||
minLineHeight: 1.05
|
||
});
|
||
```
|
||
|
||
**模块版本处理**:
|
||
```javascript
|
||
// TextFittingAdapter.js
|
||
this.options = Object.assign({
|
||
initialScale: 1.0,
|
||
minScale: 0.3,
|
||
scaleStepHigh: 0.05,
|
||
scaleStepLow: 0.1,
|
||
lineSkipCJK: 1.5,
|
||
lineSkipWestern: 1.3,
|
||
minLineHeight: 1.05,
|
||
globalFontScale: 0.85 // 新增
|
||
}, options);
|
||
```
|
||
|
||
⚠️ **改进**: 新增globalFontScale选项支持,提高配置灵活性
|
||
|
||
#### 3. 缓存管理的独立性
|
||
**差异**:
|
||
- **原始**: globalFontSizeCache 在 PDFCompareView 中管理
|
||
- **模块**: 自包含的globalFontSizeCache、_formulaCache
|
||
|
||
✅ **有利**: 模块化改进,支持clearCache()方法
|
||
|
||
### ❌ 潜在的问题或遗漏
|
||
|
||
#### 1. TextFittingEngine初始化的隐式依赖
|
||
**问题**: 模块依赖全局的 `TextFittingEngine` 类
|
||
```javascript
|
||
if (typeof TextFittingEngine === 'undefined') {
|
||
console.error('[TextFittingAdapter] TextFittingEngine 未加载!请确保 js/utils/text-fitting.js 已正确引入');
|
||
return;
|
||
}
|
||
```
|
||
|
||
**风险**:
|
||
- 如果 `text-fitting.js` 未加载,将默默失败
|
||
- 日志显示错误但继续执行,可能导致难以调试的问题
|
||
|
||
**建议**:
|
||
```javascript
|
||
initialize() {
|
||
if (typeof TextFittingEngine === 'undefined') {
|
||
throw new Error('[TextFittingAdapter] TextFittingEngine 未加载!');
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 2. wrapText方法缺少canvas context参数验证
|
||
**原始代码**: 无参数检查
|
||
**模块代码**: 同样无参数检查
|
||
|
||
```javascript
|
||
wrapText(ctx, text, maxWidth) {
|
||
if (!text) return [];
|
||
// 缺少 ctx 验证
|
||
ctx.measureText(testLine); // 可能报错
|
||
}
|
||
```
|
||
|
||
**建议**:
|
||
```javascript
|
||
wrapText(ctx, text, maxWidth) {
|
||
if (!text) return [];
|
||
if (!ctx || typeof ctx.measureText !== 'function') {
|
||
console.warn('[TextFitting] 无效的canvas context');
|
||
return text.split('\n');
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 3. globalFontSizeCache 的前置条件
|
||
**方法**: `preprocessGlobalFontSizes(contentListJson, translatedContentList)`
|
||
|
||
**缺失的验证**:
|
||
```javascript
|
||
if (!contentListJson || !Array.isArray(contentListJson)) {
|
||
console.warn('[TextFittingAdapter] 无效的contentListJson');
|
||
return;
|
||
}
|
||
```
|
||
|
||
**原始代码中没有验证,模块版本也没有加**
|
||
|
||
---
|
||
|
||
## 2️⃣ PDFExporter 模块审查
|
||
|
||
### 对应的原始方法
|
||
- `exportStructuredTranslation()` → `PDFExporter.exportStructuredTranslation()` (新提取,原始文件中在2000+行)
|
||
- `calculatePdfTextLayout()` → `PDFExporter.calculatePdfTextLayout()`
|
||
- `wrapTextForPdf()` → `PDFExporter.wrapTextForPdf()`
|
||
|
||
### ✅ 保持一致的部分
|
||
|
||
| 特性 | 状态 | 备注 |
|
||
|------|------|------|
|
||
| PDF加载和字体嵌入 | ✅ | fontkit注册逻辑相同 |
|
||
| 页面分组逻辑 | ✅ | pageContentMap创建方式相同 |
|
||
| bbox坐标转换 | ✅ | scaleX/scaleY计算相同 |
|
||
| 白色矩形覆盖算法 | ✅ | rgb(1,1,1)覆盖逻辑相同 |
|
||
| 文本布局二分查找 | ✅ | 高低指针、精度0.5算法相同 |
|
||
| wrapTextForPdf换行 | ✅ | CJK断句逻辑与Canvas版本一致 |
|
||
| PDF坐标系处理 | ✅ | y轴翻转、缩放计算相同 |
|
||
|
||
### ⚠️ 需要注意的改变
|
||
|
||
#### 1. 缺失的依赖项声明
|
||
**原始代码** (在PDFCompareView中):
|
||
```javascript
|
||
async exportStructuredTranslation(translatedContentList) {
|
||
// 使用 this.originalPdfBase64
|
||
// 使用 this.scale 和 this.dpr
|
||
// 使用 showNotification 从外部传入
|
||
}
|
||
```
|
||
|
||
**模块版本**:
|
||
```javascript
|
||
async exportStructuredTranslation(originalPdfBase64, translatedContentList, showNotification = null) {
|
||
// 显式接收所有参数
|
||
// 不依赖 this.scale
|
||
// 不依赖 this.dpr
|
||
// 独立的 dpr 处理
|
||
}
|
||
```
|
||
|
||
✅ **改进**: 参数显式化,减少隐式依赖
|
||
|
||
#### 2. 参数差异:缺少scale和dpr
|
||
**问题**: PDFExporter 中没有 scale 和 dpr 属性
|
||
|
||
**原始代码中的使用**:
|
||
```javascript
|
||
// PDFCompareView 中计算渲染时使用
|
||
const scaleX = pageWidth / BBOX_NORMALIZED_RANGE;
|
||
const scaleY = pageHeight / BBOX_NORMALIZED_RANGE;
|
||
```
|
||
|
||
**模块版本**:
|
||
```javascript
|
||
// PDFExporter.js 中
|
||
// 注意:没有使用 this.scale 或 this.dpr
|
||
const scaleX = pageWidth / BBOX_NORMALIZED_RANGE;
|
||
const scaleY = pageHeight / BBOX_NORMALIZED_RANGE;
|
||
```
|
||
|
||
⚠️ **潜在问题**: 模块直接使用 PDF 的页面宽高,而不考虑原始的 scale/dpr。这可能导致文本大小计算不同。
|
||
|
||
#### 3. 字体加载的网络依赖
|
||
**风险**: 硬编码的CDN URL
|
||
```javascript
|
||
fontUrl: 'https://gcore.jsdelivr.net/npm/source-han-sans-cn@1.0.0/SourceHanSansCN-Normal.otf',
|
||
pdfLibUrl: 'https://gcore.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js',
|
||
fontkitUrl: 'https://gcore.jsdelivr.net/npm/@pdf-lib/fontkit@1.1.1/dist/fontkit.umd.min.js',
|
||
```
|
||
|
||
⚠️ **问题**:
|
||
- CDN依赖可能导致离线失败
|
||
- URL可能变更
|
||
- 没有fallback方案
|
||
|
||
#### 4. calculatePdfTextLayout 与 drawPlainTextWithFitting 的不一致
|
||
**原始代码中的差异**:
|
||
|
||
Canvas版本 (drawPlainTextWithFitting):
|
||
```javascript
|
||
const lineHeight = mid * lineSkip;
|
||
const totalHeight = lines.length === 1
|
||
? mid * 1.2
|
||
: (lines.length - 1) * lineHeight + mid * 1.2;
|
||
```
|
||
|
||
PDF版本 (calculatePdfTextLayout):
|
||
```javascript
|
||
const lineHeight = mid * lineSkip;
|
||
const totalHeight = lines.length > 0
|
||
? (lines.length - 1) * lineHeight + mid
|
||
: 0;
|
||
```
|
||
|
||
❌ **问题**: 计算不一致!
|
||
- Canvas: 最后一行使用 `mid * 1.2`
|
||
- PDF: 最后一行使用 `mid`
|
||
- 这会导致PDF和Canvas中的文本大小不同
|
||
|
||
**建议**: 应该统一为同一个公式
|
||
|
||
#### 5. 缺少原始文本清除逻辑
|
||
**问题**: 原始代码有 `clearTextInBbox()` 方法来清除PDF中的原始文本,但PDFExporter中:
|
||
```javascript
|
||
// 用白色矩形覆盖原文
|
||
items.forEach(item => {
|
||
// ...
|
||
page.drawRectangle({
|
||
x: x,
|
||
y: y,
|
||
width: width,
|
||
height: height,
|
||
color: rgb(1, 1, 1), // 近似白色
|
||
});
|
||
});
|
||
```
|
||
|
||
⚠️ **注意**:
|
||
- 使用 `rgb(1, 1, 1)` 而不是 `rgb(255, 255, 255)`(pdf-lib的色值范围是0-1而不是0-255)
|
||
- 这会导致非纯白色覆盖,可能看到轻微的灰色背景
|
||
|
||
**建议**:
|
||
```javascript
|
||
color: rgb(255, 255, 255) // 或使用 rgb(1, 1, 1) 但需要验证
|
||
```
|
||
|
||
### ❌ 潜在的问题或遗漏
|
||
|
||
#### 1. 缺少错误恢复机制
|
||
**原始代码**:
|
||
```javascript
|
||
if (typeof PDFLib === 'undefined') {
|
||
await this.loadPdfLib();
|
||
}
|
||
```
|
||
|
||
**模块版本**: 同样存在,但缺少重试机制
|
||
|
||
**问题**: 如果加载失败,没有重试逻辑
|
||
|
||
#### 2. fontkit加载失败时的行为
|
||
**代码**:
|
||
```javascript
|
||
script.onerror = (error) => {
|
||
console.warn('[PDFExporter] fontkit 加载失败:', error);
|
||
resolve(); // fontkit失败不阻止流程
|
||
};
|
||
```
|
||
|
||
⚠️ **问题**:
|
||
- fontkit失败会导致中文字体无法嵌入
|
||
- 但流程继续,可能使用默认字体(不支持中文)
|
||
- 最终PDF中的中文会显示为空或方块
|
||
|
||
**建议**:
|
||
```javascript
|
||
// 如果fontkit失败,应该至少警告用户
|
||
if (!fontkit && needsCJKFont) {
|
||
showNotification('警告:中文字体可能无法正确显示', 'warning');
|
||
}
|
||
```
|
||
|
||
#### 3. 缺少对 showNotification 的类型检查
|
||
**代码**:
|
||
```javascript
|
||
if (showNotification) {
|
||
showNotification('没有翻译内容可导出', 'warning');
|
||
}
|
||
```
|
||
|
||
⚠️ **问题**: 假设 showNotification 是函数,但没有验证
|
||
|
||
**建议**:
|
||
```javascript
|
||
if (typeof showNotification === 'function') {
|
||
showNotification('没有翻译内容可导出', 'warning');
|
||
}
|
||
```
|
||
|
||
#### 4. 文本布局计算中的lineHeight使用
|
||
**问题**: 在 calculatePdfTextLayout 中计算最后一行时:
|
||
```javascript
|
||
const totalHeight = lines.length > 0
|
||
? (lines.length - 1) * lineHeight + mid
|
||
: 0;
|
||
```
|
||
|
||
但在实际绘制时:
|
||
```javascript
|
||
const totalHeight = lines.length > 0
|
||
? (lines.length - 1) * lineHeight + fontSize
|
||
: 0;
|
||
```
|
||
|
||
这两个值应该相同(mid === fontSize),但逻辑复杂易出错。
|
||
|
||
---
|
||
|
||
## 3️⃣ SegmentManager 模块审查
|
||
|
||
### 对应的原始方法
|
||
- `renderAllPagesContinuous()` → `SegmentManager.renderAllPagesContinuous()`
|
||
- `createSegmentDom()` → `SegmentManager.createSegmentDom()`
|
||
- `initLazyLoadingSegments()` → `SegmentManager.initLazyLoadingSegments()`
|
||
- `renderVisibleSegments()` → `SegmentManager.renderVisibleSegments()`
|
||
- `renderSegment()` → `SegmentManager.renderSegment()`
|
||
- `renderSegmentOverlays()` → `SegmentManager.renderSegmentOverlays()`
|
||
- `clearTextInSegment()` → `SegmentManager.clearTextInSegment()` (新增)
|
||
|
||
### ✅ 保持一致的部分
|
||
|
||
| 特性 | 状态 | 备注 |
|
||
|------|------|------|
|
||
| 段划分算法 | ✅ | maxSegmentPixels和页面分组逻辑相同 |
|
||
| DOM创建逻辑 | ✅ | wrapper、canvas、overlay创建完全相同 |
|
||
| DPR处理 | ✅ | 物理像素和CSS像素的转换相同 |
|
||
| 懒加载触发 | ✅ | scrollDebounceMs 和 renderVisibleSegments 逻辑相同 |
|
||
| 可见性判断 | ✅ | visibleStartPx和visibleEndPx计算相同 |
|
||
| 离屏渲染 | ✅ | 使用临时canvas避免PDF.js清除问题 |
|
||
| 点击事件处理 | ✅ | 段级别的坐标转换和命中测试逻辑相同 |
|
||
|
||
### ⚠️ 需要注意的改变
|
||
|
||
#### 1. 依赖注入模式
|
||
**原始代码** (在PDFCompareView中):
|
||
```javascript
|
||
// 方法直接访问 this 的属性
|
||
async renderSegmentOverlays(seg) {
|
||
// 直接调用 this.renderPageBboxesToCtx()
|
||
// 直接调用 this.renderPageTranslationToCtx()
|
||
// 直接访问 this.contentListJson
|
||
}
|
||
```
|
||
|
||
**模块版本**:
|
||
```javascript
|
||
// 使用依赖注入
|
||
setDependencies(deps) {
|
||
Object.assign(this, deps);
|
||
}
|
||
|
||
// 在方法中检查依赖
|
||
async renderSegmentOverlays(seg) {
|
||
if (!this.renderPageBboxesToCtx || !this.renderPageTranslationToCtx) {
|
||
console.warn('[SegmentManager] 缺少渲染函数依赖');
|
||
return;
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
✅ **改进**: 显式依赖注入,减少隐式耦合
|
||
|
||
#### 2. 容器设置方法
|
||
**原始代码** (隐式):
|
||
```javascript
|
||
// 直接在 render() 方法中设置容器
|
||
this.originalSegmentsContainer = document.getElementById('pdf-original-segments');
|
||
```
|
||
|
||
**模块版本** (显式):
|
||
```javascript
|
||
setContainers(originalSegments, translationSegments, originalScroll, translationScroll) {
|
||
this.originalSegmentsContainer = originalSegments;
|
||
this.translationSegmentsContainer = translationSegments;
|
||
this.originalScroll = originalScroll;
|
||
this.translationScroll = translationScroll;
|
||
}
|
||
```
|
||
|
||
✅ **改进**: 更清晰的初始化流程
|
||
|
||
#### 3. PDF文档依赖
|
||
**原始代码**:
|
||
```javascript
|
||
// 从 PDFCompareView.pdfDoc 继承
|
||
this.pdfDoc = pdfDoc;
|
||
```
|
||
|
||
**模块版本**:
|
||
```javascript
|
||
constructor(pdfDoc, options = {}) {
|
||
this.pdfDoc = pdfDoc;
|
||
this.totalPages = pdfDoc.numPages;
|
||
// ...
|
||
}
|
||
```
|
||
|
||
✅ **一致**: 都显式接收pdfDoc作为构造参数
|
||
|
||
### ❌ 潜在的问题或遗漏
|
||
|
||
#### 1. 事件监听器的清理问题
|
||
**代码**:
|
||
```javascript
|
||
initLazyLoadingSegments() {
|
||
if (!this._lazyInitialized) {
|
||
this.originalScroll.addEventListener('scroll', () => onScroll(this.originalScroll));
|
||
this.translationScroll.addEventListener('scroll', () => onScroll(this.translationScroll));
|
||
this._lazyInitialized = true;
|
||
}
|
||
}
|
||
|
||
destroy() {
|
||
// 移除事件监听
|
||
if (this._lazyInitialized && this.originalScroll && this.translationScroll) {
|
||
// 注意:由于事件监听使用了箭头函数,无法直接移除
|
||
// 这里设置标记位,防止继续渲染
|
||
this.segments = [];
|
||
this.pageInfos = [];
|
||
}
|
||
}
|
||
```
|
||
|
||
❌ **问题**:
|
||
- 事件监听器无法正确移除(注释中也承认了)
|
||
- 清空 segments 和 pageInfos 不能停止已经开始的渲染
|
||
- 可能导致内存泄漏和ghost渲染
|
||
|
||
**建议**:
|
||
```javascript
|
||
initLazyLoadingSegments() {
|
||
if (!this._lazyInitialized) {
|
||
// 保存回调引用以便后续移除
|
||
this._scrollHandler = (scroller) => {
|
||
clearTimeout(this._lazyScrollTimer);
|
||
this._lazyScrollTimer = setTimeout(() => {
|
||
if (!this._destroyed) { // 添加销毁标志检查
|
||
this.renderVisibleSegments(scroller);
|
||
}
|
||
}, this.options.scrollDebounceMs);
|
||
};
|
||
|
||
this.originalScroll.addEventListener('scroll',
|
||
() => this._scrollHandler(this.originalScroll)
|
||
);
|
||
this.translationScroll.addEventListener('scroll',
|
||
() => this._scrollHandler(this.translationScroll)
|
||
);
|
||
this._lazyInitialized = true;
|
||
}
|
||
}
|
||
|
||
destroy() {
|
||
this._destroyed = true;
|
||
|
||
if (this._lazyInitialized && this.originalScroll && this.translationScroll) {
|
||
this.originalScroll.removeEventListener('scroll', this._scrollHandler);
|
||
this.translationScroll.removeEventListener('scroll', this._scrollHandler);
|
||
}
|
||
|
||
// 清空容器
|
||
if (this.originalSegmentsContainer) {
|
||
this.originalSegmentsContainer.innerHTML = '';
|
||
}
|
||
if (this.translationSegmentsContainer) {
|
||
this.translationSegmentsContainer.innerHTML = '';
|
||
}
|
||
|
||
this.segments = [];
|
||
this.pageInfos = [];
|
||
}
|
||
```
|
||
|
||
#### 2. renderSegment 中的离屏canvas管理
|
||
**问题**:
|
||
```javascript
|
||
async renderSegment(seg) {
|
||
// 使用离屏画布避免 PDF.js 清除问题
|
||
const off = document.createElement('canvas');
|
||
const offCtx = off.getContext('2d', { willReadFrequently: true, alpha: false });
|
||
|
||
for (const p of seg.pages) {
|
||
if (off.width !== p.width) off.width = p.width;
|
||
if (off.height !== p.height) off.height = p.height;
|
||
|
||
offCtx.clearRect(0, 0, off.width, off.height);
|
||
await p.page.render({ canvasContext: offCtx, viewport: p.viewport }).promise;
|
||
|
||
// 绘制到左右段画布
|
||
seg.left.ctx.drawImage(off, 0, p.yInSegPx);
|
||
seg.right.ctx.drawImage(off, 0, p.yInSegPx);
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
⚠️ **性能问题**:
|
||
- 每次渲染都创建离屏canvas,没有复用
|
||
- 频繁重新分配canvas宽高
|
||
- 没有垃圾回收机制
|
||
|
||
**建议**:
|
||
```javascript
|
||
constructor(pdfDoc, options = {}) {
|
||
// ...
|
||
this._offscreenCanvas = null; // 缓存离屏canvas
|
||
}
|
||
|
||
async renderSegment(seg) {
|
||
// 复用或创建离屏canvas
|
||
let off = this._offscreenCanvas;
|
||
if (!off) {
|
||
off = document.createElement('canvas');
|
||
this._offscreenCanvas = off;
|
||
}
|
||
// ...
|
||
}
|
||
|
||
destroy() {
|
||
// ...
|
||
this._offscreenCanvas = null; // 释放
|
||
}
|
||
```
|
||
|
||
#### 3. clearTextInSegment 方法的可用性问题
|
||
**代码**:
|
||
```javascript
|
||
async clearTextInSegment(seg) {
|
||
if (!this.contentListJson || !this.clearTextInBbox) {
|
||
console.warn('[SegmentManager] 缺少清除文字依赖');
|
||
return;
|
||
}
|
||
|
||
// ...
|
||
await this.clearTextInBbox(seg.right.ctx, pageNum, { x, y, w, h }, p.yInSegPx);
|
||
}
|
||
```
|
||
|
||
⚠️ **问题**:
|
||
- 此方法在SegmentManager中定义但原始代码中没有调用
|
||
- clearTextInBbox 期望的参数需要仔细验证
|
||
- seg.right.ctx 是画布context,但注入的 clearTextInBbox 可能期望不同的接口
|
||
|
||
**需要验证**:
|
||
- 这个方法是否真的被使用?
|
||
- 参数接口是否匹配?
|
||
|
||
#### 4. 缺少 bboxNormalizedRange 的验证
|
||
**代码**:
|
||
```javascript
|
||
async clearTextInSegment(seg) {
|
||
const BBOX_NORMALIZED_RANGE = this.options.bboxNormalizedRange;
|
||
// ...
|
||
const scaleX = p.width / BBOX_NORMALIZED_RANGE;
|
||
const scaleY = p.height / BBOX_NORMALIZED_RANGE;
|
||
}
|
||
```
|
||
|
||
⚠️ **问题**: 如果 bboxNormalizedRange 是 null 或 0,会导致NaN
|
||
|
||
**建议**:
|
||
```javascript
|
||
const BBOX_NORMALIZED_RANGE = this.options.bboxNormalizedRange || 1000;
|
||
if (BBOX_NORMALIZED_RANGE <= 0) {
|
||
console.error('[SegmentManager] 无效的 bboxNormalizedRange');
|
||
return;
|
||
}
|
||
```
|
||
|
||
#### 5. 缺少对容器存在的验证
|
||
**代码**:
|
||
```javascript
|
||
createSegmentDom(seg, dpr) {
|
||
// ...
|
||
buildSide(this.originalSegmentsContainer, 'left');
|
||
buildSide(this.translationSegmentsContainer, 'right');
|
||
}
|
||
```
|
||
|
||
⚠️ **问题**: 如果容器是 null,appendChild 会抛出错误
|
||
|
||
**原始代码中的问题** (也存在):
|
||
```javascript
|
||
renderAllPagesContinuous() {
|
||
// ...
|
||
for (const seg of this.segments) {
|
||
this.createSegmentDom(seg, dpr); // 可能失败
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**建议**:
|
||
```javascript
|
||
createSegmentDom(seg, dpr) {
|
||
if (!this.originalSegmentsContainer || !this.translationSegmentsContainer) {
|
||
console.error('[SegmentManager] 容器未初始化');
|
||
return;
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总体评估矩阵
|
||
|
||
| 模块 | 功能一致性 | 依赖处理 | 状态管理 | 接口变化 | 问题严重度 |
|
||
|------|-----------|---------|---------|---------|-----------|
|
||
| TextFittingAdapter | 95% | 良好 | 良好 | 参数化 | 低 |
|
||
| PDFExporter | 90% | 需改进 | 自包含 | 参数化 | 中 |
|
||
| SegmentManager | 95% | 好(DI) | 良好 | 显式化 | 中 |
|
||
|
||
---
|
||
|
||
## 关键建议汇总
|
||
|
||
### 🔴 高优先级 (必须修复)
|
||
|
||
1. **TextFittingAdapter.wrapText** - 添加ctx验证
|
||
2. **PDFExporter** - 统一Canvas和PDF的文本高度计算公式
|
||
3. **PDFExporter.loadPdfLib** - 改进对失败的错误处理
|
||
4. **SegmentManager** - 修复事件监听器的清理问题
|
||
|
||
### 🟡 中优先级 (应该改进)
|
||
|
||
1. **TextFittingAdapter.initialize** - 改为throw而不是return
|
||
2. **PDFExporter.calculatePdfTextLayout** - 添加参数验证
|
||
3. **SegmentManager.renderSegment** - 缓存离屏canvas以提高性能
|
||
4. **SegmentManager.createSegmentDom** - 添加容器存在性验证
|
||
|
||
### 🟢 低优先级 (可选改进)
|
||
|
||
1. **TextFittingAdapter.preprocessGlobalFontSizes** - 添加参数验证
|
||
2. **PDFExporter** - 添加showNotification类型检查
|
||
3. **SegmentManager** - 文档化clearTextInSegment的使用场景
|
||
|
||
---
|
||
|
||
## 兼容性检查表
|
||
|
||
### 从 PDFCompareView 迁移时需要确保:
|
||
|
||
- [ ] TextFittingAdapter.initialize() 在 TextFittingEngine 加载后调用
|
||
- [ ] PDFExporter 实例化时接收正确的选项对象
|
||
- [ ] SegmentManager.setDependencies() 在使用前调用,提供所有必需的渲染函数
|
||
- [ ] SegmentManager.setContainers() 在 renderAllPagesContinuous() 之前调用
|
||
- [ ] 调用 SegmentManager.destroy() 来清理事件监听器和DOM
|
||
- [ ] PDFExporter.exportStructuredTranslation() 收到有效的showNotification回调
|
||
- [ ] TextFittingAdapter 的 globalFontSizeCache 在每次新PDF加载时调用 clearCache()
|
||
|
||
---
|
||
|
||
## 集成检查示例
|
||
|
||
```javascript
|
||
// 正确的初始化顺序
|
||
const textFitter = new TextFittingAdapter();
|
||
textFitter.initialize(); // 检查TextFittingEngine
|
||
|
||
const segmentManager = new SegmentManager(pdfDoc, {
|
||
maxSegmentPixels: 4096,
|
||
bboxNormalizedRange: 1000
|
||
});
|
||
|
||
// 设置依赖项
|
||
segmentManager.setDependencies({
|
||
renderPageBboxesToCtx: (ctx, pageNum, yOffset, w, h) => { /* ... */ },
|
||
renderPageTranslationToCtx: (ctx, wrapper, pageNum, yOffset, w, h) => { /* ... */ },
|
||
clearTextInBbox: (ctx, pageNum, bbox, yOffset) => { /* ... */ },
|
||
clearFormulaElementsForPageInWrapper: (pageNum, wrapper) => { /* ... */ },
|
||
onOverlayClick: (e, seg) => { /* ... */ },
|
||
contentListJson: contentData
|
||
});
|
||
|
||
segmentManager.setContainers(origContainer, transContainer, origScroll, transScroll);
|
||
|
||
await segmentManager.renderAllPagesContinuous();
|
||
|
||
const exporter = new PDFExporter();
|
||
await exporter.exportStructuredTranslation(
|
||
pdfBase64,
|
||
translatedData,
|
||
(msg, type) => console.log(`[${type}] ${msg}`)
|
||
);
|
||
|
||
// 清理
|
||
segmentManager.destroy();
|
||
textFitter.clearCache();
|
||
```
|
||
|
||
---
|
||
|
||
## 结论
|
||
|
||
整体而言,这三个模块的提取是**高质量的**,保持了原始逻辑的一致性,并通过参数化和依赖注入改进了代码架构。
|
||
|
||
**主要优点**:
|
||
- 功能逻辑保留完整
|
||
- 耦合度降低
|
||
- 模块职责清晰
|
||
- 可复用性提高
|
||
|
||
**需要关注的地方**:
|
||
- Canvas vs PDF 文本高度计算需要统一
|
||
- 事件监听器管理需要改进
|
||
- 参数验证需要加强
|
||
- 网络依赖需要fallback机制
|
||
|
||
**总体评分**: 8.5/10 - 很好的重构,少数地方需要微调。
|