svelte-pdf-view 0.1.0

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/LICENSE.md ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2025 Louis Li
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # svelte-pdf-view
2
+
3
+ A modern, modular PDF viewer component for Svelte 5. Built on top of [PDF.js](https://mozilla.github.io/pdf.js/), with full TypeScript support and Shadow DOM isolation.
4
+
5
+ ## Features
6
+
7
+ - **PDF Rendering** - High-quality PDF rendering powered by PDF.js
8
+ - **Text Search** - Full-text search with highlight navigation
9
+ - **Rotation** - Rotate pages clockwise/counter-clockwise
10
+ - **Zoom** - Zoom in/out controls
11
+ - **Responsive** - Works on desktop and mobile
12
+ - **Customizable** - Style the scrollbar, background, and more
13
+ - **Modular** - Use the default layout or build your own toolbar
14
+ - **Shadow DOM** - Renderer styles are isolated and won't leak
15
+ - **Multiple Sources** - Load from URL, ArrayBuffer, Uint8Array, or Blob
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install svelte-pdf-view
21
+ # or
22
+ pnpm add svelte-pdf-view
23
+ # or
24
+ yarn add svelte-pdf-view
25
+ ```
26
+
27
+ If you want to use the default `<PdfToolbar>` component, also install:
28
+
29
+ ```bash
30
+ npm install @lucide/svelte
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ### Basic Usage
36
+
37
+ ```svelte
38
+ <script lang="ts">
39
+ import { PdfViewer, PdfToolbar, PdfRenderer } from 'svelte-pdf-view';
40
+ </script>
41
+
42
+ <div style="height: 100vh;">
43
+ <PdfViewer src="/document.pdf">
44
+ <PdfToolbar />
45
+ <PdfRenderer src="/document.pdf" />
46
+ </PdfViewer>
47
+ </div>
48
+ ```
49
+
50
+ ### Loading from Different Sources
51
+
52
+ ```svelte
53
+ <script lang="ts">
54
+ import { PdfViewer, PdfToolbar, PdfRenderer, type PdfSource } from 'svelte-pdf-view';
55
+
56
+ // From URL
57
+ let pdfSource: PdfSource = '/document.pdf';
58
+
59
+ // From file input
60
+ function handleFileSelect(e: Event) {
61
+ const file = (e.target as HTMLInputElement).files?.[0];
62
+ if (file) {
63
+ pdfSource = file; // Blob works directly
64
+ }
65
+ }
66
+
67
+ // From fetch response
68
+ async function loadFromApi() {
69
+ const response = await fetch('/api/document');
70
+ const arrayBuffer = await response.arrayBuffer();
71
+ pdfSource = arrayBuffer;
72
+ }
73
+ </script>
74
+
75
+ <input type="file" accept=".pdf" onchange={handleFileSelect} />
76
+
77
+ <PdfViewer src={pdfSource}>
78
+ <PdfToolbar />
79
+ <PdfRenderer src={pdfSource} />
80
+ </PdfViewer>
81
+ ```
82
+
83
+ ## Components
84
+
85
+ ### `<PdfViewer>`
86
+
87
+ The main container component that provides context for toolbar and renderer.
88
+
89
+ ```svelte
90
+ <PdfViewer src={pdfSource} scale={1.0} class="my-viewer">
91
+ <!-- Children -->
92
+ </PdfViewer>
93
+ ```
94
+
95
+ | Prop | Type | Default | Description |
96
+ | ------- | ----------- | -------- | -------------------------------------------------- |
97
+ | `src` | `PdfSource` | required | PDF source (URL, ArrayBuffer, Uint8Array, or Blob) |
98
+ | `scale` | `number` | `1.0` | Initial zoom scale |
99
+ | `class` | `string` | `''` | CSS class for the container |
100
+
101
+ ### `<PdfToolbar>`
102
+
103
+ The default toolbar with page navigation, zoom, rotation, and search controls.
104
+
105
+ ```svelte
106
+ <PdfToolbar />
107
+ ```
108
+
109
+ The toolbar automatically connects to the viewer context - no props needed.
110
+
111
+ ### `<PdfRenderer>`
112
+
113
+ The PDF rendering component. Uses Shadow DOM for style isolation.
114
+
115
+ ```svelte
116
+ <PdfRenderer
117
+ src={pdfSource}
118
+ backgroundColor="#e8e8e8"
119
+ scrollbarThumbColor="#c1c1c1"
120
+ scrollbarTrackColor="#f1f1f1"
121
+ scrollbarThumbHoverColor="#a1a1a1"
122
+ scrollbarWidth="10px"
123
+ pageShadow="0 2px 8px rgba(0, 0, 0, 0.12)"
124
+ />
125
+ ```
126
+
127
+ | Prop | Type | Default | Description |
128
+ | -------------------------- | ----------- | ----------------------------------- | ------------------------------------ |
129
+ | `src` | `PdfSource` | required | PDF source |
130
+ | `backgroundColor` | `string` | `'#e8e8e8'` | Background color of scroll container |
131
+ | `pageShadow` | `string` | `'0 2px 8px rgba(0,0,0,0.12), ...'` | Box shadow for PDF pages |
132
+ | `scrollbarTrackColor` | `string` | `'#f1f1f1'` | Scrollbar track background |
133
+ | `scrollbarThumbColor` | `string` | `'#c1c1c1'` | Scrollbar thumb color |
134
+ | `scrollbarThumbHoverColor` | `string` | `'#a1a1a1'` | Scrollbar thumb hover color |
135
+ | `scrollbarWidth` | `string` | `'10px'` | Scrollbar width |
136
+
137
+ ## Custom Toolbar
138
+
139
+ You can create your own toolbar using the context API:
140
+
141
+ ```svelte
142
+ <script lang="ts">
143
+ import { PdfViewer, PdfRenderer, getPdfViewerContext } from 'svelte-pdf-view';
144
+ </script>
145
+
146
+ <PdfViewer src="/document.pdf">
147
+ <MyCustomToolbar />
148
+ <PdfRenderer src="/document.pdf" />
149
+ </PdfViewer>
150
+ ```
151
+
152
+ ```svelte
153
+ <!-- MyCustomToolbar.svelte -->
154
+ <script lang="ts">
155
+ import { getPdfViewerContext } from 'svelte-pdf-view';
156
+
157
+ const { state, actions } = getPdfViewerContext();
158
+ </script>
159
+
160
+ <div class="toolbar">
161
+ <span>Page {state.currentPage} of {state.totalPages}</span>
162
+
163
+ <button onclick={() => actions.zoomOut()}>-</button>
164
+ <span>{Math.round(state.scale * 100)}%</span>
165
+ <button onclick={() => actions.zoomIn()}>+</button>
166
+
167
+ <button onclick={() => actions.rotateCounterClockwise()}>↺</button>
168
+ <button onclick={() => actions.rotateClockwise()}>↻</button>
169
+ </div>
170
+
171
+ <style>
172
+ /* Your custom styles */
173
+ </style>
174
+ ```
175
+
176
+ ### Context API
177
+
178
+ #### `state: PdfViewerState`
179
+
180
+ | Property | Type | Description |
181
+ | --------------- | ---------------- | ---------------------------------- |
182
+ | `loading` | `boolean` | Whether the PDF is loading |
183
+ | `error` | `string \| null` | Error message if loading failed |
184
+ | `totalPages` | `number` | Total number of pages |
185
+ | `currentPage` | `number` | Current visible page |
186
+ | `scale` | `number` | Current zoom scale |
187
+ | `rotation` | `number` | Current rotation (0, 90, 180, 270) |
188
+ | `searchQuery` | `string` | Current search query |
189
+ | `searchCurrent` | `number` | Current match index |
190
+ | `searchTotal` | `number` | Total number of matches |
191
+ | `isSearching` | `boolean` | Whether search is in progress |
192
+
193
+ #### `actions: PdfViewerActions`
194
+
195
+ | Method | Description |
196
+ | -------------------------- | ---------------------------- |
197
+ | `zoomIn()` | Increase zoom level |
198
+ | `zoomOut()` | Decrease zoom level |
199
+ | `setScale(scale: number)` | Set specific zoom scale |
200
+ | `rotateClockwise()` | Rotate 90° clockwise |
201
+ | `rotateCounterClockwise()` | Rotate 90° counter-clockwise |
202
+ | `goToPage(page: number)` | Navigate to specific page |
203
+ | `search(query: string)` | Search for text |
204
+ | `searchNext()` | Go to next search match |
205
+ | `searchPrevious()` | Go to previous search match |
206
+ | `clearSearch()` | Clear search highlights |
207
+
208
+ ## Types
209
+
210
+ ```typescript
211
+ // PDF source can be URL, ArrayBuffer, Uint8Array, or Blob
212
+ type PdfSource = string | ArrayBuffer | Uint8Array | Blob;
213
+
214
+ interface PdfViewerState {
215
+ loading: boolean;
216
+ error: string | null;
217
+ totalPages: number;
218
+ currentPage: number;
219
+ scale: number;
220
+ rotation: number;
221
+ searchQuery: string;
222
+ searchCurrent: number;
223
+ searchTotal: number;
224
+ isSearching: boolean;
225
+ }
226
+
227
+ interface PdfViewerActions {
228
+ zoomIn: () => void;
229
+ zoomOut: () => void;
230
+ setScale: (scale: number) => void;
231
+ rotateClockwise: () => void;
232
+ rotateCounterClockwise: () => void;
233
+ goToPage: (page: number) => void;
234
+ search: (query: string) => Promise<void>;
235
+ searchNext: () => void;
236
+ searchPrevious: () => void;
237
+ clearSearch: () => void;
238
+ }
239
+ ```
240
+
241
+ ## Styling Examples
242
+
243
+ ### Dark Theme
244
+
245
+ ```svelte
246
+ <PdfRenderer
247
+ src={pdfSource}
248
+ backgroundColor="#1a1a1a"
249
+ scrollbarTrackColor="#2d2d2d"
250
+ scrollbarThumbColor="#555"
251
+ scrollbarThumbHoverColor="#666"
252
+ pageShadow="0 4px 12px rgba(0, 0, 0, 0.5)"
253
+ />
254
+ ```
255
+
256
+ ### Minimal Theme
257
+
258
+ ```svelte
259
+ <PdfRenderer
260
+ src={pdfSource}
261
+ backgroundColor="#ffffff"
262
+ scrollbarWidth="6px"
263
+ scrollbarTrackColor="transparent"
264
+ scrollbarThumbColor="#ddd"
265
+ pageShadow="none"
266
+ />
267
+ ```
268
+
269
+ ## Browser Support
270
+
271
+ - Chrome/Edge 88+
272
+ - Firefox 78+
273
+ - Safari 14+
274
+
275
+ ## License
276
+
277
+ Apache 2.0
278
+
279
+ ## Attribution
280
+
281
+ This project is built on top of [PDF.js](https://mozilla.github.io/pdf.js/) by Mozilla, licensed under the Apache License 2.0.
282
+
283
+ Several files in this project are derivative works based on PDF.js viewer source code:
284
+
285
+ | File | Based on |
286
+ | --------------------- | ------------------------------------------------------- |
287
+ | `EventBus.ts` | `web/event_utils.js` |
288
+ | `FindController.ts` | `web/pdf_find_controller.js`, `web/text_highlighter.js` |
289
+ | `PDFPageView.ts` | `web/pdf_page_view.js` |
290
+ | `PDFViewerCore.ts` | `web/pdf_viewer.js` |
291
+ | `renderer-styles.css` | `web/text_layer_builder.css` |
292
+
293
+ These files retain the original Apache 2.0 license headers with attribution to the Mozilla Foundation.
@@ -0,0 +1,238 @@
1
+ <script lang="ts">
2
+ import { onDestroy, onMount } from 'svelte';
3
+
4
+ const browser = typeof window !== 'undefined';
5
+ import { getPdfViewerContext, type PdfViewerActions } from './pdf-viewer/context.js';
6
+ import rendererStyles from './pdf-viewer/renderer-styles.css?raw';
7
+
8
+ /** PDF source - can be a URL string, ArrayBuffer, Uint8Array, or Blob */
9
+ export type PdfSource = string | ArrayBuffer | Uint8Array | Blob;
10
+
11
+ interface Props {
12
+ /** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
13
+ src: PdfSource;
14
+ /** Background color of the scroll container */
15
+ backgroundColor?: string;
16
+ /** Page shadow style */
17
+ pageShadow?: string;
18
+ /** Scrollbar track color */
19
+ scrollbarTrackColor?: string;
20
+ /** Scrollbar thumb color */
21
+ scrollbarThumbColor?: string;
22
+ /** Scrollbar thumb hover color */
23
+ scrollbarThumbHoverColor?: string;
24
+ /** Scrollbar width */
25
+ scrollbarWidth?: string;
26
+ }
27
+
28
+ let {
29
+ src,
30
+ backgroundColor = '#e8e8e8',
31
+ pageShadow = '0 2px 8px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08)',
32
+ scrollbarTrackColor = '#f1f1f1',
33
+ scrollbarThumbColor = '#c1c1c1',
34
+ scrollbarThumbHoverColor = '#a1a1a1',
35
+ scrollbarWidth = '10px'
36
+ }: Props = $props();
37
+
38
+ const { state: viewerState, _registerRenderer } = getPdfViewerContext();
39
+
40
+ let hostEl: HTMLDivElement | undefined = $state();
41
+ let shadowRoot: ShadowRoot | null = null;
42
+ let scrollContainerEl: HTMLDivElement | null = null;
43
+ let mounted = $state(false);
44
+
45
+ // Core instances
46
+ let viewer: import('./pdf-viewer/PDFViewerCore.js').PDFViewerCore | null = null;
47
+ let findController: import('./pdf-viewer/FindController.js').FindController | null = null;
48
+ let pdfjsLib: typeof import('pdfjs-dist') | null = null;
49
+
50
+ async function initPdfJs() {
51
+ if (!browser) return null;
52
+
53
+ pdfjsLib = await import('pdfjs-dist');
54
+ const pdfjsWorker = await import('pdfjs-dist/build/pdf.worker.min.mjs?url');
55
+ pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker.default;
56
+
57
+ return pdfjsLib;
58
+ }
59
+
60
+ async function loadPdf(source: PdfSource) {
61
+ if (!browser || !scrollContainerEl) return;
62
+
63
+ viewerState.loading = true;
64
+ viewerState.error = null;
65
+
66
+ try {
67
+ const pdfjs = await initPdfJs();
68
+ if (!pdfjs) return;
69
+
70
+ const { PDFViewerCore } = await import('./pdf-viewer/PDFViewerCore.js');
71
+ const { FindController } = await import('./pdf-viewer/FindController.js');
72
+ const { EventBus } = await import('./pdf-viewer/EventBus.js');
73
+
74
+ if (viewer) {
75
+ viewer.destroy();
76
+ }
77
+
78
+ const eventBus = new EventBus();
79
+
80
+ const newViewer = new PDFViewerCore({
81
+ container: scrollContainerEl,
82
+ eventBus,
83
+ initialScale: viewerState.scale,
84
+ initialRotation: viewerState.rotation
85
+ });
86
+
87
+ findController = new FindController(newViewer, eventBus);
88
+
89
+ // Setup event listeners
90
+ eventBus.on('scalechanged', (data: Record<string, unknown>) => {
91
+ viewerState.scale = data.scale as number;
92
+ });
93
+
94
+ eventBus.on('rotationchanged', (data: Record<string, unknown>) => {
95
+ viewerState.rotation = data.rotation as number;
96
+ });
97
+
98
+ eventBus.on('updateviewarea', (data: Record<string, unknown>) => {
99
+ const location = data.location as { pageNumber: number };
100
+ viewerState.currentPage = location.pageNumber;
101
+ });
102
+
103
+ eventBus.on('pagesloaded', (data: Record<string, unknown>) => {
104
+ viewerState.totalPages = data.pagesCount as number;
105
+ });
106
+
107
+ eventBus.on('updatefindmatchescount', (data: Record<string, unknown>) => {
108
+ const matchesCount = data.matchesCount as { current: number; total: number };
109
+ viewerState.searchCurrent = matchesCount.current;
110
+ viewerState.searchTotal = matchesCount.total;
111
+ });
112
+
113
+ // Handle different source types
114
+ let documentSource: string | { data: ArrayBuffer } | { data: Uint8Array };
115
+
116
+ if (typeof source === 'string') {
117
+ // URL string
118
+ documentSource = source;
119
+ } else if (source instanceof Blob) {
120
+ // Convert Blob to ArrayBuffer
121
+ const arrayBuffer = await source.arrayBuffer();
122
+ documentSource = { data: arrayBuffer };
123
+ } else if (source instanceof ArrayBuffer) {
124
+ documentSource = { data: source };
125
+ } else if (source instanceof Uint8Array) {
126
+ documentSource = { data: source };
127
+ } else {
128
+ throw new Error('Invalid PDF source type');
129
+ }
130
+
131
+ const loadingTask = pdfjs.getDocument(documentSource);
132
+ const pdfDocument = await loadingTask.promise;
133
+
134
+ await newViewer.setDocument(pdfDocument);
135
+ findController.setDocument(pdfDocument);
136
+
137
+ viewer = newViewer;
138
+ viewerState.loading = false;
139
+ } catch (e) {
140
+ viewerState.error = e instanceof Error ? e.message : 'Failed to load PDF';
141
+ viewerState.loading = false;
142
+ }
143
+ }
144
+
145
+ // Register actions with context
146
+ const rendererActions: PdfViewerActions = {
147
+ zoomIn: () => viewer?.zoomIn(),
148
+ zoomOut: () => viewer?.zoomOut(),
149
+ setScale: (scale: number) => {
150
+ if (viewer) viewer.scale = scale;
151
+ },
152
+ rotateClockwise: () => viewer?.rotateClockwise(),
153
+ rotateCounterClockwise: () => viewer?.rotateCounterClockwise(),
154
+ goToPage: (page: number) => viewer?.scrollToPage(page),
155
+ search: async (query: string) => {
156
+ if (!findController) return;
157
+ viewerState.isSearching = true;
158
+ viewerState.searchQuery = query;
159
+ await findController.find({
160
+ query,
161
+ highlightAll: true,
162
+ caseSensitive: false
163
+ });
164
+ viewerState.isSearching = false;
165
+ },
166
+ searchNext: () => findController?.findNext(),
167
+ searchPrevious: () => findController?.findPrevious(),
168
+ clearSearch: () => {
169
+ if (findController) {
170
+ findController.reset();
171
+ viewerState.searchQuery = '';
172
+ viewerState.searchCurrent = 0;
173
+ viewerState.searchTotal = 0;
174
+ }
175
+ }
176
+ };
177
+
178
+ onMount(async () => {
179
+ if (browser && hostEl) {
180
+ // Create shadow root for style isolation
181
+ shadowRoot = hostEl.attachShadow({ mode: 'open' });
182
+
183
+ // Inject styles
184
+ const styleEl = document.createElement('style');
185
+ styleEl.textContent = rendererStyles;
186
+ shadowRoot.appendChild(styleEl);
187
+
188
+ // Create container structure inside shadow DOM
189
+ const container = document.createElement('div');
190
+ container.className = 'pdf-renderer-container';
191
+
192
+ // Apply CSS custom properties for customization
193
+ container.style.setProperty('--pdf-background-color', backgroundColor);
194
+ container.style.setProperty('--pdf-page-shadow', pageShadow);
195
+ container.style.setProperty('--pdf-scrollbar-track-color', scrollbarTrackColor);
196
+ container.style.setProperty('--pdf-scrollbar-thumb-color', scrollbarThumbColor);
197
+ container.style.setProperty('--pdf-scrollbar-thumb-hover-color', scrollbarThumbHoverColor);
198
+ container.style.setProperty('--pdf-scrollbar-width', scrollbarWidth);
199
+
200
+ scrollContainerEl = document.createElement('div');
201
+ scrollContainerEl.className = 'pdf-scroll-container';
202
+ container.appendChild(scrollContainerEl);
203
+
204
+ shadowRoot.appendChild(container);
205
+
206
+ // Register actions
207
+ _registerRenderer(rendererActions);
208
+
209
+ mounted = true;
210
+ }
211
+ });
212
+
213
+ // Load PDF when src changes
214
+ $effect(() => {
215
+ if (browser && src && scrollContainerEl && mounted) {
216
+ loadPdf(src);
217
+ }
218
+ });
219
+
220
+ onDestroy(() => {
221
+ if (viewer) {
222
+ viewer.destroy();
223
+ viewer = null;
224
+ }
225
+ findController = null;
226
+ });
227
+ </script>
228
+
229
+ <div bind:this={hostEl} class="pdf-renderer-host"></div>
230
+
231
+ <style>
232
+ .pdf-renderer-host {
233
+ display: block;
234
+ flex: 1;
235
+ min-height: 0;
236
+ overflow: hidden;
237
+ }
238
+ </style>
@@ -0,0 +1,21 @@
1
+ /** PDF source - can be a URL string, ArrayBuffer, Uint8Array, or Blob */
2
+ export type PdfSource = string | ArrayBuffer | Uint8Array | Blob;
3
+ interface Props {
4
+ /** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
5
+ src: PdfSource;
6
+ /** Background color of the scroll container */
7
+ backgroundColor?: string;
8
+ /** Page shadow style */
9
+ pageShadow?: string;
10
+ /** Scrollbar track color */
11
+ scrollbarTrackColor?: string;
12
+ /** Scrollbar thumb color */
13
+ scrollbarThumbColor?: string;
14
+ /** Scrollbar thumb hover color */
15
+ scrollbarThumbHoverColor?: string;
16
+ /** Scrollbar width */
17
+ scrollbarWidth?: string;
18
+ }
19
+ declare const PdfRenderer: import("svelte").Component<Props, {}, "">;
20
+ type PdfRenderer = ReturnType<typeof PdfRenderer>;
21
+ export default PdfRenderer;