├── public ├── loading1.gif └── f3e48a98861245.5f4a72a481628.jpg ├── vite.config.js ├── src ├── main.jsx ├── index.css ├── App.css └── App.jsx ├── .gitignore ├── index.html ├── eslint.config.js ├── package.json ├── README.md └── server └── server.js /public/loading1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mehranlip/XImage-to-PDF/HEAD/public/loading1.gif -------------------------------------------------------------------------------- /public/f3e48a98861245.5f4a72a481628.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mehranlip/XImage-to-PDF/HEAD/public/f3e48a98861245.5f4a72a481628.jpg -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:4000', 10 | changeOrigin: true, 11 | }, 12 | }, 13 | }, 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.jsx' 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | import './index.css' 7 | 8 | createRoot(document.getElementById('root')).render( 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.pdf 26 | 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | XImage to PDF 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ximage-to-pdf", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "frontend": "vite", 8 | "backend": "node server/server.js", 9 | "dev": "concurrently \"npm run frontend\" \"npm run backend\"", 10 | "build": "vite build", 11 | "lint": "eslint .", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "bootstrap": "^5.3.3", 16 | "cors": "^2.8.5", 17 | "express": "^4.21.0", 18 | "formidable": "^3.5.1", 19 | "pdf-lib": "^1.17.1", 20 | "pdfkit": "^0.15.0", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "react-dropzone": "^14.2.9", 24 | "react-toastify": "^10.0.5" 25 | }, 26 | "devDependencies": { 27 | "@eslint/js": "^9.11.1", 28 | "@types/react": "^18.3.10", 29 | "@types/react-dom": "^18.3.0", 30 | "@vitejs/plugin-react": "^4.3.2", 31 | "concurrently": "^9.0.1", 32 | "eslint": "^9.11.1", 33 | "eslint-plugin-react": "^7.37.0", 34 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 35 | "eslint-plugin-react-refresh": "^0.4.12", 36 | "globals": "^15.9.0", 37 | "vite": "^5.4.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XImage to PDF 2 | 3 | XImage to PDF is a web application that allows users to upload images (JPG and PNG) and convert them into a single PDF document. The generated PDF can be downloaded and will be automatically deleted after 30 seconds for security purposes. 4 | 5 | ## Features 6 | 7 | - Upload multiple images (JPG and PNG) 8 | - Convert images to PDF 9 | - Download the generated PDF 10 | - Automatic deletion of PDF after 30 seconds 11 | - 12 | 13 | ## Prerequisites 14 | 15 | Before you begin, ensure you have met the following requirements: 16 | 17 | - **Node.js** (v14 or later) 18 | - **npm** (Node Package Manager) 19 | 20 | ## Installation 21 | 22 | Follow these steps to install and set up the project: 23 | 24 | 1. **Clone the repository:** 25 | 26 | ```bash 27 | git clone https://github.com/YourUsername/XImage-to-PDF.git 28 | cd XImage-to-PDF 29 | 2. **install dependency:** 30 | 31 | ```bash 32 | npm install 33 | 3. **run project:** 34 | 35 | ```bash 36 | npm run dev 37 | 38 | - The server will run on http://localhost:4000
39 | - The application will run on http://localhost:5173 40 | 41 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import formidable from 'formidable'; 3 | import { PDFDocument } from 'pdf-lib'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import cors from 'cors'; 7 | import crypto from 'crypto'; 8 | 9 | const app = express(); 10 | const PORT = 4000; 11 | 12 | app.use(cors()); 13 | app.use(express.static('public')); 14 | 15 | const isValidImageFile = (file) => { 16 | const validMimeTypes = ['image/jpeg', 'image/png']; 17 | return validMimeTypes.includes(file.mimetype); 18 | }; 19 | 20 | const pdfDir = path.join(process.cwd(), 'pdfs'); 21 | if (!fs.existsSync(pdfDir)) { 22 | fs.mkdirSync(pdfDir); 23 | } 24 | 25 | app.post('/api/upload', (req, res) => { 26 | console.log('Incoming request to /api/upload'); 27 | 28 | const form = formidable({ multiples: true }); 29 | 30 | form.parse(req, async (err, fields, files) => { 31 | if (err) { 32 | console.error('Error parsing form:', err); 33 | return res.status(500).json({ message: 'Error parsing form' }); 34 | } 35 | 36 | console.log('Uploaded files:', files); 37 | 38 | if (!files.files || files.files.length === 0) { 39 | console.log('No files uploaded.'); 40 | return res.status(400).json({ message: 'No files uploaded' }); 41 | } 42 | 43 | const validFiles = Object.values(files.files).filter(isValidImageFile); 44 | if (validFiles.length === 0) { 45 | return res.status(400).json({ message: 'Only JPG and PNG files are allowed' }); 46 | } 47 | 48 | try { 49 | const pdfDoc = await PDFDocument.create(); 50 | 51 | for (const file of validFiles) { 52 | const imgPath = file.filepath; 53 | const imgBytes = fs.readFileSync(imgPath); 54 | const img = file.mimetype === 'image/jpeg' 55 | ? await pdfDoc.embedJpg(imgBytes) 56 | : await pdfDoc.embedPng(imgBytes); 57 | 58 | const page = pdfDoc.addPage([img.width, img.height]); 59 | 60 | page.drawImage(img, { 61 | x: 0, 62 | y: 0, 63 | width: img.width, 64 | height: img.height, 65 | }); 66 | } 67 | 68 | const uniqueName = crypto.randomBytes(16).toString('hex') + '.pdf'; 69 | const outputPath = path.join(pdfDir, uniqueName); 70 | 71 | const pdfBytes = await pdfDoc.save(); 72 | fs.writeFileSync(outputPath, pdfBytes); 73 | 74 | console.log('PDF created successfully!'); 75 | res.status(200).json({ message: 'PDF created', pdfUrl: `/pdfs/${uniqueName}` }); 76 | 77 | 78 | setTimeout(() => { 79 | fs.unlink(outputPath, (err) => { 80 | if (err) { 81 | console.error('Error deleting PDF:', err); 82 | } else { 83 | console.log(`PDF ${uniqueName} deleted after 30 seconds.`); 84 | } 85 | }); 86 | }, 30000); 87 | 88 | } catch (error) { 89 | console.error('Error creating PDF:', error); 90 | res.status(500).json({ message: 'Error creating PDF' }); 91 | } 92 | }); 93 | }); 94 | 95 | app.listen(PORT, () => { 96 | console.log(`Server is running on http://localhost:${PORT}`); 97 | }); 98 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 7px; 3 | } 4 | 5 | ::-webkit-scrollbar-track { 6 | box-shadow: inset 0 0 5px grey; 7 | border-radius: 10px; 8 | } 9 | 10 | ::-webkit-scrollbar-thumb { 11 | background: rgb(160, 160, 160); 12 | border-radius: 10px; 13 | } 14 | 15 | ::-webkit-scrollbar-thumb:hover { 16 | background: #ffffff; 17 | } 18 | 19 | 20 | body { 21 | margin: 0; 22 | padding: 0; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | height: 100vh; 27 | background: url("../public/f3e48a98861245.5f4a72a481628.jpg") no-repeat fixed; 28 | background-size: cover; 29 | font-family: 'SF Pro Display', sans-serif; 30 | } 31 | 32 | .container { 33 | position: relative; 34 | z-index: 1; 35 | } 36 | 37 | .title-main { 38 | font-size: 4.5rem; 39 | font-weight: bold; 40 | color: rgb(236, 236, 236); 41 | } 42 | 43 | .title-sub { 44 | font-size: 1.5rem; 45 | font-weight: normal; 46 | color: white; 47 | } 48 | 49 | .dropzone { 50 | border: 2px dashed #0070f3; 51 | border-radius: 4px; 52 | padding: 20px; 53 | text-align: center; 54 | cursor: pointer; 55 | margin-bottom: 20px; 56 | transition: background 0.3s; 57 | } 58 | 59 | .dropzone-text { 60 | font-size: 1rem; 61 | color: white; 62 | } 63 | 64 | .title-selected-file { 65 | font-size: 20px; 66 | color: #ffffff !important; 67 | } 68 | 69 | .selected-files { 70 | max-height: 250px; 71 | overflow-y: scroll; 72 | gap: 40px; 73 | padding: 20px; 74 | background: rgba(255, 255, 255, 0.2); 75 | border-radius: 15px; 76 | backdrop-filter: blur(15px); 77 | border: 1px solid rgba(255, 255, 255, 0.3); 78 | box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); 79 | color: white; 80 | } 81 | 82 | .selected-file { 83 | position: relative; 84 | border-radius: 10px; 85 | overflow: hidden; 86 | width: 100px; 87 | transition: transform 0.2s; 88 | margin: 5px; 89 | } 90 | 91 | .selected-file:hover { 92 | transform: scale(1.05); 93 | } 94 | 95 | .selected-file img { 96 | width: 100%; 97 | height: auto; 98 | } 99 | 100 | .remove-btn { 101 | background: none; 102 | position: absolute; 103 | top: -3px; 104 | right: -15px; 105 | border: none; 106 | cursor: pointer; 107 | font-size: 16px; 108 | } 109 | 110 | .btn-gradient { 111 | background: linear-gradient(135deg, #00c6ff, #0072ff); 112 | border: none; 113 | border-radius: 25px; 114 | color: white; 115 | padding: 10px 20px; 116 | font-size: 1rem; 117 | cursor: pointer; 118 | transition: background 0.3s, transform 0.3s, box-shadow 0.3s; 119 | box-shadow: 0 0 10px rgba(0, 198, 255, 0.5), 0 0 20px rgba(0, 114, 255, 0.5); 120 | } 121 | 122 | .btn-gradient:hover { 123 | background: linear-gradient(135deg, #0072ff, #00c6ff); 124 | transform: scale(1.05); 125 | box-shadow: 0 0 20px rgba(0, 198, 255, 1), 0 0 30px rgba(0, 114, 255, 1); 126 | } 127 | 128 | .download-link { 129 | display: inline-block; 130 | padding: 10px 20px; 131 | background: rgba(0, 0, 0, 0.6); 132 | color: #ffffff; 133 | text-decoration: none; 134 | border-radius: 25px; 135 | backdrop-filter: blur(10px); 136 | border: 1px solid rgba(255, 255, 255, 0.3); 137 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); 138 | transition: background 0.3s, transform 0.3s; 139 | } 140 | 141 | .download-link:hover { 142 | background: rgba(0, 0, 0, 0.8); 143 | transform: scale(1.05); 144 | } 145 | 146 | .countdown { 147 | font-family: 'SF Pro Display', sans-serif; 148 | color: white; 149 | } 150 | 151 | .footer { 152 | font-family: 'SF Pro Display', sans-serif; 153 | color: white; 154 | font-size: 1rem; 155 | } 156 | 157 | .footer a { 158 | text-decoration: none; 159 | color: rgb(238, 95, 95); 160 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ToastContainer, toast } from 'react-toastify'; 3 | import { useDropzone } from 'react-dropzone'; 4 | import './App.css'; 5 | 6 | function App() { 7 | const [selectedFiles, setSelectedFiles] = useState([]); 8 | const [loading, setLoading] = useState(false); 9 | const [pdfUrl, setPdfUrl] = useState(''); 10 | const [countdown, setCountdown] = useState(30); 11 | const [showDownloadButton, setShowDownloadButton] = useState(false); 12 | 13 | const onDrop = (acceptedFiles) => { 14 | const validFiles = acceptedFiles.filter(file => 15 | file.type.startsWith('image/') 16 | ); 17 | 18 | if (validFiles.length === 0) { 19 | toast.error("Please upload only image files") 20 | return; 21 | } 22 | 23 | setSelectedFiles((prevFiles) => [...prevFiles, ...validFiles]); 24 | }; 25 | 26 | const handleRemoveFile = (index) => { 27 | setSelectedFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); 28 | }; 29 | 30 | const handleSubmit = async (e) => { 31 | e.preventDefault(); 32 | if (selectedFiles.length === 0) { 33 | toast.error("Please select an image before uploading"); 34 | return; 35 | } 36 | 37 | setLoading(true); 38 | const formData = new FormData(); 39 | selectedFiles.forEach(file => { 40 | formData.append('files', file); 41 | }); 42 | 43 | try { 44 | const res = await fetch('http://localhost:4000/api/upload', { 45 | method: 'POST', 46 | body: formData, 47 | }); 48 | 49 | if (!res.ok) { 50 | const errorText = await res.text(); 51 | console.error('Error response:', errorText); 52 | throw new Error('Error during upload'); 53 | } 54 | 55 | const data = await res.json(); 56 | console.log('PDF created at:', data.pdfUrl); 57 | setPdfUrl(data.pdfUrl); 58 | setSelectedFiles([]); 59 | setShowDownloadButton(true); 60 | startCountdown(); 61 | } catch (err) { 62 | console.error('Request failed:', err); 63 | } finally { 64 | setLoading(false); 65 | } 66 | }; 67 | 68 | const startCountdown = () => { 69 | setCountdown(30); 70 | const intervalId = setInterval(() => { 71 | setCountdown(prev => { 72 | if (prev <= 1) { 73 | clearInterval(intervalId); 74 | setShowDownloadButton(false); 75 | return 0; 76 | } 77 | return prev - 1; 78 | }); 79 | }, 1000); 80 | }; 81 | 82 | const { getRootProps, getInputProps } = useDropzone({ onDrop }); 83 | 84 | return ( 85 |
86 |

XImage to PDF

87 |

Image to PDF Converter

88 |
89 |
90 | 91 |

Drag 'n' drop some files here, or click to select files

92 |
93 | {selectedFiles.length > 0 && ( 94 |
95 |

Selected Files

96 |
97 | {selectedFiles.map((file, index) => ( 98 |
99 | {file.name} 104 | 111 |
112 | ))} 113 |
114 |
115 | )} 116 | 119 |
120 | {loading && Loading...} 121 | {pdfUrl && showDownloadButton && ( 122 |
123 | 124 | 127 | 128 |

This PDF will be deleted in {countdown} seconds.

129 |
130 | )} 131 | 132 | 135 | 136 |
137 | ); 138 | } 139 | 140 | export default App; 141 | --------------------------------------------------------------------------------