fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
167
server/documents/worker/server.ts
Normal file
167
server/documents/worker/server.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import express from "express";
|
||||
import { chromium } from "playwright";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const app = express();
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// ─── Healthcheck ───────────────────────────────────────────────────────────
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// ─── HTML → PDF ────────────────────────────────────────────────────────────
|
||||
|
||||
app.post("/convert/pdf", async (req, res) => {
|
||||
const { html, options = {} } = req.body;
|
||||
if (!html) {
|
||||
return res.status(400).json({ success: false, error: "HTML обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfBuffer = await convertHtmlToPdf(html, options);
|
||||
res.json({
|
||||
success: true,
|
||||
pdf: pdfBuffer.toString("base64"),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[worker] PDF conversion error:", err);
|
||||
res.status(500).json({ success: false, error: err.message || "Ошибка конвертации PDF" });
|
||||
}
|
||||
});
|
||||
|
||||
async function convertHtmlToPdf(html: string, options: any): Promise<Buffer> {
|
||||
const launchOptions: Record<string, unknown> = {
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
|
||||
};
|
||||
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_PATH) {
|
||||
launchOptions.executablePath = process.env.PLAYWRIGHT_CHROMIUM_PATH;
|
||||
}
|
||||
|
||||
const browser = await chromium.launch(launchOptions);
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const pdf = await page.pdf({
|
||||
format: options.format || "A4",
|
||||
landscape: options.landscape || false,
|
||||
printBackground: options.printBackground !== false,
|
||||
margin: options.margin
|
||||
? {
|
||||
top: `${options.margin.top / 1440}in`,
|
||||
right: `${options.margin.right / 1440}in`,
|
||||
bottom: `${options.margin.bottom / 1440}in`,
|
||||
left: `${options.margin.left / 1440}in`,
|
||||
}
|
||||
: undefined,
|
||||
displayHeaderFooter: !!(options.headerTemplate || options.footerTemplate),
|
||||
headerTemplate: options.headerTemplate || "<div></div>",
|
||||
footerTemplate: options.footerTemplate || "<div></div>",
|
||||
});
|
||||
|
||||
await page.close();
|
||||
return pdf;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── DOCX → PDF ────────────────────────────────────────────────────────────
|
||||
|
||||
app.post("/convert/docx-to-pdf", async (req, res) => {
|
||||
const { docxBase64 } = req.body;
|
||||
if (!docxBase64) {
|
||||
return res.status(400).json({ success: false, error: "DOCX base64 обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfBuffer = await convertDocxToPdf(Buffer.from(docxBase64, "base64"));
|
||||
res.json({
|
||||
success: true,
|
||||
pdf: pdfBuffer.toString("base64"),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[worker] DOCX→PDF conversion error:", err);
|
||||
res.status(500).json({ success: false, error: err.message || "Ошибка конвертации DOCX→PDF" });
|
||||
}
|
||||
});
|
||||
|
||||
async function convertDocxToPdf(docxBuffer: Buffer): Promise<Buffer> {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "docx-pdf-"));
|
||||
const docxPath = path.join(tmpDir, "input.docx");
|
||||
const pdfPath = path.join(tmpDir, "input.pdf");
|
||||
|
||||
try {
|
||||
await fs.writeFile(docxPath, docxBuffer);
|
||||
await execFileAsync("libreoffice", [
|
||||
"--headless",
|
||||
"--convert-to",
|
||||
"pdf",
|
||||
"--outdir",
|
||||
tmpDir,
|
||||
docxPath,
|
||||
]);
|
||||
return await fs.readFile(pdfPath);
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EMF → PNG ─────────────────────────────────────────────────────────────
|
||||
|
||||
app.post("/convert/emf-to-png", async (req, res) => {
|
||||
const { emfBase64 } = req.body;
|
||||
if (!emfBase64) {
|
||||
return res.status(400).json({ success: false, error: "EMF base64 обязателен" });
|
||||
}
|
||||
|
||||
try {
|
||||
const pngBuffer = await convertEmfToPng(Buffer.from(emfBase64, "base64"));
|
||||
res.json({
|
||||
success: true,
|
||||
png: pngBuffer.toString("base64"),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[worker] EMF→PNG conversion error:", err);
|
||||
res.status(500).json({ success: false, error: err.message || "Ошибка конвертации EMF→PNG" });
|
||||
}
|
||||
});
|
||||
|
||||
async function convertEmfToPng(emfBuffer: Buffer): Promise<Buffer> {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "emf-png-"));
|
||||
const inputPath = path.join(tmpDir, "image.emf");
|
||||
const outputPath = path.join(tmpDir, "image.png");
|
||||
|
||||
try {
|
||||
await fs.writeFile(inputPath, emfBuffer);
|
||||
await execFileAsync("libreoffice", [
|
||||
"--headless",
|
||||
"--convert-to",
|
||||
"png",
|
||||
"--outdir",
|
||||
tmpDir,
|
||||
inputPath,
|
||||
]);
|
||||
return await fs.readFile(outputPath);
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Start ─────────────────────────────────────────────────────────────────
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[document-worker] Running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user