${sections.join("\n")} `; zip.file("index.html", html); const blob = await generateZipBlob( zip, { type: "blob" }, "HTML 패키지 생성 중", `${pageIndices.length}페이지`, ); const outputName = `${name}-to-html.zip`; downloadBlob(blob, outputName); return result(outputName, "PDF HTML 패키지 변환 완료", blob.size, pageIndices.length, 1); }, async pdfToWord(files, options) { const file = normalizeFiles(files)[0]; const renderOptions = { ...(options || {}), renderedPageImageType: options && options.renderedPageImageType || "jpeg", renderedPageJpegQuality: options && options.renderedPageJpegQuality || 0.88, }; const zip = new JSZip(); const relationships = []; const bodyBlocks = []; zip.file("[Content_Types].xml", docxContentTypesXml()); zip.file("_rels/.rels", docxRootRelsXml()); const pageCount = await forEachRenderedPdfPage(file, renderOptions, async (renderedPage, index, totalPages) => { reportProgress("Word 문서 패키징 중", index, totalPages, `${index + 1}페이지`); const relationId = `rId${index + 1}`; const imageName = `image${index + 1}.${renderedPage.imageExtension || "png"}`; relationships.push(``); bodyBlocks.push(docxImageParagraphXml({ relationId, imageName, cx: pointsToEmu(renderedPage.widthPoints), cy: pointsToEmu(renderedPage.heightPoints), docPrId: index + 1, })); if (index !== totalPages - 1) { bodyBlocks.push(''); } zip.file(`word/media/${imageName}`, renderedPage.imageBlob); reportProgress("Word 문서 패키징 중", index + 1, totalPages, `${index + 1}페이지`); await yieldToBrowser(); }); zip.file("word/document.xml", docxDocumentXml(bodyBlocks.join(""))); zip.file("word/_rels/document.xml.rels", docxDocumentRelsXml(relationships.join(""))); reportProgress("Word 파일 생성 중", pageCount, pageCount, "", true); const blob = await generateZipBlob( zip, { type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }, "Word 파일 생성 중", `${pageCount}페이지`, ); const outputName = `${baseName(file.name)}-to-word.docx`; downloadBlob(blob, outputName); return result(outputName, "Word 변환 완료", blob.size, pageCount, 1); }, async pdfToPpt(files, options) { const file = normalizeFiles(files)[0]; const renderOptions = { ...(options || {}), renderedPageImageType: options && options.renderedPageImageType || "jpeg", renderedPageJpegQuality: options && options.renderedPageJpegQuality || 0.88, }; const zip = new JSZip(); let slideWidthEmu = 0; let slideHeightEmu = 0; const slideIdEntries = []; const presentationSlideRels = []; const pageCount = await forEachRenderedPdfPage(file, renderOptions, async (renderedPage, index, totalPages) => { reportProgress("PPT 슬라이드 패키징 중", index, totalPages, `${index + 1}페이지`); if (index === 0) { slideWidthEmu = pointsToEmu(renderedPage.widthPoints); slideHeightEmu = pointsToEmu(renderedPage.heightPoints); } const slideNumber = index + 1; const slideRelId = `rId${slideNumber + 1}`; const imageName = `image${slideNumber}.${renderedPage.imageExtension || "png"}`; const placement = fitImageToSlide( pointsToEmu(renderedPage.widthPoints), pointsToEmu(renderedPage.heightPoints), slideWidthEmu, slideHeightEmu, ); slideIdEntries.push(``); presentationSlideRels.push(``); zip.file(`ppt/slides/slide${slideNumber}.xml`, pptSlideXml({ imageRelId: "rId2", imageName, offsetX: placement.offsetX, offsetY: placement.offsetY, widthEmu: placement.widthEmu, heightEmu: placement.heightEmu, })); zip.file(`ppt/slides/_rels/slide${slideNumber}.xml.rels`, pptSlideRelsXml(imageName)); zip.file(`ppt/media/${imageName}`, renderedPage.imageBlob); reportProgress("PPT 슬라이드 패키징 중", index + 1, totalPages, `${index + 1}페이지`); await yieldToBrowser(); }); zip.file("[Content_Types].xml", pptContentTypesXml(pageCount)); zip.file("_rels/.rels", pptRootRelsXml); zip.file("ppt/slideMasters/slideMaster1.xml", pptSlideMasterXml); zip.file("ppt/slideMasters/_rels/slideMaster1.xml.rels", pptSlideMasterRelsXml); zip.file("ppt/slideLayouts/slideLayout1.xml", pptSlideLayoutXml); zip.file("ppt/slideLayouts/_rels/slideLayout1.xml.rels", pptSlideLayoutRelsXml); zip.file("ppt/theme/theme1.xml", pptThemeXml); zip.file("docProps/app.xml", pptAppPropsXml(pageCount)); zip.file("docProps/core.xml", pptCorePropsXml()); zip.file("ppt/presentation.xml", pptPresentationXml({ slideIdEntries: slideIdEntries.join(""), slideWidthEmu, slideHeightEmu, })); zip.file("ppt/_rels/presentation.xml.rels", pptPresentationRelsXml(presentationSlideRels.join(""))); reportProgress("PPT 파일 생성 중", pageCount, pageCount, "", true); const blob = await generateZipBlob( zip, { type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", }, "PPT 파일 생성 중", `${pageCount}페이지`, ); const outputName = `${baseName(file.name)}-to-ppt.pptx`; downloadBlob(blob, outputName); return result(outputName, "PPT 변환 완료", blob.size, pageCount, 1); }, async officeToPdf(files, options) { const file = normalizeFiles(files)[0]; const { bytes, pageCount } = await convertOfficeFileToPdfBytes(file, options); const outputName = `${baseName(file.name)}-from-office.pdf`; const byteCount = downloadBytes(bytes, outputName); return result(outputName, "Office PDF 변환 완료", byteCount, pageCount, 1); }, async protectPdf(files, options) { const file = normalizeFiles(files)[0]; const password = String((options && options.password) || ""); const confirmPassword = String((options && options.confirmPassword) || ""); const keyLength = Number((options && options.keyLength) || 256); if (!password) throw new Error("비밀번호를 입력하세요."); if (password.length < 4) throw new Error("비밀번호는 4자 이상이어야 합니다."); if (password !== confirmPassword) throw new Error("비밀번호가 일치하지 않습니다."); if (![40, 128, 256].includes(keyLength)) { throw new Error("지원하지 않는 키 길이입니다."); } const outputName = `${baseName(file.name)}-protected.pdf`; const byteCount = await runQpdfTransform( file, (inputPath, outputPath) => { const args = []; if (keyLength === 40) args.push("--allow-weak-crypto"); args.push("--encrypt", password, password, String(keyLength)); if (keyLength === 128) args.push("--use-aes=y"); args.push("--", inputPath, outputPath); return args; }, outputName, "PDF 암호화에 실패했습니다.", ); return result(outputName, "PDF 잠금 완료", byteCount, 0, 1); }, async unlockPdf(files, options) { const file = normalizeFiles(files)[0]; const password = String((options && options.password) || ""); if (!password) throw new Error("비밀번호를 입력하세요."); const outputName = `${baseName(file.name)}-unlocked.pdf`; const byteCount = await runQpdfTransform( file, (inputPath, outputPath) => [ `--password=${password}`, "--decrypt", inputPath, outputPath, ], outputName, "PDF 잠금 해제에 실패했습니다. 비밀번호를 확인하세요.", ); return result(outputName, "PDF 잠금 해제 완료", byteCount, 0, 1); }, async htmlToPdf(files, options) { const file = normalizeFiles(files)[0]; const lower = String(file.name || "").toLowerCase(); if (!lower.endsWith(".html") && !lower.endsWith(".htm") && file.type !== "text/html") { throw new Error("HTML/HTM 파일만 지원합니다."); } const maxHtmlBytes = Math.max(0, Number(options && options.maxHtmlBytes || 0)); if (maxHtmlBytes && file.size > maxHtmlBytes) { throw new Error(`${file.name}은 ${formatBytes(file.size)}입니다. HTML 변환은 ${formatBytes(maxHtmlBytes)} 이하 파일만 웹에서 처리합니다.`); } const analysis = await htmlAnalysisFromFile(file, { cancellable: true }); const maxHtmlCharacters = Math.max(0, Number(options && options.maxHtmlCharacters || 0)); if (maxHtmlCharacters && analysis.characterCount > maxHtmlCharacters) { throw new Error(`HTML 본문이 ${analysis.characterCount.toLocaleString("ko-KR")}자입니다. 웹 변환은 ${maxHtmlCharacters.toLocaleString("ko-KR")}자 이하만 처리합니다.`); } const { bytes, pageCount } = await htmlBlocksToPdfBytes(analysis, baseName(file.name)); const outputName = `${baseName(file.name)}-from-html.pdf`; const byteCount = downloadBytes(bytes, outputName); return result(outputName, "HTML PDF 변환 완료", byteCount, pageCount, 1); }, }; })();