diff --git a/package-lock.json b/package-lock.json index 51e2c83..250551e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "i18next-http-backend": "^3.0.2", "jimp": "^0.22.12", "js-quantities": "^1.8.0", + "jspdf": "^3.0.3", "jszip": "^3.10.1", "lint-staged": "^15.4.3", "locize": "^4.0.14", @@ -3487,6 +3488,11 @@ "integrity": "sha512-gDQJflz1rOgEcUXkMAl80bDGN46f5mp8GbcM5dyvq+zsFV6YRBRtmNxlJJ5mjY77T7BRkRFzdIBVmK90QYhCxA==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -3509,6 +3515,12 @@ "@types/node": "*" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/react": { "version": "18.3.25", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz", @@ -3523,7 +3535,6 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3584,7 +3595,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", - "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -4448,6 +4458,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4737,6 +4756,25 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/centra": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", @@ -5170,6 +5208,17 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.46.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "hasInstallScript": true, + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5268,6 +5317,15 @@ "node": ">=4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -5697,6 +5755,21 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dompurify/node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -5784,7 +5857,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.2" @@ -6728,6 +6801,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -6777,7 +6865,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { @@ -7553,6 +7640,19 @@ "void-elements": "3.1.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -7694,7 +7794,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7859,6 +7959,11 @@ "node": ">= 0.4" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==" + }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", @@ -8589,6 +8694,22 @@ "node": "*" } }, + "node_modules/jspdf": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "dependencies": { + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9395,7 +9516,6 @@ "version": "0.53.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz", "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "^1.0.6" @@ -10130,6 +10250,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/phin": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", @@ -10831,6 +10957,15 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/rc-slider": { "version": "11.1.9", "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", @@ -11406,6 +11541,15 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11578,7 +11722,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sax": { @@ -11908,6 +12052,15 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/start-server-and-test": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.2.tgz", @@ -12615,6 +12768,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -12716,6 +12878,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13045,7 +13216,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13173,6 +13344,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uzip": { "version": "0.20201231.0", "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", diff --git a/package.json b/package.json index d0f6d7e..8857a6e 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "i18next-http-backend": "^3.0.2", "jimp": "^0.22.12", "js-quantities": "^1.8.0", + "jspdf": "^3.0.3", "jszip": "^3.10.1", "lint-staged": "^15.4.3", "locize": "^4.0.14", diff --git a/public/locales/de/pdf.json b/public/locales/de/pdf.json index 5d3ba8b..e811d7b 100644 --- a/public/locales/de/pdf.json +++ b/public/locales/de/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Konvertieren Sie PDF in PNG-Bilder", "title": "PDF zu PNG" }, + "convertToPdf": { + "title": "Bilder in PDF konvertieren", + "description": "Verschiedene Bildformate (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) in PDF konvertieren, mit Optionen zum Skalieren des Bildes und zur Wahl der Seitenorientierung.", + "shortDescription": "Bilder in PDF mit Skalierungs- und Orientierungskontrolle konvertieren" + }, "protectPdf": { "description": "Fügen Sie Ihren PDF-Dateien sicher in Ihrem Browser einen Passwortschutz hinzu", "shortDescription": "PDF-Dateien sicher mit einem Passwort schützen", diff --git a/public/locales/en/pdf.json b/public/locales/en/pdf.json index b0e9b30..82e5309 100644 --- a/public/locales/en/pdf.json +++ b/public/locales/en/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Convert PDF into PNG images", "title": "PDF to PNG" }, + "convertToPdf": { + "title": "Images to PDF", + "description": "Convert various image formats (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) to PDF, with options to scale the image and choose page orientation.", + "shortDescription": "Convert images to PDF with scale and orientation control" + }, "protectPdf": { "description": "Add password protection to your PDF files securely in your browser", "shortDescription": "Password protect PDF files securely", diff --git a/public/locales/es/pdf.json b/public/locales/es/pdf.json index 05a2f68..083fd01 100644 --- a/public/locales/es/pdf.json +++ b/public/locales/es/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Convertir PDF a imágenes PNG", "title": "PDF a PNG" }, + "convertToPdf": { + "title": "Imágenes a PDF", + "description": "Convertir varios formatos de imagen (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) a PDF, con opciones para escalar la imagen y elegir la orientación de la página.", + "shortDescription": "Convertir imágenes a PDF con control de escala y orientación" + }, "protectPdf": { "description": "Agregue protección con contraseña a sus archivos PDF de forma segura en su navegador", "shortDescription": "Proteger con contraseña los archivos PDF de forma segura", diff --git a/public/locales/fr/pdf.json b/public/locales/fr/pdf.json index b296a01..fac70af 100644 --- a/public/locales/fr/pdf.json +++ b/public/locales/fr/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Convertir des PDF en images PNG", "title": "PDF en PNG" }, + "convertToPdf": { + "title": "Images vers PDF", + "description": "Convertir divers formats d'image (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) en PDF, avec des options pour redimensionner l'image et choisir l'orientation de la page.", + "shortDescription": "Convertir des images en PDF avec contrôle de taille et d'orientation" + }, "protectPdf": { "description": "Ajoutez une protection par mot de passe à vos fichiers PDF en toute sécurité dans votre navigateur", "shortDescription": "Protégez les fichiers PDF en toute sécurité avec un mot de passe", diff --git a/public/locales/hi/pdf.json b/public/locales/hi/pdf.json index 10cf326..fdd808c 100644 --- a/public/locales/hi/pdf.json +++ b/public/locales/hi/pdf.json @@ -82,6 +82,11 @@ "shortDescription": "PDF को PNG छवियों में परिवर्तित करें", "title": "पीडीएफ से पीएनजी" }, + "convertToPdf": { + "title": "PDF में छवि बदलें", + "description": "विभिन्न छवि प्रारूपों (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) को PDF में परिवर्तित करें और छवि का आकार और पृष्ठ की अभिविन्यास समायोजित करें।", + "shortDescription": "छवियों को PDF में परिवर्तित करें, आकार और अभिविन्यास समायोजित करने योग्य" + }, "protectPdf": { "allowCopying": "कॉपी करने की अनुमति दें", "allowModification": "संशोधन की अनुमति दें", diff --git a/public/locales/ja/pdf.json b/public/locales/ja/pdf.json index 1a695a7..e421665 100644 --- a/public/locales/ja/pdf.json +++ b/public/locales/ja/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "PDFをPNG画像に変換する", "title": "PDFからPNGへ" }, + "convertToPdf": { + "description": "様々な画像形式(PNG、GIF、JPG、TIF、PSD、SVG、WEBP、HEIC、RAW)をPDFに変換します。画像サイズとページの向きを調整できます。", + "shortDescription": "画像を PDF に変換し、画像のサイズやページの向きを調整できます。", + "title": "画像を PDF に変換" + }, "protectPdf": { "description": "ブラウザで安全にPDFファイルにパスワード保護を追加します", "shortDescription": "PDFファイルをパスワードで安全に保護する", diff --git a/public/locales/nl/pdf.json b/public/locales/nl/pdf.json index 27dca31..2d8d020 100644 --- a/public/locales/nl/pdf.json +++ b/public/locales/nl/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "PDF naar PNG-afbeeldingen converteren", "title": "PDF naar PNG" }, + "convertToPdf": { + "title": "Afbeeldingen naar PDF", + "description": "Verschillende afbeeldingsformaten (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) naar PDF converteren, met opties om de afbeelding te schalen en de paginaoriëntatie te kiezen.", + "shortDescription": "Afbeeldingen naar PDF converteren met schaal- en oriëntatiecontrole" + }, "protectPdf": { "description": "Voeg wachtwoordbeveiliging toe aan uw PDF-bestanden veilig in uw browser", "shortDescription": "PDF-bestanden veilig met een wachtwoord beveiligen", diff --git a/public/locales/pt/pdf.json b/public/locales/pt/pdf.json index 2689460..f1a6a80 100644 --- a/public/locales/pt/pdf.json +++ b/public/locales/pt/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Converter PDF em imagens PNG", "title": "PDF para PNG" }, + "convertToPdf": { + "title": "Imagens para PDF", + "description": "Converter vários formatos de imagem (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) em PDF, com opções para redimensionar a imagem e escolher a orientação da página.", + "shortDescription": "Converter imagens em PDF com controle de escala e orientação" + }, "protectPdf": { "description": "Adicione proteção por senha aos seus arquivos PDF com segurança no seu navegador", "shortDescription": "Proteja arquivos PDF com senha com segurança", diff --git a/public/locales/ru/pdf.json b/public/locales/ru/pdf.json index 28d5d04..547cfdb 100644 --- a/public/locales/ru/pdf.json +++ b/public/locales/ru/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "Конвертировать PDF в изображения PNG", "title": "PDF в PNG" }, + "convertToPdf": { + "title": "Изображения в PDF", + "description": "Преобразовать различные форматы изображений (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) в PDF с возможностью масштабирования изображения и выбора ориентации страницы.", + "shortDescription": "Преобразовать изображения в PDF с управлением масштабом и ориентацией" + }, "protectPdf": { "description": "Добавьте надежную защиту паролем для ваших PDF-файлов в браузере.", "shortDescription": "Надежная защита паролем PDF-файлов", diff --git a/public/locales/zh/pdf.json b/public/locales/zh/pdf.json index 1e3d400..76c8c98 100644 --- a/public/locales/zh/pdf.json +++ b/public/locales/zh/pdf.json @@ -63,6 +63,11 @@ "shortDescription": "将 PDF 转换为 PNG 图像", "title": "PDF 转 PNG" }, + "convertToPdf": { + "title": "将图像转换为 PDF", + "description": "将各种图像格式(PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW)转换为 PDF,并可调整图像大小和页面方向。", + "shortDescription": "将图像转换为 PDF,可调整大小和方向" + }, "protectPdf": { "description": "在浏览器中安全地为 PDF 文件添加密码保护", "shortDescription": "使用密码安全地保护 PDF 文件", diff --git a/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx b/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx new file mode 100644 index 0000000..107039a --- /dev/null +++ b/src/pages/tools/pdf/convert-to-pdf/convert-to-pdf.service.test.tsx @@ -0,0 +1,38 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import ConvertToPdf from './index'; +import { vi } from 'vitest'; +import '@testing-library/jest-dom'; + +describe('ConvertToPdf', () => { + it('renders with default state values (full, portrait hidden, no scale shown)', () => { + render(); + + expect(screen.getByLabelText(/Full Size \(Same as Image\)/i)).toBeChecked(); + + expect(screen.queryByLabelText(/A4 Page/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/Portrait/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Scale image:/i)).not.toBeInTheDocument(); + }); + + it('switches to A4 page type and shows orientation and scale', () => { + render(); + + const a4Option = screen.getByLabelText(/A4 Page/i); + fireEvent.click(a4Option); + expect(a4Option).toBeChecked(); + + expect(screen.getByLabelText(/Portrait/i)).toBeChecked(); + expect(screen.getByText(/Scale image:\s*100%/i)).toBeInTheDocument(); + }); + + it('updates scale when slider moves (after switching to A4)', () => { + render(); + + fireEvent.click(screen.getByLabelText(/A4 Page/i)); + + const slider = screen.getByRole('slider'); + fireEvent.change(slider, { target: { value: 80 } }); + + expect(screen.getByText(/Scale image:\s*80%/i)).toBeInTheDocument(); + }); +}); diff --git a/src/pages/tools/pdf/convert-to-pdf/index.tsx b/src/pages/tools/pdf/convert-to-pdf/index.tsx new file mode 100644 index 0000000..750e843 --- /dev/null +++ b/src/pages/tools/pdf/convert-to-pdf/index.tsx @@ -0,0 +1,159 @@ +import { + Box, + Slider, + Typography, + RadioGroup, + FormControlLabel, + Radio, + Stack +} from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import ToolImageInput from 'components/input/ToolImageInput'; +import ToolFileResult from 'components/result/ToolFileResult'; +import { ToolComponentProps } from '@tools/defineTool'; +import { FormValues, Orientation, PageType, initialValues } from './types'; +import { buildPdf } from './service'; + +const initialFormValues: FormValues = initialValues; + +export default function ConvertToPdf({ title }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [imageSize, setImageSize] = useState<{ + widthMm: number; + heightMm: number; + widthPx: number; + heightPx: number; + } | null>(null); + + const compute = async (values: FormValues) => { + if (!input) return; + const { pdfFile, imageSize } = await buildPdf({ + file: input, + pageType: values.pageType, + orientation: values.orientation, + scale: values.scale + }); + setResult(pdfFile); + setImageSize(imageSize); + }; + + return ( + + title={title} + input={input} + setInput={setInput} + initialValues={initialFormValues} + compute={compute} + inputComponent={ + + + + } + getGroups={({ values, updateField }) => { + return [ + { + title: '', + component: ( + + + PDF Type + + updateField('pageType', e.target.value as PageType) + } + > + } + label="Full Size (Same as Image)" + /> + } + label="A4 Page" + /> + + + {values.pageType === 'full' && imageSize && ( + + Image size: {imageSize.widthMm.toFixed(1)} ×{' '} + {imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '} + {imageSize.heightPx} px) + + )} + + + {values.pageType === 'a4' && ( + + Orientation + + updateField( + 'orientation', + e.target.value as Orientation + ) + } + > + } + label="Portrait (Vertical)" + /> + } + label="Landscape (Horizontal)" + /> + + + )} + + {values.pageType === 'a4' && ( + + Scale + Scale image: {values.scale}% + updateField('scale', v as number)} + min={10} + max={100} + step={1} + valueLabelDisplay="auto" + /> + + )} + + ) + } + ] as const; + }} + resultComponent={ + + } + /> + ); +} diff --git a/src/pages/tools/pdf/convert-to-pdf/meta.ts b/src/pages/tools/pdf/convert-to-pdf/meta.ts new file mode 100644 index 0000000..0fa3879 --- /dev/null +++ b/src/pages/tools/pdf/convert-to-pdf/meta.ts @@ -0,0 +1,32 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('pdf', { + i18n: { + name: 'pdf:convertToPdf.title', + description: 'pdf:convertToPdf.description', + shortDescription: 'pdf:convertToPdf.shortDescription' + }, + + path: 'convert-to-pdf', + icon: 'ph:file-pdf-thin', + + keywords: [ + 'convert', + 'pdf', + 'image', + 'jpg', + 'jpeg', + 'png', + 'gif', + 'tiff', + 'webp', + 'heic', + 'raw', + 'psd', + 'svg', + 'quality', + 'compression' + ], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/pdf/convert-to-pdf/service.ts b/src/pages/tools/pdf/convert-to-pdf/service.ts new file mode 100644 index 0000000..4a1eb22 --- /dev/null +++ b/src/pages/tools/pdf/convert-to-pdf/service.ts @@ -0,0 +1,80 @@ +import jsPDF from 'jspdf'; +import { Orientation, PageType, ImageSize } from './types'; + +export interface ComputeOptions { + file: File; + pageType: PageType; + orientation: Orientation; + scale: number; // 10..100 (only applied for A4) +} + +export interface ComputeResult { + pdfFile: File; + imageSize: ImageSize; +} + +export async function buildPdf({ + file, + pageType, + orientation, + scale +}: ComputeOptions): Promise { + const img = new Image(); + img.src = URL.createObjectURL(file); + + try { + await img.decode(); + + const pxToMm = (px: number) => px * 0.264583; + const imgWidthMm = pxToMm(img.width); + const imgHeightMm = pxToMm(img.height); + + const pdf = + pageType === 'full' + ? new jsPDF({ + orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait', + unit: 'mm', + format: [imgWidthMm, imgHeightMm] + }) + : new jsPDF({ + orientation, + unit: 'mm', + format: 'a4' + }); + + pdf.setDisplayMode('fullwidth'); + + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + + const widthRatio = pageWidth / img.width; + const heightRatio = pageHeight / img.height; + const fitScale = Math.min(widthRatio, heightRatio); + + const finalWidth = + pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100); + + const finalHeight = + pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100); + + const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2; + const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2; + + pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight); + + const blob = pdf.output('blob'); + const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf'; + + return { + pdfFile: new File([blob], fileName, { type: 'application/pdf' }), + imageSize: { + widthMm: imgWidthMm, + heightMm: imgHeightMm, + widthPx: img.width, + heightPx: img.height + } + }; + } finally { + URL.revokeObjectURL(img.src); + } +} diff --git a/src/pages/tools/pdf/convert-to-pdf/types.ts b/src/pages/tools/pdf/convert-to-pdf/types.ts new file mode 100644 index 0000000..93b9a42 --- /dev/null +++ b/src/pages/tools/pdf/convert-to-pdf/types.ts @@ -0,0 +1,21 @@ +export type Orientation = 'portrait' | 'landscape'; +export type PageType = 'a4' | 'full'; + +export interface ImageSize { + widthMm: number; + heightMm: number; + widthPx: number; + heightPx: number; +} + +export interface FormValues { + pageType: PageType; + orientation: Orientation; + scale: number; +} + +export const initialValues: FormValues = { + pageType: 'full', + orientation: 'portrait', + scale: 100 +}; diff --git a/src/pages/tools/pdf/index.ts b/src/pages/tools/pdf/index.ts index 6c3e03f..32583b9 100644 --- a/src/pages/tools/pdf/index.ts +++ b/src/pages/tools/pdf/index.ts @@ -7,6 +7,7 @@ import { tool as compressPdfTool } from './compress-pdf/meta'; import { tool as protectPdfTool } from './protect-pdf/meta'; import { meta as pdfToEpub } from './pdf-to-epub/meta'; import { tool as pdfEditor } from './editor/meta'; +import { tool as convertToPdf } from './convert-to-pdf/meta'; export const pdfTools: DefinedTool[] = [ pdfEditor, @@ -16,5 +17,6 @@ export const pdfTools: DefinedTool[] = [ protectPdfTool, mergePdf, pdfToEpub, - pdfPdfToPng + pdfPdfToPng, + convertToPdf ];