Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ In the following table the list of parameters that can be provided to the `pdf_v
| viewer_align | The alignment of the PDF viewer within its container. Can be `"center"` (default), `"left"`, or `"right"`. |
| show_page_separator | Whether to show a horizontal separator line between PDF pages. Defaults to `True`. |
| 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. |
| 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` |
| scroll_to_annotation | Scroll to a specific annotation when the component is rendered. The parameter is a 1-based positional integer referring to the annotation's order in the `annotations` list, independent of any `id` field set on the annotation. E.g. 1 scrolls to the first annotation. Default is None (don't scroll). Requires an int; values below 1 are silently coerced to None. Mutually exclusive with `scroll_to_page`. Raises an exception if used with `scroll_to_page`. |
| scroll_behavior | The scrolling behavior when navigating to a page or annotation. Can be `"smooth"` (animated scroll) or `"instant"` (immediate jump). Defaults to `"smooth"`. |
| on_annotation_click | Callback function that is called when an annotation is clicked. The function receives the annotation as a parameter. |

Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
streamlit
bump-my-version
streamlit==1.40.1
bump-my-version==1.3.0
tornado>=6.5 # not directly required, pinned by Snyk to avoid a vulnerability
requests>=2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability
protobuf>=4.25.8 # not directly required, pinned by Snyk to avoid a vulnerability
2 changes: 1 addition & 1 deletion streamlit_pdf_viewer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def pdf_viewer(
:param viewer_align: The alignment of the PDF viewer in the container. Can be "center", "left", or "right". Defaults to "center".
:param show_page_separator: Whether to show a separator between pages. Defaults to True.
: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.
: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.
:param scroll_to_annotation: Scroll to a specific annotation in the PDF. The parameter is a 1-based positional integer referring to the order of the annotation in the `annotations` list, independent of any `id` field on the annotation itself. E.g. 1 scrolls to the first annotation. Values below 1 are silently coerced to None. Defaults to None.
:param scroll_behavior: The scrolling behavior when navigating to a page or annotation. Can be "smooth" (animated scroll) or "instant" (immediate jump). Defaults to "smooth".
: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.
:param allow_clickable_annotations_with_text_rendering: When True, annotations remain clickable even when render_text is enabled. Note that text selection will not work through annotation areas. Defaults to False.
Expand Down
5 changes: 4 additions & 1 deletion streamlit_pdf_viewer/frontend/src/PdfViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export default {
annotation.id = `${annotation.id || annotationIndex}`
annotationDiv.id = `annotation-${annotation.id}`;
annotationDiv.setAttribute("data-index", annotation.id);
// 0-based positional index, independent of user-supplied annotation.id. Used by scroll_to_annotation.
annotationDiv.setAttribute("data-position", annotationIndex);
annotationDiv.style.position = 'absolute';
annotationDiv.style.left = `${annotation.x * scale}px`;
annotationDiv.style.top = `${annotation.y * scale}px`;
Expand Down Expand Up @@ -350,7 +352,8 @@ export default {
page.scrollIntoView({behavior});
}
} else if (props.args.scroll_to_annotation) {
const annotation = document.querySelector(`[id^="annotation-"][data-index="${props.args.scroll_to_annotation}"]`);
// Public API is 1-based; data-position is 0-based.
const annotation = document.querySelector(`[id^="annotation-"][data-position="${props.args.scroll_to_annotation - 1}"]`);
if (annotation) {
annotation.scrollIntoView({behavior, block: "center"});
}
Expand Down
31 changes: 31 additions & 0 deletions tests/test_scroll_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,34 @@ def test_rejects_invalid_value(self, mock_component):
def test_rejects_empty_string(self, mock_component):
with pytest.raises(ValueError, match="scroll_behavior"):
pdf_viewer(DUMMY_PDF, scroll_behavior="")


@patch("streamlit_pdf_viewer._component_func", return_value=None)
class TestScrollToAnnotationContract:
"""Python forwards scroll_to_annotation as 1-based; the frontend converts to its
own 0-based data-position attribute. Python's job is just pass-through plus the
pre-existing < 1 → None coercion."""

def test_one_passes_through(self, mock_component):
pdf_viewer(DUMMY_PDF, scroll_to_annotation=1)
assert mock_component.call_args[1]["scroll_to_annotation"] == 1

def test_arbitrary_n_passes_through(self, mock_component):
pdf_viewer(DUMMY_PDF, scroll_to_annotation=5)
assert mock_component.call_args[1]["scroll_to_annotation"] == 5

def test_none_passes_through(self, mock_component):
pdf_viewer(DUMMY_PDF)
assert mock_component.call_args[1]["scroll_to_annotation"] is None

def test_zero_is_coerced_to_none(self, mock_component):
pdf_viewer(DUMMY_PDF, scroll_to_annotation=0)
assert mock_component.call_args[1]["scroll_to_annotation"] is None

def test_negative_is_coerced_to_none(self, mock_component):
pdf_viewer(DUMMY_PDF, scroll_to_annotation=-3)
assert mock_component.call_args[1]["scroll_to_annotation"] is None

def test_combined_with_scroll_to_page_raises(self, mock_component):
with pytest.raises(ValueError, match="cannot be used together"):
pdf_viewer(DUMMY_PDF, scroll_to_page=1, scroll_to_annotation=1)