Files
LWLT-AI/archive/handoff/2026-07-12/legacy-erp-handoff/tools/erp_pdf_conversion.js
2026-07-13 19:57:46 +08:00

124 lines
4.7 KiB
JavaScript

const fs = require('node:fs/promises');
const path = require('node:path');
const DEFAULT_SEAL_SHIFT_LEFT_PX = 225;
function isLikelyHtml(buffer) {
if (!buffer || buffer.length === 0) return false;
const sample = buffer
.subarray(0, Math.min(buffer.length, 8192))
.toString('utf8')
.replace(/^\uFEFF/, '')
.trimStart()
.toLowerCase();
return (
sample.startsWith('<!doctype html') ||
sample.startsWith('<html') ||
sample.includes('<html') ||
sample.includes('<body') ||
sample.includes('xmlns:o="urn:schemas-microsoft-com:office:office"')
);
}
function shiftMarginLeft(style, shiftLeftPx = DEFAULT_SEAL_SHIFT_LEFT_PX) {
const marginPattern = /margin-left\s*:\s*(-?\d+(?:\.\d+)?)px/i;
if (!marginPattern.test(style)) return style;
return style.replace(marginPattern, (match, value) => {
const shifted = Math.max(0, Number(value) - shiftLeftPx);
return match.replace(value, String(Math.round(shifted * 100) / 100));
});
}
function normalizeSealPlacementHtml(html, options = {}) {
const source = String(html || '');
if (options.sealAnchor === 'party-b-first-page') {
const anchored = anchorXingyouSealNearPartyB(source);
if (anchored !== source) return anchored;
}
const shiftLeftPx = Number.isFinite(options.sealShiftLeftPx)
? options.sealShiftLeftPx
: DEFAULT_SEAL_SHIFT_LEFT_PX;
return source.replace(
/<span\b([^>]*\bstyle=(['"])([^'"]*position\s*:\s*absolute[^'"]*)\2[^>]*)>([\s\S]*?<img\b[^>]*(?:alt\s*=\s*['"]?zhang|zhang1\.png|bei2\.png|xy\.png)[^>]*>[\s\S]*?)<\/span>/gi,
(full, attrs, quote, style, content) => {
const shiftedStyle = shiftMarginLeft(style, shiftLeftPx);
if (shiftedStyle === style) return full;
const shiftedAttrs = attrs.replace(/\bstyle=(['"])([^'"]*)\1/i, `style=${quote}${shiftedStyle}${quote}`);
return `<span${shiftedAttrs}>${content}</span>`;
}
);
}
function anchorXingyouSealNearPartyB(html) {
const sealPattern = /<span\b([^>]*\bstyle=(['"])([^'"]*position\s*:\s*absolute[^'"]*)\2[^>]*)>([\s\S]*?<img\b[^>]*xy\.png[^>]*>[\s\S]*?)<\/span>/i;
const sealMatch = String(html || '').match(sealPattern);
if (!sealMatch) return html;
const seal = sealMatch[0]
.replace(/\sdata-erp-seal-normalized=(['"])[^'"]*\1/i, '')
.replace(/\bstyle=(['"])([^'"]*)\1/i, 'style="display:inline-block;margin-left:16px;margin-top:0;width:168px;height:96px;vertical-align:middle" data-erp-seal-normalized="party-b-first-page"');
const withoutSeal = html.replace(sealMatch[0], '');
const literalContactPattern = /(<p\b[^>]*>[\s\S]*?(?:乙方联系方式|乙方联系人|乙方联络)[\s\S]*?<\/p>)/i;
if (literalContactPattern.test(withoutSeal)) {
return withoutSeal.replace(literalContactPattern, `$1${seal}`);
}
const preferredContactPattern = /(<p\b[^>]*>[\s\S]*?(?:涔欐柟鑱旂郴鏂瑰紡|涔欐柟鑱旂郴浜|涔欐柟鑱旂粶)[\s\S]*?<\/p>)/i;
if (preferredContactPattern.test(withoutSeal)) {
return withoutSeal.replace(preferredContactPattern, `$1${seal}`);
}
const contactPattern = /(<p\b[^>]*>[\s\S]*?(?:乙方联系方式|乙方联系人|乙方联络|乙方)[\s\S]*?<\/p>)/i;
if (!contactPattern.test(withoutSeal)) return html;
return withoutSeal.replace(contactPattern, `$1${seal}`);
}
function pdfPathForSource(sourcePath) {
const parsed = path.parse(sourcePath);
return path.join(parsed.dir, `${parsed.name}.pdf`);
}
async function preparePdfRenderSource(sourcePath, options = {}) {
const buffer = await fs.readFile(sourcePath);
const isHtml = isLikelyHtml(buffer);
const ext = path.extname(sourcePath).toLowerCase();
if (!isHtml || ext === '.html' || ext === '.htm') {
return {
originalPath: sourcePath,
renderPath: sourcePath,
isHtml,
};
}
const renderDir = path.resolve(options.renderDir || path.join(path.dirname(sourcePath), '.pdf-render-html'));
await fs.mkdir(renderDir, { recursive: true });
const parsed = path.parse(sourcePath);
const renderPath = path.join(renderDir, `${parsed.name}.html`);
await fs.writeFile(renderPath, normalizeSealPlacementHtml(buffer.toString('utf8'), options), 'utf8');
return {
originalPath: sourcePath,
renderPath,
isHtml,
};
}
async function waitForPdfPageReady(page, options = {}) {
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
await page.waitForLoadState('networkidle', { timeout: timeoutMs }).catch(() => {});
await page.waitForFunction(
() => Array.from(document.images).every((img) => img.complete && img.naturalWidth > 0),
null,
{ timeout: timeoutMs }
).catch(() => {});
}
module.exports = {
isLikelyHtml,
normalizeSealPlacementHtml,
anchorXingyouSealNearPartyB,
pdfPathForSource,
preparePdfRenderSource,
waitForPdfPageReady,
};