fix: 压缩合同与报价单 PDF 并按实际内容分页,避免文件过大和空白尾页。
This commit is contained in:
1 parent
120b743a39
commit
0453e90000
4 files changed
+98
-44
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import { countContractPdfPages } from './contract-pdf-pages'
|
||||
|
||||
function pixels(width: number, height: number, marks: number[]) {
|
||||
const data = new Uint8ClampedArray(width * height * 4).fill(255)
|
||||
for (const row of marks) data[(row * width + 1) * 4] = 0
|
||||
return { width, height, data }
|
||||
}
|
||||
|
||||
describe('countContractPdfPages', () => {
|
||||
it('ignores trailing white padding that crosses onto a second page', () => {
|
||||
assert.equal(countContractPdfPages(pixels(210, 330, [20, 295])), 1)
|
||||
})
|
||||
|
||||
it('keeps even a single visible pixel on a second page', () => {
|
||||
assert.equal(countContractPdfPages(pixels(210, 330, [20, 297])), 2)
|
||||
})
|
||||
|
||||
it('does not add a page when content ends exactly at the page boundary', () => {
|
||||
assert.equal(countContractPdfPages(pixels(210, 330, [296])), 1)
|
||||
})
|
||||
|
||||
it('removes only trailing blank pages from a multipage contract', () => {
|
||||
assert.equal(countContractPdfPages(pixels(210, 900, [20, 580])), 2)
|
||||
})
|
||||
|
||||
it('keeps one page for an empty document', () => {
|
||||
assert.equal(countContractPdfPages(pixels(210, 330, [])), 1)
|
||||
})
|
||||
|
||||
it('keeps faint content while ignoring fully transparent pixels', () => {
|
||||
const image = pixels(210, 330, [20, 310])
|
||||
const offset = (310 * image.width + 1) * 4
|
||||
image.data[offset] = 254
|
||||
assert.equal(countContractPdfPages(image), 2)
|
||||
image.data[offset + 3] = 0
|
||||
assert.equal(countContractPdfPages(image), 1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
/** 按不含水印的截图计算页数,仅移除没有任何可见像素的尾页。 */
|
||||
export function countContractPdfPages(image: Pick<ImageData, 'width' | 'height' | 'data'>): number {
|
||||
const { width, height, data } = image
|
||||
const pageHeight = (width * 297) / 210
|
||||
for (let y = height - 1; y >= 0; y -= 1) {
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const offset = (y * width + x) * 4
|
||||
// 不使用深色阈值,保留浅色文字、细表格线和印章。
|
||||
if (
|
||||
data[offset + 3] > 0 &&
|
||||
(data[offset] < 255 || data[offset + 1] < 255 || data[offset + 2] < 255)
|
||||
) {
|
||||
return Math.max(1, Math.ceil((y + 1) / pageHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1059,6 +1059,7 @@ import jsPDF from 'jspdf';
|
||||
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
|
||||
import { applySavedProductTableLayout, stampSavedProductTableLayout } from '@/utils/crm/contract-table-layout';
|
||||
import { appendContractPageWatermarks, contractWatermarkPrintCss } from '@/utils/crm/contract-watermark';
|
||||
import { countContractPdfPages } from '@/utils/crm/contract-pdf-pages';
|
||||
import {
|
||||
downloadCrmAttachment,
|
||||
getCrmAttachmentCategoryLabel,
|
||||
@@ -3205,14 +3206,25 @@ const handleGeneratePdfFromDialog = async () => {
|
||||
}
|
||||
|
||||
// 2. 使用html2canvas转换为canvas(PNG 无损,避免 JPEG 压糊文字)
|
||||
const canvas = await html2canvas(tempDiv, {
|
||||
const canvasOptions = {
|
||||
scale: getContractPdfCanvasScale(),
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
width: 794,
|
||||
windowWidth: 794
|
||||
};
|
||||
// 先排除装饰水印判断实际内容页数,模板底部 padding 不应生成空白尾页。
|
||||
let canvas = await html2canvas(tempDiv, {
|
||||
...canvasOptions,
|
||||
ignoreElements: (element) => element.hasAttribute('data-contract-watermark')
|
||||
});
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) throw new Error('无法读取合同截图');
|
||||
const pageCount = countContractPdfPages(context.getImageData(0, 0, canvas.width, canvas.height));
|
||||
if (isSXSupplier) {
|
||||
canvas = await html2canvas(tempDiv, canvasOptions);
|
||||
}
|
||||
|
||||
// 3. 移除临时容器
|
||||
document.body.removeChild(tempDiv);
|
||||
@@ -3226,30 +3238,16 @@ const handleGeneratePdfFromDialog = async () => {
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
format: 'a4',
|
||||
compress: true // 无损压缩整页 PNG,避免原始像素直接写入导致 PDF 过大
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// 6. 如果内容高度小于等于一页,直接添加
|
||||
if (imgHeight <= pageHeight) {
|
||||
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
|
||||
} else {
|
||||
// 内容超过一页,需要分页
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
// 添加第一页
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
// 添加后续页面
|
||||
while (heightLeft > 0) {
|
||||
position = -(imgHeight - heightLeft);
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
// 6. 只输出有正文的页面,保留原截图比例和分页位置。
|
||||
for (let page = 0; page < pageCount; page += 1) {
|
||||
if (page > 0) pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, -page * pageHeight, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
// 7. 下载PDF
|
||||
|
||||
@@ -898,6 +898,7 @@ import yzImg from '@/assets/images/yz.png';
|
||||
import ydImg from '@/assets/images/yd.png';
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
import { countContractPdfPages as countQuotePdfPages } from '@/utils/crm/contract-pdf-pages';
|
||||
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
|
||||
|
||||
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
||||
@@ -3062,6 +3063,7 @@ const handleGeneratePdf = async () => {
|
||||
// 如果是SX编码,添加水印背景层
|
||||
if (showBackground) {
|
||||
const watermarkBg = document.createElement('div');
|
||||
watermarkBg.setAttribute('data-quote-watermark', '1');
|
||||
watermarkBg.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -3103,15 +3105,25 @@ const handleGeneratePdf = async () => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
|
||||
// 使用html2canvas转换为canvas
|
||||
const canvas = await html2canvas(tempDiv, {
|
||||
// 先排除水印判断实际内容页数,避免底部留白生成空白尾页。
|
||||
const canvasOptions = {
|
||||
scale: 2, // 提高清晰度
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
width: 794,
|
||||
windowWidth: 794
|
||||
};
|
||||
let canvas = await html2canvas(tempDiv, {
|
||||
...canvasOptions,
|
||||
ignoreElements: (element) => element.hasAttribute('data-quote-watermark')
|
||||
});
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) throw new Error('无法读取报价单截图');
|
||||
const pageCount = countQuotePdfPages(context.getImageData(0, 0, canvas.width, canvas.height));
|
||||
if (showBackground) {
|
||||
canvas = await html2canvas(tempDiv, canvasOptions);
|
||||
}
|
||||
|
||||
// 移除临时容器
|
||||
document.body.removeChild(tempDiv);
|
||||
@@ -3125,30 +3137,16 @@ const handleGeneratePdf = async () => {
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
format: 'a4',
|
||||
compress: true // 无损压缩整页 PNG,保留文字与表格清晰度
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/jpeg', 1.0);
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// 如果内容高度小于等于一页,直接添加
|
||||
if (imgHeight <= pageHeight) {
|
||||
pdf.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight);
|
||||
} else {
|
||||
// 内容超过一页,需要分页
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
// 添加第一页
|
||||
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
// 添加后续页面
|
||||
while (heightLeft > 0) {
|
||||
position = -(imgHeight - heightLeft);
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
// 只输出有实际内容的页面,保留原截图比例和分页位置。
|
||||
for (let page = 0; page < pageCount; page += 1) {
|
||||
if (page > 0) pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, -page * pageHeight, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
// 下载PDF
|
||||
|
||||
Reference in new issue
Block a user