├── 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 |
120 | {loading &&

}
121 | {pdfUrl && showDownloadButton && (
122 |
130 | )}
131 |
132 |
135 |
136 |
137 | );
138 | }
139 |
140 | export default App;
141 |
--------------------------------------------------------------------------------