reflex-pdfslick 0.0.1__py3-none-any.whl
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.
- reflex_pdfslick/PdfSlickWrapper.tsx +268 -0
- reflex_pdfslick/__init__.py +31 -0
- reflex_pdfslick/commands.py +98 -0
- reflex_pdfslick/models.py +47 -0
- reflex_pdfslick/options.py +41 -0
- reflex_pdfslick/pdfslick.py +80 -0
- reflex_pdfslick/pdfslick.pyi +113 -0
- reflex_pdfslick-0.0.1.dist-info/METADATA +262 -0
- reflex_pdfslick-0.0.1.dist-info/RECORD +12 -0
- reflex_pdfslick-0.0.1.dist-info/WHEEL +5 -0
- reflex_pdfslick-0.0.1.dist-info/licenses/LICENSE +21 -0
- reflex_pdfslick-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdfSlickWrapper — local React wrapper around `@pdfslick/react`.
|
|
3
|
+
*
|
|
4
|
+
* PDFSlick's public surface is the hook `usePDFSlick(url, options)`, not a
|
|
5
|
+
* drop-in component, and `<PDFSlickThumbnails>` uses a render-prop child that
|
|
6
|
+
* cannot be expressed from Python. This thin wrapper calls the hook internally,
|
|
7
|
+
* renders `<PDFSlickViewer>` (and optional thumbnails), subscribes to the
|
|
8
|
+
* Zustand store via `usePDFSlickStore`, and exposes a clean prop/event API for
|
|
9
|
+
* the Reflex `PdfSlick` component to bind to.
|
|
10
|
+
*
|
|
11
|
+
* It is browser-only (PDF.js), so the Reflex side wraps it as a NoSSRComponent.
|
|
12
|
+
*
|
|
13
|
+
* Responsibilities by phase:
|
|
14
|
+
* - Phase 1: call the hook, render the viewer, import CSS.
|
|
15
|
+
* - Phase 2: subscribe to store slices, forward serializable scalars to Reflex
|
|
16
|
+
* events (page, scale, load, error, metadata). Only serializable values
|
|
17
|
+
* cross the boundary; Dates become ISO strings, Maps become arrays.
|
|
18
|
+
* - Phase 3: apply declarative `command` objects (goto/zoom/rotate/modes/
|
|
19
|
+
* print/download) against the imperative `pdfSlick` instance, apply-once.
|
|
20
|
+
* - Phase 4: render an optional thumbnails sidebar (vertical or horizontal).
|
|
21
|
+
*/
|
|
22
|
+
import * as React from "react";
|
|
23
|
+
import { GlobalWorkerOptions } from "pdfjs-dist";
|
|
24
|
+
// Vite resolves `?url` to a served/hashed asset URL for the worker file. This
|
|
25
|
+
// uses the SAME hoisted `pdfjs-dist` instance that `@pdfslick/core` imports, so
|
|
26
|
+
// the worker version always matches the PDF.js API version.
|
|
27
|
+
import pdfWorkerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
|
|
28
|
+
import { usePDFSlick, PDFSlickViewer, PDFSlickThumbnails } from "@pdfslick/react";
|
|
29
|
+
import "@pdfslick/react/dist/pdf_viewer.css";
|
|
30
|
+
|
|
31
|
+
// PDF.js worker fix. `@pdfslick/core` sets
|
|
32
|
+
// GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url)
|
|
33
|
+
// at module load, which resolves RELATIVE to the core module directory
|
|
34
|
+
// (.../@pdfslick/core/dist/esm/pdfjs-dist/build/...) — a path that does not
|
|
35
|
+
// exist under Reflex's Vite server, so the worker fails to load ("Setting up
|
|
36
|
+
// fake worker failed"). All ES imports above are evaluated before this module
|
|
37
|
+
// body runs, so core's (broken) assignment has already happened; we override it
|
|
38
|
+
// here with the correctly-resolved worker URL on the same GlobalWorkerOptions.
|
|
39
|
+
if (pdfWorkerUrl) {
|
|
40
|
+
GlobalWorkerOptions.workerSrc = pdfWorkerUrl as string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type PdfCommand = {
|
|
44
|
+
action?: string;
|
|
45
|
+
page?: number;
|
|
46
|
+
value?: number | string;
|
|
47
|
+
mode?: number;
|
|
48
|
+
degrees?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type PdfSlickWrapperProps = {
|
|
52
|
+
url: string;
|
|
53
|
+
scaleValue?: string;
|
|
54
|
+
singlePageViewer?: boolean;
|
|
55
|
+
removePageBorders?: boolean;
|
|
56
|
+
thumbnailWidth?: number;
|
|
57
|
+
showThumbnails?: boolean;
|
|
58
|
+
horizontalThumbnails?: boolean;
|
|
59
|
+
command?: PdfCommand | null;
|
|
60
|
+
onDocumentLoad?: (numPages: number) => void;
|
|
61
|
+
onPageChange?: (page: number) => void;
|
|
62
|
+
onScaleChange?: (scale: number) => void;
|
|
63
|
+
onError?: (err: { name: string; message: string }) => void;
|
|
64
|
+
onMetadata?: (meta: Record<string, unknown>) => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Convert a possible Date to an ISO string; pass through null/undefined. */
|
|
68
|
+
function toIso(value: unknown): string | null {
|
|
69
|
+
if (value instanceof Date && !isNaN(value.getTime())) return value.toISOString();
|
|
70
|
+
if (typeof value === "string" && value) return value;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Normalize an unknown error into a serializable {name, message} object. */
|
|
75
|
+
function normalizeError(error: unknown): { name: string; message: string } {
|
|
76
|
+
if (error && typeof error === "object") {
|
|
77
|
+
const e = error as { name?: string; message?: string };
|
|
78
|
+
return { name: e.name ?? "Error", message: e.message ?? String(error) };
|
|
79
|
+
}
|
|
80
|
+
return { name: "Error", message: String(error) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function PdfSlickWrapper({
|
|
84
|
+
url,
|
|
85
|
+
scaleValue = "page-fit",
|
|
86
|
+
singlePageViewer = false,
|
|
87
|
+
removePageBorders = false,
|
|
88
|
+
thumbnailWidth = 212,
|
|
89
|
+
showThumbnails = false,
|
|
90
|
+
horizontalThumbnails = false,
|
|
91
|
+
command = null,
|
|
92
|
+
onDocumentLoad,
|
|
93
|
+
onPageChange,
|
|
94
|
+
onScaleChange,
|
|
95
|
+
onError,
|
|
96
|
+
onMetadata,
|
|
97
|
+
}: PdfSlickWrapperProps) {
|
|
98
|
+
const { viewerRef, thumbsRef, usePDFSlickStore, error } = usePDFSlick(url, {
|
|
99
|
+
scaleValue,
|
|
100
|
+
singlePageViewer,
|
|
101
|
+
removePageBorders,
|
|
102
|
+
thumbnailWidth,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const pageNumber = usePDFSlickStore((s) => s.pageNumber);
|
|
106
|
+
const numPages = usePDFSlickStore((s) => s.numPages);
|
|
107
|
+
const scale = usePDFSlickStore((s) => s.scale);
|
|
108
|
+
const isLoaded = usePDFSlickStore((s) => s.isDocumentLoaded);
|
|
109
|
+
const pdfSlick = usePDFSlickStore((s) => s.pdfSlick);
|
|
110
|
+
const store = usePDFSlickStore((s) => s);
|
|
111
|
+
|
|
112
|
+
// --- Phase 2: forward store changes to Reflex events --------------------
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
if (!isLoaded) return;
|
|
115
|
+
onDocumentLoad?.(numPages);
|
|
116
|
+
if (onMetadata) {
|
|
117
|
+
onMetadata({
|
|
118
|
+
numPages,
|
|
119
|
+
title: store.title ?? "",
|
|
120
|
+
author: store.author ?? "",
|
|
121
|
+
subject: store.subject ?? "",
|
|
122
|
+
creator: store.creator ?? "",
|
|
123
|
+
producer: store.producer ?? "",
|
|
124
|
+
version: store.version ?? "",
|
|
125
|
+
filename: store.filename ?? "",
|
|
126
|
+
filesize: store.filesize ?? 0,
|
|
127
|
+
isLinearized: !!store.isLinearized,
|
|
128
|
+
creationDate: toIso(store.creationDate),
|
|
129
|
+
modificationDate: toIso(store.modificationDate),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
+
}, [isLoaded, numPages]);
|
|
134
|
+
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
onPageChange?.(pageNumber);
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [pageNumber]);
|
|
139
|
+
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
onScaleChange?.(scale);
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}, [scale]);
|
|
144
|
+
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
if (error) onError?.(normalizeError(error));
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [error]);
|
|
149
|
+
|
|
150
|
+
// --- Phase 3: apply declarative commands once ---------------------------
|
|
151
|
+
const lastCommandKey = React.useRef<string>("");
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
if (!command || !command.action || !pdfSlick) return;
|
|
154
|
+
const key = JSON.stringify(command);
|
|
155
|
+
if (key === lastCommandKey.current) return;
|
|
156
|
+
lastCommandKey.current = key;
|
|
157
|
+
|
|
158
|
+
const ps = pdfSlick as any;
|
|
159
|
+
const current = store.pageNumber ?? 1;
|
|
160
|
+
const total = store.numPages ?? 1;
|
|
161
|
+
switch (command.action) {
|
|
162
|
+
case "goto":
|
|
163
|
+
if (typeof command.page === "number") ps.gotoPage(command.page);
|
|
164
|
+
break;
|
|
165
|
+
case "next":
|
|
166
|
+
ps.gotoPage(Math.min(total, current + 1));
|
|
167
|
+
break;
|
|
168
|
+
case "prev":
|
|
169
|
+
ps.gotoPage(Math.max(1, current - 1));
|
|
170
|
+
break;
|
|
171
|
+
case "zoomIn":
|
|
172
|
+
(ps.viewer ?? ps).increaseScale?.();
|
|
173
|
+
break;
|
|
174
|
+
case "zoomOut":
|
|
175
|
+
(ps.viewer ?? ps).decreaseScale?.();
|
|
176
|
+
break;
|
|
177
|
+
case "zoomPreset":
|
|
178
|
+
if (command.value != null) ps.currentScaleValue = String(command.value);
|
|
179
|
+
break;
|
|
180
|
+
case "zoomValue":
|
|
181
|
+
if (command.value != null) ps.currentScale = Number(command.value);
|
|
182
|
+
break;
|
|
183
|
+
case "rotate":
|
|
184
|
+
if (typeof command.degrees === "number") {
|
|
185
|
+
const base = store.pagesRotation ?? 0;
|
|
186
|
+
ps.setRotation(base + command.degrees);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case "scrollMode":
|
|
190
|
+
if (typeof command.mode === "number") ps.setScrollMode(command.mode);
|
|
191
|
+
break;
|
|
192
|
+
case "spreadMode":
|
|
193
|
+
if (typeof command.mode === "number") ps.setSpreadMode(command.mode);
|
|
194
|
+
break;
|
|
195
|
+
case "print":
|
|
196
|
+
ps.triggerPrinting?.();
|
|
197
|
+
break;
|
|
198
|
+
case "download":
|
|
199
|
+
ps.downloadOrSave?.();
|
|
200
|
+
break;
|
|
201
|
+
default:
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
205
|
+
}, [command, pdfSlick]);
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
return (
|
|
209
|
+
<div className="pdfSlick" style={{ padding: "1rem", color: "#b91c1c" }}>
|
|
210
|
+
Failed to load PDF: {normalizeError(error).message}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const horizontal = showThumbnails && horizontalThumbnails;
|
|
216
|
+
const thumbsPanel = showThumbnails ? (
|
|
217
|
+
<div
|
|
218
|
+
ref={thumbsRef as any}
|
|
219
|
+
className="pdfSlickThumbs"
|
|
220
|
+
style={
|
|
221
|
+
horizontal
|
|
222
|
+
? { height: 160, width: "100%", overflowX: "auto", display: "flex", gap: 8, padding: 8 }
|
|
223
|
+
: { width: 240, height: "100%", overflowY: "auto", padding: 8 }
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
<PDFSlickThumbnails {...{ thumbsRef, usePDFSlickStore }}>
|
|
227
|
+
{({ pageNumber: pn, src, pageLabel, loaded }) => (
|
|
228
|
+
<div
|
|
229
|
+
key={pn}
|
|
230
|
+
onClick={() => pdfSlick?.gotoPage(pn)}
|
|
231
|
+
style={{
|
|
232
|
+
cursor: "pointer",
|
|
233
|
+
padding: 4,
|
|
234
|
+
outline: pn === pageNumber ? "2px solid #2563eb" : "1px solid #e5e7eb",
|
|
235
|
+
borderRadius: 4,
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{loaded && src ? (
|
|
239
|
+
<img src={src} alt={`Page ${pageLabel ?? pn}`} style={{ display: "block", width: "100%" }} />
|
|
240
|
+
) : (
|
|
241
|
+
<div style={{ height: horizontal ? 120 : 260, width: 160, background: "#e5e7eb" }} />
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</PDFSlickThumbnails>
|
|
246
|
+
</div>
|
|
247
|
+
) : null;
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
className="pdfSlick"
|
|
252
|
+
style={{
|
|
253
|
+
position: "absolute",
|
|
254
|
+
inset: 0,
|
|
255
|
+
display: "flex",
|
|
256
|
+
flexDirection: horizontal ? "column" : "row",
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{showThumbnails && !horizontal && thumbsPanel}
|
|
260
|
+
{showThumbnails && horizontal && thumbsPanel}
|
|
261
|
+
<div style={{ flex: 1, position: "relative" }}>
|
|
262
|
+
<PDFSlickViewer {...{ viewerRef, usePDFSlickStore }} />
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default PdfSlickWrapper;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""reflex-pdfslick — a native Reflex component wrapping PDFSlick (PDF.js + Zustand).
|
|
2
|
+
|
|
3
|
+
Public API (stable surface):
|
|
4
|
+
|
|
5
|
+
from reflex_pdfslick import pdf_slick, PdfSlick
|
|
6
|
+
from reflex_pdfslick import ScrollMode, SpreadMode, ScaleValue
|
|
7
|
+
from reflex_pdfslick import PdfSlickOptions
|
|
8
|
+
from reflex_pdfslick import commands
|
|
9
|
+
|
|
10
|
+
``pdf_slick(url=...)`` renders a reactive PDF viewer; viewer state flows into
|
|
11
|
+
Reflex via events (``on_page_change``, ``on_document_load``, ``on_scale_change``,
|
|
12
|
+
``on_error``, ``on_metadata``) and Python drives the viewer through the
|
|
13
|
+
declarative ``command`` prop built with the ``commands`` helpers.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from . import commands
|
|
17
|
+
from .models import ScaleValue, ScrollMode, SpreadMode
|
|
18
|
+
from .options import PdfSlickOptions
|
|
19
|
+
from .pdfslick import PdfSlick, pdf_slick
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"PdfSlick",
|
|
23
|
+
"pdf_slick",
|
|
24
|
+
"PdfSlickOptions",
|
|
25
|
+
"ScaleValue",
|
|
26
|
+
"ScrollMode",
|
|
27
|
+
"SpreadMode",
|
|
28
|
+
"commands",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Declarative imperative-control commands for ``PdfSlick``.
|
|
2
|
+
|
|
3
|
+
PDFSlick's imperative actions live on the ``pdfSlick`` store instance, which
|
|
4
|
+
stays in JS. To drive them from Python we send a small JSON ``command`` object
|
|
5
|
+
into the component's ``command`` prop; the JS wrapper watches it and applies the
|
|
6
|
+
matching action once (apply-then-clear by command identity).
|
|
7
|
+
|
|
8
|
+
These helpers build those objects so app code never hand-writes the action
|
|
9
|
+
strings. Bind the result to ``PdfSlick(..., command=State.command)`` and set
|
|
10
|
+
``State.command`` from an event handler::
|
|
11
|
+
|
|
12
|
+
@rx.event
|
|
13
|
+
def go_next(self):
|
|
14
|
+
self.command = commands.next_page()
|
|
15
|
+
|
|
16
|
+
The action vocabulary mirrors ``specs/api/component-api.md`` §4 and the switch in
|
|
17
|
+
``js/PdfSlickWrapper.tsx``. ``None`` is the idle/cleared value (no-op).
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
- ``specs/api/component-api.md`` §4
|
|
21
|
+
- ``specs/technical/architecture.md`` §4.5
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
Command = dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def goto_page(page: int) -> Command:
|
|
32
|
+
"""Jump to a 1-based page number (``pdfSlick.gotoPage(page)``)."""
|
|
33
|
+
return {"action": "goto", "page": int(page)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def next_page() -> Command:
|
|
37
|
+
"""Go to the next page (clamped to the last page)."""
|
|
38
|
+
return {"action": "next"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prev_page() -> Command:
|
|
42
|
+
"""Go to the previous page (clamped to the first page)."""
|
|
43
|
+
return {"action": "prev"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def zoom_in() -> Command:
|
|
47
|
+
"""Step zoom in (``viewer.increaseScale()``)."""
|
|
48
|
+
return {"action": "zoomIn"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def zoom_out() -> Command:
|
|
52
|
+
"""Step zoom out (``viewer.decreaseScale()``)."""
|
|
53
|
+
return {"action": "zoomOut"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_zoom_preset(preset: str) -> Command:
|
|
57
|
+
"""Set a named zoom preset (``pdfSlick.currentScaleValue = preset``).
|
|
58
|
+
|
|
59
|
+
``preset`` is one of ``"auto" | "page-actual" | "page-fit" | "page-width"``
|
|
60
|
+
(see :class:`~reflex_pdfslick.ScaleValue`).
|
|
61
|
+
"""
|
|
62
|
+
return {"action": "zoomPreset", "value": str(preset)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_zoom_value(scale: float) -> Command:
|
|
66
|
+
"""Set an absolute numeric zoom (``pdfSlick.currentScale = scale``)."""
|
|
67
|
+
return {"action": "zoomValue", "value": float(scale)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def rotate(degrees: int) -> Command:
|
|
71
|
+
"""Rotate pages by a relative delta in degrees (e.g. ``90`` / ``-90``)."""
|
|
72
|
+
return {"action": "rotate", "degrees": int(degrees)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_scroll_mode(mode: int) -> Command:
|
|
76
|
+
"""Set the scroll mode (``pdfSlick.setScrollMode``).
|
|
77
|
+
|
|
78
|
+
Accepts a :class:`~reflex_pdfslick.ScrollMode` or a plain int.
|
|
79
|
+
"""
|
|
80
|
+
return {"action": "scrollMode", "mode": int(mode)}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def set_spread_mode(mode: int) -> Command:
|
|
84
|
+
"""Set the spread mode (``pdfSlick.setSpreadMode``).
|
|
85
|
+
|
|
86
|
+
Accepts a :class:`~reflex_pdfslick.SpreadMode` or a plain int.
|
|
87
|
+
"""
|
|
88
|
+
return {"action": "spreadMode", "mode": int(mode)}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def print_document() -> Command:
|
|
92
|
+
"""Open the print dialog (``pdfSlick.triggerPrinting()``)."""
|
|
93
|
+
return {"action": "print"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def download() -> Command:
|
|
97
|
+
"""Download / save the document (``pdfSlick.downloadOrSave()``)."""
|
|
98
|
+
return {"action": "download"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Enums and value types mirroring PDFSlick / PDF.js constants.
|
|
2
|
+
|
|
3
|
+
These mirror the numeric enums re-exported by ``@pdfslick/react`` (originally
|
|
4
|
+
from PDF.js ``ui_utils``) and the accepted ``scaleValue`` strings. Keeping them
|
|
5
|
+
in Python lets the demo app and downstream users avoid magic numbers.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- ``specs/data-model/store-schema.md``
|
|
9
|
+
- ``specs/api/component-api.md``
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from enum import Enum, IntEnum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScrollMode(IntEnum):
|
|
18
|
+
"""PDF.js scroll modes (values match ``pdfjs-dist`` ``ScrollMode``)."""
|
|
19
|
+
|
|
20
|
+
VERTICAL = 0
|
|
21
|
+
HORIZONTAL = 1
|
|
22
|
+
WRAPPED = 2
|
|
23
|
+
PAGE = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpreadMode(IntEnum):
|
|
27
|
+
"""PDF.js spread modes (values match ``pdfjs-dist`` ``SpreadMode``)."""
|
|
28
|
+
|
|
29
|
+
NONE = 0
|
|
30
|
+
ODD = 1
|
|
31
|
+
EVEN = 2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ScaleValue(str, Enum):
|
|
35
|
+
"""Named zoom presets accepted by PDFSlick's ``scaleValue`` option.
|
|
36
|
+
|
|
37
|
+
A numeric string (e.g. ``"1.5"``) is also valid and maps to an absolute
|
|
38
|
+
scale; use it directly as a plain ``str`` where a preset is not enough.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
AUTO = "auto"
|
|
42
|
+
PAGE_ACTUAL = "page-actual"
|
|
43
|
+
PAGE_FIT = "page-fit"
|
|
44
|
+
PAGE_WIDTH = "page-width"
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str: # pragma: no cover - convenience
|
|
47
|
+
return self.value
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Typed PDFSlick options object.
|
|
2
|
+
|
|
3
|
+
Mirrors the ``PDFSlickOptions`` type from ``@pdfslick/core@4.x``. Subclassing
|
|
4
|
+
``rx.PropsBase`` means snake_case Python fields are auto-converted to the
|
|
5
|
+
camelCase keys PDFSlick expects when serialized to a JS object prop.
|
|
6
|
+
|
|
7
|
+
Only the most commonly used options are surfaced as first-class fields; the full
|
|
8
|
+
upstream option set is documented in ``specs/api/component-api.md``. Unknown or
|
|
9
|
+
advanced keys can still be passed through ``extra``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
import reflex as rx
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PdfSlickOptions(rx.PropsBase):
|
|
20
|
+
"""Options forwarded to ``usePDFSlick(url, options)``.
|
|
21
|
+
|
|
22
|
+
Field names are snake_case in Python and emitted as camelCase to JS, e.g.
|
|
23
|
+
``single_page_viewer`` -> ``singlePageViewer``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
scale_value: Optional[str] = None # "auto" | "page-fit" | "page-width" | "page-actual" | numeric
|
|
27
|
+
single_page_viewer: Optional[bool] = None
|
|
28
|
+
remove_page_borders: Optional[bool] = None
|
|
29
|
+
enable_print_auto_rotate: Optional[bool] = None
|
|
30
|
+
use_only_css_zoom: Optional[bool] = None
|
|
31
|
+
text_layer_mode: Optional[int] = None
|
|
32
|
+
annotation_mode: Optional[int] = None
|
|
33
|
+
annotation_editor_mode: Optional[int] = None
|
|
34
|
+
thumbnail_width: Optional[int] = None
|
|
35
|
+
print_resolution: Optional[int] = None
|
|
36
|
+
max_canvas_pixels: Optional[int] = None
|
|
37
|
+
filename: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
# Escape hatch for advanced/less-common upstream options (e.g.
|
|
40
|
+
# ``getDocumentParams``). Merged verbatim into the JS options object.
|
|
41
|
+
extra: dict[str, Any] = {}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The ``PdfSlick`` Reflex component.
|
|
2
|
+
|
|
3
|
+
PDFSlick's public entry point is the **hook** ``usePDFSlick(url, options)``, not
|
|
4
|
+
a drop-in component, and its ``<PDFSlickThumbnails>`` uses a render-prop child
|
|
5
|
+
that cannot be expressed from Python. So we ship a small local React wrapper
|
|
6
|
+
(``PdfSlickWrapper.tsx``, beside this module) that calls the hook internally,
|
|
7
|
+
renders
|
|
8
|
+
``<PDFSlickViewer>`` (+ optional thumbnails), subscribes to ``usePDFSlickStore``
|
|
9
|
+
selectors, and exposes a clean prop/event API. See
|
|
10
|
+
``specs/technical/architecture.md`` for the full rationale.
|
|
11
|
+
|
|
12
|
+
Wiring (Reflex 0.9.x):
|
|
13
|
+
|
|
14
|
+
- ``library`` points at the local wrapper via ``rx.asset(..., shared=True)``'s
|
|
15
|
+
``$/public`` importable path; the npm package is declared in
|
|
16
|
+
``lib_dependencies`` so Reflex installs it.
|
|
17
|
+
- PDF.js is browser-only (touches ``window``/``document``/canvas), so the
|
|
18
|
+
component is a ``rx.NoSSRComponent`` (emits ``next/dynamic(..., {ssr:false})``).
|
|
19
|
+
- The required stylesheet ``@pdfslick/react/dist/pdf_viewer.css`` is imported
|
|
20
|
+
once via ``add_imports``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import reflex as rx
|
|
28
|
+
|
|
29
|
+
# Pin the upstream npm package for reproducible builds.
|
|
30
|
+
PDFSLICK_REACT = "@pdfslick/react@4.0.0"
|
|
31
|
+
PDFSLICK_CSS = "@pdfslick/react/dist/pdf_viewer.css"
|
|
32
|
+
# Declared so the wrapper can import the PDF.js worker as a Vite asset and
|
|
33
|
+
# override PDFSlick's broken default ``workerSrc``. The caret range matches
|
|
34
|
+
# ``@pdfslick/core``'s own dependency so the installer resolves a single shared
|
|
35
|
+
# copy — guaranteeing the worker version matches the PDF.js API version.
|
|
36
|
+
PDFJS_DIST = "pdfjs-dist@^6.0.227"
|
|
37
|
+
|
|
38
|
+
# Resolve the local JS wrapper into a build-time importable module path
|
|
39
|
+
# ("$/public/...") so Reflex bundles our source instead of trying to import a
|
|
40
|
+
# named export from the npm package.
|
|
41
|
+
_WRAPPER_ASSET = rx.asset("PdfSlickWrapper.tsx", shared=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PdfSlick(rx.NoSSRComponent):
|
|
45
|
+
"""Reactive PDF viewer wrapping ``@pdfslick/react``."""
|
|
46
|
+
|
|
47
|
+
# --- Library wiring -----------------------------------------------------
|
|
48
|
+
library = _WRAPPER_ASSET.importable_path
|
|
49
|
+
tag = "PdfSlickWrapper"
|
|
50
|
+
is_default = True
|
|
51
|
+
lib_dependencies = [PDFSLICK_REACT, PDFJS_DIST]
|
|
52
|
+
|
|
53
|
+
# --- Props (camelCased to JS automatically) -----------------------------
|
|
54
|
+
url: rx.Var[str]
|
|
55
|
+
scale_value: rx.Var[str]
|
|
56
|
+
single_page_viewer: rx.Var[bool]
|
|
57
|
+
remove_page_borders: rx.Var[bool]
|
|
58
|
+
thumbnail_width: rx.Var[int]
|
|
59
|
+
show_thumbnails: rx.Var[bool]
|
|
60
|
+
horizontal_thumbnails: rx.Var[bool]
|
|
61
|
+
|
|
62
|
+
# Declarative imperative-control channel (Phase 3): a command object the
|
|
63
|
+
# wrapper applies then clears. Built with helpers in ``commands.py``.
|
|
64
|
+
command: rx.Var[dict[str, Any]]
|
|
65
|
+
|
|
66
|
+
# --- Events -------------------------------------------------------------
|
|
67
|
+
# Each forwards the JS callback's first argument to the Python handler.
|
|
68
|
+
on_document_load: rx.EventHandler[lambda num_pages: [num_pages]]
|
|
69
|
+
on_page_change: rx.EventHandler[lambda page: [page]]
|
|
70
|
+
on_scale_change: rx.EventHandler[lambda scale: [scale]]
|
|
71
|
+
on_error: rx.EventHandler[lambda err: [err]]
|
|
72
|
+
on_metadata: rx.EventHandler[lambda meta: [meta]]
|
|
73
|
+
|
|
74
|
+
def add_imports(self) -> dict[str, Any]:
|
|
75
|
+
"""Import PDFSlick's required stylesheet exactly once."""
|
|
76
|
+
return {"": [PDFSLICK_CSS]}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Snake_case factory following Reflex convention.
|
|
80
|
+
pdf_slick = PdfSlick.create
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Stub file for custom_components/reflex_pdfslick/pdfslick.py"""
|
|
2
|
+
|
|
3
|
+
# ------------------- DO NOT EDIT ----------------------
|
|
4
|
+
# This file was generated by `reflex/utils/pyi_generator.py`!
|
|
5
|
+
# ------------------------------------------------------
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
from reflex_components_core.core.breakpoints import Breakpoints
|
|
9
|
+
from reflex_base.event import (
|
|
10
|
+
EventType,
|
|
11
|
+
PointerEventInfo,
|
|
12
|
+
)
|
|
13
|
+
from reflex_base.vars.base import Var
|
|
14
|
+
import reflex as rx
|
|
15
|
+
|
|
16
|
+
PDFSLICK_REACT = "@pdfslick/react@4.0.0"
|
|
17
|
+
PDFSLICK_CSS = "@pdfslick/react/dist/pdf_viewer.css"
|
|
18
|
+
PDFJS_DIST = "pdfjs-dist@^6.0.227"
|
|
19
|
+
_WRAPPER_ASSET = rx.asset("PdfSlickWrapper.tsx", shared=True)
|
|
20
|
+
|
|
21
|
+
class PdfSlick(rx.NoSSRComponent):
|
|
22
|
+
def add_imports(self) -> dict[str, Any]: ...
|
|
23
|
+
@classmethod
|
|
24
|
+
def create(
|
|
25
|
+
cls,
|
|
26
|
+
*children,
|
|
27
|
+
url: Var[str] | str | None = None,
|
|
28
|
+
scale_value: Var[str] | str | None = None,
|
|
29
|
+
single_page_viewer: Var[bool] | bool | None = None,
|
|
30
|
+
remove_page_borders: Var[bool] | bool | None = None,
|
|
31
|
+
thumbnail_width: Var[int] | int | None = None,
|
|
32
|
+
show_thumbnails: Var[bool] | bool | None = None,
|
|
33
|
+
horizontal_thumbnails: Var[bool] | bool | None = None,
|
|
34
|
+
command: Var[dict[str, Any]] | dict[str, Any] | None = None,
|
|
35
|
+
style: Sequence[Mapping[str, Any]]
|
|
36
|
+
| Mapping[str, Any]
|
|
37
|
+
| Var[Mapping[str, Any]]
|
|
38
|
+
| Breakpoints
|
|
39
|
+
| None = None,
|
|
40
|
+
key: Any | None = None,
|
|
41
|
+
id: Any | None = None,
|
|
42
|
+
ref: Var | None = None,
|
|
43
|
+
class_name: Any | None = None,
|
|
44
|
+
custom_attrs: dict[str, Any | Var] | None = None,
|
|
45
|
+
on_blur: Optional[EventType[()]] = None,
|
|
46
|
+
on_click: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
|
|
47
|
+
on_context_menu: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
|
|
48
|
+
on_document_load: Optional[EventType[Any]] = None,
|
|
49
|
+
on_double_click: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
|
|
50
|
+
on_error: Optional[EventType[Any]] = None,
|
|
51
|
+
on_focus: Optional[EventType[()]] = None,
|
|
52
|
+
on_metadata: Optional[EventType[Any]] = None,
|
|
53
|
+
on_mount: Optional[EventType[()]] = None,
|
|
54
|
+
on_mouse_down: Optional[EventType[()]] = None,
|
|
55
|
+
on_mouse_enter: Optional[EventType[()]] = None,
|
|
56
|
+
on_mouse_leave: Optional[EventType[()]] = None,
|
|
57
|
+
on_mouse_move: Optional[EventType[()]] = None,
|
|
58
|
+
on_mouse_out: Optional[EventType[()]] = None,
|
|
59
|
+
on_mouse_over: Optional[EventType[()]] = None,
|
|
60
|
+
on_mouse_up: Optional[EventType[()]] = None,
|
|
61
|
+
on_page_change: Optional[EventType[Any]] = None,
|
|
62
|
+
on_scale_change: Optional[EventType[Any]] = None,
|
|
63
|
+
on_scroll: Optional[EventType[()]] = None,
|
|
64
|
+
on_scroll_end: Optional[EventType[()]] = None,
|
|
65
|
+
on_unmount: Optional[EventType[()]] = None,
|
|
66
|
+
**props,
|
|
67
|
+
) -> "PdfSlick":
|
|
68
|
+
"""Create the component.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
*children: The children of the component.
|
|
72
|
+
url: --- Props (camelCased to JS automatically) -----------------------------
|
|
73
|
+
scale_value: no description
|
|
74
|
+
single_page_viewer: no description
|
|
75
|
+
remove_page_borders: no description
|
|
76
|
+
thumbnail_width: no description
|
|
77
|
+
show_thumbnails: no description
|
|
78
|
+
horizontal_thumbnails: no description
|
|
79
|
+
command: Declarative imperative-control channel (Phase 3): a command object the wrapper applies then clears. Built with helpers in ``commands.py``.
|
|
80
|
+
style: The style of the component.
|
|
81
|
+
key: A unique key for the component.
|
|
82
|
+
id: The id for the component.
|
|
83
|
+
ref: The Var to pass as the ref to the component.
|
|
84
|
+
class_name: The class name for the component.
|
|
85
|
+
custom_attrs: Attributes passed directly to the component.
|
|
86
|
+
on_focus: Fired when the element (or some element inside of it) receives focus. For example, it is called when the user clicks on a text input.
|
|
87
|
+
on_blur: Fired when focus has left the element (or left some element inside of it). For example, it is called when the user clicks outside of a focused text input.
|
|
88
|
+
on_click: Fired when the user clicks on an element. For example, it's called when the user clicks on a button.
|
|
89
|
+
on_context_menu: Fired when the user right-clicks on an element.
|
|
90
|
+
on_double_click: Fired when the user double-clicks on an element.
|
|
91
|
+
on_mouse_down: Fired when the user presses a mouse button on an element.
|
|
92
|
+
on_mouse_enter: Fired when the mouse pointer enters the element.
|
|
93
|
+
on_mouse_leave: Fired when the mouse pointer leaves the element.
|
|
94
|
+
on_mouse_move: Fired when the mouse pointer moves over the element.
|
|
95
|
+
on_mouse_out: Fired when the mouse pointer moves out of the element.
|
|
96
|
+
on_mouse_over: Fired when the mouse pointer moves onto the element.
|
|
97
|
+
on_mouse_up: Fired when the user releases a mouse button on an element.
|
|
98
|
+
on_scroll: Fired when the user scrolls the element.
|
|
99
|
+
on_scroll_end: Fired when scrolling ends on the element.
|
|
100
|
+
on_mount: Fired when the component is mounted to the page.
|
|
101
|
+
on_unmount: Fired when the component is removed from the page. Only called during navigation, not on page refresh.
|
|
102
|
+
on_document_load: --- Events ------------------------------------------------------------- Each forwards the JS callback's first argument to the Python handler.
|
|
103
|
+
on_page_change: no description
|
|
104
|
+
on_scale_change: no description
|
|
105
|
+
on_error: no description
|
|
106
|
+
on_metadata: no description
|
|
107
|
+
**props: The props of the component.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The component."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
pdf_slick = PdfSlick.create
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reflex-pdfslick
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A native Reflex component wrapping PDFSlick (PDF.js + Zustand) to view and interact with PDF documents in pure Python.
|
|
5
|
+
Author-email: Ernesto Crespo <ecrespo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ecrespo/reflex-pdfslick
|
|
8
|
+
Project-URL: Repository, https://github.com/ecrespo/reflex-pdfslick
|
|
9
|
+
Project-URL: Issues, https://github.com/ecrespo/reflex-pdfslick/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/ecrespo/reflex-pdfslick#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/ecrespo/reflex-pdfslick/releases
|
|
12
|
+
Project-URL: PyPI, https://pypi.org/project/reflex-pdfslick/
|
|
13
|
+
Project-URL: PDFSlick, https://pdfslick.dev/
|
|
14
|
+
Keywords: reflex,reflex-custom-components,pdf,pdfslick,pdfjs,pdf-viewer,component,viewer,thumbnails
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Framework :: AsyncIO
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: reflex>=0.9.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: build; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# reflex-pdfslick
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/reflex-pdfslick/)
|
|
34
|
+
[](https://pypi.org/project/reflex-pdfslick/)
|
|
35
|
+
[](https://github.com/ecrespo/reflex-pdfslick/blob/main/LICENSE)
|
|
36
|
+
[](https://github.com/ecrespo/reflex-pdfslick)
|
|
37
|
+
|
|
38
|
+
- **Source:** https://github.com/ecrespo/reflex-pdfslick
|
|
39
|
+
- **Issues:** https://github.com/ecrespo/reflex-pdfslick/issues
|
|
40
|
+
- **PDFSlick (upstream):** https://github.com/pdfslick/pdfslick — https://pdfslick.dev
|
|
41
|
+
|
|
42
|
+
A native [Reflex](https://reflex.dev/) component that wraps
|
|
43
|
+
[**PDFSlick**](https://pdfslick.dev/) so you can view and interact with PDF
|
|
44
|
+
documents in **pure Python** — no HTML, no `iframe`, no hand-written
|
|
45
|
+
JavaScript.
|
|
46
|
+
|
|
47
|
+
PDFSlick is built on top of Mozilla's [PDF.js](https://github.com/mozilla/pdf.js)
|
|
48
|
+
and uses [Zustand](https://github.com/pmndrs/zustand) to expose a reactive store
|
|
49
|
+
for the loaded document. `reflex-pdfslick` bridges that store and PDFSlick's
|
|
50
|
+
imperative API into Reflex's declarative, state-driven model: document state
|
|
51
|
+
(page number, page count, zoom, rotation, metadata, thumbnails) flows back to
|
|
52
|
+
Python as events, and Python triggers actions (go to page, zoom, rotate, print,
|
|
53
|
+
download) on the viewer.
|
|
54
|
+
|
|
55
|
+
> **Project status — `ALPHA` / implemented, pre-release.**
|
|
56
|
+
> The component runtime, imperative controls, and the full 7-example demo
|
|
57
|
+
> gallery are built and covered by tests; the demo compiles to a Reflex
|
|
58
|
+
> production build in CI. The remaining step is the PyPI/GitHub release. See
|
|
59
|
+
> [Roadmap](#roadmap) for details.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import reflex as rx
|
|
63
|
+
from reflex_pdfslick import pdf_slick
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class State(rx.State):
|
|
67
|
+
page_number: int = 1
|
|
68
|
+
num_pages: int = 0
|
|
69
|
+
|
|
70
|
+
@rx.event
|
|
71
|
+
def on_page_change(self, page: int):
|
|
72
|
+
self.page_number = page
|
|
73
|
+
|
|
74
|
+
@rx.event
|
|
75
|
+
def on_document_load(self, num_pages: int):
|
|
76
|
+
self.num_pages = num_pages
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def index() -> rx.Component:
|
|
80
|
+
return pdf_slick(
|
|
81
|
+
url="/sample.pdf",
|
|
82
|
+
scale_value="page-fit",
|
|
83
|
+
single_page_viewer=True,
|
|
84
|
+
on_page_change=State.on_page_change,
|
|
85
|
+
on_document_load=State.on_document_load,
|
|
86
|
+
height="700px",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
app = rx.App()
|
|
91
|
+
app.add_page(index)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Why a native component instead of an `iframe`?
|
|
95
|
+
|
|
96
|
+
Embedding a PDF viewer through an `iframe` or a raw `<embed>` works, but it is
|
|
97
|
+
isolated from your app: you cannot react to page changes, drive the zoom from a
|
|
98
|
+
Python toolbar, or read the document's metadata into your state. Wrapping
|
|
99
|
+
PDFSlick as a first-class Reflex component means:
|
|
100
|
+
|
|
101
|
+
- **Reactive state** — `page_number`, `num_pages`, `scale`, `rotation`,
|
|
102
|
+
`is_document_loaded` and document metadata are pushed into Reflex state via
|
|
103
|
+
typed events.
|
|
104
|
+
- **Imperative control from Python** — go to page, zoom in/out, fit width/page,
|
|
105
|
+
rotate, switch scroll/spread modes, print and download are exposed as
|
|
106
|
+
component-level controls.
|
|
107
|
+
- **Reusable & versioned** — installable with `pip`, with the npm dependency
|
|
108
|
+
(`@pdfslick/react`) pinned for reproducible builds.
|
|
109
|
+
- **SSR-safe** — PDF.js is browser-only, so the component is wrapped as a
|
|
110
|
+
Reflex `NoSSRComponent` and only ever renders on the client.
|
|
111
|
+
|
|
112
|
+
## Installation
|
|
113
|
+
|
|
114
|
+
```shell
|
|
115
|
+
pip install reflex-pdfslick
|
|
116
|
+
# or, from source
|
|
117
|
+
git clone https://github.com/ecrespo/reflex-pdfslick.git
|
|
118
|
+
cd reflex-pdfslick
|
|
119
|
+
pip install -e .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Reflex installs the underlying npm packages automatically on first run. The
|
|
123
|
+
component pins `@pdfslick/react` and `pdfjs-dist` (which pull in
|
|
124
|
+
`@pdfslick/core` and `zustand`) and imports the required stylesheet
|
|
125
|
+
`@pdfslick/react/dist/pdf_viewer.css` for you.
|
|
126
|
+
|
|
127
|
+
## Custom-component packaging (build & publish)
|
|
128
|
+
|
|
129
|
+
This repository is a standard [Reflex custom
|
|
130
|
+
component](https://reflex.dev/docs/custom-components/overview/): the package
|
|
131
|
+
lives in `custom_components/reflex_pdfslick/` (shipping the `PdfSlickWrapper.tsx`
|
|
132
|
+
wrapper and a generated `pdfslick.pyi` stub), the demo app in `pdfslick_demo/`,
|
|
133
|
+
and `pyproject.toml` carries the `reflex-custom-components` keyword used by the
|
|
134
|
+
Reflex component gallery to discover it on PyPI.
|
|
135
|
+
|
|
136
|
+
```shell
|
|
137
|
+
# Build the wheel + sdist into dist/ (also regenerates the .pyi type stubs).
|
|
138
|
+
# PYTHONPATH=. lets the stub generator import custom_components/.
|
|
139
|
+
PYTHONPATH=. uv run reflex component build
|
|
140
|
+
|
|
141
|
+
# Inspect, then publish to PyPI (bump the version in pyproject.toml first):
|
|
142
|
+
uv run twine upload dist/* # or: uv publish
|
|
143
|
+
|
|
144
|
+
# Share on the Reflex community gallery:
|
|
145
|
+
uv run reflex component share
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The built wheel includes the React wrapper (`reflex_pdfslick/PdfSlickWrapper.tsx`)
|
|
149
|
+
and the type stub, so `rx.asset(...)` resolves the wrapper when the package is
|
|
150
|
+
installed from PyPI — not only in editable mode.
|
|
151
|
+
|
|
152
|
+
## Planned API (summary)
|
|
153
|
+
|
|
154
|
+
The full contract lives in [specs/api/component-api.md](specs/api/component-api.md).
|
|
155
|
+
|
|
156
|
+
| Prop | Type | Description |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `url` | `str` | Path/URL of the PDF to load. |
|
|
159
|
+
| `scale_value` | `str` | `"auto"`, `"page-fit"`, `"page-width"`, `"page-actual"` or a numeric string. |
|
|
160
|
+
| `single_page_viewer` | `bool` | Render a single page at a time. |
|
|
161
|
+
| `thumbnail_width` | `int` | Width (px) of generated thumbnails. |
|
|
162
|
+
| `remove_page_borders` | `bool` | Hide page borders. |
|
|
163
|
+
|
|
164
|
+
| Event | Payload | Fired when |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `on_document_load` | `int` (num pages) | The document finishes loading. |
|
|
167
|
+
| `on_page_change` | `int` (page) | The current page changes. |
|
|
168
|
+
| `on_scale_change` | `float` (scale) | The zoom level changes. |
|
|
169
|
+
| `on_error` | `dict` | Loading/parsing fails. |
|
|
170
|
+
|
|
171
|
+
Imperative controls (next/previous page, zoom, rotate, print, download) are
|
|
172
|
+
driven from Python state — see the API spec for the dispatch mechanism.
|
|
173
|
+
|
|
174
|
+
## Examples (demo gallery)
|
|
175
|
+
|
|
176
|
+
The `pdfslick_demo/` Reflex app mirrors PDFSlick's own example set. Each example
|
|
177
|
+
is documented in the [PRD](specs/prd/pdfslick-component.md) and
|
|
178
|
+
[task breakdown](specs/tasks/task-breakdown.md):
|
|
179
|
+
|
|
180
|
+
1. **Simple Viewer** — basic viewer with prev/next navigation and zoom.
|
|
181
|
+
2. **Full Viewer App** — toolbar, zoom selector, rotation, scroll/spread modes,
|
|
182
|
+
document info, search and thumbnails sidebar.
|
|
183
|
+
3. **Thumbnails Layout** — a grid of page thumbnails as the primary surface.
|
|
184
|
+
4. **Horizontal Thumbnails** — a horizontal thumbnail strip.
|
|
185
|
+
5. **Multiple Documents** — several independent viewers on one page.
|
|
186
|
+
6. **Load from ArrayBuffer** — open a PDF from in-memory bytes.
|
|
187
|
+
7. **Error Handling** — graceful handling of load/parse errors.
|
|
188
|
+
|
|
189
|
+
Run the demo:
|
|
190
|
+
|
|
191
|
+
```shell
|
|
192
|
+
cd pdfslick_demo
|
|
193
|
+
pip install -r requirements.txt
|
|
194
|
+
reflex run
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Architecture (at a glance)
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
Python (Reflex app)
|
|
201
|
+
pdf_slick(url=..., scale_value=..., on_page_change=...)
|
|
202
|
+
│ compiles to
|
|
203
|
+
▼
|
|
204
|
+
React / Next.js (generated by Reflex)
|
|
205
|
+
<PdfSlickWrapper> ← local .tsx, NoSSR, calls usePDFSlick(url, options)
|
|
206
|
+
│ renders <PDFSlickViewer/> + (optional) thumbnails
|
|
207
|
+
│ subscribes to usePDFSlickStore selectors → emits Reflex events
|
|
208
|
+
▼
|
|
209
|
+
@pdfslick/react → @pdfslick/core → PDF.js (pdfjs-dist) + Zustand store
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Because PDFSlick's public surface is the **hook** `usePDFSlick(...)` (not a
|
|
213
|
+
drop-in component) and its thumbnails component uses a render-prop child, the
|
|
214
|
+
wrapper is a small local `.tsx` component that calls the hook internally and
|
|
215
|
+
exposes a clean prop/event API to Python. Full rationale and trade-offs are in
|
|
216
|
+
[specs/technical/architecture.md](specs/technical/architecture.md).
|
|
217
|
+
|
|
218
|
+
## Spec-Driven Design documentation
|
|
219
|
+
|
|
220
|
+
This project is documented before it is built. The `specs/` directory is the
|
|
221
|
+
source of truth:
|
|
222
|
+
|
|
223
|
+
| Document | Purpose |
|
|
224
|
+
|---|---|
|
|
225
|
+
| [`specs/prd/pdfslick-component.md`](specs/prd/pdfslick-component.md) | Product requirements, goals, scope, examples. |
|
|
226
|
+
| [`specs/technical/architecture.md`](specs/technical/architecture.md) | Technical design, SSR strategy, wrapper pattern. |
|
|
227
|
+
| [`specs/api/component-api.md`](specs/api/component-api.md) | Public Python API: props, events, controls. |
|
|
228
|
+
| [`specs/data-model/store-schema.md`](specs/data-model/store-schema.md) | PDFSlick store schema and Python ↔ JS mapping. |
|
|
229
|
+
| [`specs/plans/implementation-plan.md`](specs/plans/implementation-plan.md) | Phases, milestones, sequencing. |
|
|
230
|
+
| [`specs/tasks/task-breakdown.md`](specs/tasks/task-breakdown.md) | Granular, trackable task list per phase. |
|
|
231
|
+
| [`specs/research/pdfslick-and-reflex-research.md`](specs/research/pdfslick-and-reflex-research.md) | Primary-source research notes that ground the design. |
|
|
232
|
+
|
|
233
|
+
## Roadmap
|
|
234
|
+
|
|
235
|
+
- [x] **Phase 0 — Foundation & SDD**: repository, packaging, SDD artifacts,
|
|
236
|
+
README, demo skeleton.
|
|
237
|
+
- [x] **Phase 1 — Core wrapper**: `PdfSlickWrapper.tsx`, `pdf_slick` component,
|
|
238
|
+
props, CSS import, NoSSR.
|
|
239
|
+
- [x] **Phase 2 — Events & state bridge**: page/scale/load/error/metadata events.
|
|
240
|
+
- [x] **Phase 3 — Imperative controls**: navigation, zoom, rotation, modes,
|
|
241
|
+
print, download (declarative `command` prop + `commands` helpers).
|
|
242
|
+
- [x] **Phase 4 — Thumbnails & multi-document**: vertical/horizontal thumbnails,
|
|
243
|
+
independent multi-instance viewers.
|
|
244
|
+
- [x] **Phase 5 — Demo gallery** (all 7 examples) **& docs polish**.
|
|
245
|
+
- [x] **Phase 6 — Tests & CI**: 35 unit tests, GitHub Actions (pytest 3.10–3.13,
|
|
246
|
+
TSX transpile, demo build smoke). Demo compiles to a Reflex production build.
|
|
247
|
+
- [ ] **Release** — PyPI publish + tagged GitHub release *(awaiting maintainer
|
|
248
|
+
credentials)*.
|
|
249
|
+
|
|
250
|
+
## Credits
|
|
251
|
+
|
|
252
|
+
- [PDFSlick](https://pdfslick.dev/) by
|
|
253
|
+
[Vancho Stojkov](https://github.com/van100j) — the library being wrapped (MIT).
|
|
254
|
+
- [PDF.js](https://github.com/mozilla/pdf.js) by Mozilla.
|
|
255
|
+
- [Reflex](https://reflex.dev/) — the Python web framework.
|
|
256
|
+
|
|
257
|
+
This wrapper is an independent project and is not affiliated with or endorsed by
|
|
258
|
+
the PDFSlick authors.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
[MIT](LICENSE) © Ernesto Crespo
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
reflex_pdfslick/PdfSlickWrapper.tsx,sha256=b2snBAZtNqun420O4-Wj_drk1EnZArw49R2-Oift3lk,9399
|
|
2
|
+
reflex_pdfslick/__init__.py,sha256=lBFbdhpNUcYZJgsAEROfObpwL4ANaFXFzJYnCfXLV8A,951
|
|
3
|
+
reflex_pdfslick/commands.py,sha256=TeSq9dv9dhQDs2BnJQvi8mzjBYfbmwIOjXr4oOhMG5A,3011
|
|
4
|
+
reflex_pdfslick/models.py,sha256=6dJn3Y1A33j18iQnv9wysV6dY3ZBchZp_EVhHq6Hysg,1227
|
|
5
|
+
reflex_pdfslick/options.py,sha256=4XdfjLzyNbcmViOwF0lXtVRAZ4bcSmTz3XzP4qF8EDI,1566
|
|
6
|
+
reflex_pdfslick/pdfslick.py,sha256=JcKcR36uYb9um42-r4DmCgHHw2r0Sl9RzIFJX5ytInU,3354
|
|
7
|
+
reflex_pdfslick/pdfslick.pyi,sha256=cWPslTVrGjMuGo-GdZm9QtFOnOSrluJhVxTBvhQOWug,5853
|
|
8
|
+
reflex_pdfslick-0.0.1.dist-info/licenses/LICENSE,sha256=ikDZVzB6LJXytfIXkllDZj9ningVU9BduOAcL8jLKYI,1071
|
|
9
|
+
reflex_pdfslick-0.0.1.dist-info/METADATA,sha256=alXTw9d7ZxZOVLPmsN3KMq2O1lyCTb16queIV5gkC2o,11332
|
|
10
|
+
reflex_pdfslick-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
reflex_pdfslick-0.0.1.dist-info/top_level.txt,sha256=LdjY4d1vmIUeKMRRQ_rJ8y7XuJ7qhsyKfHaHXpnn7zs,16
|
|
12
|
+
reflex_pdfslick-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Crespo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reflex_pdfslick
|