const fs = require('fs') const path = require('path') const compressing = require('compressing') const rimrif = require('rimraf') const shell = require('shelljs'); const crypto = require('crypto'); // 文档模板生成 const PizZip = require("pizzip"); const Docxtemplater = require("docxtemplater"); // 文档转换 import { Chromiumly } from "chromiumly"; // Chromiumly.configure({ endpoint: "http://8.140.98.43/docs" }); Chromiumly.configure({ endpoint: "http://123.57.204.89/docs" }); import { PDFEngines } from "chromiumly"; const { LibreOffice } = require("chromiumly"); // const { PDFEngines } = require("chromiumly"); const tempDir = path.join(__dirname , "temp"); if(!fs.existsSync(tempDir)){fs.mkdirSync(tempDir)}; const OSS = require("ali-oss"); const ALI_OSS_BUCKET = process.env.ALI_OSS_BUCKET || "hep-textbook" const ALI_OSS_ACCESS_KEY_ID = process.env.ALI_OSS_ACCESS_KEY_ID || "LTAI5t6AbTiAvXmeoVdJZhL3" const ALI_OSS_ACCESS_KEY_SECRET = process.env.ALI_OSS_ACCESS_KEY_SECRET || "KLtQRdIW69KLP7jnzHNUf7eKmdptxH" const bwipjs = require("bwip-js") export async function toBarCode(text){ return new Promise(resolve=>{ bwipjs.toBuffer({ bcid:"code128", text:text, scale:1.5, height:3, includetext:false, textalign:"center" },(err,png)=>{ if(err){ console.error(err) resolve(null) }else{ resolve(png) } }) }) } export async function uploadFileToOSS(filePath){ let client = new OSS({ // yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。 region: "oss-cn-beijing", accessKeyId: ALI_OSS_ACCESS_KEY_ID, accessKeySecret: ALI_OSS_ACCESS_KEY_SECRET, // 填写Bucket名称。 bucket: ALI_OSS_BUCKET || "hep-textbook", }); let now = new Date(); let fileName = getFileName(filePath); let fileKey = `export/report/${fileName}`; const r1 = await client?.put(fileKey, filePath); console.log('put success: %j', r1); return r1 } export function getFileName(filePath) { // 使用 '/' 或 '\' 作为分隔符,分割路径 const parts = filePath.split(/[/\\]/); // 返回最后一个部分,即文件名 return parts.pop(); } module.exports.uploadFileToOSS = uploadFileToOSS /** * 将给定的文件路径数组打包成指定名称的zip压缩包 * @param {Array} filePathList - 要打包的文件路径数组 * @param {string} outputZipName - 输出的zip文件名称 */ export function createZip(filePathList, outputZipName,options) { let zipStream = new compressing.zip.Stream(); return new Promise((resolve)=>{ try { let outputPath = path.join(options?.tempDir||tempDir,outputZipName) // 遍历文件路径列表,将每个文件添加到zip流中 for (const filePath of filePathList) { // 检查文件是否存在 if (fs.existsSync(filePath)) { // 将文件添加到zip流中 zipStream.addEntry(filePath); } else { console.error(`文件不存在: ${filePath}`); } } // 创建一个写入流 const output = fs.createWriteStream(outputPath); // 使用 compressing 库的 zip 方法将文件打包 // console.log(filePathList) // await compressing.zip.compressDir(filePathList, output); // 将zip流写入文件 zipStream.pipe(output); output.on('finish', () => { // console.log(`成功创建压缩包: ${outputPath}`); resolve(outputPath) }); output.on('error', (error) => { console.error('写入压缩包时出错:', error); resolve(null) }); // console.log(`成功创建压缩包: ${outputPath}`); // return outputPath } catch (error) { console.error('创建压缩包时出错:', error); return null } }) } module.exports.createZip = createZip const download = require('download') async function downloadUrl(url,options) { // console.log(url) if(url?.startsWith("/")) {return url}; let md5 = crypto.createHash('md5'); let extname = path.extname(url)?.toLocaleLowerCase(); let filename = md5.update(url).digest('hex') + extname; let filepath = path.join(options?.tempDir||tempDir,filename) // console.log(filename,filepath) try{ // if(fs.existsSync(filepath)){fs.rmSync(filepath)} // 存在则删除 if(fs.existsSync(filepath)){return filepath} // 存在则直接返回(md5相同) fs.writeFileSync(filepath, await download(url)); return filepath }catch(err){ console.error(err) return null } } /** * 将 DOCX 文件转换为 PDF * * @param {string} docxPath - 要转换的 DOCX 文件的路径 * @param {string} outputPath - 输出 PDF 文件的路径 * @returns {Promise} */ export async function docxToPdf(docxPath, outputPath,options) { let mergeFiles = options?.mergeFiles || [] let merge = false; let mergeFileMap = {}; if(mergeFiles?.length){ let plist = [] for (let index = 0; index < mergeFiles.length; index++) { let filePath plist.push((async ()=>{ try{ filePath = await downloadUrl(mergeFiles[index],options); }catch(err){} if(filePath){ mergeFileMap[index] = filePath // 按原有顺序整理 // filePathList.push(filePath) } return })()) } await Promise.all(plist); merge = true; } let filePathList = mergeFiles?.map((item,index)=>mergeFileMap[index]).filter(item=>item) // console.log("DOWNLOADED:",filePathList) filePathList = filePathList.map((filepath,index)=>{ // 按顺序修改文件前缀数字为字母表顺序 let fileDir = path.dirname(filepath); let abc = String.fromCharCode(96+(index+1)); // 字母顺序不会出现 把 1 10 11 12 放在一起的情况 let num = index+110; // 数字顺序从百位开始,避免首数字排序错乱 let md5 = crypto.createHash('md5'); let outmd5 = md5.update(path.basename(filepath)).digest('hex'); let fileName = num + "_" + outmd5 + path.extname(filepath); let orderPath = path.join(fileDir,fileName) fs.cpSync(filepath,orderPath); fs.readFileSync(filepath); return orderPath }) try { let files = [] if(docxPath){ let docxBuffer = fs.readFileSync(docxPath); files.push({ data: docxBuffer, ext: "docx" }) } files = [...files,...filePathList] // console.log("files",files) let convertOpts = { files, properties: { // 设置页面属性,例如纸张大小和方向 pageSize: 'A4', // orientation: 'portrait', margin: { top: 0, right: 0, bottom: 0, left: 0 } }, pdfa: false, // 根据需要设置 pdfUA: false, // 根据需要设置 merge: merge, // 如果只转换一个文件,设置为false // metadata: { // // 你可以在这里添加元数据 // }, // losslessImageCompression: false, // reduceImageResolution: false, // quality: 90, // JPG 导出质量 // maxImageResolution: 300 // 最大图像分辨率 } // console.log("convertOpts",convertOpts) let pdfPath,pdfBuffer // 方式1:逐个合并 // let pdfBuffer // for (let index = 1; index < files.length; index++) { // let file = files[index]; // if(pdfBuffer){ // convertOpts.files = [{data:pdfBuffer,ext:"pdf"},file] // }else{ // convertOpts.files = [file] // } // pdfBuffer = await LibreOffice.convert(convertOpts); // } let mainPdfPath = docxPath if(docxPath){ convertOpts.files = [files[0]]; console.log(convertOpts) let mainPdfBuffer = await LibreOffice.convert(convertOpts); let md5 = crypto.createHash('md5'); let outmd5 = md5.update(path.basename(docxPath)).digest('hex'); mainPdfPath = path.dirname(docxPath)+"/109_"+outmd5+".pdf" fs.writeFileSync(mainPdfPath,mainPdfBuffer) } // 方式2:先合并pdf,后合并docx if(files?.length>=2){ // console.log(files) let tmpFiles = files if(docxPath){ tmpFiles = files.slice(1) // 携带首个docx时,从第二个开始 } let pdfList = [mainPdfPath,...tmpFiles]; pdfList = pdfList.filter(item=>item) let mergedFileList = await mergePdfListReduce(pdfList,convertOpts) pdfPath = mergedFileList[0]; // convertOpts.files = [files[0],...mergedFileList] // console.log(convertOpts) // pdfBuffer = await LibreOffice.convert(convertOpts); }else{ pdfBuffer = await LibreOffice.convert(convertOpts); } // 方式3:全部合并 // let pdfBuffer = await LibreOffice.convert(convertOpts); if(pdfPath){ fs.cpSync(pdfPath,outputPath); } // 将 Buffer 写入输出文件 if(pdfBuffer){ fs.writeFileSync(outputPath, pdfBuffer); console.log(`成功输出 ${outputPath}`); } return outputPath } catch (error) { console.error('转换失败:', error); return null } } module.exports.docxToPdf = docxToPdf const ImageModule = require("@slosarek/docxtemplater-image-module-free"); const sizeOf = require("image-size"); /** * 每三个pdf合并一次,直到合并为一个pdf为止 * @param {} pdfList * @param {*} convertOpts * @returns */ export async function mergePdfListReduce(pdfList,convertOpts){ console.log("pdfList",pdfList) // 所有非PDF转PDF for (let index = 0; index < pdfList.length; index++) { let file = pdfList[index]; if(typeof file == "string" && file?.toLocaleLowerCase()?.indexOf("pdf")==-1){ convertOpts.files = [file]; let pdfBuffer = await LibreOffice.convert(convertOpts); fs.writeFileSync(file+".pdf",pdfBuffer) pdfList[index] = file+".pdf" } } let mergeList = [] let plist = [] let length = pdfList.length for (let index = 0; index < length; index++) { let file = pdfList.shift(); // console.log(file,index,length) if(!file) break; let files = [file,pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), // pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), // pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), // pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(), // ,pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift() // ,pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift() // ,pdfList.shift(),pdfList.shift(),pdfList.shift() // ,pdfList.shift(),pdfList.shift(),pdfList.shift() // ,pdfList.shift(),pdfList.shift(),pdfList.shift(),pdfList.shift() ]; // 每次合并四个 files=files?.filter(item=>item); // console.log(files) plist.push(new Promise(async resolve=>{ if(files?.length==1){ // 单文件直接加载 自动获取后缀 let onefile = files[0] // if(!onefile?.ext){ // let extname = path.extname(files[0]).slice(1)?.toLocaleLowerCase(); // onefile = {data:fs.readFileSync(onefile),ext:extname} // } resolve(onefile); }else{ // 多文件合并 convertOpts = {} convertOpts.files = files; // console.log("多文件合并",convertOpts) // pdfEngine合并 if(false){ let mergeBuffer = await PDFEngines.merge(convertOpts) let mergeFilePath = files[0]+".merge.pdf" fs.writeFileSync(mergeFilePath,mergeBuffer) resolve(mergeFilePath) } // pdfunite合并 if(true){ let mergeFilePath = files[0]+".merge.pdf" pdfUnite(files,mergeFilePath) resolve(mergeFilePath) } } })) } if(plist?.length){ mergeList = await Promise.all(plist); } // console.log("mergeList",mergeList) if(mergeList?.length<=1){ return mergeList; }else{ // console.log("mergePdfListReduce continue:",mergeList) return await mergePdfListReduce(mergeList,convertOpts) } } function pdfUnite(pdfList,outputPath){ let params = ["pdfunite",...pdfList,outputPath].join(" ") try{ shell.exec(params) }catch(err){} if(fs.existsSync(outputPath)){ return outputPath }else{ throw "error: pdfunit merge error" } } export function renderDocx(inputDocxPath, outputDocxName, data,options){ let imageOptions = { getImage(tagValue,tagName) { if(!fs.existsSync(tagValue)){ throw new Error(`Image not found: ${tagValue}`); } return fs.readFileSync(tagValue); }, getSize(img) { const sizeObj = sizeOf(img); console.log(sizeObj); return [sizeObj.width, sizeObj.height]; }, }; let outputDocxPath = path.join(options?.tempDir||tempDir,outputDocxName) // Load the docx file as binary content let content = fs.readFileSync( inputDocxPath, "binary" ); // Unzip the content of the file let zip = new PizZip(content); let doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, modules: [new ImageModule(imageOptions)], }); // Render the document (Replace {first_name} by John, {last_name} by Doe, ...) Object.keys(data).forEach(key=>{ // 除去空值 if(data[key]==undefined){ data[key] = "" } }) doc.render(data); // Get the zip document and generate it as a nodebuffer let buf = doc.getZip().generate({ type: "nodebuffer", // compression: DEFLATE adds a compression step. // For a 50MB output document, expect 500ms additional CPU time compression: "DEFLATE", }); // buf is a nodejs Buffer, you can either write it to a // file or res.send it with express for example. fs.writeFileSync(outputDocxPath, buf); return outputDocxPath } /** * docx 替换模板字符串内容 * @example // 要替换内容的模板 let inputDocx = 'cs.docx' // 替换完成的docx文件 let outputDocx = 'dd.docx' // {{xx}} 处要替换的内容 let replaceData = { name: '替换name处的内容', age: '替换age处的内容', } replaceDocx(inputDocx, outputDocx, replaceData) */ export function replaceDocx(inputDocxPath, outputDocxPath, options,eventMap) { return new Promise((resolve,reject)=>{ // 解压出来的临时目录 let md5 = crypto.createHash('md5'); let outmd5 = md5.update(outputDocxPath).digest('hex') let tempDocxPath = path.join(options?.tempDir||tempDir , outmd5) // 要替换的xml文件位置 let tempDocxXMLName = path.join(tempDocxPath,`word/document.xml`) // 压缩文件夹为文件 let dir_to_docx = (inputFilePath, outputFilePath) => { outputFilePath = path.join(options?.tempDir||tempDir,outputFilePath) // 创建压缩流 let zipStream = new compressing.zip.Stream() // 写出流 let outStream = fs.createWriteStream(outputFilePath) fs.readdir(inputFilePath, null, (err, files) => { if (!err) { files.map(file => path.join(inputFilePath, file)) .forEach(file => { zipStream.addEntry(file) }) } }) // 写入文件内容 zipStream.pipe(outStream) .on('close', () => { // 打包完成后删除临时目录 // console.log(tempDocxPath) eventMap["onDocxComplete"]&&eventMap["onDocxComplete"](outputFilePath) shell.rm("-r",tempDocxPath) // rimrif.rimrafSync(tempDocxPath) resolve(true) }) } // 替换word/document.xml文件中{{xx}}处的内容 let replaceXML = (data, text) => { Object.keys(data).forEach(key => { text = text.replaceAll(`{{${key}}}`, data[key]) }) return text } // 解压docx文件替换内容重新打包成docx文件 compressing.zip.uncompress(inputDocxPath, tempDocxPath) .then(() => { // 读写要替换内容的xml文件 fs.readFile(tempDocxXMLName, null, (err, data) => { if (!err) { let text = data.toString() text = replaceXML(options, text) fs.writeFile(tempDocxXMLName, text, (err) => { if (!err) { dir_to_docx(tempDocxPath, outputDocxPath) } else { reject(err) } }) } else { reject(err) } }) }).catch(err => { reject(err) }) }) } module.exports.replaceDocx = replaceDocx function generateObjectId(inputString) { inputString = inputString || "" inputString = String(inputString) const hash = crypto.createHash('sha256').update(inputString).digest('hex'); const objectId = hash; return objectId; }