diff --git a/wasm_dist/css/style.css b/wasm_dist/css/style.css new file mode 100644 index 0000000..6ef810d --- /dev/null +++ b/wasm_dist/css/style.css @@ -0,0 +1,50 @@ +/* style.css */ +button[type="submit"] { + width: 100%; /* Tam genişlik */ + background-color: #4a90e2; /* Modern mavi renk */ + transition: background-color 0.3s ease, transform 0.3s ease; /* Geçiş efektleri */ +} + +button[type="submit"]:hover { + background-color: #357ab8; /* Hover rengi */ + transform: scale(1.02); /* Hover'da büyütme efekti */ +} + +button[type="submit"].processing { + background-color: #000000; /* İşlem sırasında arka plan rengi */ +} + +.progress-bar { + height: 6px; /* Çubuğun yüksekliği */ + background-color: #38a169; /* Modern yeşil renk */ + transition: width 0.3s ease-in-out; /* Geçiş efekti */ +} + +footer { + position: fixed; + left: 0; + bottom: 0; + width: 100%; + background-color: #f3f4f6; + text-align: center; + padding: 10px 0; + z-index: 1000; + border-top: 1px solid #e5e7eb; +} + +/* Animatie */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.processing { + animation: pulse 2s infinite; +} + +@media (max-width: 640px) { /* Mobil uyumlu stil */ + button[type="submit"] { + padding: 12px; /* Mobilde buton içi boşluğu artır */ + } +} \ No newline at end of file diff --git a/wasm_dist/guide.html b/wasm_dist/guide.html new file mode 100644 index 0000000..274d275 --- /dev/null +++ b/wasm_dist/guide.html @@ -0,0 +1,292 @@ + + + + + + + Museum PDF Tool — User Guide + + + + + + + +
+ + +
+

Museum PDF Tool

+

User Guide v2.0

+
+ + + + 100% Offline & Secure — No server processing +
+
+ + +
+

What is Museum PDF Tool?

+

+ Museum PDF Tool is a browser-based application designed for museum staff to process monthly magazine + PDFs. + It can split PDFs into individual articles, remove advertisement + pages, + merge article blocks, extract text (OCR), and generate + thumbnail images — all packaged into a downloadable ZIP file. +

+
+ Key Advantage: All processing happens entirely within your browser. + Your PDF files are never uploaded to any server. The tool works even without an internet + connection after the first visit. +
+
+ + +
+

Getting Started

+ +
+
+ 1 +
+

Open the Tool

+

Open your web browser (Chrome, Edge, Firefox, or Safari) and + navigate to the Museum PDF Tool URL provided by your organization.

+
+
+ +
+ 2 +
+

Select Your PDF

+

Click "Choose File" and select the magazine + PDF you want to process. The tool will immediately display the total number of pages.

+
+
+ +
+ 3 +
+

Fill in Year & Issue Number

+

Enter the publication year (e.g. 2024) and issue + number (e.g. 05). These are used to generate output filenames.

+
+
+ +
+ 4 +
+

Define Page Ranges (Articles)

+

+ In the "Page Ranges to Process" field, specify which pages belong to each + article. Use commas to separate articles and dashes for page ranges. +

+
+ Example: 1-3,4-6,7,8-10
+ → Article 1: pages 1–3 | Article 2: pages 4–6 | Article 3: page 7 | Article 4: pages 8–10 +
+

Leave empty to process the entire PDF as one block.

+
+
+ +
+ 5 +
+

Exclude Pages (Remove Ads)

+

+ In the "Exclude Pages" field, list the page numbers of advertisement or + unwanted pages. These will be permanently removed from the output. +

+
+ Example: 5,10,12 → Pages 5, 10, and 12 will be excluded from all outputs. +
+
+
+ +
+ 6 +
+

Merge Blocks (Optional)

+

+ If two articles should be combined into one, enter their block numbers in "Merge + Blocks". +

+
+ Example: 1,3 → Merges Article Block 1 and Article Block 3 into a single + PDF. +
+
+
+ +
+ 7 +
+

Click "Process PDF"

+

+ Press the blue Process PDF button. A terminal log will appear showing + real-time progress. When complete, a notification sound plays and a download button appears. +

+
+
+ +
+ 8 +
+

Download Your Output

+

Click the Download button. A ZIP file will be + saved to your computer with the following folder structure:

+
+ output202405.zip/
+ ├── pdf/ → Split article PDFs
+ ├── ocr/ → Extracted text (.txt) files
+ ├── small/ → Small thumbnail images (500×700)
+ └── large/ → Large cover images (1024×1280) +
+
+
+
+
+ + +
+

📱 Install as Desktop App (Recommended)

+

+ You can install Museum PDF Tool as a standalone desktop application. This gives you a dedicated app + window (without browser tabs) and guarantees offline access forever. +

+ +
+ After installing, you can use the tool even without any internet connection — just like a native + desktop program. +
+ +

Chrome / Edge:

+
    +
  1. Open the Museum PDF Tool in your browser.
  2. +
  3. Look for the install icon (⊕ or a small monitor icon) in the right side of the + address bar.
  4. +
  5. Click it and select "Install".
  6. +
  7. The app will appear on your desktop and in your Start Menu / Applications folder.
  8. +
  9. Open it anytime — it works without internet!
  10. +
+ +

Firefox / Safari:

+

+ These browsers do not support PWA app installation, but you can still use the tool normally in the + browser tab. It will still work offline after the first visit as long as you don't clear your browser + cache. +

+
+ + +
+

🔌 Offline Usage

+
+

+ After visiting the tool once, all required libraries are cached in your browser. You can then: +

+
    +
  • Disconnect from the internet
  • +
  • Open the tool URL (or the installed desktop app)
  • +
  • Process PDFs of any size — entirely on your own computer
  • +
  • Download results directly to your machine
  • +
+
+
+ ⚠️ Note: If you manually clear all browser data ("Clear cache and cookies"), the + offline files will be removed. Simply revisit the URL once with internet to restore offline capability. +
+
+ + +
+

🌐 Supported Browsers

+
+
✅ Google Chrome +
+
✅ Microsoft Edge +
+
✅ Mozilla Firefox +
+
✅ Safari (macOS/iOS) +
+
✅ Opera / Brave +
+
✅ Samsung Internet +
+
+
+ + +
+

❓ FAQ

+
+
+ Is my PDF data safe? +

Yes. Your files never leave your computer. All processing is + done locally in your browser's memory. No data is sent to any server.

+
+
+ Is there a file size limit? +

There is no server-imposed limit. The only limit is your + computer's available RAM. Most modern computers can handle PDFs up to 500MB+ without issues.

+
+
+ Do I need to install any software? +

No. You only need a modern web browser. No Python, Tesseract, + or any other software is required.

+
+
+ Can I use this on multiple computers? +

Yes. Simply open the URL on each computer once. The tool will + cache itself automatically for offline use on every device.

+
+
+
+ + + + +
+ + + \ No newline at end of file diff --git a/wasm_dist/icons/icon-192.png b/wasm_dist/icons/icon-192.png new file mode 100644 index 0000000..d8789b0 Binary files /dev/null and b/wasm_dist/icons/icon-192.png differ diff --git a/wasm_dist/icons/icon-512.png b/wasm_dist/icons/icon-512.png new file mode 100644 index 0000000..4a03882 Binary files /dev/null and b/wasm_dist/icons/icon-512.png differ diff --git a/wasm_dist/index.html b/wasm_dist/index.html new file mode 100644 index 0000000..90066cc --- /dev/null +++ b/wasm_dist/index.html @@ -0,0 +1,264 @@ + + + + + + + Museum PDF Tool + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

Museum PDF Tool

+

+ + + + + 100% Secure & Offline + + Files never leave your device. Processed locally via WebAssembly. +

+ + 📖 Open User Guide + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/wasm_dist/js/wasm_engine.js b/wasm_dist/js/wasm_engine.js new file mode 100644 index 0000000..db26cf2 --- /dev/null +++ b/wasm_dist/js/wasm_engine.js @@ -0,0 +1,250 @@ +const { PDFDocument } = PDFLib; + +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('uploadForm'); + const pdfPageCount = document.getElementById('pdfPageCount'); + const logArea = document.getElementById('logArea'); + const resultDiv = document.getElementById('result'); + const downloadLinks = document.getElementById('downloadLinks'); + const btnText = document.getElementById('btn-text'); + const progressBar = document.getElementById('progress-bar'); + const submitBtn = form.querySelector('button[type="submit"]'); + + let globalPdfBytes = null; + let totalPdfPages = 0; + + function logProgress(msg) { + logArea.classList.remove('hidden'); + logArea.innerHTML += `
[${new Date().toLocaleTimeString()}] ${msg}
`; + logArea.scrollTop = logArea.scrollHeight; + console.log(msg); + } + + // PDF Load Listener + document.getElementById('pdf_file').addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) { + pdfPageCount.textContent = ""; + return; + } + + pdfPageCount.textContent = "Reading PDF file..."; + const arrayBuffer = await file.arrayBuffer(); + globalPdfBytes = new Uint8Array(arrayBuffer); + + try { + const pdfDoc = await PDFDocument.load(globalPdfBytes); + totalPdfPages = pdfDoc.getPageCount(); + pdfPageCount.textContent = `Total Pages: ${totalPdfPages}`; + logProgress(`PDF loaded. Total ${totalPdfPages} pages.`); + } catch (err) { + pdfPageCount.textContent = "ERROR: Cannot read PDF."; + console.error(err); + } + }); + + // Parsers (Ported from python process_ranges & process_remove_pages) + function parseRanges(rangesStr, maxPages) { + if (!rangesStr) return [[...Array(maxPages).keys()].map(i => i + 1)]; + const ranges = []; + rangesStr.split(',').forEach(part => { + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + if (start > end || start < 1 || end > maxPages) throw new Error(`Invalid page range: ${part}`); + const r = []; + for (let i = start; i <= end; i++) r.push(i); + ranges.push(r); + } else { + const page = Number(part); + if (page < 1 || page > maxPages) throw new Error(`Invalid page range: ${part}`); + ranges.push([page]); + } + }); + return ranges; + } + + function parseRemovePages(removeStr, maxPages) { + if (!removeStr) return []; + const pages = removeStr.split(',').map(s => Number(s.trim())).filter(n => n); + if (pages.some(p => p < 1 || p > maxPages)) throw new Error("Invalid pages to remove"); + return pages; + } + + function mergeArticles(ranges, mergeStr) { + if (!mergeStr) return ranges; + const mergeIndices = mergeStr.split(',').map(s => Number(s.trim())).filter(n => n); + if (mergeIndices.length < 2) return ranges; + + const validIndices = mergeIndices.filter(i => i >= 1 && i <= ranges.length).sort((a, b) => a - b); + if (validIndices.length < 2) return ranges; + + let allPages = []; + const indicesSet = new Set(validIndices.map(i => i - 1)); + validIndices.forEach(i => allPages.push(...ranges[i - 1])); + allPages.sort((a, b) => a - b); + + const result = []; + for (let i = 0; i < ranges.length; i++) { + if (i === validIndices[0] - 1) result.push(allPages); + else if (!indicesSet.has(i)) result.push(ranges[i]); + } + return result; + } + + // Helper Format Name + function generateFilename(year, number, start, end) { + const pYear = String(year); + const pNum = String(number).padStart(2, '0'); + const pStart = String(start).padStart(2, '0'); + const pEnd = String(end).padStart(2, '0'); + return `${pYear}${pNum}${pStart}${pEnd}`; + } + + // Extract Text natively without Tesseract (Matches Python PyPDF2 logic) + async function extractTextFromPdf(pdfBytes) { + const loadingTask = pdfjsLib.getDocument({ data: pdfBytes }); + const pdf = await loadingTask.promise; + let fullText = ""; + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + const pageText = textContent.items.map(item => item.str).join(' '); + fullText += pageText + "\n\n"; + } + return fullText; + } + + // PDF.js Render Page as Image (Matches Python PIL dimensions) + async function renderScaledImages(pdfBytes, pageNumber) { + const loadingTask = pdfjsLib.getDocument({ data: pdfBytes }); + const pdf = await loadingTask.promise; + const page = await pdf.getPage(pageNumber); + + // Render at a high scale initially (scale 3.0) for good base quality + const viewport = page.getViewport({ scale: 3.0 }); + + const sourceCanvas = document.createElement('canvas'); + sourceCanvas.height = viewport.height; + sourceCanvas.width = viewport.width; + + await page.render({ canvasContext: sourceCanvas.getContext('2d'), viewport: viewport }).promise; + + function resizeCanvas(maxW, maxH) { + let ratio = Math.min(maxW / viewport.width, maxH / viewport.height); + let targetW = viewport.width * ratio; + let targetH = viewport.height * ratio; + + const targetCanvas = document.createElement('canvas'); + targetCanvas.width = targetW; + targetCanvas.height = targetH; + const ctx = targetCanvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(sourceCanvas, 0, 0, targetW, targetH); + return targetCanvas.toDataURL('image/jpeg', 0.85).split(',')[1]; + } + + // Python used 500x700 for small, 1024x1280 for large + const smallBase64 = resizeCanvas(500, 700); + const largeBase64 = resizeCanvas(1024, 1280); + + return { small: smallBase64, large: largeBase64 }; + } + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + if (!globalPdfBytes) return alert("Please select a PDF file first"); + + btnText.textContent = "Processing... (Do not close tab)"; + submitBtn.disabled = true; + resultDiv.classList.add('hidden'); + logArea.innerHTML = ''; + progressBar.style.width = '10%'; + + logProgress("System initialized..."); + + try { + const year = document.getElementById('year').value; + const number = document.getElementById('number').value; + const pRangesStr = document.getElementById('article_ranges').value; + const pMergeStr = document.getElementById('merge_ranges').value; + const pRemoveStr = document.getElementById('remove_pages').value; + + // Parsers + let articleRanges = parseRanges(pRangesStr, totalPdfPages); + articleRanges = mergeArticles(articleRanges, pMergeStr); + const removePages = parseRemovePages(pRemoveStr, totalPdfPages); + + logProgress("Inputs parsed. Preparing JSZip compression engine..."); + const zip = new JSZip(); + const originalDoc = await PDFDocument.load(globalPdfBytes); + + progressBar.style.width = '30%'; + + for (let i = 0; i < articleRanges.length; i++) { + const range = articleRanges[i]; + logProgress(`Processing: Article Block ${i + 1}/${articleRanges.length} (Pages: ${range.join(',')})`); + + // PDF Splitting (pdf-lib) + const newPdf = await PDFDocument.create(); + const pagesToCopy = range.filter(p => !removePages.includes(p)).map(p => p - 1); + + if (pagesToCopy.length === 0) continue; + + const copiedPages = await newPdf.copyPages(originalDoc, pagesToCopy); + copiedPages.forEach(p => newPdf.addPage(p)); + const newPdfBytes = await newPdf.save(); + + const startP = Math.min(...range); + const endP = Math.max(...range); + const baseName = generateFilename(year, number, startP, endP); + + // Add PDF + zip.file(`output${year}${number}/pdf/${baseName}.pdf`, newPdfBytes); + + // Generate Images (Canvas pdf.js) + logProgress(` -> Generating thumbnail images (Small & Large)...`); + const { small, large } = await renderScaledImages(newPdfBytes, 1); + zip.file(`output${year}${number}/small/${baseName}.jpg`, small, { base64: true }); + zip.file(`output${year}${number}/large/${baseName}.jpg`, large, { base64: true }); + + // Extract Text (PyPDF2 mapping) + logProgress(` -> Extracting raw text from PDF...`); + // Directly extracting embedded text from newPdfBytes + const extractedText = await extractTextFromPdf(newPdfBytes); + zip.file(`output${year}${number}/ocr/${baseName}.txt`, extractedText); + } + + progressBar.style.width = '80%'; + logProgress("All PDFs processed. Creating ZIP archive..."); + + const content = await zip.generateAsync({ type: "blob" }); + + progressBar.style.width = '100%'; + logProgress("Process completed successfully!"); + + document.getElementById('notificationSound').play(); + + const outputName = `output${year}${number}.zip`; + downloadLinks.innerHTML = ` + + `; + + document.getElementById('downloadBtn').addEventListener('click', () => { + saveAs(content, outputName); + }); + + resultDiv.classList.remove('hidden'); + + } catch (e) { + logProgress("ERROR: " + e.message); + alert("Error during processing: " + e.message); + } finally { + btnText.textContent = "Process PDF"; + submitBtn.disabled = false; + } + }); + +}); diff --git a/wasm_dist/manifest.json b/wasm_dist/manifest.json new file mode 100644 index 0000000..5d17c95 --- /dev/null +++ b/wasm_dist/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Museum PDF Tool", + "short_name": "MuseumPDF", + "description": "Process museum magazine PDFs entirely in your browser — offline and secure.", + "start_url": "./index.html", + "display": "standalone", + "background_color": "#f3f4f6", + "theme_color": "#4f46e5", + "orientation": "portrait-primary", + "icons": [ + { + "src": "icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/wasm_dist/service-worker.js b/wasm_dist/service-worker.js new file mode 100644 index 0000000..b6b6238 --- /dev/null +++ b/wasm_dist/service-worker.js @@ -0,0 +1,73 @@ +const CACHE_NAME = 'museum-pdf-tool-v2'; + +// All assets to cache for offline use +const ASSETS_TO_CACHE = [ + './', + './index.html', + './js/wasm_engine.js', + './guide.html', + './css/style.css', + './sound/notification.mp3', + // CDN Libraries (cached locally after first load) + 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css', + 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', + 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.worker.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js' +]; + +// Install Event: Cache all critical assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + console.log('[ServiceWorker] Caching all app assets for offline use'); + return cache.addAll(ASSETS_TO_CACHE); + }) + ); + self.skipWaiting(); +}); + +// Activate Event: Clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('[ServiceWorker] Removing old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +// Fetch Event: Serve from cache first, fall back to network +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((cachedResponse) => { + if (cachedResponse) { + return cachedResponse; + } + return fetch(event.request).then((networkResponse) => { + // Cache any new font files that Google Fonts loads dynamically + if (event.request.url.includes('fonts.gstatic.com')) { + const responseClone = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseClone); + }); + } + return networkResponse; + }).catch(() => { + // If both cache and network fail, return a basic offline page + if (event.request.mode === 'navigate') { + return caches.match('./index.html'); + } + }); + }) + ); +}); diff --git a/wasm_dist/sound/notification.mp3 b/wasm_dist/sound/notification.mp3 new file mode 100644 index 0000000..ec82ecf Binary files /dev/null and b/wasm_dist/sound/notification.mp3 differ