nodebb-plugin-pdf-secure 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
 
43
43
  async function openSecureViewer(filename) {
44
44
  try {
45
- // Request nonce from server
45
+ // 1. Request nonce from server
46
46
  const response = await fetch(
47
47
  `${config.relative_path}/api/v3/plugins/pdf-secure/nonce?file=${encodeURIComponent(filename)}`,
48
48
  {
@@ -65,7 +65,7 @@
65
65
  const result = await response.json();
66
66
  const { nonce, isPremium } = result.response;
67
67
 
68
- // Fetch PDF binary in parent (before iframe creation)
68
+ // 2. Fetch PDF binary data
69
69
  const pdfResponse = await fetch(
70
70
  `${config.relative_path}/api/v3/plugins/pdf-secure/pdf-data?nonce=${encodeURIComponent(nonce)}`,
71
71
  { credentials: 'same-origin' }
@@ -76,45 +76,51 @@
76
76
  }
77
77
  const pdfArrayBuffer = await pdfResponse.arrayBuffer();
78
78
 
79
- // Create overlay with iframe
79
+ // 3. Create overlay DOM (no iframe)
80
80
  const overlay = document.createElement('div');
81
81
  overlay.className = 'pdf-secure-overlay';
82
82
  overlay.id = 'pdf-secure-overlay';
83
-
84
- const iframe = document.createElement('iframe');
85
- const viewerUrl = `${config.relative_path}/plugins/nodebb-plugin-pdf-secure/static/lib/viewer.html`;
86
- iframe.src = viewerUrl;
87
- iframe.className = 'pdf-secure-iframe';
88
- iframe.setAttribute('sandbox', 'allow-scripts');
89
-
90
- overlay.appendChild(iframe);
83
+ overlay.innerHTML = `
84
+ <div class="pdf-secure-viewer">
85
+ <div id="pdf-toolbar">
86
+ <button id="pdf-prev-btn" disabled>&laquo; Prev</button>
87
+ <span id="pdf-page-info">Loading...</span>
88
+ <button id="pdf-next-btn" disabled>Next &raquo;</button>
89
+ <button id="pdf-close-btn">&times; Close</button>
90
+ </div>
91
+ <div id="pdf-premium-banner">
92
+ This is a preview (page 1 only). Upgrade to Premium to view the full document.
93
+ </div>
94
+ <div id="pdf-canvas-wrapper">
95
+ <div id="pdf-loading">Loading PDF...</div>
96
+ <div id="pdf-error-msg"></div>
97
+ <canvas id="pdf-canvas"></canvas>
98
+ </div>
99
+ </div>
100
+ `;
91
101
  document.body.appendChild(overlay);
92
102
 
93
- // Listen for messages from viewer
94
- function onMessage(e) {
95
- if (e.source !== iframe.contentWindow) return;
96
- if (e.data && e.data.type === 'pdf-viewer-close') {
97
- closeOverlay();
98
- } else if (e.data && e.data.type === 'pdf-viewer-ready') {
99
- iframe.contentWindow.postMessage(
100
- { type: 'pdf-data', pdf: pdfArrayBuffer, isPremium: isPremium },
101
- '*',
102
- [pdfArrayBuffer]
103
- );
104
- }
105
- }
106
- window.addEventListener('message', onMessage);
103
+ // 4. Dynamically import viewer.js
104
+ const { initViewer } = await import(
105
+ `${config.relative_path}/plugins/nodebb-plugin-pdf-secure/static/lib/viewer.js`
106
+ );
107
+
108
+ // 5. Initialize viewer
109
+ const cleanupViewer = await initViewer(
110
+ overlay.querySelector('.pdf-secure-viewer'),
111
+ pdfArrayBuffer,
112
+ isPremium,
113
+ closeOverlay
114
+ );
107
115
 
108
- // Close on Escape key
116
+ // 6. Escape key handler (in main.js scope)
109
117
  function onKeydown(e) {
110
- if (e.key === 'Escape') {
111
- closeOverlay();
112
- }
118
+ if (e.key === 'Escape') closeOverlay();
113
119
  }
114
120
  document.addEventListener('keydown', onKeydown);
115
121
 
116
122
  function closeOverlay() {
117
- window.removeEventListener('message', onMessage);
123
+ if (cleanupViewer) cleanupViewer();
118
124
  document.removeEventListener('keydown', onKeydown);
119
125
  const el = document.getElementById('pdf-secure-overlay');
120
126
  if (el) el.remove();
@@ -1,149 +1,122 @@
1
1
  import { getDocument, GlobalWorkerOptions } from './pdf.min.mjs';
2
2
 
3
- // Set worker path
4
- GlobalWorkerOptions.workerSrc = './pdf.worker.min.mjs';
3
+ // Worker path resolved relative to this file's URL
4
+ GlobalWorkerOptions.workerSrc = new URL('./pdf.worker.min.mjs', import.meta.url).href;
5
+
6
+ export async function initViewer(container, pdfArrayBuffer, isPremium, onClose) {
7
+ // DOM elements scoped to container
8
+ const canvas = container.querySelector('#pdf-canvas');
9
+ const ctx = canvas.getContext('2d');
10
+ const prevBtn = container.querySelector('#pdf-prev-btn');
11
+ const nextBtn = container.querySelector('#pdf-next-btn');
12
+ const pageInfo = container.querySelector('#pdf-page-info');
13
+ const loadingEl = container.querySelector('#pdf-loading');
14
+ const errorEl = container.querySelector('#pdf-error-msg');
15
+ const premiumBanner = container.querySelector('#pdf-premium-banner');
16
+ const closeBtn = container.querySelector('#pdf-close-btn');
17
+ const canvasWrapper = container.querySelector('#pdf-canvas-wrapper');
18
+
19
+ let pdfDoc = null;
20
+ let currentPage = 1;
21
+ let totalPages = 0;
22
+ let rendering = false;
23
+
24
+ // --- Security: scoped to container ---
25
+ container.addEventListener('contextmenu', (e) => e.preventDefault());
26
+ container.addEventListener('dragstart', (e) => e.preventDefault());
27
+ container.addEventListener('selectstart', (e) => e.preventDefault());
28
+
29
+ // Keyboard shortcuts (Ctrl+S/P/U/A, F12) — document level
30
+ function onSecurityKeydown(e) {
31
+ if (e.key === 'F12') {
32
+ e.preventDefault();
33
+ return;
34
+ }
35
+ if ((e.ctrlKey || e.metaKey) && ['s', 'p', 'u', 'a'].includes(e.key.toLowerCase())) {
36
+ e.preventDefault();
37
+ }
38
+ }
39
+ document.addEventListener('keydown', onSecurityKeydown);
5
40
 
6
- // --- Security event listeners ---
41
+ // Premium banner
42
+ if (!isPremium) {
43
+ premiumBanner.style.display = 'block';
44
+ }
7
45
 
8
- // Block right-click context menu
9
- document.addEventListener('contextmenu', (e) => {
10
- e.preventDefault();
11
- });
46
+ // Close button
47
+ closeBtn.addEventListener('click', onClose);
12
48
 
13
- // Block keyboard shortcuts (Ctrl+S, Ctrl+P, Ctrl+U, F12)
14
- document.addEventListener('keydown', (e) => {
15
- if (e.key === 'F12') {
16
- e.preventDefault();
17
- return;
18
- }
19
- if ((e.ctrlKey || e.metaKey) && ['s', 'p', 'u', 'a'].includes(e.key.toLowerCase())) {
20
- e.preventDefault();
21
- return;
22
- }
23
- });
24
-
25
- // Block drag
26
- document.addEventListener('dragstart', (e) => {
27
- e.preventDefault();
28
- });
29
-
30
- // Block text selection via JS as well
31
- document.addEventListener('selectstart', (e) => {
32
- e.preventDefault();
33
- });
34
-
35
- // --- PDF Viewer Logic ---
36
-
37
- const canvas = document.getElementById('pdf-canvas');
38
- const ctx = canvas.getContext('2d');
39
- const prevBtn = document.getElementById('prev-btn');
40
- const nextBtn = document.getElementById('next-btn');
41
- const pageInfo = document.getElementById('page-info');
42
- const loadingEl = document.getElementById('loading');
43
- const errorEl = document.getElementById('error-msg');
44
- const premiumBanner = document.getElementById('premium-banner');
45
- const closeBtn = document.getElementById('close-btn');
46
-
47
- let pdfDoc = null;
48
- let currentPage = 1;
49
- let totalPages = 0;
50
- let rendering = false;
51
-
52
- // Close button handler
53
- closeBtn.addEventListener('click', () => {
54
- if (window.parent && window.parent !== window) {
55
- window.parent.postMessage({ type: 'pdf-viewer-close' }, '*');
56
- } else {
57
- window.close();
58
- }
59
- });
49
+ // --- Render ---
50
+ async function renderPage(pageNum) {
51
+ if (rendering) return;
52
+ rendering = true;
60
53
 
61
- async function renderPage(pageNum) {
62
- if (rendering) return;
63
- rendering = true;
54
+ try {
55
+ const page = await pdfDoc.getPage(pageNum);
64
56
 
65
- try {
66
- const page = await pdfDoc.getPage(pageNum);
67
-
68
- // Calculate scale to fit container width
69
- const wrapper = document.getElementById('canvas-wrapper');
70
- const maxWidth = wrapper.clientWidth - 40; // padding
71
- const viewport = page.getViewport({ scale: 1 });
72
- const scale = Math.min(maxWidth / viewport.width, 2.0); // max 2x
73
- const scaledViewport = page.getViewport({ scale });
74
-
75
- canvas.width = scaledViewport.width;
76
- canvas.height = scaledViewport.height;
77
-
78
- await page.render({
79
- canvasContext: ctx,
80
- viewport: scaledViewport,
81
- }).promise;
82
-
83
- currentPage = pageNum;
84
- pageInfo.textContent = `Page ${currentPage} / ${totalPages}`;
85
- prevBtn.disabled = currentPage <= 1;
86
- nextBtn.disabled = currentPage >= totalPages;
87
- } catch (err) {
88
- showError('Error rendering page.');
89
- }
57
+ const maxWidth = canvasWrapper.clientWidth - 40;
58
+ const viewport = page.getViewport({ scale: 1 });
59
+ const scale = Math.min(maxWidth / viewport.width, 2.0);
60
+ const scaledViewport = page.getViewport({ scale });
90
61
 
91
- rendering = false;
92
- }
62
+ canvas.width = scaledViewport.width;
63
+ canvas.height = scaledViewport.height;
93
64
 
94
- function showError(msg) {
95
- loadingEl.style.display = 'none';
96
- canvas.style.display = 'none';
97
- errorEl.style.display = 'flex';
98
- errorEl.textContent = msg;
99
- }
65
+ await page.render({
66
+ canvasContext: ctx,
67
+ viewport: scaledViewport,
68
+ }).promise;
69
+
70
+ currentPage = pageNum;
71
+ pageInfo.textContent = `Page ${currentPage} / ${totalPages}`;
72
+ prevBtn.disabled = currentPage <= 1;
73
+ nextBtn.disabled = currentPage >= totalPages;
74
+ } catch (err) {
75
+ showError('Error rendering page.');
76
+ }
100
77
 
101
- // Navigation
102
- prevBtn.addEventListener('click', () => {
103
- if (currentPage > 1) renderPage(currentPage - 1);
104
- });
105
-
106
- nextBtn.addEventListener('click', () => {
107
- if (currentPage < totalPages) renderPage(currentPage + 1);
108
- });
109
-
110
- // Keyboard navigation
111
- document.addEventListener('keydown', (e) => {
112
- if (e.key === 'ArrowLeft' && currentPage > 1) {
113
- renderPage(currentPage - 1);
114
- } else if (e.key === 'ArrowRight' && currentPage < totalPages) {
115
- renderPage(currentPage + 1);
116
- } else if (e.key === 'Escape') {
117
- closeBtn.click();
78
+ rendering = false;
118
79
  }
119
- });
120
-
121
- // Handle window resize
122
- let resizeTimeout;
123
- window.addEventListener('resize', () => {
124
- clearTimeout(resizeTimeout);
125
- resizeTimeout = setTimeout(() => {
126
- if (pdfDoc) renderPage(currentPage);
127
- }, 250);
128
- });
129
-
130
- // 30 second timeout for receiving PDF data
131
- const dataTimeout = setTimeout(() => {
132
- showError('Timed out waiting for PDF data.');
133
- }, 30000);
134
-
135
- // Listen for PDF data from parent
136
- window.addEventListener('message', async (e) => {
137
- if (!e.data || e.data.type !== 'pdf-data') return;
138
- clearTimeout(dataTimeout);
139
-
140
- const { pdf, isPremium } = e.data;
141
- if (!isPremium) {
142
- premiumBanner.style.display = 'block';
80
+
81
+ function showError(msg) {
82
+ loadingEl.style.display = 'none';
83
+ canvas.style.display = 'none';
84
+ errorEl.style.display = 'flex';
85
+ errorEl.textContent = msg;
143
86
  }
144
87
 
88
+ // Navigation buttons
89
+ prevBtn.addEventListener('click', () => {
90
+ if (currentPage > 1) renderPage(currentPage - 1);
91
+ });
92
+
93
+ nextBtn.addEventListener('click', () => {
94
+ if (currentPage < totalPages) renderPage(currentPage + 1);
95
+ });
96
+
97
+ // Keyboard navigation
98
+ function onNavKeydown(e) {
99
+ if (e.key === 'ArrowLeft' && currentPage > 1) {
100
+ renderPage(currentPage - 1);
101
+ } else if (e.key === 'ArrowRight' && currentPage < totalPages) {
102
+ renderPage(currentPage + 1);
103
+ }
104
+ }
105
+ document.addEventListener('keydown', onNavKeydown);
106
+
107
+ // Handle window resize
108
+ let resizeTimeout;
109
+ function onResize() {
110
+ clearTimeout(resizeTimeout);
111
+ resizeTimeout = setTimeout(() => {
112
+ if (pdfDoc) renderPage(currentPage);
113
+ }, 250);
114
+ }
115
+ window.addEventListener('resize', onResize);
116
+
117
+ // Load PDF
145
118
  try {
146
- const data = new Uint8Array(pdf);
119
+ const data = new Uint8Array(pdfArrayBuffer);
147
120
  pdfDoc = await getDocument({ data }).promise;
148
121
  totalPages = pdfDoc.numPages;
149
122
  loadingEl.style.display = 'none';
@@ -152,7 +125,12 @@ window.addEventListener('message', async (e) => {
152
125
  } catch (err) {
153
126
  showError('Failed to load PDF. Please try again.');
154
127
  }
155
- });
156
128
 
157
- // Notify parent that viewer is ready (after listener is registered)
158
- window.parent.postMessage({ type: 'pdf-viewer-ready' }, '*');
129
+ // Return cleanup function
130
+ return function cleanup() {
131
+ document.removeEventListener('keydown', onSecurityKeydown);
132
+ document.removeEventListener('keydown', onNavKeydown);
133
+ window.removeEventListener('resize', onResize);
134
+ if (pdfDoc) pdfDoc.destroy();
135
+ };
136
+ }
package/static/style.less CHANGED
@@ -25,18 +25,162 @@
25
25
  justify-content: center;
26
26
  }
27
27
 
28
- .pdf-secure-iframe {
28
+ .pdf-secure-viewer {
29
29
  width: 95%;
30
30
  height: 95%;
31
31
  border: none;
32
32
  border-radius: 8px;
33
33
  background: #2b2b2b;
34
+ overflow: hidden;
35
+ position: relative;
36
+ display: flex;
37
+ flex-direction: column;
38
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
39
+ user-select: none;
40
+ -webkit-user-select: none;
41
+ -moz-user-select: none;
42
+ -ms-user-select: none;
43
+
44
+ &, & *, & *::before, & *::after {
45
+ box-sizing: border-box;
46
+ margin: 0;
47
+ padding: 0;
48
+ }
49
+
50
+ #pdf-toolbar {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ gap: 12px;
55
+ padding: 8px 16px;
56
+ background: #333;
57
+ color: #fff;
58
+ flex-shrink: 0;
59
+ z-index: 10;
60
+ }
61
+
62
+ #pdf-toolbar button {
63
+ background: #555;
64
+ color: #fff;
65
+ border: none;
66
+ padding: 6px 14px;
67
+ border-radius: 4px;
68
+ cursor: pointer;
69
+ font-size: 14px;
70
+ }
71
+
72
+ #pdf-toolbar button:hover {
73
+ background: #666;
74
+ }
75
+
76
+ #pdf-toolbar button:disabled {
77
+ opacity: 0.4;
78
+ cursor: not-allowed;
79
+ }
80
+
81
+ #pdf-page-info {
82
+ font-size: 14px;
83
+ min-width: 100px;
84
+ text-align: center;
85
+ }
86
+
87
+ #pdf-close-btn {
88
+ position: absolute;
89
+ right: 16px;
90
+ top: 8px;
91
+ background: #c0392b !important;
92
+ font-size: 16px !important;
93
+ padding: 6px 12px !important;
94
+ z-index: 20;
95
+ }
96
+
97
+ #pdf-close-btn:hover {
98
+ background: #e74c3c !important;
99
+ }
100
+
101
+ #pdf-canvas-wrapper {
102
+ flex: 1;
103
+ overflow: auto;
104
+ display: flex;
105
+ justify-content: center;
106
+ align-items: flex-start;
107
+ padding: 20px;
108
+ background: #2b2b2b;
109
+ }
110
+
111
+ #pdf-canvas {
112
+ pointer-events: none;
113
+ display: none;
114
+ max-width: 100%;
115
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
116
+ }
117
+
118
+ #pdf-loading {
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ height: 100%;
123
+ color: #aaa;
124
+ font-size: 18px;
125
+ }
126
+
127
+ #pdf-error-msg {
128
+ display: none;
129
+ align-items: center;
130
+ justify-content: center;
131
+ height: 100%;
132
+ color: #e74c3c;
133
+ font-size: 18px;
134
+ text-align: center;
135
+ padding: 20px;
136
+ }
137
+
138
+ #pdf-premium-banner {
139
+ display: none;
140
+ background: linear-gradient(135deg, #f39c12, #e67e22);
141
+ color: #fff;
142
+ text-align: center;
143
+ padding: 10px 16px;
144
+ font-size: 14px;
145
+ font-weight: 600;
146
+ flex-shrink: 0;
147
+ }
148
+ }
149
+
150
+ /* Anti-print: hide overlay when printing */
151
+ @media print {
152
+ .pdf-secure-overlay {
153
+ display: none !important;
154
+ }
34
155
  }
35
156
 
36
157
  @media (max-width: 768px) {
37
- .pdf-secure-iframe {
158
+ .pdf-secure-viewer {
38
159
  width: 100%;
39
160
  height: 100%;
40
161
  border-radius: 0;
41
162
  }
42
163
  }
164
+
165
+ @media (max-width: 600px) {
166
+ .pdf-secure-viewer {
167
+ #pdf-toolbar {
168
+ gap: 6px;
169
+ padding: 6px 8px;
170
+ }
171
+
172
+ #pdf-toolbar button {
173
+ padding: 4px 8px;
174
+ font-size: 12px;
175
+ }
176
+
177
+ #pdf-page-info {
178
+ font-size: 12px;
179
+ min-width: 70px;
180
+ }
181
+
182
+ #pdf-canvas-wrapper {
183
+ padding: 10px;
184
+ }
185
+ }
186
+ }
@@ -1,158 +0,0 @@
1
- /* PDF Secure Viewer Styles */
2
- * {
3
- margin: 0;
4
- padding: 0;
5
- box-sizing: border-box;
6
- }
7
-
8
- html, body {
9
- width: 100%;
10
- height: 100%;
11
- overflow: hidden;
12
- background: #2b2b2b;
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
- user-select: none;
15
- -webkit-user-select: none;
16
- -moz-user-select: none;
17
- -ms-user-select: none;
18
- }
19
-
20
- #viewer-container {
21
- display: flex;
22
- flex-direction: column;
23
- height: 100%;
24
- width: 100%;
25
- }
26
-
27
- #toolbar {
28
- display: flex;
29
- align-items: center;
30
- justify-content: center;
31
- gap: 12px;
32
- padding: 8px 16px;
33
- background: #333;
34
- color: #fff;
35
- flex-shrink: 0;
36
- z-index: 10;
37
- }
38
-
39
- #toolbar button {
40
- background: #555;
41
- color: #fff;
42
- border: none;
43
- padding: 6px 14px;
44
- border-radius: 4px;
45
- cursor: pointer;
46
- font-size: 14px;
47
- }
48
-
49
- #toolbar button:hover {
50
- background: #666;
51
- }
52
-
53
- #toolbar button:disabled {
54
- opacity: 0.4;
55
- cursor: not-allowed;
56
- }
57
-
58
- #page-info {
59
- font-size: 14px;
60
- min-width: 100px;
61
- text-align: center;
62
- }
63
-
64
- #close-btn {
65
- position: absolute;
66
- right: 16px;
67
- top: 8px;
68
- background: #c0392b !important;
69
- font-size: 16px !important;
70
- padding: 6px 12px !important;
71
- z-index: 20;
72
- }
73
-
74
- #close-btn:hover {
75
- background: #e74c3c !important;
76
- }
77
-
78
- #canvas-wrapper {
79
- flex: 1;
80
- overflow: auto;
81
- display: flex;
82
- justify-content: center;
83
- align-items: flex-start;
84
- padding: 20px;
85
- background: #2b2b2b;
86
- }
87
-
88
- #pdf-canvas {
89
- pointer-events: none;
90
- display: block;
91
- max-width: 100%;
92
- box-shadow: 0 4px 20px rgba(0,0,0,0.5);
93
- }
94
-
95
- #loading {
96
- display: flex;
97
- align-items: center;
98
- justify-content: center;
99
- height: 100%;
100
- color: #aaa;
101
- font-size: 18px;
102
- }
103
-
104
- #error-msg {
105
- display: none;
106
- align-items: center;
107
- justify-content: center;
108
- height: 100%;
109
- color: #e74c3c;
110
- font-size: 18px;
111
- text-align: center;
112
- padding: 20px;
113
- }
114
-
115
- #premium-banner {
116
- display: none;
117
- background: linear-gradient(135deg, #f39c12, #e67e22);
118
- color: #fff;
119
- text-align: center;
120
- padding: 10px 16px;
121
- font-size: 14px;
122
- font-weight: 600;
123
- flex-shrink: 0;
124
- }
125
-
126
- /* Anti-print */
127
- @media print {
128
- html, body {
129
- visibility: hidden !important;
130
- }
131
- body::after {
132
- content: 'Printing is not allowed.';
133
- visibility: visible;
134
- display: block;
135
- text-align: center;
136
- padding: 50px;
137
- font-size: 24px;
138
- }
139
- }
140
-
141
- /* Responsive */
142
- @media (max-width: 600px) {
143
- #toolbar {
144
- gap: 6px;
145
- padding: 6px 8px;
146
- }
147
- #toolbar button {
148
- padding: 4px 8px;
149
- font-size: 12px;
150
- }
151
- #page-info {
152
- font-size: 12px;
153
- min-width: 70px;
154
- }
155
- #canvas-wrapper {
156
- padding: 10px;
157
- }
158
- }
@@ -1,29 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PDF Viewer</title>
7
- <link rel="stylesheet" href="viewer.css">
8
- </head>
9
- <body>
10
- <div id="viewer-container">
11
- <div id="toolbar">
12
- <button id="prev-btn" disabled>&laquo; Prev</button>
13
- <span id="page-info">Loading...</span>
14
- <button id="next-btn" disabled>Next &raquo;</button>
15
- <button id="close-btn">&times; Close</button>
16
- </div>
17
- <div id="premium-banner">
18
- This is a preview (page 1 only). Upgrade to Premium to view the full document.
19
- </div>
20
- <div id="canvas-wrapper">
21
- <div id="loading">Loading PDF...</div>
22
- <div id="error-msg"></div>
23
- <canvas id="pdf-canvas"></canvas>
24
- </div>
25
- </div>
26
-
27
- <script type="module" src="viewer.js"></script>
28
- </body>
29
- </html>