diff --git a/app/assets/fonts/NotoSansCJKsc-Regular.otf b/app/assets/fonts/NotoSansCJKsc-Regular.otf new file mode 100644 index 0000000..dc15562 Binary files /dev/null and b/app/assets/fonts/NotoSansCJKsc-Regular.otf differ diff --git a/app/controllers/report.controller.js b/app/controllers/report.controller.js index 75e8b6e..485aa8f 100644 --- a/app/controllers/report.controller.js +++ b/app/controllers/report.controller.js @@ -5,6 +5,7 @@ const path = require("path"); const moment = require("moment-timezone"); const archiver = require("archiver"); const { PDFDocument } = require("pdf-lib"); +const fontkit = require("@pdf-lib/fontkit"); const Report = db.report; const RoutePath = db.route_path; const Employee = db.employee; @@ -44,6 +45,26 @@ const findTemplatePathBySite = (site) => { return candidatePaths.find((candidate) => fs.existsSync(candidate)) || ""; }; +const findUnicodeFontPath = (site) => { + const safeSite = [1, 2, 3].includes(Number(site)) ? Number(site) : 1; + const fileName = "NotoSansCJKsc-Regular.otf"; + const cwd = process.cwd(); + const candidatePaths = [ + path.join(ROOT_DIR, "app", "assets", "fonts", fileName), + path.join(ROOT_DIR, "app", "views", "upload", fileName), + path.join(ROOT_DIR, "client", "build", "upload", fileName), + path.join(ROOT_DIR, "client", "public", "upload", fileName), + path.join(cwd, "app", "assets", "fonts", fileName), + path.join(cwd, "app", "views", "upload", fileName), + path.join(cwd, "client", "build", "upload", fileName), + path.join(cwd, "client", "public", "upload", fileName), + path.join("/www/wwwroot/upload", fileName), + path.join(`/www/wwwroot/worldshine${safeSite}`, "app", "assets", "fonts", fileName), + path.join(`/www/wwwroot/worldshine${safeSite}-tspt`, "app", "assets", "fonts", fileName) + ]; + return candidatePaths.find((candidate) => fs.existsSync(candidate)) || ""; +}; + const formatUtcToLocalHm = (dateLike) => { if (!dateLike) return ""; const parsed = moment.utc(dateLike); @@ -74,8 +95,10 @@ const findOutboundStatusByCustomerId = (outboundCustomerStatuses, customerId) => return outboundCustomerStatuses.find((item) => toObjectIdText(item?.customer_id) === targetId) || {}; }; -const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, vehiclesMap, outboundCustomerStatuses) => { +const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, vehiclesMap, outboundCustomerStatuses, unicodeFontBytes) => { const pdfDoc = await PDFDocument.load(templateBytes); + pdfDoc.registerFontkit(fontkit); + const unicodeFont = await pdfDoc.embedFont(unicodeFontBytes, { subset: true }); const form = pdfDoc.getForm(); const routeName = route?.name || ""; const scheduleDate = route?.schedule_date || ""; @@ -107,11 +130,11 @@ const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, veh const enterCenterTime = formatUtcToLocalHm(customer?.customer_enter_center_time); if (enterCenterTime) { safeSetField(form, `arrive_${row}`, enterCenterTime); - safeSetField(form, `y_${row}`, "Y"); + safeSetField(form, `y_${row}`, "✓"); } else if (customer?.customer_route_status === "inCenter") { - safeSetField(form, `y_${row}`, "Y"); + safeSetField(form, `y_${row}`, "✓"); } else { - safeSetField(form, `n_${row}`, "N"); + safeSetField(form, `n_${row}`, "✕"); } const outboundStatus = findOutboundStatusByCustomerId(outboundCustomerStatuses, customer?.customer_id); @@ -121,6 +144,8 @@ const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, veh if (dropoffTime) safeSetField(form, `drop_${row}`, dropoffTime); }); + // Rebuild form appearances with a Unicode font so UTF-8 (e.g., Chinese) renders correctly. + form.updateFieldAppearances(unicodeFont); form.flatten(); return pdfDoc.save(); }; @@ -248,9 +273,13 @@ exports.exportRouteReportZip = async (req, res) => { const site = splitSite.findSiteNumber(req); const templatePath = findTemplatePathBySite(site); + const unicodeFontPath = findUnicodeFontPath(site); if (!templatePath) { return res.status(500).send({ message: `Missing PDF template for site ${site}.` }); } + if (!unicodeFontPath) { + return res.status(500).send({ message: "Missing Unicode font file: NotoSansCJKsc-Regular.otf" }); + } const [routes, drivers, vehicles] = await Promise.all([ RoutePath.find(splitSite.splitSiteGet(req, { schedule_date: date, status: { $ne: "disabled" } })), @@ -278,6 +307,7 @@ exports.exportRouteReportZip = async (req, res) => { ]) ); const templateBytes = fs.readFileSync(templatePath); + const unicodeFontBytes = fs.readFileSync(unicodeFontPath); const filenameDate = (date || "").replace(/\//g, "-"); const zipName = `route_report_${filenameDate || "date"}.zip`; @@ -303,7 +333,8 @@ exports.exportRouteReportZip = async (req, res) => { i + 1, driversMap, vehiclesMap, - outboundCustomerStatuses + outboundCustomerStatuses, + unicodeFontBytes ); const base = sanitizeFileName(route?.name || `route_${i + 1}`) || `route_${i + 1}`; const existingCount = filenameCounter.get(base) || 0; diff --git a/client/scripts/copy-route-report-templates.js b/client/scripts/copy-route-report-templates.js index 5a58d91..adc7ea2 100644 --- a/client/scripts/copy-route-report-templates.js +++ b/client/scripts/copy-route-report-templates.js @@ -5,6 +5,8 @@ const rootDir = path.resolve(__dirname, ".."); const sourceDir = path.join(rootDir, "public", "upload"); const targetDir = path.join(rootDir, "build", "upload"); const templates = ["pdf_templete1.pdf", "pdf_templete2.pdf", "pdf_templete3.pdf"]; +const unicodeFontFile = "NotoSansCJKsc-Regular.otf"; +const appFontSourcePath = path.join(rootDir, "..", "app", "assets", "fonts", unicodeFontFile); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); @@ -19,4 +21,8 @@ templates.forEach((templateName) => { fs.copyFileSync(sourcePath, targetPath); }); -console.log("Route report PDF templates copied to build/upload."); +if (fs.existsSync(appFontSourcePath)) { + fs.copyFileSync(appFontSourcePath, path.join(targetDir, unicodeFontFile)); +} + +console.log("Route report PDF templates and Unicode font copied to build/upload."); diff --git a/package-lock.json b/package-lock.json index 9f9fc2a..b71717e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@pdf-lib/fontkit": "^1.1.1", "archiver": "^7.0.1", "axios": "^0.27.2", "bcryptjs": "^2.4.3", @@ -733,6 +734,21 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@pdf-lib/fontkit": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz", + "integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/fontkit/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", diff --git a/package.json b/package.json index 905dc82..0faf50f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "yangli", "license": "ISC", "dependencies": { + "@pdf-lib/fontkit": "^1.1.1", "archiver": "^7.0.1", "axios": "^0.27.2", "bcryptjs": "^2.4.3",