124 lines
4.7 KiB
JavaScript
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,
|
|
};
|