mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092f84be23 | ||
|
|
2aa26e496c | ||
|
|
900196879f | ||
|
|
bd985f06f8 | ||
|
|
98467f4809 | ||
|
|
151060204d | ||
|
|
3019195fe5 | ||
|
|
3673b6e434 | ||
|
|
ad915b7527 | ||
|
|
bbafb11246 | ||
|
|
e33e79ef4d | ||
|
|
1e557d5b2a |
@@ -180,7 +180,7 @@ The easiest way to self-host is to download the pre-built distribution file from
|
|||||||
**Navigate to the extracted Folder:**
|
**Navigate to the extracted Folder:**
|
||||||
```bash
|
```bash
|
||||||
# Navigate to the extracted folder
|
# Navigate to the extracted folder
|
||||||
cd dist-1.6.2 # Replace with your version
|
cd dist-1.7.3 # Replace with your version
|
||||||
```
|
```
|
||||||
|
|
||||||
**Serve with Python:**
|
**Serve with Python:**
|
||||||
|
|||||||
+2
-2
@@ -261,7 +261,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-2
@@ -133,7 +133,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -319,7 +319,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-2
@@ -594,7 +594,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -664,8 +664,9 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/main.ts"></script>
|
<script type="module" src="src/js/main.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -34,5 +34,7 @@ http {
|
|||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Resource-Policy "cross-origin" always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "1.6.2",
|
"version": "1.7.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "1.6.2",
|
"version": "1.7.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.1",
|
"version": "1.7.6",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,10 +16,12 @@
|
|||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"build:docker": "vite build",
|
"build:docker": "vite build",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
|
"update-version": "node scripts/update-version.js",
|
||||||
"release": "node scripts/release.js patch",
|
"release": "node scripts/release.js patch",
|
||||||
"release:minor": "node scripts/release.js minor",
|
"release:minor": "node scripts/release.js minor",
|
||||||
"release:major": "node scripts/release.js major",
|
"release:major": "node scripts/release.js major",
|
||||||
"serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000",
|
"serve:simple": "SIMPLE_MODE=true npm run build && npm run preview -- --port 3000",
|
||||||
|
"serve": "npm run build && npm run preview -- --port 3000",
|
||||||
"package": "node scripts/package-dist.js"
|
"package": "node scripts/package-dist.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+2
-2
@@ -244,7 +244,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12372,7 +12372,10 @@ const PDFViewerApplication = {
|
|||||||
let file;
|
let file;
|
||||||
const queryString = document.location.search.substring(1);
|
const queryString = document.location.search.substring(1);
|
||||||
const params = parseQueryString(queryString);
|
const params = parseQueryString(queryString);
|
||||||
file = params.get("file") ?? AppOptions.get("defaultUrl");
|
file = params.get("file");
|
||||||
|
if (!file) {
|
||||||
|
file = "";
|
||||||
|
}
|
||||||
validateFileURL(file);
|
validateFileURL(file);
|
||||||
const fileInput = this._openFileInput = document.createElement("input");
|
const fileInput = this._openFileInput = document.createElement("input");
|
||||||
fileInput.id = "fileInput";
|
fileInput.id = "fileInput";
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
viewer: document.getElementById('viewer'),
|
viewer: document.getElementById('viewer'),
|
||||||
eventBus,
|
eventBus,
|
||||||
linkService,
|
linkService,
|
||||||
annotationEditorMode: 0,
|
annotationEditorMode: 1,
|
||||||
enableScripting: true,
|
enableScripting: true,
|
||||||
renderer: 'canvas'
|
renderer: 'canvas'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1512,7 +1512,7 @@ class BaseExternalServices {
|
|||||||
class BasePreferences {
|
class BasePreferences {
|
||||||
#defaults = Object.freeze({
|
#defaults = Object.freeze({
|
||||||
altTextLearnMoreUrl: "",
|
altTextLearnMoreUrl: "",
|
||||||
annotationEditorMode: 0,
|
annotationEditorMode: 1,
|
||||||
annotationMode: 2,
|
annotationMode: 2,
|
||||||
capCanvasAreaFactor: 200,
|
capCanvasAreaFactor: 200,
|
||||||
commentLearnMoreUrl: "",
|
commentLearnMoreUrl: "",
|
||||||
@@ -16429,7 +16429,10 @@ const PDFViewerApplication = {
|
|||||||
let file;
|
let file;
|
||||||
const queryString = document.location.search.substring(1);
|
const queryString = document.location.search.substring(1);
|
||||||
const params = parseQueryString(queryString);
|
const params = parseQueryString(queryString);
|
||||||
file = params.get("file") ?? AppOptions.get("defaultUrl");
|
file = params.get("file");
|
||||||
|
if (!file) {
|
||||||
|
file = "";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
file = new URL(decodeURIComponent(file)).href;
|
file = new URL(decodeURIComponent(file)).href;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface AddAttachmentsMessage {
|
||||||
|
command: 'add-attachments';
|
||||||
|
pdfBuffer: ArrayBuffer;
|
||||||
|
attachmentBuffers: ArrayBuffer[];
|
||||||
|
attachmentNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddAttachmentsSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
modifiedPDF: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddAttachmentsErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddAttachmentsResponse = AddAttachmentsSuccessResponse | AddAttachmentsErrorResponse;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
function parsePageRange(rangeString, totalPages) {
|
||||||
|
const pages = new Set();
|
||||||
|
const parts = rangeString.split(',').map(s => s.trim());
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.includes('-')) {
|
||||||
|
const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
|
||||||
|
if (isNaN(start) || isNaN(end)) continue;
|
||||||
|
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
|
||||||
|
pages.add(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pageNum = parseInt(part, 10);
|
||||||
|
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
|
||||||
|
pages.add(pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(pages).sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNames, attachmentLevel, pageRange) {
|
||||||
|
try {
|
||||||
|
const uint8Array = new Uint8Array(pdfBuffer);
|
||||||
|
|
||||||
|
let pdf;
|
||||||
|
try {
|
||||||
|
pdf = coherentpdf.fromMemory(uint8Array, '');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error.message || error.toString();
|
||||||
|
|
||||||
|
if (errorMsg.includes('Failed to read PDF') ||
|
||||||
|
errorMsg.includes('Could not read object') ||
|
||||||
|
errorMsg.includes('No /Root entry') ||
|
||||||
|
errorMsg.includes('PDFError')) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: 'The PDF file has structural issues and cannot be processed. The file may be corrupted, incomplete, or created with non-standard tools. Please try:\n\n• Opening and re-saving the PDF in another PDF viewer\n• Using a different PDF file\n• Repairing the PDF with a PDF repair tool'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: `Failed to load PDF: ${errorMsg}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = coherentpdf.pages(pdf);
|
||||||
|
|
||||||
|
let targetPages = [];
|
||||||
|
if (attachmentLevel === 'page') {
|
||||||
|
if (!pageRange) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Page range is required for page-level attachments.'
|
||||||
|
});
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetPages = parsePageRange(pageRange, totalPages);
|
||||||
|
if (targetPages.length === 0) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Invalid page range specified.'
|
||||||
|
});
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < attachmentBuffers.length; i++) {
|
||||||
|
try {
|
||||||
|
const attachmentData = new Uint8Array(attachmentBuffers[i]);
|
||||||
|
const attachmentName = attachmentNames[i];
|
||||||
|
|
||||||
|
if (attachmentLevel === 'document') {
|
||||||
|
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
|
||||||
|
} else {
|
||||||
|
for (const pageNum of targetPages) {
|
||||||
|
coherentpdf.attachFileToPageFromMemory(attachmentData, attachmentName, pdf, pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: `Failed to attach file ${attachmentNames[i]}: ${error.message || error}`
|
||||||
|
});
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the modified PDF
|
||||||
|
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
|
||||||
|
const buffer = modifiedBytes.buffer.slice(
|
||||||
|
modifiedBytes.byteOffset,
|
||||||
|
modifiedBytes.byteOffset + modifiedBytes.byteLength
|
||||||
|
);
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
status: 'success',
|
||||||
|
modifiedPDF: buffer
|
||||||
|
}, [buffer]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred while adding attachments.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
if (e.data.command === 'add-attachments') {
|
||||||
|
addAttachmentsToPDFInWorker(
|
||||||
|
e.data.pdfBuffer,
|
||||||
|
e.data.attachmentBuffers,
|
||||||
|
e.data.attachmentNames,
|
||||||
|
e.data.attachmentLevel || 'document',
|
||||||
|
e.data.pageRange || ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+9
-5
@@ -75,20 +75,24 @@ function main() {
|
|||||||
const newVersion = updateVersion(type);
|
const newVersion = updateVersion(type);
|
||||||
console.log(`📦 Updated version to ${newVersion}`);
|
console.log(`📦 Updated version to ${newVersion}`);
|
||||||
|
|
||||||
// 2. Add and commit changes
|
// 2. Update version in HTML files
|
||||||
execSync('git add package.json', { stdio: 'inherit' });
|
console.log(`📝 Updating version in HTML files...`);
|
||||||
|
execSync('npm run update-version', { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// 3. Add and commit changes
|
||||||
|
execSync('git add package.json *.html src/pages/*.html', { stdio: 'inherit' });
|
||||||
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
||||||
console.log(`💾 Committed version change`);
|
console.log(`💾 Committed version change`);
|
||||||
|
|
||||||
// 3. Create git tag
|
// 4. Create git tag
|
||||||
const tagName = createGitTag(newVersion);
|
const tagName = createGitTag(newVersion);
|
||||||
|
|
||||||
// 4. Build and package the distribution files
|
// 5. Build and package the distribution files
|
||||||
console.log(`📦 Building and packaging distribution files...`);
|
console.log(`📦 Building and packaging distribution files...`);
|
||||||
execSync('npm run package', { stdio: 'inherit' });
|
execSync('npm run package', { stdio: 'inherit' });
|
||||||
console.log(`📦 Distribution files packaged successfully`);
|
console.log(`📦 Distribution files packaged successfully`);
|
||||||
|
|
||||||
// 5. Push everything to main
|
// 6. Push everything to main
|
||||||
console.log(`📤 Pushing to main...`);
|
console.log(`📤 Pushing to main...`);
|
||||||
execSync('git push origin main', { stdio: 'inherit' });
|
execSync('git push origin main', { stdio: 'inherit' });
|
||||||
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to update version numbers in HTML files from package.json
|
||||||
|
* Run this script whenever you need to sync HTML versions with package.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Read version from package.json
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
|
||||||
|
);
|
||||||
|
const version = packageJson.version;
|
||||||
|
|
||||||
|
// HTML files to update
|
||||||
|
const htmlFiles = [
|
||||||
|
'index.html',
|
||||||
|
'about.html',
|
||||||
|
'contact.html',
|
||||||
|
'faq.html',
|
||||||
|
'privacy.html',
|
||||||
|
'terms.html',
|
||||||
|
'src/pages/add-stamps.html',
|
||||||
|
'src/pages/bookmark.html',
|
||||||
|
'src/pages/json-to-pdf.html',
|
||||||
|
'src/pages/pdf-multi-tool.html',
|
||||||
|
'src/pages/pdf-to-json.html',
|
||||||
|
'src/pages/table-of-contents.html',
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`Updating version to ${version} in HTML files...`);
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
htmlFiles.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, '..', file);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.log(`⚠️ Skipping ${file} (not found)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Replace version in <span id="app-version">X.X.X</span>
|
||||||
|
const regex = /(<span id="app-version">)[^<]+(<\/span>)/g;
|
||||||
|
const newContent = content.replace(regex, `$1${version}$2`);
|
||||||
|
|
||||||
|
if (content !== newContent) {
|
||||||
|
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||||
|
console.log(`✓ Updated ${file}`);
|
||||||
|
updatedCount++;
|
||||||
|
} else {
|
||||||
|
console.log(`- ${file} (already up to date)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\nDone! Updated ${updatedCount} file(s) to version ${version}`);
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "**/*",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cross-Origin-Opener-Policy",
|
||||||
|
"value": "same-origin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Cross-Origin-Embedder-Policy",
|
||||||
|
"value": "require-corp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-Frame-Options",
|
||||||
|
"value": "SAMEORIGIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-Content-Type-Options",
|
||||||
|
"value": "nosniff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -176,7 +176,7 @@ export const categories = [
|
|||||||
id: 'image-to-pdf',
|
id: 'image-to-pdf',
|
||||||
name: 'Image to PDF',
|
name: 'Image to PDF',
|
||||||
icon: 'images',
|
icon: 'images',
|
||||||
subtitle: 'Combine various images into one PDF.',
|
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'jpg-to-pdf',
|
id: 'jpg-to-pdf',
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ import {
|
|||||||
} from '../config/pdf-tools.js';
|
} from '../config/pdf-tools.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
|
// Global state for rotation tracking (used by Rotate tool)
|
||||||
|
const rotationState: number[] = [];
|
||||||
|
let imageSortableInstance: Sortable | null = null;
|
||||||
|
const activeImageUrls = new Map<File, string>();
|
||||||
|
|
||||||
|
// Export getter for rotation state (used by ui.ts)
|
||||||
|
export function getRotationState(): readonly number[] {
|
||||||
|
return rotationState;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSinglePdfUpload(toolId, file) {
|
async function handleSinglePdfUpload(toolId, file) {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
try {
|
||||||
@@ -101,6 +111,12 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
if (toolId === 'rotate') {
|
||||||
|
// Initialize rotation state for all pages
|
||||||
|
rotationState.length = 0;
|
||||||
|
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||||
|
rotationState.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
const rotateAllControls = document.getElementById(
|
const rotateAllControls = document.getElementById(
|
||||||
'rotate-all-controls'
|
'rotate-all-controls'
|
||||||
);
|
);
|
||||||
@@ -113,11 +129,15 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (direction) => {
|
const rotateAll = (direction) => {
|
||||||
|
// Update rotation state for ALL pages (including unrendered ones)
|
||||||
|
for (let i = 0; i < rotationState.length; i++) {
|
||||||
|
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update DOM for currently rendered pages
|
||||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
const currentRotation = parseInt(
|
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
|
||||||
(item as HTMLElement).dataset.rotation || '0'
|
const newRotation = rotationState[pageIndex];
|
||||||
);
|
|
||||||
const newRotation = (currentRotation + direction * 90 + 360) % 360;
|
|
||||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
@@ -468,6 +488,8 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
toolId === 'alternate-merge' ||
|
toolId === 'alternate-merge' ||
|
||||||
toolId === 'reverse-pages'
|
toolId === 'reverse-pages'
|
||||||
) {
|
) {
|
||||||
|
showLoader('Loading PDF documents...');
|
||||||
|
|
||||||
const pdfFilesUnloaded: File[] = [];
|
const pdfFilesUnloaded: File[] = [];
|
||||||
|
|
||||||
state.files.forEach((file) => {
|
state.files.forEach((file) => {
|
||||||
@@ -502,6 +524,7 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
|
|
||||||
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
|
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
|
||||||
|
|
||||||
|
hideLoader(); // Hide loader before showing alert
|
||||||
showAlert('Protected PDFs', errorMessage);
|
showAlert('Protected PDFs', errorMessage);
|
||||||
|
|
||||||
switchView('grid');
|
switchView('grid');
|
||||||
@@ -526,9 +549,27 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
toolLogic['alternate-merge'].setup();
|
toolLogic['alternate-merge'].setup();
|
||||||
} else if (toolId === 'image-to-pdf') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
imageList.textContent = '';
|
|
||||||
|
const renderedFiles = new Set(
|
||||||
|
Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName)
|
||||||
|
);
|
||||||
|
|
||||||
state.files.forEach((file) => {
|
state.files.forEach((file) => {
|
||||||
const url = URL.createObjectURL(file);
|
if (!file) {
|
||||||
|
console.error('Invalid file encountered in state.files');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderedFiles.has(file.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = activeImageUrls.get(file);
|
||||||
|
if (!url) {
|
||||||
|
url = URL.createObjectURL(file);
|
||||||
|
activeImageUrls.set(file, url);
|
||||||
|
}
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'relative group cursor-move';
|
li.className = 'relative group cursor-move';
|
||||||
li.dataset.fileName = file.name;
|
li.dataset.fileName = file.name;
|
||||||
@@ -550,10 +591,28 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
imageList.appendChild(li);
|
imageList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
Sortable.create(imageList);
|
const syncStateWithDOM = () => {
|
||||||
// Show image-to-pdf options and wire up slider text
|
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName);
|
||||||
|
state.files.sort((a, b) => {
|
||||||
|
const aIndex = domOrder.indexOf(a.name);
|
||||||
|
const bIndex = domOrder.indexOf(b.name);
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!imageSortableInstance) {
|
||||||
|
imageSortableInstance = Sortable.create(imageList, {
|
||||||
|
animation: 150,
|
||||||
|
onEnd: () => {
|
||||||
|
syncStateWithDOM();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
syncStateWithDOM();
|
||||||
|
|
||||||
const opts = document.getElementById('image-to-pdf-options');
|
const opts = document.getElementById('image-to-pdf-options');
|
||||||
if (opts) {
|
if (opts && opts.classList.contains('hidden')) {
|
||||||
opts.classList.remove('hidden');
|
opts.classList.remove('hidden');
|
||||||
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
|
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
|
||||||
const value = document.getElementById('image-pdf-quality-value');
|
const value = document.getElementById('image-pdf-quality-value');
|
||||||
@@ -617,6 +676,18 @@ export function setupFileInputHandler(toolId) {
|
|||||||
const processFiles = async (newFiles) => {
|
const processFiles = async (newFiles) => {
|
||||||
if (newFiles.length === 0) return;
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
|
if (toolId === 'image-to-pdf') {
|
||||||
|
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'];
|
||||||
|
const validFiles = newFiles.filter(file => validTypes.includes(file.type));
|
||||||
|
|
||||||
|
if (validFiles.length < newFiles.length) {
|
||||||
|
showAlert('Invalid Files', 'Some files were skipped because they are not supported images.');
|
||||||
|
}
|
||||||
|
|
||||||
|
newFiles = validFiles;
|
||||||
|
if (newFiles.length === 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMultiFileTool || isFirstUpload) {
|
if (!isMultiFileTool || isFirstUpload) {
|
||||||
state.files = newFiles;
|
state.files = newFiles;
|
||||||
} else {
|
} else {
|
||||||
@@ -731,6 +802,9 @@ export function setupFileInputHandler(toolId) {
|
|||||||
const clearBtn = document.getElementById('clear-files-btn');
|
const clearBtn = document.getElementById('clear-files-btn');
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
|
activeImageUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
activeImageUrls.clear();
|
||||||
|
|
||||||
state.files = [];
|
state.files = [];
|
||||||
isFirstUpload = true;
|
isFirstUpload = true;
|
||||||
(fileInput as HTMLInputElement).value = '';
|
(fileInput as HTMLInputElement).value = '';
|
||||||
|
|||||||
+114
-27
@@ -1,10 +1,40 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
|
|
||||||
|
const worker = new Worker('/workers/add-attachments.worker.js');
|
||||||
|
|
||||||
let attachments: File[] = [];
|
let attachments: File[] = [];
|
||||||
|
|
||||||
|
worker.onmessage = (e) => {
|
||||||
|
const data = e.data;
|
||||||
|
|
||||||
|
if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||||
|
`attached-${state.files[0].name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
showAlert('Success', `${attachments.length} file(s) attached successfully.`);
|
||||||
|
clearAttachments();
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||||
|
clearAttachments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
hideLoader();
|
||||||
|
console.error('Worker error:', error);
|
||||||
|
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||||
|
clearAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
export async function addAttachments() {
|
export async function addAttachments() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.files || state.files.length === 0) {
|
||||||
showAlert('Error', 'Main PDF is not loaded.');
|
showAlert('Error', 'Main PDF is not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -13,39 +43,57 @@ export async function addAttachments() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachmentLevel = (
|
||||||
|
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
|
||||||
|
)?.value || 'document';
|
||||||
|
|
||||||
|
let pageRange: string = '';
|
||||||
|
|
||||||
|
if (attachmentLevel === 'page') {
|
||||||
|
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
|
||||||
|
pageRange = pageRangeInput?.value?.trim() || '';
|
||||||
|
|
||||||
|
if (!pageRange) {
|
||||||
|
showAlert('Error', 'Please specify a page range for page-level attachments.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Embedding files into PDF...');
|
showLoader('Embedding files into PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = state.pdfDoc;
|
const pdfFile = state.files[0];
|
||||||
|
const pdfBuffer = (await readFileAsArrayBuffer(pdfFile)) as ArrayBuffer;
|
||||||
|
|
||||||
|
const attachmentBuffers: ArrayBuffer[] = [];
|
||||||
|
const attachmentNames: string[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < attachments.length; i++) {
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
const file = attachments[i];
|
const file = attachments[i];
|
||||||
showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
|
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`);
|
||||||
|
|
||||||
const fileBytes = await readFileAsArrayBuffer(file);
|
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
||||||
|
attachmentBuffers.push(fileBuffer);
|
||||||
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
|
attachmentNames.push(file.name);
|
||||||
mimeType: file.type || 'application/octet-stream',
|
|
||||||
description: `Attached file: ${file.name}`,
|
|
||||||
creationDate: new Date(),
|
|
||||||
modificationDate: new Date(file.lastModified),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
showLoader('Attaching files to PDF...');
|
||||||
downloadFile(
|
|
||||||
new Blob([pdfBytes], { type: 'application/pdf' }),
|
const message = {
|
||||||
`attached-${state.files[0].name}`
|
command: 'add-attachments',
|
||||||
);
|
pdfBuffer: pdfBuffer,
|
||||||
|
attachmentBuffers: attachmentBuffers,
|
||||||
|
attachmentNames: attachmentNames,
|
||||||
|
attachmentLevel: attachmentLevel,
|
||||||
|
pageRange: pageRange
|
||||||
|
};
|
||||||
|
|
||||||
|
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||||
|
worker.postMessage(message, transferables);
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Success',
|
|
||||||
`${attachments.length} file(s) attached successfully.`
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error attaching files:', error);
|
console.error('Error attaching files:', error);
|
||||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
clearAttachments();
|
clearAttachments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +107,8 @@ function clearAttachments() {
|
|||||||
const processBtn = document.getElementById(
|
const processBtn = document.getElementById(
|
||||||
'process-btn'
|
'process-btn'
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||||
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
|
|
||||||
if (fileListDiv) fileListDiv.innerHTML = '';
|
if (fileListDiv) fileListDiv.innerHTML = '';
|
||||||
if (attachmentInput) attachmentInput.value = '';
|
if (attachmentInput) attachmentInput.value = '';
|
||||||
@@ -66,14 +116,20 @@ function clearAttachments() {
|
|||||||
processBtn.disabled = true;
|
processBtn.disabled = true;
|
||||||
processBtn.classList.add('hidden');
|
processBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
if (attachmentLevelOptions) {
|
||||||
|
attachmentLevelOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (pageRangeWrapper) {
|
||||||
|
pageRangeWrapper.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
|
||||||
|
if (documentRadio) {
|
||||||
|
documentRadio.checked = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isSetup = false; // Prevent duplicate setup
|
|
||||||
|
|
||||||
export function setupAddAttachmentsTool() {
|
export function setupAddAttachmentsTool() {
|
||||||
if (isSetup) return; // Already set up
|
|
||||||
isSetup = true;
|
|
||||||
|
|
||||||
const optionsDiv = document.getElementById('attachment-options');
|
const optionsDiv = document.getElementById('attachment-options');
|
||||||
const attachmentInput = document.getElementById(
|
const attachmentInput = document.getElementById(
|
||||||
'attachment-files-input'
|
'attachment-files-input'
|
||||||
@@ -82,14 +138,29 @@ export function setupAddAttachmentsTool() {
|
|||||||
const processBtn = document.getElementById(
|
const processBtn = document.getElementById(
|
||||||
'process-btn'
|
'process-btn'
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||||
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
|
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
||||||
|
|
||||||
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
||||||
console.error('Attachment tool UI elements not found.');
|
console.error('Attachment tool UI elements not found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.files || state.files.length === 0) {
|
||||||
|
console.error('No PDF file loaded for adding attachments.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
optionsDiv.classList.remove('hidden');
|
optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (totalPagesSpan && state.pdfDoc) {
|
||||||
|
totalPagesSpan.textContent = state.pdfDoc.getPageCount().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachmentInput.dataset.listenerAttached) return;
|
||||||
|
attachmentInput.dataset.listenerAttached = 'true';
|
||||||
|
|
||||||
attachmentInput.addEventListener('change', (e) => {
|
attachmentInput.addEventListener('change', (e) => {
|
||||||
const files = (e.target as HTMLInputElement).files;
|
const files = (e.target as HTMLInputElement).files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@@ -114,6 +185,10 @@ export function setupAddAttachmentsTool() {
|
|||||||
fileListDiv.appendChild(div);
|
fileListDiv.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (attachmentLevelOptions) {
|
||||||
|
attachmentLevelOptions.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
processBtn.disabled = false;
|
processBtn.disabled = false;
|
||||||
processBtn.classList.remove('hidden');
|
processBtn.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
@@ -121,5 +196,17 @@ export function setupAddAttachmentsTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
|
||||||
|
attachmentLevelRadios.forEach((radio) => {
|
||||||
|
radio.addEventListener('change', (e) => {
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
if (value === 'page' && pageRangeWrapper) {
|
||||||
|
pageRangeWrapper.classList.remove('hidden');
|
||||||
|
} else if (pageRangeWrapper) {
|
||||||
|
pageRangeWrapper.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
processBtn.onclick = addAttachments;
|
processBtn.onclick = addAttachments;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,23 @@ async function loadPdfInViewer(file: File) {
|
|||||||
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
|
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
|
||||||
currentBlobUrl = URL.createObjectURL(blob)
|
currentBlobUrl = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences')
|
||||||
|
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}
|
||||||
|
delete (existingPrefs as any).annotationEditorMode
|
||||||
|
const newPrefs = {
|
||||||
|
...existingPrefs,
|
||||||
|
enablePermissions: false,
|
||||||
|
}
|
||||||
|
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
iframe.className = 'w-full h-full border-0'
|
iframe.className = 'w-full h-full border-0'
|
||||||
iframe.allowFullscreen = true
|
iframe.allowFullscreen = true
|
||||||
|
|
||||||
iframe.src = `/pdfjs-annotation-viewer/web/viewer.html?file=${encodeURIComponent(currentBlobUrl)}`
|
const viewerUrl = new URL('/pdfjs-annotation-viewer/web/viewer.html', window.location.origin)
|
||||||
|
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}`
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
setupAnnotationViewer(iframe)
|
setupAnnotationViewer(iframe)
|
||||||
@@ -85,10 +97,14 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
|||||||
await app.initializedPromise
|
await app.initializedPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const stampBtn = doc.getElementById('editorStamp') as HTMLButtonElement | null
|
const eventBus = app?.eventBus
|
||||||
if (stampBtn) {
|
if (eventBus && typeof eventBus._on === 'function') {
|
||||||
stampBtn.classList.remove('hidden')
|
eventBus._on('annotationeditoruimanager', () => {
|
||||||
stampBtn.disabled = false
|
try {
|
||||||
|
const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null
|
||||||
|
stampBtn?.click()
|
||||||
|
} catch {}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
|
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
@@ -82,6 +83,9 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Cleanup any previous lazy loading observers
|
||||||
|
cleanupLazyRendering();
|
||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
const pdfData = await state.pdfDoc.save();
|
const pdfData = await state.pdfDoc.save();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
@@ -89,20 +93,13 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
|
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
// Function to create wrapper element for each page
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
const viewport = page.getViewport({ scale: 0.5 });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
|
|
||||||
.promise;
|
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className =
|
wrapper.className =
|
||||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.originalPageIndex = i - 1;
|
wrapper.dataset.originalPageIndex = pageNumber - 1;
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className =
|
imgContainer.className =
|
||||||
@@ -116,7 +113,7 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
const pageNumberSpan = document.createElement('span');
|
const pageNumberSpan = document.createElement('span');
|
||||||
pageNumberSpan.className =
|
pageNumberSpan.className =
|
||||||
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||||
pageNumberSpan.textContent = i.toString();
|
pageNumberSpan.textContent = pageNumber.toString();
|
||||||
|
|
||||||
const controlsDiv = document.createElement('div');
|
const controlsDiv = document.createElement('div');
|
||||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||||
@@ -141,13 +138,38 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
|
|
||||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||||
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
||||||
grid.appendChild(wrapper);
|
|
||||||
attachEventListeners(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
initializePageGridSortable();
|
attachEventListeners(wrapper);
|
||||||
createIcons({ icons });
|
|
||||||
hideLoader();
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Render pages progressively with lazy loading
|
||||||
|
await renderPagesProgressively(
|
||||||
|
pdfjsDoc,
|
||||||
|
grid,
|
||||||
|
createWrapper,
|
||||||
|
{
|
||||||
|
batchSize: 8,
|
||||||
|
useLazyLoading: true,
|
||||||
|
lazyLoadMargin: '400px',
|
||||||
|
onProgress: (current, total) => {
|
||||||
|
showLoader(`Rendering page previews: ${current}/${total}`);
|
||||||
|
},
|
||||||
|
onBatchComplete: () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
initializePageGridSortable();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering thumbnails:', error);
|
||||||
|
showAlert('Error', 'Failed to render page previews');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processAndSave() {
|
export async function processAndSave() {
|
||||||
@@ -156,11 +178,28 @@ export async function processAndSave() {
|
|||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||||
|
|
||||||
const finalIndices = Array.from(finalPageElements).map((el) =>
|
const finalIndices = Array.from(finalPageElements)
|
||||||
parseInt((el as HTMLElement).dataset.originalPageIndex)
|
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10))
|
||||||
);
|
.filter(index => !isNaN(index) && index >= 0);
|
||||||
|
|
||||||
|
console.log('Saving PDF with indices:', finalIndices);
|
||||||
|
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
|
||||||
|
|
||||||
|
if (finalIndices.length === 0) {
|
||||||
|
showAlert('Error', 'No valid pages to save.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
const invalidIndices = finalIndices.filter(i => i >= totalPages);
|
||||||
|
if (invalidIndices.length > 0) {
|
||||||
|
console.error('Found invalid indices:', invalidIndices);
|
||||||
|
showAlert('Error', 'Some pages could not be processed. Please try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
|
|
||||||
@@ -170,8 +209,8 @@ export async function processAndSave() {
|
|||||||
'organized.pdf'
|
'organized.pdf'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Save error:', e);
|
||||||
showAlert('Error', 'Failed to save the new PDF.');
|
showAlert('Error', 'Failed to save the new PDF. Check console for details.');
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { showAlert } from '../ui.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
const worker = new Worker('/workers/extract-attachments.worker.js');
|
const worker = new Worker('/workers/extract-attachments.worker.js');
|
||||||
@@ -24,7 +25,7 @@ export async function extractAttachments() {
|
|||||||
|
|
||||||
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
|
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
|
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
|
||||||
|
|
||||||
showStatus('Reading files (Main Thread)...', 'info');
|
showStatus('Reading files (Main Thread)...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -66,9 +67,33 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
|
|||||||
if (e.data.status === 'success') {
|
if (e.data.status === 'success') {
|
||||||
const attachments = e.data.attachments;
|
const attachments = e.data.attachments;
|
||||||
|
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||||
|
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
|
||||||
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
|
if (fileDisplayArea) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
if (fileControls) {
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
zip.file(attachment.name, new Uint8Array(attachment.data));
|
zip.file(attachment.name, new Uint8Array(attachment.data));
|
||||||
totalSize += attachment.data.byteLength;
|
totalSize += attachment.data.byteLength;
|
||||||
@@ -76,28 +101,59 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
|
|||||||
|
|
||||||
zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
|
zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
|
||||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||||
|
|
||||||
|
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
|
||||||
|
|
||||||
showStatus(
|
showStatus(
|
||||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
|
|
||||||
state.files = [];
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
fileDisplayArea.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
}
|
}
|
||||||
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
|
|
||||||
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
if (fileControls) {
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (e.data.status === 'error') {
|
} else if (e.data.status === 'error') {
|
||||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
console.error('Worker Error:', errorMessage);
|
console.error('Worker Error:', errorMessage);
|
||||||
showStatus(`Error: ${errorMessage}`, 'error');
|
|
||||||
|
if (errorMessage.includes('No attachments were found')) {
|
||||||
|
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||||
|
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
|
||||||
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
|
if (fileDisplayArea) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
if (fileControls) {
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showStatus(`Error: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,15 +167,14 @@ worker.onerror = (error) => {
|
|||||||
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||||
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
||||||
if (!statusMessage) return;
|
if (!statusMessage) return;
|
||||||
|
|
||||||
statusMessage.textContent = message;
|
statusMessage.textContent = message;
|
||||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
||||||
type === 'success'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-green-900 text-green-200'
|
: type === 'error'
|
||||||
: type === 'error'
|
? 'bg-red-900 text-red-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: 'bg-blue-900 text-blue-200'
|
||||||
: 'bg-blue-900 text-blue-200'
|
}`;
|
||||||
}`;
|
|
||||||
statusMessage.classList.remove('hidden');
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+69
-49
@@ -1,7 +1,9 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
|
||||||
import { state } from '../state.ts';
|
import { state } from '../state.ts';
|
||||||
|
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
|
||||||
|
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
@@ -129,15 +131,53 @@ async function renderPageMergeThumbnails() {
|
|||||||
mergeState.isRendering = true;
|
mergeState.isRendering = true;
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
|
|
||||||
let currentPageNumber = 0;
|
// Cleanup any previous lazy loading observers
|
||||||
|
cleanupLazyRendering();
|
||||||
|
|
||||||
let totalPages = state.files.reduce((sum, file) => {
|
let totalPages = state.files.reduce((sum, file) => {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnailsHTML = [];
|
let currentPageNumber = 0;
|
||||||
|
|
||||||
|
// Function to create wrapper element for each page
|
||||||
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||||
|
wrapper.dataset.fileName = fileName || '';
|
||||||
|
wrapper.dataset.pageIndex = (pageNumber - 1).toString();
|
||||||
|
|
||||||
|
const imgContainer = document.createElement('div');
|
||||||
|
imgContainer.className = 'relative';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||||
|
|
||||||
|
const pageNumDiv = document.createElement('div');
|
||||||
|
pageNumDiv.className =
|
||||||
|
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
||||||
|
pageNumDiv.textContent = pageNumber.toString();
|
||||||
|
|
||||||
|
imgContainer.append(img, pageNumDiv);
|
||||||
|
|
||||||
|
const fileNamePara = document.createElement('p');
|
||||||
|
fileNamePara.className =
|
||||||
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
|
const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`;
|
||||||
|
fileNamePara.title = fullTitle;
|
||||||
|
fileNamePara.textContent = fileName
|
||||||
|
? `${fileName.substring(0, 10)}... (p${pageNumber})`
|
||||||
|
: `Page ${pageNumber}`;
|
||||||
|
|
||||||
|
wrapper.append(imgContainer, fileNamePara);
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render pages from all files progressively
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
if (!pdfDoc) continue;
|
if (!pdfDoc) continue;
|
||||||
@@ -145,55 +185,35 @@ async function renderPageMergeThumbnails() {
|
|||||||
const pdfData = await pdfDoc.save();
|
const pdfData = await pdfDoc.save();
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
// Create a wrapper function that includes the file name
|
||||||
currentPageNumber++;
|
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
showLoader(
|
return createWrapper(canvas, pageNumber, file.name);
|
||||||
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
};
|
||||||
);
|
|
||||||
const page = await pdfjsDoc.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale: 0.3 });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
const context = canvas.getContext('2d')!;
|
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
canvas: canvas,
|
|
||||||
viewport,
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
// Render pages progressively with lazy loading
|
||||||
wrapper.className =
|
await renderPagesProgressively(
|
||||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
pdfjsDoc,
|
||||||
wrapper.dataset.fileName = file.name;
|
container,
|
||||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
createWrapperWithFileName,
|
||||||
|
{
|
||||||
|
batchSize: 6,
|
||||||
|
useLazyLoading: true,
|
||||||
|
lazyLoadMargin: '300px',
|
||||||
|
onProgress: (current, total) => {
|
||||||
|
currentPageNumber++;
|
||||||
|
showLoader(
|
||||||
|
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBatchComplete: () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
// TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it!
|
||||||
imgContainer.className = 'relative';
|
// It will be garbage collected automatically when no longer referenced
|
||||||
|
// pdfjsDoc.destroy(); // REMOVED
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = canvas.toDataURL();
|
|
||||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
|
||||||
|
|
||||||
const pageNumDiv = document.createElement('div');
|
|
||||||
pageNumDiv.className =
|
|
||||||
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
|
||||||
pageNumDiv.textContent = i.toString();
|
|
||||||
|
|
||||||
imgContainer.append(img, pageNumDiv);
|
|
||||||
|
|
||||||
const fileNamePara = document.createElement('p');
|
|
||||||
fileNamePara.className =
|
|
||||||
'text-xs text-gray-400 truncate w-full text-center';
|
|
||||||
const fullTitle = `${file.name} (page ${i})`;
|
|
||||||
fileNamePara.title = fullTitle;
|
|
||||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
|
||||||
|
|
||||||
wrapper.append(imgContainer, fileNamePara);
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfjsDoc.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeState.cachedThumbnails = true;
|
mergeState.cachedThumbnails = true;
|
||||||
|
|||||||
+359
-120
@@ -1,12 +1,10 @@
|
|||||||
// @TODO:@ALAM- sometimes I think... and then I forget...
|
|
||||||
//
|
|
||||||
|
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { downloadFile } from '../utils/helpers';
|
import { downloadFile } from '../utils/helpers';
|
||||||
|
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -14,15 +12,20 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
interface PageData {
|
interface PageData {
|
||||||
|
id: string; // Unique ID for DOM reconciliation
|
||||||
pdfIndex: number;
|
pdfIndex: number;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
visualRotation: number;
|
visualRotation: number;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement | null;
|
||||||
pdfDoc: PDFLibDocument;
|
pdfDoc: PDFLibDocument;
|
||||||
originalPageIndex: number;
|
originalPageIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateId(): string {
|
||||||
|
return Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
|
||||||
|
}
|
||||||
|
|
||||||
let allPages: PageData[] = [];
|
let allPages: PageData[] = [];
|
||||||
let selectedPages: Set<number> = new Set();
|
let selectedPages: Set<number> = new Set();
|
||||||
let currentPdfDocs: PDFLibDocument[] = [];
|
let currentPdfDocs: PDFLibDocument[] = [];
|
||||||
@@ -90,6 +93,8 @@ function hideModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showLoading(current: number, total: number) {
|
function showLoading(current: number, total: number) {
|
||||||
|
// renderPagesProgressively handles loading UI via onProgress callback
|
||||||
|
// but we can keep this for compatibility if needed
|
||||||
const loader = document.getElementById('loading-overlay');
|
const loader = document.getElementById('loading-overlay');
|
||||||
const progress = document.getElementById('loading-progress');
|
const progress = document.getElementById('loading-progress');
|
||||||
const text = document.getElementById('loading-text');
|
const text = document.getElementById('loading-text');
|
||||||
@@ -102,16 +107,43 @@ function showLoading(current: number, total: number) {
|
|||||||
text.textContent = `Rendering pages... ${current} of ${total}`;
|
text.textContent = `Rendering pages... ${current} of ${total}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function withButtonLoading(buttonId: string, action: () => Promise<void>) {
|
||||||
|
const button = document.getElementById(buttonId) as HTMLButtonElement;
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const originalContent = button.innerHTML;
|
||||||
|
const originalPointerEvents = button.style.pointerEvents;
|
||||||
|
|
||||||
|
try {
|
||||||
|
button.disabled = true;
|
||||||
|
button.style.pointerEvents = 'none';
|
||||||
|
button.innerHTML = '<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
||||||
|
|
||||||
|
await action();
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
button.style.pointerEvents = originalPointerEvents;
|
||||||
|
button.innerHTML = originalContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hideLoading() {
|
function hideLoading() {
|
||||||
const loader = document.getElementById('loading-overlay');
|
const loader = document.getElementById('loading-overlay');
|
||||||
if (loader) loader.classList.add('hidden');
|
if (loader) loader.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('PDF Multi Tool: DOMContentLoaded');
|
||||||
|
initializeTool();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately');
|
||||||
initializeTool();
|
initializeTool();
|
||||||
});
|
}
|
||||||
|
|
||||||
function initializeTool() {
|
function initializeTool() {
|
||||||
|
console.log('PDF Multi Tool: Initializing...');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('close-tool-btn')?.addEventListener('click', () => {
|
document.getElementById('close-tool-btn')?.addEventListener('click', () => {
|
||||||
@@ -119,6 +151,7 @@ function initializeTool() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
|
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
|
||||||
|
console.log('Upload button clicked, isRendering:', isRendering);
|
||||||
if (isRendering) {
|
if (isRendering) {
|
||||||
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
|
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
|
||||||
return;
|
return;
|
||||||
@@ -156,19 +189,24 @@ function initializeTool() {
|
|||||||
});
|
});
|
||||||
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
bulkDownload();
|
if (selectedPages.size === 0) {
|
||||||
});
|
showModal('No Pages Selected', 'Please select at least one page to download.', 'info');
|
||||||
document.getElementById('select-all-btn')?.addEventListener('click', () => {
|
return;
|
||||||
if (isRendering) return;
|
}
|
||||||
selectAll();
|
withButtonLoading('bulk-download-btn', async () => {
|
||||||
});
|
await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf');
|
||||||
document.getElementById('deselect-all-btn')?.addEventListener('click', () => {
|
});
|
||||||
if (isRendering) return;
|
|
||||||
deselectAll();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
|
document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
downloadAll();
|
if (allPages.length === 0) {
|
||||||
|
showModal('No Pages', 'There are no pages to export.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
withButtonLoading('export-pdf-btn', async () => {
|
||||||
|
await downloadAll();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
|
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
@@ -240,21 +278,25 @@ function initializeTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetAll() {
|
function resetAll() {
|
||||||
|
renderCancelled = true;
|
||||||
|
isRendering = false;
|
||||||
snapshot();
|
snapshot();
|
||||||
allPages = [];
|
allPages = [];
|
||||||
selectedPages.clear();
|
selectedPages.clear();
|
||||||
splitMarkers.clear();
|
splitMarkers.clear();
|
||||||
currentPdfDocs = [];
|
currentPdfDocs = [];
|
||||||
pageCanvasCache.clear();
|
pageCanvasCache.clear();
|
||||||
renderCancelled = false;
|
cleanupLazyRendering();
|
||||||
isRendering = false;
|
|
||||||
|
|
||||||
// Destroy sortable instance
|
|
||||||
if (sortableInstance) {
|
if (sortableInstance) {
|
||||||
sortableInstance.destroy();
|
sortableInstance.destroy();
|
||||||
sortableInstance = null;
|
sortableInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force clear DOM to prevent ghost pages
|
||||||
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
if (pagesContainer) pagesContainer.innerHTML = '';
|
||||||
|
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
document.getElementById('upload-area')?.classList.remove('hidden');
|
document.getElementById('upload-area')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
@@ -276,43 +318,79 @@ async function loadPdfs(files: File[]) {
|
|||||||
|
|
||||||
const uploadArea = document.getElementById('upload-area');
|
const uploadArea = document.getElementById('upload-area');
|
||||||
if (uploadArea) uploadArea.classList.add('hidden');
|
if (uploadArea) uploadArea.classList.add('hidden');
|
||||||
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
isRendering = true;
|
isRendering = true;
|
||||||
|
console.log('PDF Multi Tool: Starting render, isRendering set to true');
|
||||||
renderCancelled = false;
|
renderCancelled = false;
|
||||||
let totalPages = 0;
|
|
||||||
let currentPage = 0;
|
// Cleanup previous observers
|
||||||
|
cleanupLazyRendering();
|
||||||
|
|
||||||
|
showLoading(0, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First pass: count total pages
|
|
||||||
const pdfDocs: PDFLibDocument[] = [];
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
if (renderCancelled) break;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
pdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
totalPages += pdfDoc.getPageCount();
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
||||||
|
const numPages = pdfjsDoc.numPages;
|
||||||
|
|
||||||
|
// Pre-fill allPages with placeholders to maintain order/state
|
||||||
|
const startIndex = allPages.length;
|
||||||
|
for (let i = 0; i < numPages; i++) {
|
||||||
|
allPages.push({
|
||||||
|
id: generateId(),
|
||||||
|
pdfIndex,
|
||||||
|
pageIndex: i,
|
||||||
|
rotation: 0,
|
||||||
|
visualRotation: 0,
|
||||||
|
canvas: null, // Will be filled when rendered
|
||||||
|
pdfDoc,
|
||||||
|
originalPageIndex: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderPagesProgressively(
|
||||||
|
pdfjsDoc,
|
||||||
|
pagesContainer,
|
||||||
|
(canvas, pageNumber) => {
|
||||||
|
const globalIndex = startIndex + pageNumber - 1;
|
||||||
|
|
||||||
|
if (allPages[globalIndex]) {
|
||||||
|
allPages[globalIndex].canvas = canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPageElement(canvas, globalIndex);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchSize: 8,
|
||||||
|
useLazyLoading: true,
|
||||||
|
lazyLoadMargin: '400px',
|
||||||
|
onProgress: (current, total) => {
|
||||||
|
showLoading(current, total);
|
||||||
|
},
|
||||||
|
onBatchComplete: () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
},
|
||||||
|
shouldCancel: () => renderCancelled, // Pass cancellation check
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to load PDF ${file.name}:`, e);
|
console.error(`Failed to load PDF ${file.name}:`, e);
|
||||||
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
|
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: render pages
|
|
||||||
for (const pdfDoc of pdfDocs) {
|
|
||||||
if (renderCancelled) break;
|
|
||||||
|
|
||||||
currentPdfDocs.push(pdfDoc);
|
|
||||||
const numPages = pdfDoc.getPageCount();
|
|
||||||
|
|
||||||
for (let i = 0; i < numPages; i++) {
|
|
||||||
if (renderCancelled) break;
|
|
||||||
|
|
||||||
currentPage++;
|
|
||||||
showLoading(currentPage, totalPages);
|
|
||||||
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!renderCancelled) {
|
if (!renderCancelled) {
|
||||||
setupSortable();
|
setupSortable();
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
@@ -320,6 +398,7 @@ async function loadPdfs(files: File[]) {
|
|||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
isRendering = false;
|
isRendering = false;
|
||||||
|
console.log('PDF Multi Tool: Render finished/cancelled, isRendering set to false');
|
||||||
if (renderCancelled) {
|
if (renderCancelled) {
|
||||||
renderCancelled = false;
|
renderCancelled = false;
|
||||||
}
|
}
|
||||||
@@ -330,79 +409,55 @@ function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
|||||||
return `${pdfIndex}-${pageIndex}`;
|
return `${pdfIndex}-${pageIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) {
|
// Wrapper for compatibility with updatePageDisplay
|
||||||
const pagesContainer = document.getElementById('pages-container');
|
|
||||||
if (!pagesContainer) return;
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
const cacheKey = getCacheKey(pdfIndex, pageIndex);
|
|
||||||
let canvas: HTMLCanvasElement;
|
|
||||||
|
|
||||||
if (pageCanvasCache.has(cacheKey)) {
|
|
||||||
canvas = pageCanvasCache.get(cacheKey)!;
|
|
||||||
} else {
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
|
||||||
const page = await pdf.getPage(pageIndex + 1);
|
|
||||||
|
|
||||||
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
|
|
||||||
|
|
||||||
canvas = document.createElement('canvas');
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
if (!context) return;
|
|
||||||
|
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport,
|
|
||||||
background: 'white',
|
|
||||||
canvas
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
// Cache the canvas
|
|
||||||
pageCanvasCache.set(cacheKey, canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageData: PageData = {
|
|
||||||
pdfIndex,
|
|
||||||
pageIndex,
|
|
||||||
rotation: 0, // Actual rotation to apply when saving PDF
|
|
||||||
visualRotation: 0, // Visual rotation for display only
|
|
||||||
canvas,
|
|
||||||
pdfDoc,
|
|
||||||
originalPageIndex: pageIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
allPages.push(pageData);
|
|
||||||
createPageCard(pageData, allPages.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPageCard(pageData: PageData, index: number) {
|
function createPageCard(pageData: PageData, index: number) {
|
||||||
const pagesContainer = document.getElementById('pages-container');
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
if (!pagesContainer) return;
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
|
const card = createPageElement(pageData.canvas, index);
|
||||||
|
pagesContainer.appendChild(card);
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified to return the element instead of appending it
|
||||||
|
function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTMLElement {
|
||||||
|
const pageData = allPages[index];
|
||||||
|
if (!pageData) {
|
||||||
|
console.error(`Page data not found for index ${index}`);
|
||||||
|
return document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move';
|
card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move';
|
||||||
card.dataset.pageIndex = index.toString();
|
card.dataset.pageIndex = index.toString();
|
||||||
|
card.dataset.pageId = pageData.id; // Set ID for reconciliation
|
||||||
if (selectedPages.has(index)) {
|
if (selectedPages.has(index)) {
|
||||||
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
|
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page preview
|
|
||||||
const preview = document.createElement('div');
|
const preview = document.createElement('div');
|
||||||
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
||||||
preview.style.minHeight = '160px';
|
preview.style.minHeight = '160px';
|
||||||
preview.style.height = '250px';
|
preview.style.height = '250px';
|
||||||
|
|
||||||
const previewCanvas = pageData.canvas;
|
if (canvas) {
|
||||||
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
const previewCanvas = canvas;
|
||||||
|
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
||||||
|
|
||||||
// Apply visual rotation using CSS transform
|
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||||
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
previewCanvas.style.transition = 'transform 0.2s ease';
|
||||||
previewCanvas.style.transition = 'transform 0.2s ease';
|
preview.appendChild(previewCanvas);
|
||||||
|
} else {
|
||||||
preview.appendChild(previewCanvas);
|
// Show loading placeholder if canvas is null
|
||||||
|
const loading = document.createElement('div');
|
||||||
|
loading.className = 'flex flex-col items-center justify-center text-gray-400';
|
||||||
|
loading.innerHTML = `
|
||||||
|
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
|
||||||
|
<span class="text-xs">Loading...</span>
|
||||||
|
`;
|
||||||
|
preview.appendChild(loading);
|
||||||
|
preview.classList.add('bg-gray-700'); // Darker background for loading
|
||||||
|
}
|
||||||
|
|
||||||
// Page info
|
// Page info
|
||||||
const info = document.createElement('div');
|
const info = document.createElement('div');
|
||||||
@@ -491,9 +546,16 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
|
|
||||||
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
||||||
card.append(preview, info, actions, selectBtn);
|
card.append(preview, info, actions, selectBtn);
|
||||||
pagesContainer.appendChild(card);
|
|
||||||
|
|
||||||
createIcons({ icons });
|
// Check for split marker
|
||||||
|
if (splitMarkers.has(index)) {
|
||||||
|
const marker = document.createElement('div');
|
||||||
|
marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
|
||||||
|
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
|
||||||
|
card.appendChild(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSortable() {
|
function setupSortable() {
|
||||||
@@ -600,6 +662,7 @@ function duplicatePage(index: number) {
|
|||||||
|
|
||||||
const newPageData: PageData = {
|
const newPageData: PageData = {
|
||||||
...originalPageData,
|
...originalPageData,
|
||||||
|
id: generateId(), // New ID for the duplicate
|
||||||
canvas: newCanvas,
|
canvas: newCanvas,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -643,18 +706,66 @@ async function handleInsertPdf(e: Event) {
|
|||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
currentPdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
|
// Load PDF.js document for rendering
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
||||||
|
const numPages = pdfjsDoc.numPages;
|
||||||
|
|
||||||
const numPages = pdfDoc.getPageCount();
|
|
||||||
const newPages: PageData[] = [];
|
const newPages: PageData[] = [];
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
// Use the existing renderPage function, which adds to allPages
|
newPages.push({
|
||||||
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
|
id: generateId(),
|
||||||
// Move the newly added page data to the temporary array
|
pdfIndex,
|
||||||
newPages.push(allPages.pop()!);
|
pageIndex: i,
|
||||||
|
rotation: 0,
|
||||||
|
visualRotation: 0,
|
||||||
|
canvas: null, // Placeholder
|
||||||
|
pdfDoc,
|
||||||
|
originalPageIndex: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert new pages into allPages
|
||||||
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
|
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
|
||||||
|
|
||||||
|
// Update display to show placeholders immediately
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
|
|
||||||
|
// Render pages progressively
|
||||||
|
for (let i = 0; i < numPages; i++) {
|
||||||
|
const globalIndex = insertAfterIndex + 1 + i;
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
const canvas = await renderPageToCanvas(pdfjsDoc, i + 1);
|
||||||
|
|
||||||
|
// Update data
|
||||||
|
if (allPages[globalIndex]) {
|
||||||
|
allPages[globalIndex].canvas = canvas;
|
||||||
|
|
||||||
|
// Update UI if card exists
|
||||||
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
const card = pagesContainer?.querySelector(`div[data-page-index="${globalIndex}"]`);
|
||||||
|
if (card) {
|
||||||
|
const preview = card.querySelector('.bg-gray-700') || card.querySelector('.bg-white');
|
||||||
|
if (preview) {
|
||||||
|
// Re-create the preview content
|
||||||
|
preview.innerHTML = '';
|
||||||
|
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
||||||
|
(preview as HTMLElement).style.minHeight = '160px';
|
||||||
|
(preview as HTMLElement).style.height = '250px';
|
||||||
|
|
||||||
|
const previewCanvas = canvas;
|
||||||
|
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
||||||
|
previewCanvas.style.transform = `rotate(${allPages[globalIndex].visualRotation}deg)`;
|
||||||
|
previewCanvas.style.transition = 'transform 0.2s ease';
|
||||||
|
preview.appendChild(previewCanvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to insert PDF:', e);
|
console.error('Failed to insert PDF:', e);
|
||||||
showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error');
|
showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error');
|
||||||
@@ -695,6 +806,7 @@ function addBlankPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blankPageData: PageData = {
|
const blankPageData: PageData = {
|
||||||
|
id: generateId(),
|
||||||
pdfIndex: -1,
|
pdfIndex: -1,
|
||||||
pageIndex: -1,
|
pageIndex: -1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -716,11 +828,26 @@ function bulkRotate(delta: number) {
|
|||||||
|
|
||||||
selectedPages.forEach(index => {
|
selectedPages.forEach(index => {
|
||||||
const pageData = allPages[index];
|
const pageData = allPages[index];
|
||||||
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
if (pageData) {
|
||||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
// Update state
|
||||||
|
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
||||||
|
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
||||||
|
|
||||||
|
// Update DOM immediately if it exists
|
||||||
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
const card = pagesContainer?.querySelector(`div[data-page-index="${index}"]`);
|
||||||
|
if (card) {
|
||||||
|
const canvas = card.querySelector('canvas');
|
||||||
|
if (canvas) {
|
||||||
|
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||||
|
}
|
||||||
|
// If no canvas (placeholder), the state update is enough.
|
||||||
|
// When it eventually renders, createPageElement will use the new rotation.
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updatePageDisplay();
|
// TODO@ALAM - Do NOT call updatePageDisplay() as it destroys lazy loading observers
|
||||||
}
|
}
|
||||||
|
|
||||||
function bulkDelete() {
|
function bulkDelete() {
|
||||||
@@ -769,14 +896,6 @@ function bulkSplit() {
|
|||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDownload() {
|
|
||||||
if (selectedPages.size === 0) {
|
|
||||||
showModal('No Selection', 'Please select pages to download.', 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const indices = Array.from(selectedPages);
|
|
||||||
await downloadPagesAsPdf(indices, 'selected-pages.pdf');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadAll() {
|
async function downloadAll() {
|
||||||
if (allPages.length === 0) {
|
if (allPages.length === 0) {
|
||||||
@@ -826,6 +945,10 @@ async function downloadSplitPdfs() {
|
|||||||
|
|
||||||
for (const index of segment) {
|
for (const index of segment) {
|
||||||
const pageData = allPages[index];
|
const pageData = allPages[index];
|
||||||
|
if (!pageData) {
|
||||||
|
console.warn(`Page data missing for index ${index}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
|
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
|
||||||
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
|
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
|
||||||
const page = newPdf.addPage(copiedPage);
|
const page = newPdf.addPage(copiedPage);
|
||||||
@@ -840,7 +963,7 @@ async function downloadSplitPdfs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
zip.file(`document-${segIndex + 1}.pdf`, pdfBytes);
|
zip.file(`document - ${segIndex + 1}.pdf`, pdfBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and download ZIP
|
// Generate and download ZIP
|
||||||
@@ -851,6 +974,8 @@ async function downloadSplitPdfs() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to create split PDFs:', e);
|
console.error('Failed to create split PDFs:', e);
|
||||||
showModal('Error', 'Failed to create split PDFs.', 'error');
|
showModal('Error', 'Failed to create split PDFs.', 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading(); // Ensure loader is hidden if we used it (though showModal replaces it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,6 +985,10 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
|
|||||||
|
|
||||||
for (const index of indices) {
|
for (const index of indices) {
|
||||||
const pageData = allPages[index];
|
const pageData = allPages[index];
|
||||||
|
if (!pageData) {
|
||||||
|
console.warn(`Page data missing for index ${index}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
|
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
|
||||||
// Copy page from original PDF
|
// Copy page from original PDF
|
||||||
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
|
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
|
||||||
@@ -878,6 +1007,7 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
|
|||||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
||||||
|
|
||||||
downloadFile(blob, filename);
|
downloadFile(blob, filename);
|
||||||
|
showModal('Success', 'PDF downloaded successfully.', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to create PDF:', e);
|
console.error('Failed to create PDF:', e);
|
||||||
showModal('Error', 'Failed to create PDF.', 'error');
|
showModal('Error', 'Failed to create PDF.', 'error');
|
||||||
@@ -888,15 +1018,124 @@ function updatePageDisplay() {
|
|||||||
const pagesContainer = document.getElementById('pages-container');
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
if (!pagesContainer) return;
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
pagesContainer.innerHTML = '';
|
// 1. Create a map of existing elements by ID
|
||||||
allPages.forEach((pageData, index) => {
|
const existingElements = new Map<string, HTMLElement>();
|
||||||
createPageCard(pageData, index);
|
Array.from(pagesContainer.children).forEach((child) => {
|
||||||
|
const el = child as HTMLElement;
|
||||||
|
if (el.dataset.pageId) {
|
||||||
|
existingElements.set(el.dataset.pageId, el);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. Iterate through allPages and reconcile DOM
|
||||||
|
allPages.forEach((pageData, index) => {
|
||||||
|
let card = existingElements.get(pageData.id);
|
||||||
|
|
||||||
|
if (card) {
|
||||||
|
// Element exists, update it
|
||||||
|
existingElements.delete(pageData.id); // Remove from map so we know it's still in use
|
||||||
|
|
||||||
|
// Check if position changed
|
||||||
|
if (pagesContainer.children[index] !== card) {
|
||||||
|
if (index < pagesContainer.children.length) {
|
||||||
|
pagesContainer.insertBefore(card, pagesContainer.children[index]);
|
||||||
|
} else {
|
||||||
|
pagesContainer.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update index-dependent attributes
|
||||||
|
card.dataset.pageIndex = index.toString();
|
||||||
|
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
|
||||||
|
if (info) info.textContent = `Page ${index + 1} `;
|
||||||
|
|
||||||
|
// Update selection state
|
||||||
|
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]');
|
||||||
|
if (selectBtn) {
|
||||||
|
if (selectedPages.has(index)) {
|
||||||
|
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
|
||||||
|
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
|
||||||
|
} else {
|
||||||
|
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500');
|
||||||
|
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
|
||||||
|
}
|
||||||
|
// Update click handler to use new index
|
||||||
|
(selectBtn as HTMLElement).onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelectOptimized(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update action buttons
|
||||||
|
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
|
||||||
|
if (actionsInner) {
|
||||||
|
const buttons = actionsInner.querySelectorAll('button');
|
||||||
|
if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
|
||||||
|
if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
|
||||||
|
if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
|
||||||
|
if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
|
||||||
|
if (buttons[4]) (buttons[4] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
|
||||||
|
if (buttons[5]) (buttons[5] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Element doesn't exist, create it
|
||||||
|
card = createPageElement(pageData.canvas, index);
|
||||||
|
card.dataset.pageId = pageData.id; // IMPORTANT: Set the ID
|
||||||
|
|
||||||
|
if (index < pagesContainer.children.length) {
|
||||||
|
pagesContainer.insertBefore(card, pagesContainer.children[index]);
|
||||||
|
} else {
|
||||||
|
pagesContainer.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Remove remaining elements (deleted pages)
|
||||||
|
existingElements.forEach((el) => el.remove());
|
||||||
|
|
||||||
setupSortable();
|
setupSortable();
|
||||||
renderSplitMarkers();
|
renderSplitMarkers();
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePageNumbers() {
|
function updatePageNumbers() {
|
||||||
updatePageDisplay();
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
|
const cards = Array.from(pagesContainer.children) as HTMLElement[];
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
// Update data attribute
|
||||||
|
card.dataset.pageIndex = index.toString();
|
||||||
|
|
||||||
|
// Update visible page number text
|
||||||
|
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
|
||||||
|
if (info) {
|
||||||
|
info.textContent = `Page ${index + 1} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-attach event listeners for buttons
|
||||||
|
// We need to find the buttons and update their onclick handlers
|
||||||
|
// This is necessary because the original handlers captured the old index
|
||||||
|
|
||||||
|
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]') as HTMLButtonElement;
|
||||||
|
if (selectBtn) {
|
||||||
|
selectBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelectOptimized(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
|
||||||
|
if (actionsInner) {
|
||||||
|
const buttons = actionsInner.querySelectorAll('button');
|
||||||
|
// Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete
|
||||||
|
if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
|
||||||
|
if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
|
||||||
|
if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
|
||||||
|
if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
|
||||||
|
if (buttons[4]) buttons[4].onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
|
||||||
|
if (buttons[5]) buttons[5].onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { getRotationState } from '../handlers/fileHandler.js';
|
||||||
|
|
||||||
import { degrees } from 'pdf-lib';
|
import { degrees } from 'pdf-lib';
|
||||||
|
|
||||||
@@ -8,12 +9,11 @@ export async function rotate() {
|
|||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
const rotationStateArray = getRotationState();
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
|
||||||
const pageIndex = parseInt(item.dataset.pageIndex);
|
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
rotationStateArray.forEach((rotation, pageIndex) => {
|
||||||
const rotation = parseInt(item.dataset.rotation || '0');
|
if (rotation !== 0 && pages[pageIndex]) {
|
||||||
if (rotation !== 0) {
|
|
||||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-19
@@ -40,9 +40,21 @@ export async function setupSignTool() {
|
|||||||
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
|
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const viewerBase = '/pdfjs-viewer/viewer.html';
|
try {
|
||||||
|
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||||
|
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||||
|
delete (existingPrefs as any).annotationEditorMode;
|
||||||
|
const newPrefs = {
|
||||||
|
...existingPrefs,
|
||||||
|
enableSignatureEditor: true,
|
||||||
|
enablePermissions: false,
|
||||||
|
};
|
||||||
|
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const viewerUrl = new URL('/pdfjs-viewer/viewer.html', window.location.origin);
|
||||||
const query = new URLSearchParams({ file: blobUrl });
|
const query = new URLSearchParams({ file: blobUrl });
|
||||||
iframe.src = `${viewerBase}?${query.toString()}`;
|
iframe.src = `${viewerUrl.toString()}?${query.toString()}`;
|
||||||
|
|
||||||
iframe.onload = () => {
|
iframe.onload = () => {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -52,23 +64,27 @@ export async function setupSignTool() {
|
|||||||
if (viewerWindow && viewerWindow.PDFViewerApplication) {
|
if (viewerWindow && viewerWindow.PDFViewerApplication) {
|
||||||
const app = viewerWindow.PDFViewerApplication;
|
const app = viewerWindow.PDFViewerApplication;
|
||||||
const doc = viewerWindow.document;
|
const doc = viewerWindow.document;
|
||||||
|
const eventBus = app.eventBus;
|
||||||
const editorModeButtons = doc.getElementById('editorModeButtons');
|
eventBus?._on('annotationeditoruimanager', () => {
|
||||||
editorModeButtons?.classList.remove('hidden');
|
const editorModeButtons = doc.getElementById('editorModeButtons');
|
||||||
|
editorModeButtons?.classList.remove('hidden');
|
||||||
const editorSignature = doc.getElementById('editorSignature');
|
const editorSignature = doc.getElementById('editorSignature');
|
||||||
editorSignature?.removeAttribute('hidden');
|
editorSignature?.removeAttribute('hidden');
|
||||||
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
|
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
|
||||||
if (editorSignatureButton) {
|
if (editorSignatureButton) {
|
||||||
editorSignatureButton.disabled = false;
|
editorSignatureButton.disabled = false;
|
||||||
}
|
}
|
||||||
|
const editorStamp = doc.getElementById('editorStamp');
|
||||||
const editorStamp = doc.getElementById('editorStamp');
|
editorStamp?.removeAttribute('hidden');
|
||||||
editorStamp?.removeAttribute('hidden');
|
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
|
||||||
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
|
if (editorStampButton) {
|
||||||
if (editorStampButton) {
|
editorStampButton.disabled = false;
|
||||||
editorStampButton.disabled = false;
|
}
|
||||||
}
|
try {
|
||||||
|
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null;
|
||||||
|
highlightBtn?.click();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Could not initialize base PDF.js viewer for signing:', e);
|
console.error('Could not initialize base PDF.js viewer for signing:', e);
|
||||||
|
|||||||
+33
-17
@@ -1,6 +1,9 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
@@ -17,35 +20,29 @@ async function renderVisualSelector() {
|
|||||||
|
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
|
|
||||||
|
// Cleanup any previous lazy loading observers
|
||||||
|
cleanupLazyRendering();
|
||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pdfData = await state.pdfDoc.save();
|
const pdfData = await state.pdfDoc.save();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
// Function to create wrapper element for each page
|
||||||
const page = await pdf.getPage(i);
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
const viewport = page.getViewport({ scale: 0.4 });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({
|
|
||||||
canvasContext: canvas.getContext('2d'),
|
|
||||||
viewport: viewport,
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className =
|
wrapper.className =
|
||||||
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.pageIndex = i - 1;
|
wrapper.dataset.pageIndex = pageNumber - 1;
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = canvas.toDataURL();
|
img.src = canvas.toDataURL();
|
||||||
img.className = 'rounded-md w-full h-auto';
|
img.className = 'rounded-md w-full h-auto';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'text-center text-xs mt-1 text-gray-300';
|
p.className = 'text-center text-xs mt-1 text-gray-300';
|
||||||
p.textContent = `Page ${i}`;
|
p.textContent = `Page ${pageNumber}`;
|
||||||
wrapper.append(img, p);
|
wrapper.append(img, p);
|
||||||
|
|
||||||
const handleSelection = (e: any) => {
|
const handleSelection = (e: any) => {
|
||||||
@@ -69,12 +66,31 @@ async function renderVisualSelector() {
|
|||||||
wrapper.addEventListener('touchstart', (e) => {
|
wrapper.addEventListener('touchstart', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render pages progressively with lazy loading
|
||||||
|
await renderPagesProgressively(
|
||||||
|
pdf,
|
||||||
|
container,
|
||||||
|
createWrapper,
|
||||||
|
{
|
||||||
|
batchSize: 8,
|
||||||
|
useLazyLoading: true,
|
||||||
|
lazyLoadMargin: '400px',
|
||||||
|
onProgress: (current, total) => {
|
||||||
|
showLoader(`Rendering page previews: ${current}/${total}`);
|
||||||
|
},
|
||||||
|
onBatchComplete: () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering visual selector:', error);
|
console.error('Error rendering visual selector:', error);
|
||||||
showAlert('Error', 'Failed to render page previews.');
|
showAlert('Error', 'Failed to render page previews.');
|
||||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
// Reset the flag on error so the user can try again.
|
||||||
visualSelectorRendered = false;
|
visualSelectorRendered = false;
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|||||||
+6
-3
@@ -5,6 +5,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import '../css/styles.css';
|
import '../css/styles.css';
|
||||||
import { formatStars } from './utils/helpers.js';
|
import { formatStars } from './utils/helpers.js';
|
||||||
|
import { APP_VERSION, injectVersion } from '../version.js';
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -28,9 +29,11 @@ const init = () => {
|
|||||||
simpleNav.innerHTML = `
|
simpleNav.innerHTML = `
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-start items-center h-16">
|
<div class="flex justify-start items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center">
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||||
<span class="text-white font-bold text-xl ml-2">BentoPDF</span>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +97,7 @@ const init = () => {
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version-simple">1.6.2</span>
|
Version <span id="app-version-simple">${APP_VERSION}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
+325
-247
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
|||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for progressive rendering
|
||||||
|
*/
|
||||||
|
export interface RenderConfig {
|
||||||
|
batchSize?: number;
|
||||||
|
useLazyLoading?: boolean;
|
||||||
|
lazyLoadMargin?: string;
|
||||||
|
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1)
|
||||||
|
onProgress?: (current: number, total: number) => void;
|
||||||
|
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
|
||||||
|
onBatchComplete?: () => void;
|
||||||
|
shouldCancel?: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page rendering task
|
||||||
|
*/
|
||||||
|
interface PageTask {
|
||||||
|
pageNumber: number;
|
||||||
|
pdfjsDoc: any;
|
||||||
|
fileName?: string;
|
||||||
|
container: HTMLElement;
|
||||||
|
scale?: number;
|
||||||
|
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy loading state
|
||||||
|
*/
|
||||||
|
interface LazyLoadState {
|
||||||
|
observer: IntersectionObserver | null;
|
||||||
|
pendingTasks: Map<HTMLElement, PageTask>;
|
||||||
|
isRendering: boolean;
|
||||||
|
eagerLoadQueue: PageTask[];
|
||||||
|
nextEagerIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lazyLoadState: LazyLoadState = {
|
||||||
|
observer: null,
|
||||||
|
pendingTasks: new Map(),
|
||||||
|
isRendering: false,
|
||||||
|
eagerLoadQueue: [],
|
||||||
|
nextEagerIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a placeholder element for a page that will be lazy-loaded
|
||||||
|
*/
|
||||||
|
export function createPlaceholder(pageNumber: number, fileName?: string): HTMLElement {
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors';
|
||||||
|
placeholder.dataset.pageNumber = pageNumber.toString();
|
||||||
|
if (fileName) {
|
||||||
|
placeholder.dataset.fileName = fileName;
|
||||||
|
}
|
||||||
|
placeholder.dataset.lazyLoad = 'true';
|
||||||
|
|
||||||
|
// Create skeleton loader
|
||||||
|
const skeletonContainer = document.createElement('div');
|
||||||
|
skeletonContainer.className = 'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
|
||||||
|
|
||||||
|
const loadingText = document.createElement('span');
|
||||||
|
loadingText.className = 'text-gray-500 text-xs';
|
||||||
|
loadingText.textContent = 'Loading...';
|
||||||
|
|
||||||
|
skeletonContainer.appendChild(loadingText);
|
||||||
|
placeholder.appendChild(skeletonContainer);
|
||||||
|
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single page to canvas
|
||||||
|
*/
|
||||||
|
export async function renderPageToCanvas(
|
||||||
|
pdfjsDoc: any,
|
||||||
|
pageNumber: number,
|
||||||
|
scale: number = 0.5
|
||||||
|
): Promise<HTMLCanvasElement> {
|
||||||
|
const page = await pdfjsDoc.getPage(pageNumber);
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
canvas: canvas,
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a batch of pages in parallel
|
||||||
|
*/
|
||||||
|
async function renderPageBatch(
|
||||||
|
tasks: PageTask[],
|
||||||
|
onProgress?: (current: number, total: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const renderPromises = tasks.map(async (task) => {
|
||||||
|
try {
|
||||||
|
const canvas = await renderPageToCanvas(
|
||||||
|
task.pdfjsDoc,
|
||||||
|
task.pageNumber,
|
||||||
|
task.scale || 0.5
|
||||||
|
);
|
||||||
|
|
||||||
|
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName);
|
||||||
|
|
||||||
|
// Find and replace the placeholder for this specific page number
|
||||||
|
const placeholder = task.container.querySelector(
|
||||||
|
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
// Replace placeholder with rendered page
|
||||||
|
task.container.replaceChild(wrapper, placeholder);
|
||||||
|
} else {
|
||||||
|
// Fallback: shouldn't happen with new approach, but just in case
|
||||||
|
console.warn(`No placeholder found for page ${task.pageNumber}, appending instead`);
|
||||||
|
task.container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error rendering page ${task.pageNumber}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(renderPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up Intersection Observer for lazy loading
|
||||||
|
*/
|
||||||
|
function setupLazyRendering(
|
||||||
|
container: HTMLElement,
|
||||||
|
config: RenderConfig
|
||||||
|
): IntersectionObserver {
|
||||||
|
const options = {
|
||||||
|
root: container.closest('.overflow-auto') || null,
|
||||||
|
rootMargin: config.lazyLoadMargin || '200px',
|
||||||
|
threshold: 0.01,
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const placeholder = entry.target as HTMLElement;
|
||||||
|
const task = lazyLoadState.pendingTasks.get(placeholder);
|
||||||
|
|
||||||
|
if (task) {
|
||||||
|
// Immediately unobserve to prevent multiple triggers
|
||||||
|
observer.unobserve(placeholder);
|
||||||
|
lazyLoadState.pendingTasks.delete(placeholder);
|
||||||
|
|
||||||
|
// Render this page immediately (not waiting for isRendering flag)
|
||||||
|
renderPageBatch([task], config.onProgress)
|
||||||
|
.then(() => {
|
||||||
|
// Trigger callback after lazy load batch
|
||||||
|
if (config.onBatchComplete) {
|
||||||
|
config.onBatchComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all pages are rendered
|
||||||
|
if (lazyLoadState.pendingTasks.size === 0 && lazyLoadState.observer) {
|
||||||
|
lazyLoadState.observer.disconnect();
|
||||||
|
lazyLoadState.observer = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`Error lazy loading page ${task.pageNumber}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
lazyLoadState.observer = observer;
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request idle callback with fallback
|
||||||
|
*/
|
||||||
|
function requestIdleCallbackPolyfill(callback: () => void): void {
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(callback);
|
||||||
|
} else {
|
||||||
|
setTimeout(callback, 16); // ~60fps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to render pages progressively with optional lazy loading
|
||||||
|
*/
|
||||||
|
export async function renderPagesProgressively(
|
||||||
|
pdfjsDoc: any,
|
||||||
|
container: HTMLElement,
|
||||||
|
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement,
|
||||||
|
config: RenderConfig = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const {
|
||||||
|
batchSize = 8, // Increased from 5 to 8 for faster initial render
|
||||||
|
useLazyLoading = true,
|
||||||
|
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
|
||||||
|
onProgress,
|
||||||
|
onBatchComplete,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const totalPages = pdfjsDoc.numPages;
|
||||||
|
|
||||||
|
// Render more pages initially to reduce lazy loading issues
|
||||||
|
const initialRenderCount = useLazyLoading
|
||||||
|
? Math.min(20, totalPages) // Increased from 12 to 20 pages
|
||||||
|
: totalPages;
|
||||||
|
|
||||||
|
// CRITICAL FIX: Create placeholders for ALL pages first to maintain order
|
||||||
|
const placeholders: HTMLElement[] = [];
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
const placeholder = createPlaceholder(i);
|
||||||
|
container.appendChild(placeholder);
|
||||||
|
placeholders.push(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks: PageTask[] = [];
|
||||||
|
|
||||||
|
// Create tasks for all pages
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
tasks.push({
|
||||||
|
pageNumber: i,
|
||||||
|
pdfjsDoc,
|
||||||
|
container,
|
||||||
|
scale: config.useLazyLoading ? 0.3 : 0.5,
|
||||||
|
createWrapper,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If lazy loading is enabled, set up observer for pages beyond initial render
|
||||||
|
if (useLazyLoading && totalPages > initialRenderCount) {
|
||||||
|
const observer = setupLazyRendering(container, config);
|
||||||
|
|
||||||
|
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
|
||||||
|
const placeholder = placeholders[i - 1];
|
||||||
|
// Store the task for lazy rendering
|
||||||
|
lazyLoadState.pendingTasks.set(placeholder, tasks[i - 1]);
|
||||||
|
observer.observe(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare eager load queue
|
||||||
|
const eagerStartIndex = initialRenderCount;
|
||||||
|
const eagerEndIndex = Math.min(
|
||||||
|
eagerStartIndex + (eagerLoadBatches * batchSize),
|
||||||
|
totalPages
|
||||||
|
);
|
||||||
|
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
|
||||||
|
lazyLoadState.nextEagerIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render initial pages in batches
|
||||||
|
const initialTasks = tasks.slice(0, initialRenderCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < initialTasks.length; i += batchSize) {
|
||||||
|
if (config.shouldCancel?.()) return;
|
||||||
|
|
||||||
|
const batch = initialTasks.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
requestIdleCallbackPolyfill(async () => {
|
||||||
|
await renderPageBatch(batch, onProgress);
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress(Math.min(i + batchSize, initialRenderCount), totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onBatchComplete) {
|
||||||
|
onBatchComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start eager loading AFTER initial batch is complete
|
||||||
|
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) {
|
||||||
|
renderEagerBatch(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually observe a placeholder element (useful for dynamically created placeholders)
|
||||||
|
*/
|
||||||
|
export function observePlaceholder(
|
||||||
|
placeholder: HTMLElement,
|
||||||
|
task: PageTask
|
||||||
|
): void {
|
||||||
|
if (!lazyLoadState.observer) {
|
||||||
|
console.warn('No active observer to register placeholder');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lazyLoadState.pendingTasks.set(placeholder, task);
|
||||||
|
lazyLoadState.observer.observe(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eagerly renders the next batch in the background
|
||||||
|
*/
|
||||||
|
function renderEagerBatch(config: RenderConfig): void {
|
||||||
|
const { eagerLoadBatches = 1, batchSize = 8 } = config;
|
||||||
|
|
||||||
|
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.shouldCancel?.()) return;
|
||||||
|
|
||||||
|
const { nextEagerIndex, eagerLoadQueue } = lazyLoadState;
|
||||||
|
|
||||||
|
if (nextEagerIndex >= eagerLoadQueue.length) {
|
||||||
|
return; // All eager batches rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchEnd = Math.min(nextEagerIndex + batchSize, eagerLoadQueue.length);
|
||||||
|
const batch = eagerLoadQueue.slice(nextEagerIndex, batchEnd);
|
||||||
|
|
||||||
|
requestIdleCallbackPolyfill(async () => {
|
||||||
|
if (config.shouldCancel?.()) return;
|
||||||
|
|
||||||
|
// Remove these tasks from pending since we're rendering them eagerly
|
||||||
|
batch.forEach(task => {
|
||||||
|
const placeholder = Array.from(lazyLoadState.pendingTasks.entries())
|
||||||
|
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
|
||||||
|
if (placeholder && lazyLoadState.observer) {
|
||||||
|
lazyLoadState.observer.unobserve(placeholder);
|
||||||
|
lazyLoadState.pendingTasks.delete(placeholder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderPageBatch(batch, config.onProgress);
|
||||||
|
|
||||||
|
if (config.onBatchComplete) {
|
||||||
|
config.onBatchComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update next eager index
|
||||||
|
lazyLoadState.nextEagerIndex = batchEnd;
|
||||||
|
|
||||||
|
// Queue next eager batch
|
||||||
|
const remainingBatches = Math.ceil((eagerLoadQueue.length - batchEnd) / batchSize);
|
||||||
|
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
|
||||||
|
// Continue eager loading if we have more batches within the eager threshold
|
||||||
|
renderEagerBatch(config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup function to disconnect observers
|
||||||
|
*/
|
||||||
|
export function cleanupLazyRendering(): void {
|
||||||
|
if (lazyLoadState.observer) {
|
||||||
|
lazyLoadState.observer.disconnect();
|
||||||
|
lazyLoadState.observer = null;
|
||||||
|
}
|
||||||
|
lazyLoadState.pendingTasks.clear();
|
||||||
|
lazyLoadState.isRendering = false;
|
||||||
|
lazyLoadState.eagerLoadQueue = [];
|
||||||
|
lazyLoadState.nextEagerIndex = 0;
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,8 +186,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/logic/add-stamps.ts"></script>
|
<script type="module" src="../js/logic/add-stamps.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -420,7 +420,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -489,8 +489,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/logic/bookmark-pdf.ts"></script>
|
<script type="module" src="../js/logic/bookmark-pdf.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +186,8 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/logic/json-to-pdf.ts"></script>
|
<script type="module" src="../js/logic/json-to-pdf.ts"></script>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,7 +180,8 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/logic/pdf-to-json.ts"></script>
|
<script type="module" src="../js/logic/pdf-to-json.ts"></script>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
export const APP_VERSION = packageJson.version;
|
||||||
|
|
||||||
|
export function injectVersion() {
|
||||||
|
const versionElements = document.querySelectorAll('#app-version, #app-version-simple');
|
||||||
|
versionElements.forEach((element) => {
|
||||||
|
element.textContent = APP_VERSION;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', injectVersion);
|
||||||
|
} else {
|
||||||
|
injectVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -234,7 +234,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Version <span id="app-version">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="src/version.ts"></script>
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
/* Path aliases (optional but helpful) */
|
/* Path aliases (optional but helpful) */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ export default defineConfig(({ mode }) => ({
|
|||||||
exclude: ['coherentpdf'],
|
exclude: ['coherentpdf'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
host: true,
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
headers: {
|
headers: {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
@@ -51,6 +58,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
|
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
|
||||||
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
|
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
|
||||||
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
|
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
|
||||||
|
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user