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 { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
|
||||||
import { applySavedProductTableLayout, stampSavedProductTableLayout } from '@/utils/crm/contract-table-layout';
|
import { applySavedProductTableLayout, stampSavedProductTableLayout } from '@/utils/crm/contract-table-layout';
|
||||||
import { appendContractPageWatermarks, contractWatermarkPrintCss } from '@/utils/crm/contract-watermark';
|
import { appendContractPageWatermarks, contractWatermarkPrintCss } from '@/utils/crm/contract-watermark';
|
||||||
|
import { countContractPdfPages } from '@/utils/crm/contract-pdf-pages';
|
||||||
import {
|
import {
|
||||||
downloadCrmAttachment,
|
downloadCrmAttachment,
|
||||||
getCrmAttachmentCategoryLabel,
|
getCrmAttachmentCategoryLabel,
|
||||||
@@ -3205,14 +3206,25 @@ const handleGeneratePdfFromDialog = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 使用html2canvas转换为canvas(PNG 无损,避免 JPEG 压糊文字)
|
// 2. 使用html2canvas转换为canvas(PNG 无损,避免 JPEG 压糊文字)
|
||||||
const canvas = await html2canvas(tempDiv, {
|
const canvasOptions = {
|
||||||
scale: getContractPdfCanvasScale(),
|
scale: getContractPdfCanvasScale(),
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
allowTaint: true,
|
allowTaint: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
width: 794,
|
width: 794,
|
||||||
windowWidth: 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. 移除临时容器
|
// 3. 移除临时容器
|
||||||
document.body.removeChild(tempDiv);
|
document.body.removeChild(tempDiv);
|
||||||
@@ -3226,30 +3238,16 @@ const handleGeneratePdfFromDialog = async () => {
|
|||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
unit: 'mm',
|
unit: 'mm',
|
||||||
format: 'a4'
|
format: 'a4',
|
||||||
|
compress: true // 无损压缩整页 PNG,避免原始像素直接写入导致 PDF 过大
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgData = canvas.toDataURL('image/png');
|
const imgData = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
// 6. 如果内容高度小于等于一页,直接添加
|
// 6. 只输出有正文的页面,保留原截图比例和分页位置。
|
||||||
if (imgHeight <= pageHeight) {
|
for (let page = 0; page < pageCount; page += 1) {
|
||||||
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
|
if (page > 0) pdf.addPage();
|
||||||
} else {
|
pdf.addImage(imgData, 'PNG', 0, -page * pageHeight, imgWidth, imgHeight);
|
||||||
// 内容超过一页,需要分页
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 下载PDF
|
// 7. 下载PDF
|
||||||
|
|||||||
@@ -898,6 +898,7 @@ import yzImg from '@/assets/images/yz.png';
|
|||||||
import ydImg from '@/assets/images/yd.png';
|
import ydImg from '@/assets/images/yd.png';
|
||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
|
import { countContractPdfPages as countQuotePdfPages } from '@/utils/crm/contract-pdf-pages';
|
||||||
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
|
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
|
||||||
|
|
||||||
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
||||||
@@ -3062,6 +3063,7 @@ const handleGeneratePdf = async () => {
|
|||||||
// 如果是SX编码,添加水印背景层
|
// 如果是SX编码,添加水印背景层
|
||||||
if (showBackground) {
|
if (showBackground) {
|
||||||
const watermarkBg = document.createElement('div');
|
const watermarkBg = document.createElement('div');
|
||||||
|
watermarkBg.setAttribute('data-quote-watermark', '1');
|
||||||
watermarkBg.style.cssText = `
|
watermarkBg.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -3103,15 +3105,25 @@ const handleGeneratePdf = async () => {
|
|||||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用html2canvas转换为canvas
|
// 先排除水印判断实际内容页数,避免底部留白生成空白尾页。
|
||||||
const canvas = await html2canvas(tempDiv, {
|
const canvasOptions = {
|
||||||
scale: 2, // 提高清晰度
|
scale: 2, // 提高清晰度
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
allowTaint: true,
|
allowTaint: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
width: 794,
|
width: 794,
|
||||||
windowWidth: 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);
|
document.body.removeChild(tempDiv);
|
||||||
@@ -3125,30 +3137,16 @@ const handleGeneratePdf = async () => {
|
|||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
unit: 'mm',
|
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) {
|
for (let page = 0; page < pageCount; page += 1) {
|
||||||
pdf.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight);
|
if (page > 0) pdf.addPage();
|
||||||
} else {
|
pdf.addImage(imgData, 'PNG', 0, -page * pageHeight, imgWidth, imgHeight);
|
||||||
// 内容超过一页,需要分页
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载PDF
|
// 下载PDF
|
||||||
|
|||||||
Reference in new issue
Block a user