Added a print button to the PDF viewer for direct printing. Implemented a scripting manager to handle XFA forms, ensuring proper rendering and interaction. Updated form-filler logic to support XFA detection and improved error handling for unsupported save operations. Refactored file handling to streamline PDF loading and viewer initialization.
343 lines
9.7 KiB
HTML
343 lines
9.7 KiB
HTML
<!DOCTYPE html>
|
||
<html dir="ltr" mozdisallowselectionprint>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||
<meta name="google" content="notranslate">
|
||
<title>PDF Form Filler - Bento PDF</title>
|
||
<link rel="stylesheet" href="pdf_viewer.css">
|
||
<link rel="stylesheet" href="viewer.css">
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: sans-serif;
|
||
background-color: #525252;
|
||
}
|
||
|
||
#viewerContainer {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
overflow: auto;
|
||
background-color: #404040;
|
||
}
|
||
|
||
.toolbar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 48px;
|
||
background-color: #323639;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
z-index: 1000;
|
||
gap: 10px;
|
||
}
|
||
|
||
.toolbar button {
|
||
background-color: rgba(255,255,255,0.1);
|
||
border: none;
|
||
color: #fff;
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.toolbar button:hover {
|
||
background-color: rgba(255,255,255,0.2);
|
||
}
|
||
|
||
.toolbar button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.toolbar .page-info {
|
||
color: #fff;
|
||
font-size: 13px;
|
||
margin: 0 10px;
|
||
}
|
||
|
||
.toolbar input[type="number"] {
|
||
width: 60px;
|
||
padding: 4px 8px;
|
||
background-color: rgba(255,255,255,0.1);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
#viewerContainer {
|
||
top: 48px;
|
||
}
|
||
|
||
#viewer {
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.page {
|
||
margin: 10px auto;
|
||
border: 1px solid #999;
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.toolbar .spacer {
|
||
flex: 1;
|
||
}
|
||
|
||
.toolbar .zoom-controls {
|
||
display: flex;
|
||
gap: 5px;
|
||
align-items: center;
|
||
}
|
||
|
||
.toolbar select {
|
||
background-color: rgba(255,255,255,0.1);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
color: #fff;
|
||
padding: 6px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.toolbar select option {
|
||
background-color: #323639;
|
||
color: #fff;
|
||
}
|
||
|
||
/* Print styles */
|
||
/* @media print {
|
||
.toolbar {
|
||
display: none !important;
|
||
}
|
||
|
||
#viewerContainer {
|
||
position: static !important;
|
||
overflow: visible !important;
|
||
background: white !important;
|
||
}
|
||
|
||
.page {
|
||
page-break-after: always;
|
||
page-break-inside: avoid;
|
||
margin: 0 auto !important;
|
||
}
|
||
} */
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="toolbar">
|
||
<button id="previousPage" title="Previous Page">‹</button>
|
||
<input type="number" id="pageNumber" min="1" value="1">
|
||
<span class="page-info">/ <span id="numPages">--</span></span>
|
||
<button id="nextPage" title="Next Page">›</button>
|
||
|
||
<div class="spacer"></div>
|
||
|
||
<div class="zoom-controls">
|
||
<button id="zoomOut" title="Zoom Out">−</button>
|
||
<select id="scaleSelect">
|
||
<option value="auto">Auto</option>
|
||
<option value="page-fit">Fit Page</option>
|
||
<option value="page-width">Fit Width</option>
|
||
<option value="0.5">50%</option>
|
||
<option value="0.75">75%</option>
|
||
<option value="1" selected>100%</option>
|
||
<option value="1.25">125%</option>
|
||
<option value="1.5">150%</option>
|
||
<option value="2">200%</option>
|
||
</select>
|
||
<button id="zoomIn" title="Zoom In">+</button>
|
||
</div>
|
||
|
||
<button id="print" title="Print to PDF">Print to PDF</button>
|
||
<button id="download" title="Save & Download">Download</button>
|
||
</div>
|
||
|
||
<div id="viewerContainer">
|
||
<div id="viewer" class="pdfViewer"></div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import * as pdfjsLib from './pdf.mjs';
|
||
import { EventBus, PDFViewer, PDFLinkService, PDFScriptingManager } from './pdf_viewer.mjs';
|
||
|
||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.mjs';
|
||
|
||
const eventBus = new EventBus();
|
||
const linkService = new PDFLinkService({ eventBus });
|
||
|
||
// Create scripting manager for XFA forms with JavaScript
|
||
const scriptingManager = new PDFScriptingManager({
|
||
eventBus,
|
||
// Use the packaged sandbox for PDF.js JavaScript (required for XFA)
|
||
sandboxBundleSrc: './pdf.sandbox.mjs',
|
||
docProperties: async (pdfDocument) => {
|
||
// Minimal doc properties are fine; XFA scripts mostly need field objects
|
||
return {
|
||
title: '',
|
||
author: '',
|
||
subject: '',
|
||
keywords: '',
|
||
creator: '',
|
||
producer: '',
|
||
creationDate: null,
|
||
modDate: null
|
||
};
|
||
}
|
||
});
|
||
|
||
const pdfViewer = new PDFViewer({
|
||
container: document.getElementById('viewerContainer'),
|
||
viewer: document.getElementById('viewer'),
|
||
eventBus,
|
||
linkService,
|
||
scriptingManager,
|
||
renderer: 'canvas'
|
||
});
|
||
|
||
linkService.setViewer(pdfViewer);
|
||
scriptingManager.setViewer(pdfViewer);
|
||
|
||
let pdfDocument = null;
|
||
let currentScale = 1.0;
|
||
|
||
// Listen for messages from parent window
|
||
window.addEventListener('message', async (event) => {
|
||
if (event.data.type === 'loadPDF') {
|
||
const { data } = event.data;
|
||
loadPDF(data);
|
||
} else if (event.data.type === 'getData') {
|
||
const pdfData = await getPDFData();
|
||
window.parent.postMessage({ type: 'pdfData', data: pdfData }, '*');
|
||
}
|
||
});
|
||
|
||
async function loadPDF(data) {
|
||
try {
|
||
const loadingTask = pdfjsLib.getDocument({
|
||
data,
|
||
enableXfa: true,
|
||
});
|
||
pdfDocument = await loadingTask.promise;
|
||
// Wire scripting to the loaded document so XFA/JS executes
|
||
await scriptingManager.setDocument(pdfDocument);
|
||
pdfViewer.setDocument(pdfDocument);
|
||
linkService.setDocument(pdfDocument);
|
||
|
||
document.getElementById('numPages').textContent = pdfDocument.numPages;
|
||
document.getElementById('pageNumber').max = pdfDocument.numPages;
|
||
|
||
// Notify parent that PDF is loaded
|
||
window.parent.postMessage({ type: 'pdfLoaded', numPages: pdfDocument.numPages }, '*');
|
||
} catch (error) {
|
||
console.error('Error loading PDF:', error);
|
||
window.parent.postMessage({ type: 'error', message: error.message }, '*');
|
||
}
|
||
}
|
||
|
||
async function getPDFData() {
|
||
if (!pdfDocument) return null;
|
||
|
||
// Check if this is an XFA form
|
||
if (pdfDocument.isPureXfa) {
|
||
console.warn('Pure XFA form detected - saving with filled data is not supported');
|
||
window.parent.postMessage({
|
||
type: 'error',
|
||
message: 'XFA forms cannot be saved with filled data. Please print to PDF instead.'
|
||
}, '*');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// For AcroForms, saveDocument() should work
|
||
const data = await pdfDocument.saveDocument();
|
||
return data;
|
||
} catch (error) {
|
||
console.error('Error saving PDF document:', error);
|
||
window.parent.postMessage({
|
||
type: 'error',
|
||
message: 'Failed to save PDF. This may be an XFA form or protected PDF.'
|
||
}, '*');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Navigation controls
|
||
document.getElementById('previousPage').addEventListener('click', () => {
|
||
if (pdfViewer.currentPageNumber > 1) {
|
||
pdfViewer.currentPageNumber--;
|
||
}
|
||
});
|
||
|
||
document.getElementById('nextPage').addEventListener('click', () => {
|
||
if (pdfViewer.currentPageNumber < pdfDocument?.numPages) {
|
||
pdfViewer.currentPageNumber++;
|
||
}
|
||
});
|
||
|
||
document.getElementById('pageNumber').addEventListener('change', (e) => {
|
||
const pageNum = parseInt(e.target.value);
|
||
if (pageNum >= 1 && pageNum <= pdfDocument?.numPages) {
|
||
pdfViewer.currentPageNumber = pageNum;
|
||
}
|
||
});
|
||
|
||
// Zoom controls
|
||
document.getElementById('zoomIn').addEventListener('click', () => {
|
||
currentScale = Math.min(currentScale + 0.1, 3.0);
|
||
pdfViewer.currentScale = currentScale;
|
||
document.getElementById('scaleSelect').value = currentScale.toFixed(2);
|
||
});
|
||
|
||
document.getElementById('zoomOut').addEventListener('click', () => {
|
||
currentScale = Math.max(currentScale - 0.1, 0.1);
|
||
pdfViewer.currentScale = currentScale;
|
||
document.getElementById('scaleSelect').value = currentScale.toFixed(2);
|
||
});
|
||
|
||
document.getElementById('scaleSelect').addEventListener('change', (e) => {
|
||
const value = e.target.value;
|
||
if (value === 'auto' || value === 'page-fit' || value === 'page-width') {
|
||
pdfViewer.currentScaleValue = value;
|
||
} else {
|
||
currentScale = parseFloat(value);
|
||
pdfViewer.currentScale = currentScale;
|
||
}
|
||
});
|
||
|
||
// Print button - trigger browser print
|
||
document.getElementById('print').addEventListener('click', () => {
|
||
if (!pdfDocument) return;
|
||
window.print();
|
||
});
|
||
|
||
// Download button
|
||
document.getElementById('download').addEventListener('click', async () => {
|
||
const data = await getPDFData();
|
||
if (data) {
|
||
window.parent.postMessage({ type: 'downloadPDF', data: Array.from(data) }, '*');
|
||
}
|
||
});
|
||
|
||
// Update page number display when page changes
|
||
eventBus.on('pagechanging', (evt) => {
|
||
document.getElementById('pageNumber').value = evt.pageNumber;
|
||
document.getElementById('previousPage').disabled = evt.pageNumber <= 1;
|
||
document.getElementById('nextPage').disabled = evt.pageNumber >= pdfDocument?.numPages;
|
||
});
|
||
|
||
// Notify parent that viewer is ready
|
||
window.parent.postMessage({ type: 'viewerReady' }, '*');
|
||
</script>
|
||
</body>
|
||
</html>
|