playwright-snap 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. playwright_snap-0.1.0/LICENSE +21 -0
  2. playwright_snap-0.1.0/MANIFEST.in +4 -0
  3. playwright_snap-0.1.0/PKG-INFO +284 -0
  4. playwright_snap-0.1.0/README.md +250 -0
  5. playwright_snap-0.1.0/pyproject.toml +56 -0
  6. playwright_snap-0.1.0/setup.cfg +4 -0
  7. playwright_snap-0.1.0/src/playsnap/__init__.py +23 -0
  8. playwright_snap-0.1.0/src/playsnap/__main__.py +5 -0
  9. playwright_snap-0.1.0/src/playsnap/cli.py +66 -0
  10. playwright_snap-0.1.0/src/playsnap/errors.py +47 -0
  11. playwright_snap-0.1.0/src/playsnap/options.py +56 -0
  12. playwright_snap-0.1.0/src/playsnap/py.typed +0 -0
  13. playwright_snap-0.1.0/src/playsnap/session.py +155 -0
  14. playwright_snap-0.1.0/src/playsnap/snapshot.py +90 -0
  15. playwright_snap-0.1.0/src/playwright_snap.egg-info/PKG-INFO +284 -0
  16. playwright_snap-0.1.0/src/playwright_snap.egg-info/SOURCES.txt +31 -0
  17. playwright_snap-0.1.0/src/playwright_snap.egg-info/dependency_links.txt +1 -0
  18. playwright_snap-0.1.0/src/playwright_snap.egg-info/entry_points.txt +2 -0
  19. playwright_snap-0.1.0/src/playwright_snap.egg-info/requires.txt +7 -0
  20. playwright_snap-0.1.0/src/playwright_snap.egg-info/top_level.txt +1 -0
  21. playwright_snap-0.1.0/tests/conftest.py +104 -0
  22. playwright_snap-0.1.0/tests/pages/fresh-context.html +10 -0
  23. playwright_snap-0.1.0/tests/pages/late-font.html +21 -0
  24. playwright_snap-0.1.0/tests/pages/never-loads.html +5 -0
  25. playwright_snap-0.1.0/tests/pages/no-viewport-tag.html +7 -0
  26. playwright_snap-0.1.0/tests/pages/print-taller.html +13 -0
  27. playwright_snap-0.1.0/tests/pages/sticks-out.html +9 -0
  28. playwright_snap-0.1.0/tests/test_browser.py +308 -0
  29. playwright_snap-0.1.0/tests/test_cli.py +113 -0
  30. playwright_snap-0.1.0/tests/test_errors.py +77 -0
  31. playwright_snap-0.1.0/tests/test_options.py +59 -0
  32. playwright_snap-0.1.0/tests/test_package.py +39 -0
  33. playwright_snap-0.1.0/tests/test_snapshot.py +141 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nat Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ # setuptools puts tests/test_*.py into the sdist on its own; add the rest of the
2
+ # test suite (conftest.py, the test pages) so the tests run from the sdist too.
3
+ graft tests
4
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: playwright-snap
3
+ Version: 0.1.0
4
+ Summary: Snapshots of web pages as base64 PNG or PDF, taken with Playwright's headless Chromium.
5
+ Author-email: Nat Lee <natlee.work@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/NatLee/playwright-snap
8
+ Project-URL: Source, https://github.com/NatLee/playwright-snap
9
+ Project-URL: Issues, https://github.com/NatLee/playwright-snap/issues
10
+ Keywords: playwright,chromium,headless,snapshot,screenshot,pdf,base64
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
22
+ Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: playwright>=1.49
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Requires-Dist: pillow; extra == "test"
31
+ Requires-Dist: pypdf; extra == "test"
32
+ Requires-Dist: fonttools; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ <p align="center">
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/hero-dark.webp">
38
+ <img src="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/hero-light.webp" alt="playwright-snap: give it a URL, a headless Chromium driven by Playwright renders the page, and you get a PNG (str) or a PDF (bytes) back as base64. Coming from pyppeteer-snap, only the import changes.">
39
+ </picture>
40
+ </p>
41
+
42
+ <p align="center">
43
+ <a href="https://github.com/NatLee/playwright-snap/actions/workflows/test.yml"><img src="https://github.com/NatLee/playwright-snap/actions/workflows/test.yml/badge.svg" alt="Test"></a>
44
+ <a href="https://pypi.org/project/playwright-snap/"><img src="https://img.shields.io/pypi/v/playwright-snap" alt="PyPI version"></a>
45
+ <a href="https://pypi.org/project/playwright-snap/"><img src="https://img.shields.io/pypi/pyversions/playwright-snap" alt="Python versions"></a>
46
+ <a href="https://github.com/NatLee/playwright-snap/blob/main/LICENSE"><img src="https://img.shields.io/github/license/NatLee/playwright-snap" alt="License: MIT"></a>
47
+ </p>
48
+
49
+ playwright-snap takes a snapshot of a web page and hands it to you: a PNG of what fits in the browser
50
+ window, or a PDF of the whole page on a single sheet. It opens the page in a headless Chromium (no
51
+ window) through [Playwright](https://playwright.dev/python/), so JavaScript-heavy pages render like they
52
+ do in a real browser. One function call, nothing written to disk.
53
+
54
+ It is the maintained successor of [pyppeteer-snap](https://github.com/NatLee/pyppeteer-snap), which is
55
+ built on the no longer maintained Pyppeteer and its old Chromium. The functions have the same names,
56
+ arguments and return values, so moving over is a one-line change, see
57
+ [Coming from pyppeteer-snap](#coming-from-pyppeteer-snap).
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install playwright-snap
63
+ python -m playwright install chromium
64
+ ```
65
+
66
+ The second line downloads the Chromium build that your Playwright version drives, into your user
67
+ cache folder. Do it once, and again after upgrading Playwright. Only taking snapshots? Add
68
+ `--only-shell` to download just the headless part, which saves about 400 MB of disk space.
69
+
70
+ On Linux, Chromium also needs some system libraries. Playwright installs them for you on Debian and
71
+ Ubuntu (for example in a `python:3.12-slim` Docker image); it needs root:
72
+
73
+ ```bash
74
+ python -m playwright install --with-deps chromium
75
+ ```
76
+
77
+ The package is called playwright-snap on PyPI and is imported as `playsnap`: a name that starts with
78
+ `playwright` would make Playwright mistake this package's code for its own. It works on Python
79
+ 3.10–3.14, on Linux, macOS and Windows. The latest code is a
80
+ `pip install git+https://github.com/NatLee/playwright-snap` away.
81
+
82
+ ## Quick start
83
+
84
+ ```python
85
+ from playsnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
86
+
87
+ png = get_image_snapshot_by_url("https://example.com") # PNG as base64 str
88
+ pdf = get_pdf_snapshot_by_url("https://example.com") # PDF as base64 bytes
89
+
90
+ print(png[:20]) # iVBORw0KGgoAAAANSUhE
91
+ print(pdf[:20]) # b'JVBERi0xLjQKJdPr6eEK'
92
+ ```
93
+
94
+ Here is what you get for the same page (real output, decoded):
95
+
96
+ <p align="center">
97
+ <picture>
98
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/demo-dark.webp">
99
+ <img src="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/demo-light.webp" alt="The same page captured three times: get_image_snapshot_by_url returns a PNG of the 1600 x 900 browser window, is_mobile=True gives the phone layout of a 390 x 844 window, and get_pdf_snapshot_by_url returns the whole page on a single PDF sheet">
100
+ </picture>
101
+ </p>
102
+
103
+ Rather have the file itself, other window sizes, and an exception when it fails?
104
+
105
+ ```python
106
+ from playsnap import SnapshotOptions, snap_pdf, snap_png
107
+
108
+ phone = SnapshotOptions(width=390, height=844, is_mobile=True, device_scale_factor=2)
109
+
110
+ with open("example.png", "wb") as f:
111
+ f.write(snap_png("https://example.com", phone)) # a 780 x 1688 PNG
112
+ with open("example.pdf", "wb") as f:
113
+ f.write(snap_pdf("https://example.com")) # one sheet, 1600 px wide
114
+ ```
115
+
116
+ ## API
117
+
118
+ Everything below is imported from `playsnap`.
119
+
120
+ ### Functions
121
+
122
+ | Function | Returns | When it fails |
123
+ |---|---|---|
124
+ | `get_image_snapshot_by_url(url, width=1600, height=900, is_mobile=False)` | PNG as base64 `str` | logs why, returns `None` |
125
+ | `get_pdf_snapshot_by_url(url, width=1600, height=900, is_mobile=False)` | PDF as base64 `bytes` | logs why, returns `None` |
126
+ | `snap_png(url, options=None)` | PNG `bytes` | raises `SnapshotError` |
127
+ | `snap_pdf(url, options=None)` | PDF `bytes` | raises `SnapshotError` |
128
+
129
+ The PNG is what fits in the browser window. The PDF is the whole page, laid out as wide as the window,
130
+ on one sheet as long as the page (with the page's print styles, as when you print it).
131
+
132
+ - `url` needs its scheme: `https://example.com`, not `example.com`.
133
+ - The first two are pyppeteer-snap's functions, kept exactly as they were. They log the reason for a
134
+ failure to the `playsnap` logger, which Python shows on stderr unless you set up logging.
135
+ - Each call starts its own Chromium and closes it again, also when something goes wrong.
136
+ - They are synchronous and also work where an event loop is already running, such as a Jupyter
137
+ notebook. In async code, use [`SnapSession`](#snapsession) instead.
138
+
139
+ ### SnapshotOptions
140
+
141
+ All settings live in one frozen dataclass. Sizes are CSS pixels, times are seconds, and every number must
142
+ be greater than 0 (you get a `ValueError` right away otherwise).
143
+
144
+ | Option | Default | What it does |
145
+ |---|---|---|
146
+ | `width` | `1600` | width of the browser window; the PNG and the PDF are this wide |
147
+ | `height` | `900` | height of the browser window; the PNG is this high |
148
+ | `is_mobile` | `False` | lay the page out like a phone browser, honouring its `<meta name="viewport">` |
149
+ | `device_scale_factor` | `1` | image pixels per CSS pixel; `2` gives a retina-sharp PNG twice the size |
150
+ | `wait_until` | `"load"` | when the page counts as loaded: `"load"`, `"domcontentloaded"`, `"networkidle"` or `"commit"` |
151
+ | `navigation_timeout` | `30` | how long the page may take to load |
152
+ | `snapshot_timeout` | `120` | how long one whole snapshot may take, starting Chromium included |
153
+
154
+ `wait_until="networkidle"` helps with pages that keep loading content after the load event.
155
+
156
+ ### SnapSession
157
+
158
+ For async code, or many pages in a row: one Chromium for all snapshots, which is quicker than starting
159
+ one per page.
160
+
161
+ ```python
162
+ import asyncio
163
+ from playsnap import SnapSession, SnapshotOptions
164
+
165
+ async def main():
166
+ async with SnapSession() as session:
167
+ png = await session.png("https://example.com")
168
+ pdf = await session.pdf("https://example.com", SnapshotOptions(width=800))
169
+ print(len(png), len(pdf))
170
+
171
+ asyncio.run(main())
172
+ ```
173
+
174
+ `session.png(url, options=None)` and `session.pdf(url, options=None)` return `bytes` and raise like
175
+ `snap_png()`. Each snapshot opens the page in a fresh browser context, so cookies and storage never carry
176
+ over from one page to the next. Chromium closes when the `async with` block ends.
177
+
178
+ ### Errors
179
+
180
+ Every failure is a `SnapshotError`, and `str(error)` says what went wrong in one line. The subclasses say
181
+ which step failed:
182
+
183
+ - `BrowserLaunchError`: Chromium did not start. It is not installed, or on Linux it is missing system
184
+ libraries; the message says which command fixes it.
185
+ - `NavigationError`: the page did not load. The URL is wrong or unreachable, or `navigation_timeout` ran
186
+ out.
187
+ - `SnapshotTimeoutError`: the whole snapshot took longer than `snapshot_timeout`.
188
+
189
+ The full Playwright error is chained as `error.__cause__`. Invalid options are a `ValueError`, not a
190
+ `SnapshotError`, because they are a bug in the calling code rather than a page that failed.
191
+
192
+ ## Command line
193
+
194
+ ```console
195
+ $ playwright-snap --url https://example.com
196
+ ------------- Parameters ---------------
197
+ URL: https://example.com
198
+ Type: img
199
+ --------------- Base64 ------------------
200
+ iVBORw0KGgoAAAANSUhEUgAABkAAAAOECAIAAAB2L2r1AAAQAElEQVR4nOz9…
201
+ -------------------------------------
202
+ ```
203
+
204
+ `--type img` (the default) prints the PNG, `--type pdf` the PDF. The base64 text is always on line 5,
205
+ so you can save the file from the shell:
206
+
207
+ ```bash
208
+ playwright-snap --url https://example.com --type pdf | sed -n 5p | base64 -d > example.pdf
209
+ ```
210
+
211
+ `python -m playsnap` does the same. The exit status is 0 when it worked, 1 when the snapshot
212
+ failed (the reason goes to stderr), and 2 when `--url` is missing or `--type` is not `img` or `pdf`.
213
+
214
+ ## Coming from pyppeteer-snap
215
+
216
+ Install playwright-snap and Chromium as shown in [Installation](#installation), then change the import:
217
+
218
+ ```diff
219
+ -from pyppesnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
220
+ +from playsnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
221
+ ```
222
+
223
+ The functions take the same arguments, use the same defaults (a 1600 × 900 window, 30 s to load, 120 s
224
+ per snapshot) and return the same types: a PNG as base64 `str`, a PDF as base64 `bytes`, `None` when
225
+ the snapshot failed. `from pyppesnap.snapshot import …` becomes `from playsnap.snapshot import …`.
226
+ The command line tool keeps its options and its output; only its name changes from `pyppesnap` to
227
+ `playwright-snap`.
228
+
229
+ What is different, and why:
230
+
231
+ | pyppeteer-snap | playwright-snap | Why |
232
+ |---|---|---|
233
+ | `pip install PyppeSnap`, `import pyppesnap` | `pip install playwright-snap`, `import playsnap` | An import name that starts with `playwright` would make Playwright mistake this package's code for its own |
234
+ | Downloads Chromium on the first snapshot | `python -m playwright install chromium`, once | No surprise 100–200 MB download in the middle of a request, and the browser matches your Playwright version |
235
+ | Prints the reason for a failure to stdout | Logs it (logger `playsnap`, stderr by default) | It cannot end up in data you pipe somewhere, and you can silence or redirect it |
236
+ | Time limits are module constants (`pyppesnap.snapshot.NAVIGATION_TIMEOUT`) | Fields of `SnapshotOptions`, passed to `snap_png()` / `snap_pdf()` | No global state to patch; each call says what it wants |
237
+ | Any error returns `None` | Invalid arguments (`width=0`) raise `ValueError` | A bug in the calling code should not look like a page that failed to load |
238
+ | Python 3.7–3.14 | Python 3.10–3.14 | Playwright needs Python 3.10 or newer |
239
+ | `pyppesnap` command, errors on stdout | `playwright-snap` command, errors on stderr | Same reason as the logging |
240
+
241
+ The pyppeteer-specific helpers that were never in its README (`get_snapshot_by_url()` with a capture
242
+ function that receives a pyppeteer `Page`, `shot_with_image()`, `launch_ptppeteer_with_url()`) have no
243
+ counterpart: use `SnapSession` for anything beyond the two snapshot types.
244
+
245
+ ## Use it in a web app
246
+
247
+ [`example/`](https://github.com/NatLee/playwright-snap/tree/main/example) is a small Flask app: type a URL,
248
+ press a button, and the server returns the snapshot, which the page shows as an image or an inline PDF.
249
+ It is pyppeteer-snap's example with the import changed.
250
+
251
+ ```bash
252
+ pip install playwright-snap flask
253
+ python -m playwright install chromium
254
+ cd example
255
+ python main.py # then open http://127.0.0.1:9898
256
+ ```
257
+
258
+ Don't put it on the internet as it is: it opens any URL a visitor types, including addresses inside your
259
+ own network.
260
+
261
+ ## Development
262
+
263
+ ```bash
264
+ pip install -e ".[test]"
265
+ python -m playwright install chromium
266
+ pytest # unit tests, plus real snapshots of local test pages on port 18030
267
+ pytest -m "not browser" # only the tests that do not start Chromium
268
+ ```
269
+
270
+ CI runs the tests on Linux with Python 3.10–3.14, on Windows and macOS, and with the oldest Playwright
271
+ that `pyproject.toml` allows. It also checks the type hints (`mypy`) and builds the package.
272
+
273
+ The README images come from the HTML files in
274
+ [`docs/images/src/`](https://github.com/NatLee/playwright-snap/tree/main/docs/images/src):
275
+ `take_demo_snapshots.py` takes the real snapshots with playwright-snap, and each HTML file says how to
276
+ render it.
277
+
278
+ Releases go to PyPI through [`.github/workflows/release.yml`](https://github.com/NatLee/playwright-snap/blob/main/.github/workflows/release.yml)
279
+ when a GitHub Release is published, using the repository secret `PYPI_API_TOKEN`; the steps are at the
280
+ top of that file.
281
+
282
+ ## License
283
+
284
+ [MIT](https://github.com/NatLee/playwright-snap/blob/main/LICENSE)
@@ -0,0 +1,250 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/hero-dark.webp">
4
+ <img src="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/hero-light.webp" alt="playwright-snap: give it a URL, a headless Chromium driven by Playwright renders the page, and you get a PNG (str) or a PDF (bytes) back as base64. Coming from pyppeteer-snap, only the import changes.">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/NatLee/playwright-snap/actions/workflows/test.yml"><img src="https://github.com/NatLee/playwright-snap/actions/workflows/test.yml/badge.svg" alt="Test"></a>
10
+ <a href="https://pypi.org/project/playwright-snap/"><img src="https://img.shields.io/pypi/v/playwright-snap" alt="PyPI version"></a>
11
+ <a href="https://pypi.org/project/playwright-snap/"><img src="https://img.shields.io/pypi/pyversions/playwright-snap" alt="Python versions"></a>
12
+ <a href="https://github.com/NatLee/playwright-snap/blob/main/LICENSE"><img src="https://img.shields.io/github/license/NatLee/playwright-snap" alt="License: MIT"></a>
13
+ </p>
14
+
15
+ playwright-snap takes a snapshot of a web page and hands it to you: a PNG of what fits in the browser
16
+ window, or a PDF of the whole page on a single sheet. It opens the page in a headless Chromium (no
17
+ window) through [Playwright](https://playwright.dev/python/), so JavaScript-heavy pages render like they
18
+ do in a real browser. One function call, nothing written to disk.
19
+
20
+ It is the maintained successor of [pyppeteer-snap](https://github.com/NatLee/pyppeteer-snap), which is
21
+ built on the no longer maintained Pyppeteer and its old Chromium. The functions have the same names,
22
+ arguments and return values, so moving over is a one-line change, see
23
+ [Coming from pyppeteer-snap](#coming-from-pyppeteer-snap).
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install playwright-snap
29
+ python -m playwright install chromium
30
+ ```
31
+
32
+ The second line downloads the Chromium build that your Playwright version drives, into your user
33
+ cache folder. Do it once, and again after upgrading Playwright. Only taking snapshots? Add
34
+ `--only-shell` to download just the headless part, which saves about 400 MB of disk space.
35
+
36
+ On Linux, Chromium also needs some system libraries. Playwright installs them for you on Debian and
37
+ Ubuntu (for example in a `python:3.12-slim` Docker image); it needs root:
38
+
39
+ ```bash
40
+ python -m playwright install --with-deps chromium
41
+ ```
42
+
43
+ The package is called playwright-snap on PyPI and is imported as `playsnap`: a name that starts with
44
+ `playwright` would make Playwright mistake this package's code for its own. It works on Python
45
+ 3.10–3.14, on Linux, macOS and Windows. The latest code is a
46
+ `pip install git+https://github.com/NatLee/playwright-snap` away.
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ from playsnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
52
+
53
+ png = get_image_snapshot_by_url("https://example.com") # PNG as base64 str
54
+ pdf = get_pdf_snapshot_by_url("https://example.com") # PDF as base64 bytes
55
+
56
+ print(png[:20]) # iVBORw0KGgoAAAANSUhE
57
+ print(pdf[:20]) # b'JVBERi0xLjQKJdPr6eEK'
58
+ ```
59
+
60
+ Here is what you get for the same page (real output, decoded):
61
+
62
+ <p align="center">
63
+ <picture>
64
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/demo-dark.webp">
65
+ <img src="https://raw.githubusercontent.com/NatLee/playwright-snap/main/docs/images/demo-light.webp" alt="The same page captured three times: get_image_snapshot_by_url returns a PNG of the 1600 x 900 browser window, is_mobile=True gives the phone layout of a 390 x 844 window, and get_pdf_snapshot_by_url returns the whole page on a single PDF sheet">
66
+ </picture>
67
+ </p>
68
+
69
+ Rather have the file itself, other window sizes, and an exception when it fails?
70
+
71
+ ```python
72
+ from playsnap import SnapshotOptions, snap_pdf, snap_png
73
+
74
+ phone = SnapshotOptions(width=390, height=844, is_mobile=True, device_scale_factor=2)
75
+
76
+ with open("example.png", "wb") as f:
77
+ f.write(snap_png("https://example.com", phone)) # a 780 x 1688 PNG
78
+ with open("example.pdf", "wb") as f:
79
+ f.write(snap_pdf("https://example.com")) # one sheet, 1600 px wide
80
+ ```
81
+
82
+ ## API
83
+
84
+ Everything below is imported from `playsnap`.
85
+
86
+ ### Functions
87
+
88
+ | Function | Returns | When it fails |
89
+ |---|---|---|
90
+ | `get_image_snapshot_by_url(url, width=1600, height=900, is_mobile=False)` | PNG as base64 `str` | logs why, returns `None` |
91
+ | `get_pdf_snapshot_by_url(url, width=1600, height=900, is_mobile=False)` | PDF as base64 `bytes` | logs why, returns `None` |
92
+ | `snap_png(url, options=None)` | PNG `bytes` | raises `SnapshotError` |
93
+ | `snap_pdf(url, options=None)` | PDF `bytes` | raises `SnapshotError` |
94
+
95
+ The PNG is what fits in the browser window. The PDF is the whole page, laid out as wide as the window,
96
+ on one sheet as long as the page (with the page's print styles, as when you print it).
97
+
98
+ - `url` needs its scheme: `https://example.com`, not `example.com`.
99
+ - The first two are pyppeteer-snap's functions, kept exactly as they were. They log the reason for a
100
+ failure to the `playsnap` logger, which Python shows on stderr unless you set up logging.
101
+ - Each call starts its own Chromium and closes it again, also when something goes wrong.
102
+ - They are synchronous and also work where an event loop is already running, such as a Jupyter
103
+ notebook. In async code, use [`SnapSession`](#snapsession) instead.
104
+
105
+ ### SnapshotOptions
106
+
107
+ All settings live in one frozen dataclass. Sizes are CSS pixels, times are seconds, and every number must
108
+ be greater than 0 (you get a `ValueError` right away otherwise).
109
+
110
+ | Option | Default | What it does |
111
+ |---|---|---|
112
+ | `width` | `1600` | width of the browser window; the PNG and the PDF are this wide |
113
+ | `height` | `900` | height of the browser window; the PNG is this high |
114
+ | `is_mobile` | `False` | lay the page out like a phone browser, honouring its `<meta name="viewport">` |
115
+ | `device_scale_factor` | `1` | image pixels per CSS pixel; `2` gives a retina-sharp PNG twice the size |
116
+ | `wait_until` | `"load"` | when the page counts as loaded: `"load"`, `"domcontentloaded"`, `"networkidle"` or `"commit"` |
117
+ | `navigation_timeout` | `30` | how long the page may take to load |
118
+ | `snapshot_timeout` | `120` | how long one whole snapshot may take, starting Chromium included |
119
+
120
+ `wait_until="networkidle"` helps with pages that keep loading content after the load event.
121
+
122
+ ### SnapSession
123
+
124
+ For async code, or many pages in a row: one Chromium for all snapshots, which is quicker than starting
125
+ one per page.
126
+
127
+ ```python
128
+ import asyncio
129
+ from playsnap import SnapSession, SnapshotOptions
130
+
131
+ async def main():
132
+ async with SnapSession() as session:
133
+ png = await session.png("https://example.com")
134
+ pdf = await session.pdf("https://example.com", SnapshotOptions(width=800))
135
+ print(len(png), len(pdf))
136
+
137
+ asyncio.run(main())
138
+ ```
139
+
140
+ `session.png(url, options=None)` and `session.pdf(url, options=None)` return `bytes` and raise like
141
+ `snap_png()`. Each snapshot opens the page in a fresh browser context, so cookies and storage never carry
142
+ over from one page to the next. Chromium closes when the `async with` block ends.
143
+
144
+ ### Errors
145
+
146
+ Every failure is a `SnapshotError`, and `str(error)` says what went wrong in one line. The subclasses say
147
+ which step failed:
148
+
149
+ - `BrowserLaunchError`: Chromium did not start. It is not installed, or on Linux it is missing system
150
+ libraries; the message says which command fixes it.
151
+ - `NavigationError`: the page did not load. The URL is wrong or unreachable, or `navigation_timeout` ran
152
+ out.
153
+ - `SnapshotTimeoutError`: the whole snapshot took longer than `snapshot_timeout`.
154
+
155
+ The full Playwright error is chained as `error.__cause__`. Invalid options are a `ValueError`, not a
156
+ `SnapshotError`, because they are a bug in the calling code rather than a page that failed.
157
+
158
+ ## Command line
159
+
160
+ ```console
161
+ $ playwright-snap --url https://example.com
162
+ ------------- Parameters ---------------
163
+ URL: https://example.com
164
+ Type: img
165
+ --------------- Base64 ------------------
166
+ iVBORw0KGgoAAAANSUhEUgAABkAAAAOECAIAAAB2L2r1AAAQAElEQVR4nOz9…
167
+ -------------------------------------
168
+ ```
169
+
170
+ `--type img` (the default) prints the PNG, `--type pdf` the PDF. The base64 text is always on line 5,
171
+ so you can save the file from the shell:
172
+
173
+ ```bash
174
+ playwright-snap --url https://example.com --type pdf | sed -n 5p | base64 -d > example.pdf
175
+ ```
176
+
177
+ `python -m playsnap` does the same. The exit status is 0 when it worked, 1 when the snapshot
178
+ failed (the reason goes to stderr), and 2 when `--url` is missing or `--type` is not `img` or `pdf`.
179
+
180
+ ## Coming from pyppeteer-snap
181
+
182
+ Install playwright-snap and Chromium as shown in [Installation](#installation), then change the import:
183
+
184
+ ```diff
185
+ -from pyppesnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
186
+ +from playsnap import get_image_snapshot_by_url, get_pdf_snapshot_by_url
187
+ ```
188
+
189
+ The functions take the same arguments, use the same defaults (a 1600 × 900 window, 30 s to load, 120 s
190
+ per snapshot) and return the same types: a PNG as base64 `str`, a PDF as base64 `bytes`, `None` when
191
+ the snapshot failed. `from pyppesnap.snapshot import …` becomes `from playsnap.snapshot import …`.
192
+ The command line tool keeps its options and its output; only its name changes from `pyppesnap` to
193
+ `playwright-snap`.
194
+
195
+ What is different, and why:
196
+
197
+ | pyppeteer-snap | playwright-snap | Why |
198
+ |---|---|---|
199
+ | `pip install PyppeSnap`, `import pyppesnap` | `pip install playwright-snap`, `import playsnap` | An import name that starts with `playwright` would make Playwright mistake this package's code for its own |
200
+ | Downloads Chromium on the first snapshot | `python -m playwright install chromium`, once | No surprise 100–200 MB download in the middle of a request, and the browser matches your Playwright version |
201
+ | Prints the reason for a failure to stdout | Logs it (logger `playsnap`, stderr by default) | It cannot end up in data you pipe somewhere, and you can silence or redirect it |
202
+ | Time limits are module constants (`pyppesnap.snapshot.NAVIGATION_TIMEOUT`) | Fields of `SnapshotOptions`, passed to `snap_png()` / `snap_pdf()` | No global state to patch; each call says what it wants |
203
+ | Any error returns `None` | Invalid arguments (`width=0`) raise `ValueError` | A bug in the calling code should not look like a page that failed to load |
204
+ | Python 3.7–3.14 | Python 3.10–3.14 | Playwright needs Python 3.10 or newer |
205
+ | `pyppesnap` command, errors on stdout | `playwright-snap` command, errors on stderr | Same reason as the logging |
206
+
207
+ The pyppeteer-specific helpers that were never in its README (`get_snapshot_by_url()` with a capture
208
+ function that receives a pyppeteer `Page`, `shot_with_image()`, `launch_ptppeteer_with_url()`) have no
209
+ counterpart: use `SnapSession` for anything beyond the two snapshot types.
210
+
211
+ ## Use it in a web app
212
+
213
+ [`example/`](https://github.com/NatLee/playwright-snap/tree/main/example) is a small Flask app: type a URL,
214
+ press a button, and the server returns the snapshot, which the page shows as an image or an inline PDF.
215
+ It is pyppeteer-snap's example with the import changed.
216
+
217
+ ```bash
218
+ pip install playwright-snap flask
219
+ python -m playwright install chromium
220
+ cd example
221
+ python main.py # then open http://127.0.0.1:9898
222
+ ```
223
+
224
+ Don't put it on the internet as it is: it opens any URL a visitor types, including addresses inside your
225
+ own network.
226
+
227
+ ## Development
228
+
229
+ ```bash
230
+ pip install -e ".[test]"
231
+ python -m playwright install chromium
232
+ pytest # unit tests, plus real snapshots of local test pages on port 18030
233
+ pytest -m "not browser" # only the tests that do not start Chromium
234
+ ```
235
+
236
+ CI runs the tests on Linux with Python 3.10–3.14, on Windows and macOS, and with the oldest Playwright
237
+ that `pyproject.toml` allows. It also checks the type hints (`mypy`) and builds the package.
238
+
239
+ The README images come from the HTML files in
240
+ [`docs/images/src/`](https://github.com/NatLee/playwright-snap/tree/main/docs/images/src):
241
+ `take_demo_snapshots.py` takes the real snapshots with playwright-snap, and each HTML file says how to
242
+ render it.
243
+
244
+ Releases go to PyPI through [`.github/workflows/release.yml`](https://github.com/NatLee/playwright-snap/blob/main/.github/workflows/release.yml)
245
+ when a GitHub Release is published, using the repository secret `PYPI_API_TOKEN`; the steps are at the
246
+ top of that file.
247
+
248
+ ## License
249
+
250
+ [MIT](https://github.com/NatLee/playwright-snap/blob/main/LICENSE)
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "playwright-snap"
7
+ dynamic = ["version"]
8
+ description = "Snapshots of web pages as base64 PNG or PDF, taken with Playwright's headless Chromium."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Nat Lee", email = "natlee.work@gmail.com" }]
13
+ # Playwright itself needs Python 3.10 or newer since 1.61.
14
+ requires-python = ">=3.10"
15
+ # 1.49 is the oldest Playwright the "oldest Playwright" CI job tests. CI reads the
16
+ # number from this line, so raising it here is all it takes.
17
+ dependencies = ["playwright>=1.49"]
18
+ keywords = ["playwright", "chromium", "headless", "snapshot", "screenshot", "pdf", "base64"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
31
+ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
32
+ "Typing :: Typed",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ # pillow and pypdf read the real PNGs and PDFs in tests/test_browser.py; fonttools
37
+ # makes the web font for one of its test pages (tests/conftest.py).
38
+ test = ["pytest", "pillow", "pypdf", "fonttools"]
39
+
40
+ [project.scripts]
41
+ playwright-snap = "playsnap.cli:main"
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/NatLee/playwright-snap"
45
+ Source = "https://github.com/NatLee/playwright-snap"
46
+ Issues = "https://github.com/NatLee/playwright-snap/issues"
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "playsnap.__version__" }
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ markers = ["browser: starts a real headless Chromium (needs `python -m playwright install chromium`)"]
54
+
55
+ [tool.mypy]
56
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """playwright-snap: snapshots of web pages as PNG or PDF, taken with Playwright's headless Chromium."""
2
+ __version__ = "0.1.0"
3
+
4
+ from .cli import main
5
+ from .errors import BrowserLaunchError, NavigationError, SnapshotError, SnapshotTimeoutError
6
+ from .options import SnapshotOptions, WaitUntil
7
+ from .session import SnapSession
8
+ from .snapshot import get_image_snapshot_by_url, get_pdf_snapshot_by_url, snap_pdf, snap_png
9
+
10
+ __all__ = [
11
+ "BrowserLaunchError",
12
+ "NavigationError",
13
+ "SnapSession",
14
+ "SnapshotError",
15
+ "SnapshotOptions",
16
+ "SnapshotTimeoutError",
17
+ "WaitUntil",
18
+ "get_image_snapshot_by_url",
19
+ "get_pdf_snapshot_by_url",
20
+ "main",
21
+ "snap_pdf",
22
+ "snap_png",
23
+ ]
@@ -0,0 +1,5 @@
1
+ """``python -m playsnap``: the same as the ``playwright-snap`` command."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())