/
melnik-demyan
/
ipdocgen
Обзор
Документация
Войти
/
melnik-demyan
/
ipdocgen
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/renderDocx.ts
101 строка
4 KB
Demyan Melnik
Increase signature size + migrate client data
11 июн 2026, 14:34
11 июн 2026, 14:34
e279ca3
Код
Авторство
О чём код?
import fs from 'fs'; import path from 'path'; import JSZip from 'jszip'; // @ts-ignore — html-to-docx ships no type definitions import htmlToDocx from 'html-to-docx'; const MIME_BY_EXT: Record<string, string> = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', }; const inlineLocalImages = (html: string): string => html.replace(/<img([^>]*?)src=["']([^"']+)["']([^>]*)>/gi, (match, pre, src, post) => { if (/^(https?:|data:)/i.test(src)) return match; const filePath = path.resolve(src.replace(/^\.\.?\//, '')); if (!fs.existsSync(filePath)) return match.replace(src, ''); const ext = path.extname(filePath).toLowerCase(); const mime = MIME_BY_EXT[ext] ?? 'application/octet-stream'; const data = fs.readFileSync(filePath).toString('base64'); return `<img${pre}src="data:${mime};base64,${data}"${post}>`; }); const DOCX_OPTIONS = { font: 'Arial', fontSize: 20, complexScriptFontSize: 20, margins: { top: 1134, right: 1134, bottom: 1134, left: 1134, header: 720, footer: 720, gutter: 0, }, table: { row: { cantSplit: true, }, }, }; const keepFirstTableRowWithBody = (xml: string): string => xml.replace(/<w:tbl>([\s\S]*?)<\/w:tbl>/g, (_match, tblContent: string) => { const newContent = tblContent.replace( /<w:tr>[\s\S]*?<\/w:tr>/, (trMatch) => trMatch.replace(/<w:pPr>/g, '<w:pPr><w:keepNext/>'), ); return `<w:tbl>${newContent}</w:tbl>`; }); const SIG_MAX_H_EMU = 1080000; const SIG_MAX_W_EMU = 3000000; const CLIENT_SIG_SCALE = 1.6; const floatSignatureImages = (xml: string): string => xml.replace(/<w:drawing>[\s\S]*?<\/w:drawing>/g, (drawing) => { if (!/descr="bgsig/.test(drawing)) return drawing; const extentMatch = drawing.match(/<wp:extent cx="(\d+)" cy="(\d+)"\s*\/>/); const graphicMatch = drawing.match(/<a:graphic[\s\S]*?<\/a:graphic>/); if (!extentMatch || !graphicMatch) return drawing; const isClient = /descr="bgsig-left"/.test(drawing); const maxH = isClient ? SIG_MAX_H_EMU * CLIENT_SIG_SCALE : SIG_MAX_H_EMU; const maxW = isClient ? SIG_MAX_W_EMU * CLIENT_SIG_SCALE : SIG_MAX_W_EMU; const cx = parseInt(extentMatch[1], 10); const cy = parseInt(extentMatch[2], 10); const scale = Math.min(maxH / cy, maxW / cx); const newCx = Math.round(cx * scale); const newCy = Math.round(cy * scale); const graphic = graphicMatch[0].replace( /<a:ext cx="\d+" cy="\d+"\s*\/>/, `<a:ext cx="${newCx}" cy="${newCy}"/>`, ); return `<w:drawing><wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="251658240" behindDoc="1" locked="0" layoutInCell="1" allowOverlap="1"><wp:simplePos x="0" y="0"/><wp:positionH relativeFrom="column"><wp:posOffset>360000</wp:posOffset></wp:positionH><wp:positionV relativeFrom="margin"><wp:align>center</wp:align></wp:positionV><wp:extent cx="${newCx}" cy="${newCy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:wrapNone/><wp:docPr id="1" name="bgsig"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>${graphic}</wp:anchor></w:drawing>`; }); const postProcess = (xml: string): string => floatSignatureImages(keepFirstTableRowWithBody(xml)); const finalizeDocx = async (buffer: Buffer): Promise<Buffer> => { const zip = await JSZip.loadAsync(buffer); const file = zip.file('word/document.xml'); if (!file) return buffer; const xml = await file.async('string'); zip.file('word/document.xml', postProcess(xml)); return await zip.generateAsync({type: 'nodebuffer'}); }; export const renderDocx = async (html: string): Promise<Buffer> => { const prepared = inlineLocalImages(html); const result = (await htmlToDocx(prepared, undefined, DOCX_OPTIONS)) as Buffer; return await finalizeDocx(result); };