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.
- playwright_snap-0.1.0/LICENSE +21 -0
- playwright_snap-0.1.0/MANIFEST.in +4 -0
- playwright_snap-0.1.0/PKG-INFO +284 -0
- playwright_snap-0.1.0/README.md +250 -0
- playwright_snap-0.1.0/pyproject.toml +56 -0
- playwright_snap-0.1.0/setup.cfg +4 -0
- playwright_snap-0.1.0/src/playsnap/__init__.py +23 -0
- playwright_snap-0.1.0/src/playsnap/__main__.py +5 -0
- playwright_snap-0.1.0/src/playsnap/cli.py +66 -0
- playwright_snap-0.1.0/src/playsnap/errors.py +47 -0
- playwright_snap-0.1.0/src/playsnap/options.py +56 -0
- playwright_snap-0.1.0/src/playsnap/py.typed +0 -0
- playwright_snap-0.1.0/src/playsnap/session.py +155 -0
- playwright_snap-0.1.0/src/playsnap/snapshot.py +90 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/PKG-INFO +284 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/SOURCES.txt +31 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/dependency_links.txt +1 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/entry_points.txt +2 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/requires.txt +7 -0
- playwright_snap-0.1.0/src/playwright_snap.egg-info/top_level.txt +1 -0
- playwright_snap-0.1.0/tests/conftest.py +104 -0
- playwright_snap-0.1.0/tests/pages/fresh-context.html +10 -0
- playwright_snap-0.1.0/tests/pages/late-font.html +21 -0
- playwright_snap-0.1.0/tests/pages/never-loads.html +5 -0
- playwright_snap-0.1.0/tests/pages/no-viewport-tag.html +7 -0
- playwright_snap-0.1.0/tests/pages/print-taller.html +13 -0
- playwright_snap-0.1.0/tests/pages/sticks-out.html +9 -0
- playwright_snap-0.1.0/tests/test_browser.py +308 -0
- playwright_snap-0.1.0/tests/test_cli.py +113 -0
- playwright_snap-0.1.0/tests/test_errors.py +77 -0
- playwright_snap-0.1.0/tests/test_options.py +59 -0
- playwright_snap-0.1.0/tests/test_package.py +39 -0
- 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,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,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
|
+
]
|