Replace PDF.js canvas with native browser iframe PDF viewer

- Browser renders PDF exactly as-is using its built-in viewer (Chrome/Edge/Firefox)
- No external dependencies - just a blob URL in an <iframe>
- Page navigation via #page=N fragment syncs PDF view with playback position
- Fixed flex layout so iframe fills remaining height correctly
- Removed PDF.js script tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ifedan-ed 2026-04-11 04:43:52 +02:00
parent 00af54c95a
commit 7afbe3a22a
3 changed files with 51 additions and 67 deletions

View file

@ -21,9 +21,8 @@ const audioCache = new Map();
let allModels = {};
let currentUser = null;
// PDF.js state
let pdfDoc = null;
const pageCanvases = []; // canvas elements per page
// PDF blob URL for native browser viewer
let pdfBlobUrl = null;
// ── DOM ────────────────────────────────────────────────────────────
const $ = id => document.getElementById(id);
@ -184,70 +183,45 @@ function renderText(sentences) {
// ── VIEW SWITCHING ─────────────────────────────────────────────────
function setView(mode) {
state.currentView = mode;
const textViewer = $('text-viewer');
const pdfViewer = $('pdf-viewer');
const contentArea = $('content-area');
const btnText = $('btn-view-text');
const btnPdf = $('btn-view-pdf');
if (mode === 'pdf') {
textViewer.style.display = 'none';
pdfViewer.style.display = '';
$('text-viewer').style.display = 'none';
$('pdf-viewer').style.display = 'flex';
contentArea.classList.add('pdf-mode');
btnPdf.classList.add('active');
btnText.classList.remove('active');
$('btn-view-pdf').classList.add('active');
$('btn-view-text').classList.remove('active');
$('doc-hint').textContent = 'Viewing original PDF · switch to Text to click sentences';
} else {
textViewer.style.display = '';
pdfViewer.style.display = 'none';
$('text-viewer').style.display = '';
$('pdf-viewer').style.display = 'none';
contentArea.classList.remove('pdf-mode');
btnText.classList.add('active');
btnPdf.classList.remove('active');
$('btn-view-text').classList.add('active');
$('btn-view-pdf').classList.remove('active');
$('doc-hint').textContent = 'Click any sentence to jump there · Space to play/pause · ← → to skip';
}
}
window.setView = setView;
// ── PDF.js RENDERING ───────────────────────────────────────────────
async function renderPDF(b64) {
if (typeof pdfjsLib === 'undefined') return;
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const pdfPages = $('pdf-pages');
pdfPages.innerHTML = '';
pageCanvases.length = 0;
// ── PDF NATIVE RENDERING ───────────────────────────────────────────
function renderPDF(b64) {
// Revoke previous blob to free memory
if (pdfBlobUrl) { URL.revokeObjectURL(pdfBlobUrl); pdfBlobUrl = null; }
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const blob = new Blob([bytes], { type: 'application/pdf' });
pdfBlobUrl = URL.createObjectURL(blob);
pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise;
const pdfPages = $('pdf-pages');
pdfPages.innerHTML = '';
for (let i = 1; i <= pdfDoc.numPages; i++) {
const page = await pdfDoc.getPage(i);
const vp = page.getViewport({ scale: 1.5 });
const wrap = document.createElement('div');
wrap.className = 'pdf-page-wrap';
wrap.dataset.page = i - 1;
const canvas = document.createElement('canvas');
canvas.width = vp.width;
canvas.height = vp.height;
canvas.style.width = Math.min(vp.width, 800) + 'px';
canvas.style.height = (Math.min(vp.width, 800) / vp.width * vp.height) + 'px';
await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
wrap.appendChild(canvas);
const lbl = document.createElement('div');
lbl.className = 'pdf-page-num';
lbl.textContent = `Page ${i}`;
pdfPages.appendChild(wrap);
pdfPages.appendChild(lbl);
pageCanvases.push(wrap);
}
const iframe = document.createElement('iframe');
iframe.id = 'pdf-iframe';
iframe.src = pdfBlobUrl;
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
pdfPages.appendChild(iframe);
}
function buildSentencePageMap(pageTexts, sentences) {
@ -269,8 +243,12 @@ function buildSentencePageMap(pageTexts, sentences) {
}
function scrollPDFToPage(pageIdx) {
if (pageIdx < 0 || pageIdx >= pageCanvases.length) return;
pageCanvases[pageIdx].scrollIntoView({ behavior: 'smooth', block: 'start' });
if (!pdfBlobUrl) return;
const iframe = $('pdf-iframe');
if (!iframe) return;
// #page= fragment navigates inside Chrome/Edge native PDF viewer
const target = pdfBlobUrl + '#page=' + (pageIdx + 1);
if (iframe.src !== target) iframe.src = target;
}
// ── LOAD DOCUMENT ──────────────────────────────────────────────────

View file

@ -219,7 +219,6 @@
<p id="loading-text">Loading…</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="/static/app.js"></script>
</body>
</html>

View file

@ -201,8 +201,12 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
.content-area {
flex: 1; overflow-y: auto; padding: 44px 56px;
display: flex; flex-direction: column;
min-height: 0;
}
.content-area.pdf-mode {
padding: 20px 20px 0;
overflow: hidden; /* iframe fills remaining height */
}
.content-area.pdf-mode { padding: 24px 24px 0; overflow: hidden; }
.document-header { margin-bottom: 28px; }
.doc-title-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px; }
.document-title { font-size: 22px; font-weight: 700; flex: 1; }
@ -281,22 +285,25 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
/* ── PDF VIEWER ──────────────────────────── */
.pdf-viewer {
flex: 1; overflow-y: auto; padding: 24px; background: #1a1a1a;
display: flex; flex-direction: column; align-items: center; gap: 16px;
flex: 1;
min-height: 0; /* essential: lets flex child shrink below content size */
display: flex;
flex-direction: column;
overflow: hidden;
}
#pdf-pages { display: flex; flex-direction: column; gap: 16px; align-items: center; width: 100%; }
.pdf-page-wrap {
position: relative; box-shadow: 0 4px 24px rgba(0,0,0,.5);
border-radius: 4px; overflow: hidden; max-width: 100%;
#pdf-pages {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pdf-page-wrap canvas { display: block; max-width: 100%; height: auto; }
.pdf-page-highlight {
position: absolute; border-radius: 2px;
background: rgba(124,58,237,.35);
pointer-events: none; transition: opacity .2s;
}
.pdf-page-num {
text-align: center; font-size: 11px; color: var(--text-d); margin-top: 4px;
#pdf-iframe {
flex: 1;
min-height: 0;
width: 100%;
border: none;
display: block;
}
/* ── SETTINGS SIDEBAR ────────────────────── */