Skip to content

Commit 469edb8

Browse files
committed
Add fullscreen toggle button
1 parent 08dd8fe commit 469edb8

File tree

3 files changed

+94
-44
lines changed

3 files changed

+94
-44
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ In the following table the list of parameters that can be provided to the `pdf_v
9696
| annotation_outline_size | Size of the outline around each annotation in pixels. Defaults to 1 pixel. |
9797
| pages_to_render | Filter the rendering to a specific set of pages. By default, all pages are rendered. |
9898
| render_text | Enable a layer of text on top of the PDF document. The text may be selected and copied. **NOTE** to avoid breaking existing deployments, we made this optional at first, also considering that having many annotations might interfere with the copy-paste. |
99-
| zoom_level | The zoom level of the PDF viewer. Can be a float (0.1-10.0), `"auto"` for fit-to-width, `"auto-height"` for fit-to-height, or `None` (defaults to auto-fit to width). When zoom controls are enabled, users can interactively adjust the zoom level. |
100-
| viewer_align | The alignment of the PDF viewer within its container. Can be `"center"` (default), `"left"`, or `"right"`. |
99+
| zoom_level | The zoom level of the PDF viewer. Can be a float (0.1-10.0), `"auto"` for fit-to-width, `"auto-height"` for fit-to-height, or `None` (defaults to auto-fit to width). When zoom controls are enabled, users can interactively adjust the zoom level. |
100+
| viewer_align | The alignment of the PDF viewer within its container. Can be `"center"` (default), `"left"`, or `"right"`. |
101101
| show_page_separator | Whether to show a horizontal separator line between PDF pages. Defaults to `True`. |
102102
| scroll_to_page | Scroll to a specific page when the component is rendered. The parameter is an integer, which represent the positional value of the page. E.g. 1, will be the first page. Default is None. Require ints and ignores the parameters below zero. |
103103
| scroll_to_annotation | Scroll to a specific annotation when the component is rendered. The parameter is an integer, which represent the positional value of the annotation. E.g. 1, will be the first annotation. Default is None (don't scroll). Mutually exclusive with `scroll_to_page`. Raise an exception if used with `scroll_to_page` |
104104
| on_annotation_click | Callback function that is called when an annotation is clicked. The function receives the annotation as a parameter. |
105+
| show_fullscreen_toggle | Whether to show a button to toggle fullscreen mode. Defaults to `True`. |
105106

106107
### Annotation format
107108

streamlit_pdf_viewer/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def pdf_viewer(
3939
scroll_to_page: Optional[int] = None,
4040
scroll_to_annotation: Optional[int] = None,
4141
on_annotation_click: Optional[Callable[[dict], None]] = None,
42+
show_fullscreen_toggle: bool = True,
4243
):
4344
"""
4445
pdf_viewer function to display a PDF file in a Streamlit app.
@@ -59,6 +60,7 @@ def pdf_viewer(
5960
:param scroll_to_page: Scroll to a specific page in the PDF. The parameter is an integer, which represent the positional value of the page. E.g. 1, will be the first page. Defaults to None.
6061
:param scroll_to_annotation: Scroll to a specific annotation in the PDF. The parameter is an integer, which represent the positional value of the annotation. E.g. 1, will be the first annotation. Defaults to None.
6162
:param on_annotation_click: A callback function that will be called when an annotation is clicked. The function should accept a single argument, which is the annotation that was clicked. Defaults to None.
63+
:param show_fullscreen_toggle: Whether to show button to toggle fullscreen. Defaults to True.
6264
6365
The function reads the PDF file (from a file path, URL, or binary data), encodes it in base64,
6466
and uses a Streamlit component to render it in the app. It supports optional annotations and adjustable margins.
@@ -132,7 +134,8 @@ def pdf_viewer(
132134
viewer_align=viewer_align,
133135
show_page_separator=show_page_separator,
134136
scroll_to_page=scroll_to_page,
135-
scroll_to_annotation=scroll_to_annotation
137+
scroll_to_annotation=scroll_to_annotation,
138+
show_fullscreen_toggle=show_fullscreen_toggle,
136139
)
137140

138141
# Execute the custom callback function

streamlit_pdf_viewer/frontend/src/PdfViewer.vue

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,51 @@
11
<template>
22
<div :style="pdfContainerStyle" ref="pdfContainer" class="container-wrapper">
33
<div id="pdfViewer"></div>
4-
<button class="zoom-button" @click.stop="toggleZoomPanel">
5-
{{ Math.round(currentZoom * 100) }}%
6-
</button>
7-
<div v-if="showZoomPanel" class="zoom-panel">
8-
<div class="zoom-input-container">
9-
<input
10-
type="number"
11-
class="zoom-input"
12-
v-model="manualZoomInput"
13-
@keyup.enter="applyManualZoom"
14-
@blur="applyManualZoom"
15-
/>
16-
<span class="zoom-input-percent">%</span>
4+
<div class="control-buttons">
5+
<div class="top-buttons">
6+
<button v-if="showFullscreen" class="control-button" @click.stop="toggleFullscreen" title="Toggle Fullscreen">
7+
<svg v-if="!isFullscreen" style="width:16px; height:16px; vertical-align: middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
8+
<svg v-else style="width:16px; height:16px; vertical-align: middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
9+
</button>
10+
<button class="control-button" @click.stop="toggleZoomPanel">
11+
{{ Math.round(currentZoom * 100) }}%
12+
</button>
13+
</div>
14+
<div v-if="showZoomPanel" class="zoom-panel">
15+
<div class="zoom-input-container">
16+
<input
17+
type="number"
18+
class="zoom-input"
19+
v-model="manualZoomInput"
20+
@keyup.enter="applyManualZoom"
21+
@blur="applyManualZoom"
22+
/>
23+
<span class="zoom-input-percent">%</span>
24+
</div>
25+
<div class="zoom-separator"></div>
26+
<button class="zoom-option" @click="zoomIn">
27+
<span class="zoom-icon">+</span> Zoom In
28+
</button>
29+
<button class="zoom-option" @click="zoomOut">
30+
<span class="zoom-icon">−</span> Zoom Out
31+
</button>
32+
<div class="zoom-separator"></div>
33+
<button class="zoom-option" @click="fitToWidth">
34+
<span class="zoom-icon">↔</span> Fit to Width
35+
</button>
36+
<button class="zoom-option" @click="fitToHeight">
37+
<span class="zoom-icon">↕</span> Fit to Height
38+
</button>
39+
<button
40+
v-for="preset in zoomPresets"
41+
:key="preset"
42+
class="zoom-option zoom-preset"
43+
:class="{ active: Math.abs(currentZoom - preset) < 0.01 }"
44+
@click="setZoom(preset)"
45+
>
46+
{{ Math.round(preset * 100) }}%
47+
</button>
1748
</div>
18-
<div class="zoom-separator"></div>
19-
<button class="zoom-option" @click="zoomIn">
20-
<span class="zoom-icon">+</span> Zoom In
21-
</button>
22-
<button class="zoom-option" @click="zoomOut">
23-
<span class="zoom-icon">−</span> Zoom Out
24-
</button>
25-
<div class="zoom-separator"></div>
26-
<button class="zoom-option" @click="fitToWidth">
27-
<span class="zoom-icon">↔</span> Fit to Width
28-
</button>
29-
<button class="zoom-option" @click="fitToHeight">
30-
<span class="zoom-icon">↕</span> Fit to Height
31-
</button>
32-
<button
33-
v-for="preset in zoomPresets"
34-
:key="preset"
35-
class="zoom-option zoom-preset"
36-
:class="{ active: Math.abs(currentZoom - preset) < 0.01 }"
37-
@click="setZoom(preset)"
38-
>
39-
{{ Math.round(preset * 100) }}%
40-
</button>
4149
</div>
4250
</div>
4351
</template>
@@ -72,6 +80,9 @@ export default {
7280
const localZoomLevel = ref(initialZoom);
7381
const currentZoom = ref(1); // Will be updated on render
7482
83+
const isFullscreen = ref(false);
84+
const showFullscreen = props.args.show_fullscreen_toggle === true;
85+
7586
const zoomPresets = [0.5, 0.75, 1, 1.25, 1.5, 2];
7687
const pdfInstance = ref(null);
7788
const pdfContainer = ref(null);
@@ -400,6 +411,27 @@ export default {
400411
handleResize();
401412
});
402413
414+
const enterFullscreen = () => {
415+
const el = pdfContainer.value;
416+
if (el && el.requestFullscreen) {
417+
el.requestFullscreen();
418+
}
419+
};
420+
421+
const exitFullscreen = () => {
422+
if (document.exitFullscreen) {
423+
document.exitFullscreen();
424+
}
425+
};
426+
427+
const toggleFullscreen = () => {
428+
if (!isFullscreen.value) {
429+
enterFullscreen();
430+
} else {
431+
exitFullscreen();
432+
}
433+
};
434+
403435
const setZoom = (zoomLevel) => {
404436
localZoomLevel.value = zoomLevel;
405437
showZoomPanel.value = false;
@@ -449,22 +481,28 @@ export default {
449481
};
450482
451483
const handleClickOutside = (event) => {
452-
if (showZoomPanel.value && !event.target.closest('.zoom-controls')) {
484+
if (showZoomPanel.value && !event.target.closest('.control-buttons')) {
453485
showZoomPanel.value = false;
454486
}
455487
};
456488
489+
const onFullscreenChange = () => {
490+
isFullscreen.value = !!document.fullscreenElement;
491+
};
492+
457493
const debouncedHandleResize = debounce(handleResize, 200);
458494
459495
onMounted(() => {
460496
debouncedHandleResize();
461497
window.addEventListener("resize", debouncedHandleResize);
462498
document.addEventListener('click', handleClickOutside);
499+
document.addEventListener('fullscreenchange', onFullscreenChange);
463500
});
464501
465502
onUnmounted(() => {
466503
window.removeEventListener("resize", debouncedHandleResize);
467504
document.removeEventListener('click', handleClickOutside);
505+
document.removeEventListener('fullscreenchange', onFullscreenChange);
468506
});
469507
470508
return {
@@ -482,6 +520,9 @@ export default {
482520
fitToHeight,
483521
toggleZoomPanel,
484522
applyManualZoom,
523+
showFullscreen,
524+
isFullscreen,
525+
toggleFullscreen,
485526
};
486527
},
487528
};
@@ -495,7 +536,7 @@ export default {
495536
height: 100%;
496537
overflow: auto;
497538
}
498-
.zoom-controls {
539+
.control-buttons {
499540
position: absolute;
500541
top: 20px;
501542
right: 20px;
@@ -505,7 +546,13 @@ export default {
505546
align-items: flex-end;
506547
}
507548
508-
.zoom-button {
549+
.top-buttons {
550+
display: flex;
551+
gap: 8px;
552+
margin-bottom: 8px;
553+
}
554+
555+
.control-button {
509556
background: rgba(40, 40, 40, 0.9);
510557
border: 1px solid rgba(255, 255, 255, 0.2);
511558
color: white;
@@ -516,10 +563,9 @@ export default {
516563
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
517564
font-weight: 500;
518565
transition: background 0.2s;
519-
margin-bottom: 8px;
520566
}
521567
522-
.zoom-button:hover {
568+
.control-button:hover {
523569
background: rgba(50, 50, 50, 0.9);
524570
}
525571

0 commit comments

Comments
 (0)