Skip to content
Draft
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
12 changes: 7 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ repos:
|^streamlit_pdf/frontend/package-lock\.json$
|^streamlit_pdf/frontend/\.prettierrc$
|^streamlit_pdf/frontend/\.env$
|^streamlit_pdf/frontend/streamlit-component-v2-lib-.*\.tgz$
|^streamlit-.*\.whl$
- id: format-ts-js
name: Checks that all JS/TS files are formatted correctly
entry: bash -c 'cd streamlit_pdf/frontend && npm run format'
Expand All @@ -60,7 +62,7 @@ repos:
files: \.(s?css|jsx?|tsx?)$
args:
- --comment-style
- "/**| *| */"
- '/**| *| */'
- --license-filepath
- scripts/license-template.txt
- --fuzzy-match-generates-todo
Expand All @@ -73,7 +75,7 @@ repos:
files: \.py$|\.pyi$
args:
- --comment-style
- "|#|"
- '|#|'
- --license-filepath
- scripts/license-template.txt
- --fuzzy-match-generates-todo
Expand All @@ -82,7 +84,7 @@ repos:
files: \.html$
args:
- --comment-style
- "<!--||-->"
- '<!--||-->'
- --license-filepath
- scripts/license-template.txt
- --fuzzy-match-generates-todo
Expand All @@ -96,7 +98,7 @@ repos:
|^streamlit_pdffrontend/public/fonts
|^NOTICES$
- id: check-added-large-files
args: ["--maxkb=1024"]
args: ['--maxkb=1024']
exclude: |
(?x)
|^NOTICES$
|^NOTICES$
1 change: 0 additions & 1 deletion MANIFEST.in

This file was deleted.

18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,22 @@ Feel free to file issues in [our Streamlit Repository](https://github.com/stream

Contributions are welcome 🚀, however, please inform us before building a feature.

### Development flow

Prerequisites:

```bash
uv pip install -e '.[with-streamlit]' --find-links .
cd streamlit_pdf/frontend && npm install
```

- Build the frontend once:
```bash
npm run build
```
- Or run the frontend in watch/development mode:
```bash
npm run start
```

---
62 changes: 21 additions & 41 deletions e2e_playwright/pdf_viewer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,18 @@
from pathlib import Path

import pytest
from playwright.sync_api import Page, expect

from e2e_utils import StreamlitRunner
from playwright.sync_api import Page, expect

ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
PDF_VIEWER_FILE = Path(__file__).parent / "pdf_viewer.py"


def _wait_for_pdf_content_ready(page: Page, timeout: int = 10000):
"""Wait for PDF content to actually load by checking iframe content."""
# Get the PDF frame
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

"""Wait for PDF content to actually load by checking document container."""
try:
# Wait for the PDF document container to be visible with custom timeout
pdf_frame.get_by_test_id("pdf-document-container").wait_for(
page.get_by_test_id("pdf-document-container").wait_for(
state="visible", timeout=timeout
)
except Exception:
Expand Down Expand Up @@ -72,12 +68,8 @@ def test_pdf_viewer_renders(page: Page):
# Check that the title is present
expect(page.get_by_role("heading", name="📄 PDF Viewer Component")).to_be_visible()

# The first option (Bytes) should be selected by default
# Check that the PDF viewer iframe is present
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

# The PDF viewer should be visible within the iframe
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
# The PDF viewer container should be visible
expect(page.get_by_test_id("pdf-container")).to_be_visible()


def test_pdf_viewer_height_control(page: Page):
Expand Down Expand Up @@ -107,21 +99,17 @@ def test_pdf_viewer_height_control(page: Page):
page.wait_for_load_state("domcontentloaded")

# Verify the PDF viewer is still visible after changes
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()


def test_pdf_viewer_displays_pdf(page: Page):
"""Test that the PDF viewer actually displays PDF content."""
# Check the PDF viewer iframe
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

# Wait for PDF to start loading with better conditions
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()

# Check if there's an error state
try:
error_container = pdf_frame.get_by_test_id("pdf-error")
error_container = page.get_by_test_id("pdf-error")
error_container.wait_for(state="visible")
error_text = error_container.text_content()
# If error is visible, the test should fail
Expand All @@ -143,15 +131,14 @@ def test_pdf_viewer_responsive(page: Page):
page.set_viewport_size({"width": 1200, "height": 800})

# Check PDF viewer is visible
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()

# Change viewport to mobile size
page.set_viewport_size({"width": 375, "height": 667})
page.wait_for_load_state("domcontentloaded")

# Check PDF viewer is still visible and responsive
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()


def _test_pdf_viewer_with_selectbox(
Expand Down Expand Up @@ -182,14 +169,12 @@ def _test_pdf_viewer_with_selectbox(
_wait_for_pdf_content_ready(page)

# Since we're using a single PDF viewer, we can directly locate it
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

# Wait for PDF to start loading
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()

# Check if there's an error state with shorter timeout to avoid hanging
try:
error_container = pdf_frame.get_by_test_id("pdf-error")
error_container = page.get_by_test_id("pdf-error")
error_container.wait_for(state="visible", timeout=2000)
error_text = error_container.text_content()
assert False, f"PDF failed to load for {option_name}: {error_text}"
Expand All @@ -200,11 +185,11 @@ def _test_pdf_viewer_with_selectbox(
# For Data URI, we've already waited for the document container above
# For other types, wait for the document container to appear
if option_name != "Data URI":
expect(pdf_frame.get_by_test_id("pdf-document-container")).to_be_visible()
expect(page.get_by_test_id("pdf-document-container")).to_be_visible()

# Check for actual PDF content (canvas elements)
try:
canvas = pdf_frame.locator("canvas").first
canvas = page.locator("canvas").first
expect(canvas).to_be_visible()
except Exception:
pass
Expand Down Expand Up @@ -253,22 +238,19 @@ def test_pdf_viewer_selectbox_renders_properly(page: Page):
selectbox.locator('div[data-baseweb="select"] input').click()
page.get_by_role("option", name="Path", exact=True).click()

# Check the PDF viewer iframe
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

# Check if container exists
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()

# Now check if actual PDF pages are rendered
# Look for page elements that would only exist if PDF is actually rendered
pdf_pages_visible = False
try:
# Check for actual PDF page elements (not just containers)
pdf_page = pdf_frame.locator('[data-page-number="1"]').first
pdf_page = page.locator('[data-page-number="1"]').first
pdf_page.wait_for(state="visible")

# Check if the page has actual content (canvas or similar)
canvas = pdf_frame.locator("canvas").first
canvas = page.locator("canvas").first
canvas.wait_for(state="visible")
pdf_pages_visible = True
except Exception:
Expand All @@ -293,7 +275,7 @@ def test_pdf_viewer_selectbox_renders_properly(page: Page):

# Check again if PDF is now rendered
try:
canvas = pdf_frame.locator("canvas").first
canvas = page.locator("canvas").first
canvas.wait_for(state="visible")
pdf_pages_visible = True
except Exception:
Expand All @@ -306,20 +288,18 @@ def test_pdf_viewer_selectbox_renders_properly(page: Page):
def test_pdf_viewer_actual_content_visible(page: Page):
"""Test that verifies actual PDF content is rendered, not just container elements."""
# The default selection should work
pdf_frame = page.frame_locator('iframe[title="streamlit_pdf\\.pdf_viewer"]')

# Wait for container
expect(pdf_frame.get_by_test_id("pdf-container")).to_be_visible()
expect(page.get_by_test_id("pdf-container")).to_be_visible()

# Now check for actual rendered content
# PDF.js renders pages as canvas elements
try:
# Wait for at least one canvas element (rendered PDF page)
canvas = pdf_frame.locator("canvas").first
canvas = page.locator("canvas").first
expect(canvas).to_be_visible()

# Also check for page container with page number
page_container = pdf_frame.locator('[data-page-number="1"]').first
page_container = page.locator('[data-page-number="1"]').first
expect(page_container).to_be_visible()

except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion e2e_playwright/test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ pytest-playwright>=0.3.3
pytest-xdist

dist/streamlit_pdf-1.0.7-py3-none-any.whl
streamlit>=1.28.0
streamlit-1.50.0-py3-none-any.whl
23 changes: 10 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,9 @@ name = "streamlit-pdf"
version = "1.0.7"
description = "A Streamlit component for viewing PDF files"
readme = "README.md"
license = {text = "Apache-2.0"}
authors = [
{name = "Snowflake Inc", email = "[email protected]"},
]
maintainers = [
{name = "Snowflake Inc", email = "[email protected]"},
]
license = { text = "Apache-2.0" }
authors = [{ name = "Snowflake Inc", email = "[email protected]" }]
maintainers = [{ name = "Snowflake Inc", email = "[email protected]" }]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
Expand Down Expand Up @@ -60,17 +56,18 @@ devel = [
"pytest-playwright-snapshot==1.0",
"pytest-rerunfailures==12.0",
]
with-streamlit = ["streamlit>=1.28.0"]
with-streamlit = [
# Pin Streamlit and install the wheel via --find-links to use the local
# wheel
"streamlit==1.50.0",
]

[tool.setuptools]
packages = ["streamlit_pdf"]
include-package-data = true

[tool.setuptools.package-data]
streamlit_pdf = [
"frontend/build/**/*",
"frontend/index.html",
]
streamlit_pdf = ["frontend/build/**/*", "frontend/index.html", "pyproject.toml"]

[tool.setuptools.exclude-package-data]
streamlit_pdf = [
Expand All @@ -82,4 +79,4 @@ streamlit_pdf = [
"frontend/vite.config.ts",
"frontend/vitest.config.ts",
"frontend/.gitignore",
]
]
2 changes: 2 additions & 0 deletions scripts/check_license_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
r"^\.(github)/"
# Exclude images.
r"|\.(?:png|jpg|jpeg|gif|ttf|woff|otf|eot|woff2|ico|svg)$"
# Exclude binary archives.
r"|\.(?:zip|tar|gz|tgz|bz2|xz|7z|whl)$"
# Exclude files, because they make it obvious which product they relate to.
r"|(LICENSE|NOTICES|CODE_OF_CONDUCT\.md|README\.md)$"
# Exclude files, because they do not support comments
Expand Down
Binary file added streamlit-1.50.0-py3-none-any.whl
Binary file not shown.
40 changes: 14 additions & 26 deletions streamlit_pdf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@
# without Streamlit being present.
try:
import streamlit as st # type: ignore
import streamlit.components.v1 as components # type: ignore

_STREAMLIT_AVAILABLE = True
except Exception: # pragma: no cover - only hits when Streamlit is absent
st = None # type: ignore
components = None # type: ignore
_STREAMLIT_AVAILABLE = False


Expand All @@ -50,30 +48,16 @@ def _raise_streamlit_required() -> None:
)


# Declare a Streamlit component for PDF viewing
if not _RELEASE:
if _STREAMLIT_AVAILABLE:
_component_func = components.declare_component(
"pdf_viewer",
url="http://localhost:3001",
)
else:

def _component_func(**_kwargs): # type: ignore
_raise_streamlit_required()
if _STREAMLIT_AVAILABLE:
_component_func = st.components.v2.component(
name="streamlit-pdf.pdf_viewer",
js="assets/index-*.js",
css="assets/index-*.css",
)
else:
# When we're distributing a production version of the component, we'll
# replace the `url` param with `path`, and point it to the component's
# build directory:
parent_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(parent_dir, "frontend/build")

if _STREAMLIT_AVAILABLE:
_component_func = components.declare_component("pdf_viewer", path=build_dir)
else:

def _component_func(**_kwargs): # type: ignore
_raise_streamlit_required()
def _component_func(**_kwargs): # type: ignore
_raise_streamlit_required()


def pdf_viewer(
Expand Down Expand Up @@ -135,9 +119,13 @@ def pdf_viewer(
# Process the file parameter
processed_file = _process_file_input(file)

# Call the component function with processed arguments
# Mount the CCv2 component with data payload
component_value = _component_func(
file=processed_file, height=height, key=key, default=None
key=key,
data={
"file": processed_file,
"height": height,
},
)

return component_value
Expand Down
Loading
Loading