Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support width in percentage #30 #39

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ In the following table the list of parameters that can be provided to the `pdf_v
| name | description |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| input | The source of the PDF file. Accepts a file path, URL, or binary data. |
| width | Width of the PDF viewer in pixels. It defaults to 700 pixels. |
| width | The width of the PDF viewer defaults to 100% of the layout. Specify in pixels with a numeric value, or as a percentage for relative sizing. |
| height | Height of the PDF viewer in pixels. If not provided, the viewer shows the whole content. |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about having height = 100% by default too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried it and pushed it here 5443edb
But, it's outer height not inner height. The custom javascript returned 0 as value when I retrieve inner height because of iframe problem
ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/outerHeight

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK forget about it, we can address the height-related issue in #41

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, do you think I should revert it? Because I've already pushed the code which supports outerHeight.

| annotations | A list of annotations to be overlaid on the PDF. Format is described here. |
| pages_vertical_spacing | The vertical space (in pixels) between each page of the PDF. Defaults to 2 pixels. |
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
streamlit
bump-my-version
bump-my-version
lfoppiano marked this conversation as resolved.
Show resolved Hide resolved
streamlit_js_eval
43 changes: 34 additions & 9 deletions streamlit_pdf_viewer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Union, List, Optional

import streamlit.components.v1 as components
from streamlit_js_eval import streamlit_js_eval
import json

_RELEASE = True
Expand All @@ -24,10 +25,25 @@
path=build_dir
)

def get_screen_size():
"""
Returns the inner width and outer height of a window.
Ideally, it should return the inner height, but JavaScript couldn't retrieve the height in an iframe.
"""
async_js_code = """
new Promise(resolve => {
if (document.readyState === "complete") {
resolve([window.innerWidth, window.outerHeight]);
} else {
window.addEventListener("load", () => resolve([window.innerWidth, window.outerHeight]));
}
})
"""
return streamlit_js_eval(js_expressions=async_js_code)

def pdf_viewer(input: Union[str, Path, bytes],
width: int = 700,
height: int = None,
width: int = None,
height: int = None,
key=None,
annotations: list = (),
pages_vertical_spacing: int = 2,
Expand All @@ -39,7 +55,7 @@ def pdf_viewer(input: Union[str, Path, bytes],
pdf_viewer function to display a PDF file in a Streamlit app.

:param input: The source of the PDF file. Accepts a file path, URL, or binary data.
:param width: Width of the PDF viewer in pixels. Defaults to 700 pixels.
:param width: The width of the PDF viewer defaults to 100% of the layout. Specify in pixels with a numeric value, or as a percentage for relative sizing.
:param height: Height of the PDF viewer in pixels. If not provided, the viewer show the whole content.
:param key: An optional key that uniquely identifies this component. Used to preserve state in Streamlit apps.
:param annotations: A list of annotations to be overlaid on the PDF. Each annotation should be a dictionary.
Expand All @@ -57,11 +73,20 @@ def pdf_viewer(input: Union[str, Path, bytes],
Returns the value of the selected component (if any).
"""

# Validate width and height parameters
if not isinstance(width, int):
raise TypeError("Width must be an integer")
if height is not None and not isinstance(height, int):
raise TypeError("Height must be an integer or None")
screen_width, screen_height = get_screen_size()

if isinstance(width, str) and width.endswith('%'):
percentage_width = float(width[:-1]) / 100
width = int(screen_width * percentage_width)
elif width is not None and not isinstance(width, int):
raise TypeError("Width must be an integer or a percentage string (e.g., '70%' or 700) or None")

if isinstance(height, str) and height.endswith('%'):
percentage_height = float(height[:-1]) / 100
height = int(screen_height * percentage_height)
elif height is not None and not isinstance(height, int):
raise TypeError("Height must be an integer, a percentage string (e.g., '70%'), or None")

if not all(isinstance(page, int) for page in pages_to_render):
raise TypeError("pages_to_render must be a list of integers")

Expand Down Expand Up @@ -102,7 +127,7 @@ def pdf_viewer(input: Union[str, Path, bytes],

viewer = pdf_viewer(
binary,
height=700,
# height=700,
width=800,
annotations=annotations
)
87 changes: 67 additions & 20 deletions streamlit_pdf_viewer/frontend/src/PdfViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div id="pdfContainer" :style="pdfContainerStyle">
<div v-if="args.rendering==='unwrap'">
<div id="pdfViewer" :style="pdfViewerStyle">
<!-- <div class="urlsAnnotations"></div>-->
<div id="pdfAnnotations" v-if="args.annotations">
<div v-for="(annotation, index) in filteredAnnotations" :key="index" :style="getPageStyle">
<div :style="getAnnotationStyle(annotation)" :id="`annotation-${index}`"></div>
Expand All @@ -26,6 +27,7 @@
import "pdfjs-dist/build/pdf.worker.entry";
import {getDocument} from "pdfjs-dist/build/pdf";
import {Streamlit} from "streamlit-component-lib";
import PDFViewerApplicationOptions from "core-js/internals/task"

export default {
props: ["args"],
Expand All @@ -36,6 +38,17 @@
const pageScales = ref([]);
const pageHeights = ref([]);

// console.log("--- INIT ---")
// console.log("inner width: " + window.innerWidth)
// console.log("inner height: " + window.innerHeight)
// console.log("outer height: " + window.outerHeight)

// console.log("Width: " + maxWidth.value)

document.addEventListener('webviewerloaded', function () {
PDFViewerApplicationOptions.set('printResolution', 300);
});

const isRenderingAllPages = props.args.pages_to_render.length === 0;

const filteredAnnotations = computed(() => {
Expand All @@ -49,7 +62,7 @@
});

const pdfContainerStyle = computed(() => ({
width: `${props.args.width}px`,
width: props.args.width ? `${props.args.width}px` : `${maxWidth.value}px`,
height: props.args.height ? `${props.args.height}px` : 'auto',
overflow: 'auto',
}));
Expand Down Expand Up @@ -96,21 +109,35 @@

const createCanvasForPage = (page, scale, rotation, pageNumber) => {
const viewport = page.getViewport({scale, rotation});

// console.log(`Page viewport size: ${viewport.width}, ${viewport.height}`)

const ratio = window.devicePixelRatio || 1

const canvas = document.createElement("canvas");
canvas.id = `canvas_page_${pageNumber}`;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.height = viewport.height * ratio;
canvas.width = viewport.width * ratio;
canvas.style.width = viewport.width + 'px';
canvas.style.height = viewport.height + 'px';
canvas.style.display = "block";
canvas.style.marginBottom = `${props.args.pages_vertical_spacing}px`;
canvas.getContext("2d").scale(ratio, ratio);
return canvas;
};


const renderPage = async (page, canvas) => {
const renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({scale: pageScales.value[page._pageIndex], rotation: page.rotate}),
viewport: page.getViewport({
scale: pageScales.value[page._pageIndex],
rotation: page.rotate,
intent: "print",
})
};
// console.log(`Scale page ${page._pageIndex}: ${pageScales.value[page._pageIndex]}`);

const renderTask = page.render(renderContext);
await renderTask.promise;
};
Expand All @@ -123,22 +150,27 @@
}
}

for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const rotation = page.rotate;
const actualViewport = page.getViewport({scale: 1.0, rotation: rotation});
const scale = props.args.width / actualViewport.width;
pageScales.value.push(scale);
pageHeights.value.push(actualViewport.height);
if (pagesToRender.includes(i)) {
const canvas = createCanvasForPage(page, scale, rotation, i);
pdfViewer?.append(canvas);
if (canvas.width > maxWidth.value) {
maxWidth.value = canvas.width;
}
totalHeight.value += canvas.height;
totalHeight.value += props.args.pages_vertical_spacing;
await renderPage(page, canvas);
// console.log("Device pixel ratio" + window.devicePixelRatio)
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
const page = await pdf.getPage(pageNumber)
const rotation = page.rotate
// Initial viewport
const unscaledViewport = page.getViewport({
scale: 1.0,
rotation: rotation,
})

const scale = maxWidth.value / unscaledViewport.width
// console.log(`Page scale: ${scale}`)

pageScales.value.push(scale)
pageHeights.value.push(unscaledViewport.height)
if (pagesToRender.includes(pageNumber)) {
const canvas = createCanvasForPage(page, scale, rotation, pageNumber)
pdfViewer?.append(canvas)
totalHeight.value += canvas.height
totalHeight.value += props.args.pages_vertical_spacing
await renderPage(page, canvas)
}
}
// Subtract the margin for the last page as it's not needed
Expand All @@ -149,7 +181,7 @@

const alertError = (error) => {
window.alert(error.message);
console.error(error);

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.7, 18)

Unexpected console statement

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.8, 18)

Unexpected console statement

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.8, 20)

Unexpected console statement

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.9, 18)

Unexpected console statement

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.10, 18)

Unexpected console statement

Check warning on line 184 in streamlit_pdf_viewer/frontend/src/PdfViewer.vue

View workflow job for this annotation

GitHub Actions / build (3.12, 18)

Unexpected console statement
};

const loadPdfs = async (url) => {
Expand All @@ -170,8 +202,23 @@
Streamlit.setFrameHeight(props.args.height || totalHeight.value);
};

const setFrameWidth = () => {
if (props.args.width === null || props.args.width === undefined) {
maxWidth.value = window.innerWidth
} else if (Number.isInteger(props.args.width)) {
maxWidth.value = props.args.width

// If the desired width is larger than the available inner width,
// we should not exceed it. To be revised
if (window.innerWidth < maxWidth.value) {
maxWidth.value = window.innerWidth
}
}
}

onMounted(() => {
const binaryDataUrl = `data:application/pdf;base64,${props.args.binary}`;
setFrameWidth();
if (props.args.rendering === "unwrap") {
loadPdfs(binaryDataUrl)
.then(setFrameHeight)
Expand Down
6 changes: 4 additions & 2 deletions tests/test_embed_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ def test_should_render_template_check_container_size(page: Page):
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
assert b_box['height'] > 0
assert round(b_box['height']) == 500
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
expect(pdf_viewer).not_to_be_visible()
Expand Down
4 changes: 3 additions & 1 deletion tests/test_embed_no_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def test_should_render_template_check_container_size(page: Page):
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']
assert b_box['height'] > 0

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
Expand Down
4 changes: 3 additions & 1 deletion tests/test_iframe_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def test_should_render_template_check_container_size(page: Page):
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']
assert b_box['height'] > 0

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
Expand Down
4 changes: 3 additions & 1 deletion tests/test_iframe_no_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def test_should_render_template_check_container_size(page: Page):
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']
assert b_box['height'] > 0

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
Expand Down
4 changes: 3 additions & 1 deletion tests/test_unwrap_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']
assert b_box['height'] == 300

Check failure on line 44 in tests/test_unwrap_height.py

View workflow job for this annotation

GitHub Actions / build (3.7, 18)

test_should_render_template_check_container_size[firefox] assert 300.00001525878906 == 300

Check failure on line 44 in tests/test_unwrap_height.py

View workflow job for this annotation

GitHub Actions / build (3.8, 18)

test_should_render_template_check_container_size[firefox] assert 300.00001525878906 == 300

Check failure on line 44 in tests/test_unwrap_height.py

View workflow job for this annotation

GitHub Actions / build (3.10, 18)

test_should_render_template_check_container_size[firefox] assert 300.00001525878906 == 300

Check failure on line 44 in tests/test_unwrap_height.py

View workflow job for this annotation

GitHub Actions / build (3.12, 18)

test_should_render_template_check_container_size[firefox] assert 300.00001525878906 == 300

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
expect(pdf_viewer).to_be_visible()
Expand Down
4 changes: 3 additions & 1 deletion tests/test_unwrap_no_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def test_should_render_template_check_container_size(page: Page):
expect(pdf_container).to_be_visible()

b_box = pdf_container.bounding_box()
assert b_box['width'] == 700
# Since we do not specify the width, we occupy all the available space, which should correspond to the
# parent element's width of the pdfContainer.
assert b_box['width'] == iframe_box['width']
assert b_box['height'] > 0

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
Expand Down
2 changes: 1 addition & 1 deletion tests/test_unwrap_width.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_should_render_template_check_container_size(page: Page):

b_box = pdf_container.bounding_box()
assert floor(b_box['width']) == 400
assert floor(b_box['height']) == 4216 # Firefox returns 4216.000091552734, while Chome 4216
assert floor(b_box['height']) == 4221

pdf_viewer = iframe_frame.locator('div[id="pdfViewer"]')
expect(pdf_viewer).to_be_visible()
Expand Down
Loading