实现报销材料合并预览与差旅概要(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -0,0 +1,100 @@
|
||||
const crypto = require("node:crypto");
|
||||
const { PDFDocument } = require("pdf-lib");
|
||||
const sharp = require("sharp");
|
||||
|
||||
const FIXED_PDF_DATE = new Date("2000-01-01T00:00:00.000Z");
|
||||
const PDF_CREATOR = "hyb-oa";
|
||||
const PDF_PRODUCER = "hyb-oa reimbursement material merger";
|
||||
|
||||
function materialCollectionHash(materials, mergerVersion) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(String(mergerVersion));
|
||||
for (const material of materials) {
|
||||
hash.update("\0");
|
||||
hash.update([
|
||||
material.materialType,
|
||||
material.id,
|
||||
material.fileSha256 ?? "",
|
||||
material.mimeType,
|
||||
String(material.fileSize),
|
||||
].join("\u001f"));
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function normalizeImageForPdf(source, limits) {
|
||||
const image = sharp(source, { failOn: "error", limitInputPixels: limits.maxImagePixels }).rotate();
|
||||
const metadata = await image.metadata();
|
||||
if (!metadata.width || !metadata.height || metadata.width * metadata.height > limits.maxImagePixels) {
|
||||
throw new Error("material_preview_image_invalid");
|
||||
}
|
||||
const normalized = await image
|
||||
.flatten({ background: "#ffffff" })
|
||||
.jpeg({ quality: 90, chromaSubsampling: "4:4:4", mozjpeg: false })
|
||||
.toBuffer();
|
||||
return { buffer: normalized, width: metadata.width, height: metadata.height };
|
||||
}
|
||||
|
||||
function fitPage(width, height) {
|
||||
const maxSide = 842;
|
||||
const scale = Math.min(1, maxSide / Math.max(width, height));
|
||||
return { width: Math.max(1, width * scale), height: Math.max(1, height * scale) };
|
||||
}
|
||||
|
||||
async function mergeMaterialsToPdf(materials, limits) {
|
||||
const output = await PDFDocument.create();
|
||||
output.setCreationDate(FIXED_PDF_DATE);
|
||||
output.setModificationDate(FIXED_PDF_DATE);
|
||||
output.setCreator(PDF_CREATOR);
|
||||
output.setProducer(PDF_PRODUCER);
|
||||
output.setTitle("Reimbursement Material Preview");
|
||||
output.setSubject("Derived preview; originals retained separately");
|
||||
|
||||
let pageCount = 0;
|
||||
for (const material of materials) {
|
||||
const source = Buffer.from(material.source);
|
||||
if (material.mimeType === "application/pdf") {
|
||||
let input;
|
||||
try {
|
||||
input = await PDFDocument.load(source, { updateMetadata: false });
|
||||
} catch {
|
||||
throw new Error("material_preview_pdf_invalid");
|
||||
}
|
||||
const indices = input.getPageIndices();
|
||||
if (!indices.length || pageCount + indices.length > limits.maxPages) {
|
||||
throw new Error(indices.length ? "material_preview_page_limit" : "material_preview_pdf_empty");
|
||||
}
|
||||
const pages = await output.copyPages(input, indices);
|
||||
pages.forEach((page) => output.addPage(page));
|
||||
pageCount += pages.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageCount + 1 > limits.maxPages) throw new Error("material_preview_page_limit");
|
||||
let normalized;
|
||||
try {
|
||||
normalized = await normalizeImageForPdf(source, limits);
|
||||
} catch (error) {
|
||||
if (error?.message === "material_preview_image_invalid") throw error;
|
||||
throw new Error("material_preview_image_invalid");
|
||||
}
|
||||
const embedded = await output.embedJpg(normalized.buffer);
|
||||
const size = fitPage(normalized.width, normalized.height);
|
||||
const page = output.addPage([size.width, size.height]);
|
||||
page.drawImage(embedded, { x: 0, y: 0, width: size.width, height: size.height });
|
||||
pageCount += 1;
|
||||
}
|
||||
|
||||
if (!pageCount) throw new Error("material_preview_empty");
|
||||
const bytes = await output.save({
|
||||
useObjectStreams: true,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: 50,
|
||||
updateFieldAppearances: false,
|
||||
});
|
||||
const buffer = Buffer.from(bytes);
|
||||
if (buffer.length > limits.maxOutputBytes) throw new Error("material_preview_output_size_limit");
|
||||
return { buffer, pageCount };
|
||||
}
|
||||
|
||||
module.exports = { materialCollectionHash, mergeMaterialsToPdf };
|
||||
在新工单中引用
屏蔽一个用户