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

Callback for clicking on annotations #76 #80

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ In the following table the list of parameters that can be provided to the `pdf_v
| 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. |
| height | Height of the PDF viewer in pixels. If not provided, the viewer shows the whole content. |
| annotations | A list of annotations to be overlaid on the PDF. Format is described [here](#annotation-format). |
| annotations | A list of annotations to be overlaid on the PDF. Format is described [here](#annotation-format). |
| pages_vertical_spacing | The vertical space (in pixels) between each page of the PDF. Defaults to 2 pixels. |
| annotation_outline_size | Size of the outline around each annotation in pixels. Defaults to 1 pixel. |
| rendering | Type of rendering: `unwrap` (default), `legacy_iframe`, or `legacy_embed`. The default value, `unwrap` shows the PDF document using pdf.js, and supports the visualisation of annotations. Other values are `legacy_iframe` and `legacy_embed` which use the legacy approach of injecting the document into an `<embed>` or `<iframe>`. They allow viewing the PDF using the viewer of the browser that contains additional features we are still working to implement in this component. **IMPORTANT**: :warning: The "legacy" methods **work only with Firefox**, and **do not support annotations**. :warning: |
| pages_to_render | Filter the rendering to a specific set of pages. By default, all pages are rendered. |
| 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.
| scroll_to_page | Scroll to a specific page when the component is rendered. Default is None. Require ints and ignores the parameters below zero. |
| scroll_to_annotation | Scroll to a specific annotation when the component is rendered. Default is None. 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. Default is None. Mutually exclusive with `scroll_to_page`. Raise an exception if used with `scroll_to_page` |
| on_annotation_click | Callback function that is called when an annotation is clicked. The function receives the annotation as a parameter. |

### Annotation format
The annotations format has been derived from the [Grobid's coordinate formats](https://grobid.readthedocs.io/en/latest/Coordinates-in-PDF/), which are described as a list of "bounding boxes".
Expand All @@ -91,6 +91,44 @@ Here an example:

The example shown in our screenshot can be found [here](resources/annotations.json).

### How to use on_annotation_click

```python
from streamlit_pdf_viewer import pdf_viewer

annotations = [
{
"page": 1,
"x": 220,
"y": 155,
"height": 22,
"width": 65,
"color": "red"
},
{
"page": 1,
"x": 220,
"y": 155,
"height": 22,
"width": 65,
"color": "red"
}
]

def my_custom_annotation_handler(annotation):
print(f"Annotation {annotation} clicked.")

pdf_viewer(
"path/to/pdf",
on_annotation_click=my_custom_annotation_handler,
annotations=annotations
)

```


###

## Developers notes

### Environment
Expand Down
12 changes: 10 additions & 2 deletions streamlit_pdf_viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import base64
import os
from pathlib import Path
from typing import Union, List, Optional
from typing import Union, List, Optional, Callable

import streamlit.components.v1 as components
import json
Expand Down Expand Up @@ -39,7 +39,8 @@ def pdf_viewer(
resolution_boost: int = 1,
scroll_to_page: int = None,
scroll_to_annotation: int = None,
):
on_annotation_click: Optional[Callable[[dict], None]] = None,
):
"""
pdf_viewer function to display a PDF file in a Streamlit app.

Expand All @@ -59,6 +60,7 @@ def pdf_viewer(
:param resolution_boost: Boost the resolution by a factor from 2 to 10. Defaults to 1.
: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 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.

The function reads the PDF file (from a file path, URL, or binary data), encodes it in base64,
and uses a Streamlit component to render it in the app. It supports optional annotations and adjustable margins.
Expand Down Expand Up @@ -118,6 +120,12 @@ def pdf_viewer(
scroll_to_page=scroll_to_page,
scroll_to_annotation=scroll_to_annotation
)

# Execute the custom callback function
if component_value and 'clicked_annotation' in component_value:
clicked_annotation = component_value['clicked_annotation']
if on_annotation_click is not None and callable(on_annotation_click):
on_annotation_click(clicked_annotation)
return component_value


Expand Down
27 changes: 23 additions & 4 deletions streamlit_pdf_viewer/frontend/src/PdfViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
<div id="pdfContainer" :style="pdfContainerStyle">
<div v-if="args.rendering==='unwrap'">
<div id="pdfViewer" :style="pdfViewerStyle">
<div id="pdfAnnotations" v-if="args.annotations">
<div v-for="(annotation, index) in filteredAnnotations" :key="index" :style="getPageStyle">
<div :style="getAnnotationStyle(annotation, index)" :id="`annotation-${index}`"></div>
<div id="pdfAnnotations" v-if="args.annotations">
<div
v-for="(annotation, index) in filteredAnnotations"
:key="index"
:style="getPageStyle"
@click="handleAnnotationClick(annotation, index)"
>
<div
:style="getAnnotationStyle(annotation, index)"
:id="`annotation-${index}`"
></div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="args.rendering==='legacy_embed'">
<embed :src="`data:application/pdf;base64,${args.binary}`" :width="`${args.width}`" :height="`${args.height}`" type="application/pdf"/>
Expand Down Expand Up @@ -55,6 +63,16 @@ export default {
})
});

const handleAnnotationClick = (annotation, index) => {
// Convert the Proxy object to a plain object
const serializedAnnotation = JSON.parse(JSON.stringify(annotation));

// Send data to Streamlit
Streamlit.setComponentValue({
clicked_annotation: { index, ...serializedAnnotation },
});
};

const renderText = props.args.render_text === true

const pdfContainerStyle = computed(() => ({
Expand Down Expand Up @@ -355,6 +373,7 @@ export default {

return {
filteredAnnotations,
handleAnnotationClick,
getAnnotationStyle,
pdfContainerStyle,
pdfViewerStyle,
Expand Down
Loading