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(' { 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( /]*\bstyle=(['"])([^'"]*position\s*:\s*absolute[^'"]*)\2[^>]*)>([\s\S]*?]*(?: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 `${content}`; } ); } function anchorXingyouSealNearPartyB(html) { const sealPattern = /]*\bstyle=(['"])([^'"]*position\s*:\s*absolute[^'"]*)\2[^>]*)>([\s\S]*?]*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 = /(]*>[\s\S]*?(?:乙方联系方式|乙方联系人|乙方联络)[\s\S]*?<\/p>)/i; if (literalContactPattern.test(withoutSeal)) { return withoutSeal.replace(literalContactPattern, `$1${seal}`); } const preferredContactPattern = /(]*>[\s\S]*?(?:涔欐柟鑱旂郴鏂瑰紡|涔欐柟鑱旂郴浜|涔欐柟鑱旂粶)[\s\S]*?<\/p>)/i; if (preferredContactPattern.test(withoutSeal)) { return withoutSeal.replace(preferredContactPattern, `$1${seal}`); } const contactPattern = /(]*>[\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, };