Capture multiple screenshots of a region you select, stitch them into a single PDF — locally, in your browser, no server, no LLM.
viewerclip is an open-source Chrome extension (Manifest V3) for paginated embedded document viewers — the kind that show a multi-page document inside an iframe or canvas widget but don't expose a "download as PDF" button. Pick a region once, click Add page on every page you scroll through inside the viewer, then click Save PDF. The extension stitches your screenshots into a single PDF file on disk.
It is not a scraper, not an automation tool, and not an LLM-based extractor. Every screenshot is a single user-initiated click. There is no outbound HTTP request of any kind.
- How it works
- Highlights
- Install
- Usage
- Privacy
- Tech stack
- Project status
- Build from source
- Contributing
- Releasing
- License
┌──────────────┐ 1. set region (auto or manual drag)
│ Active tab │ ────────────────────────────────────▶ floating panel
└──────┬───────┘
│ 2. you flip the viewer's pages yourself
│ and click "+ Add page" each time
▼
┌────────────────────────────────────────┐
│ Service worker (MV3, ephemeral) │
│ • chrome.tabs.captureVisibleTab │
│ • OffscreenCanvas crop to your rect │
│ • appended to chrome.storage.session │
└──────┬─────────────────────────────────┘
│ 3. click "Save PDF"
▼
┌────────────────────────────────────────┐
│ pdf-lib (in content script) │
│ • embed each PNG as one PDF page │
│ • A4 / Letter / fit, auto-orient │
│ • aspect-preserving fit + 24pt margin│
└──────┬─────────────────────────────────┘
│ 4. blob URL → chrome.downloads
▼
Local file: viewerclip_YYYYMMDD_HHMMSS.pdf
There is no LLM, no OCR, no text extraction. The output PDF contains rasterised images.
| Set region two ways | Auto — generic DOM heuristics offer up to 3 candidate regions to pick from. Manual — drag a rectangle anywhere. Manual always works as fallback. |
| One click per page | After region is set, click Add page on each page of the embedded viewer. The extension never auto-paginates and never makes a network request. |
| N pages → 1 PDF | pdf-lib stitches all captures into a single PDF. Per-page orientation auto-flips landscape if the captured aspect ratio is wide. |
| Zero outbound HTTP | The extension makes no fetch / XHR / WebSocket / beacon requests at runtime. No analytics, no telemetry, no LLM, no API. (host_permissions: ["<all_urls>"] is declared because Chrome requires it for chrome.tabs.captureVisibleTab — it grants screenshot/DOM read access on the active tab, not network access.) |
| Site-agnostic | No hardcoded domains, no vendor selectors, no URL allowlist. The same code runs identically on every page. |
| Session-scoped state | Captures live in chrome.storage.session and clear when the browser exits. Reset is one click. |
| MIT licensed | No closed-source server component, no proxy, no bundled key. |
Listing pending review.
Each tagged release attaches a signed .crx and .zip to the Releases page. Drag the .crx onto chrome://extensions (Developer mode enabled).
See Build from source.
- Open the page that contains the embedded paginated viewer.
- In the floating panel (bottom-right), click Auto region — the extension highlights up to 3 candidate areas; click the one that contains the document. If nothing is highlighted, click Manual and drag a rectangle around the viewer instead.
- Navigate the viewer's internal pagination yourself. On each page, click + Add page in the panel. The counter increments.
- When you've captured every page you want, click Save PDF. The extension assembles a PDF and triggers a download.
- To start over, click Reset. Captures and region clear.
The downloaded filename is viewerclip_YYYYMMDD_HHMMSS.pdf. No host-page text is included in the filename or PDF metadata.
- Zero outbound HTTP requests at runtime. The extension makes no fetch / XHR / WebSocket / beacon calls. No API, no LLM, no telemetry, no error reporting, no update beacons (beyond Chrome's own extension update check).
host_permissions: ["<all_urls>"]is required by Chrome for the screenshot API but does not enable network access. - All state lives in
chrome.storage.session, scoped to the current browser session. Closing every browser window clears it. - No site-specific code — no domain allowlist, no vendor selectors. The extension behaves identically on every page.
- No PII in the PDF metadata. Title / Creator / Producer fields are hardcoded to the literal string
viewerclip.
| Layer | Choice |
|---|---|
| Build | Vite + @crxjs/vite-plugin (MV3) |
| UI | React 18 + TypeScript (strict mode) |
| Styling | Tailwind CSS, scoped via shadow DOM |
| pdf-lib — pure JS, MIT | |
| Storage | chrome.storage.session |
| Tests | vitest |
| CI | GitHub Actions — tag → build → signed .crx + .zip draft Release |
No backend, no analytics SDK, no LLM SDK, no OCR. The extension makes zero outbound HTTP requests.
| Source version | 0.1.0 (MVP) |
| Manifest | V3 |
| Minimum Chrome | 116 |
| Web Store | Listing pending |
git clone https://github.com/JamesbbBriz/viewerclip.git
cd viewerclip
npm install
npm run buildThen in Chrome:
- Open
chrome://extensions. - Enable Developer mode.
- Click Load unpacked and select the generated
dist/folder.
npm run typecheck # tsc --noEmit, strict mode
npm run test # vitest run
npm run build # vite build → dist/
npm run package # produce release/viewerclip-v<version>.{zip,crx}viewerclip is MIT-licensed and welcomes contributions. Before opening a PR:
- Fork and create a feature branch from
main. - Run
npm install, thennpm run buildto confirm the extension builds cleanly. - Pass all quality gates (
typecheck,test,build). - Use Conventional Commits prefixes (
feat:/fix:/chore:/docs:/refactor:/test:/ci:).
- Inline scripts,
eval,Function(...), or any remote code execution. Manifest V3 forbids them and so does this project. - Any outbound HTTP request (fetch/XHR/WebSocket/beacon), any LLM/API integration, any analytics SDK.
host_permissionsis["<all_urls>"]only to allowchrome.tabs.captureVisibleTab; do not piggyback any other API on it. - Hardcoded domain names, vendor-specific CSS selectors, or DOM probes targeting any specific website. Auto-detect must remain a set of generic DOM heuristics.
- A clipcv-operated server, proxy, or any non-LLM outbound HTTP request. (There is no LLM here either.)
- User-facing copy that names a specific destination service, ATS, CRM, or recruiting platform.
- AI attribution lines in commits, PR descriptions, or code comments.
For larger changes (new dependency, architectural shift), open an issue first to discuss the design.
Releases are produced by GitHub Actions on every v* tag push.
- Bump
versioninsrc/manifest.jsonand commit. - Tag the commit:
git tag v<version>(e.g.git tag v0.2.0). - Push the tag:
git push origin v<version>. - The
release.ymlworkflow checks out the tag, runsnpm ci,npm run build,npm run package, then creates a draft GitHub Release withrelease/viewerclip-v<version>.zipandrelease/viewerclip-v<version>.crxattached. The release stays in draft so a maintainer can review the artifacts and write user-facing notes before publishing. - The CRX signing key comes from the
CRX_PRIVATE_KEYrepository secret — a PEM-encoded RSA-2048 private key. Generate it once withopenssl genrsa -out viewerclip.pem 2048and paste the contents into the secret. The same key must be used for every subsequent release so the extension's.crxretains a stable id.
MIT © viewerclip contributors.