Capture. Combine. Annotate. Export. Right in your browser.
A free, open-source Chrome extension for capturing screenshots, combining them into multi-image, multi-page compositions, annotating them, and exporting as PNG or a fully customizable PDF — 100% locally, with no uploads, no accounts, and no tracking.
| Mode | Description |
|---|---|
| 📷 Visible Area | Captures exactly what’s on screen right now |
| 📄 Full Page | Scrolls and stitches the entire page automatically |
| ✂ Crop Region | Drag to select any area of the page |
| 🖨 Save as PDF | Quick one-click capture straight to a PDF file |
| 🗂 Blank Workspace | Open the editor with nothing loaded — build a composition entirely from local images, drag-drop, or paste |
Capture opens a full editor in a new tab, built around pages and layers — not a single flat screenshot:
− / 100% / + / Fit, in the status bar. Auto-fits when a page’s content actually changes size; stays put when you switch pages.Colors aren’t limited to a preset palette — pick any color via the swatches, the native color picker, or by typing a hex code directly. Custom stroke sizes. 50-level undo, unified across annotations, layer moves, and page structure. Full keyboard shortcuts.
git clone https://github.com/AdnanTemur/snipwise.git
chrome://extensionssnipwise folderClick mode in popup
│
├── Visible Area ──→ background.captureVisibleTab() ──→ editor.html
│
├── Full Page ──────→ content.js injects
│ ↓
│ Capture fixed elements (headers/footers)
│ ↓
│ Hide fixed elements
│ ↓
│ Scroll in viewport steps
│ Read actual window.scrollY after each step
│ Draw only new pixels onto canvas
│ ↓
│ Restore + composite fixed elements
│ ↓
│ Send to background → editor.html
│
├── Crop Region ────→ content.js injects SVG mask overlay
│ User drags selection (clear cutout, dark surround)
│ Capture → crop → editor.html
│
├── Save as PDF ────→ Same as Full Page
│ Routes to pdf.html instead of editor
│ jsPDF converts canvas → .pdf download → tab closes
│
└── Blank Workspace → Opens editor.html directly, no capture —
the canvas stays unsized until you add a
first image (file picker / drag-drop / paste)
Everything past “→ editor.html” is the modular editor described below — the capture pipeline itself (background.js, content.js, popup) is unchanged from how it always worked.
The editor (editor.html + editor/) is a set of native ES modules — no bundler, no npm, loaded directly via <script type="module">. It’s organized around a strict boundary: business logic never touches the DOM, only ui/* and main.js do.
editor/
├── main.js # wires everything together; the only place that owns
│ # both the workspace state and the live canvases
├── core/ # pure logic — no `document` anywhere in this folder
│ ├── scene.js # layer model: image/text/callout layers, hit-testing,
│ │ # reorder, resize (incl. proportional font scaling)
│ ├── workspace.js # pages: create/remove/reorder, persisted snapshots
│ ├── history.js # unified undo stack (annotations + layers + pages)
│ └── geometry.js # bbox math, resize-handle hit-testing, arrow math
├── render/
│ └── compositor.js # draws the scene (image/text/callout layers back-to-
│ # front + annotation raster on top); also the one
│ # place that knows how to flatten a scene for export
├── tools/ # one file per tool — pen, arrow, line, rect, circle,
│ # text, callout, highlight, blur, step, emoji,
│ # crop-canvas, move — none of them touch `document`
├── ui/ # all DOM wiring: toolbar, layers/pages panels,
│ # PDF export dialog, inline text editor, icons
└── io/ # the only place that knows about chrome.* APIs and
# jsPDF: capture import, PNG/clipboard export,
# the PDF builder
Why layers/pages are plain objects, not classes: core/scene.js’s layer functions (hitTestLayer, reorderLayer, resizeLayer, setLayerProp, cloneLayers) work generically on {id, x, y, width, height, ...} regardless of layer type. Image layers, text layers, and callout layers all live in the same scene.layers array — the compositor just branches on layer.type when drawing. Pages work the same way one level up: a page is a persisted snapshot of a scene, and switching pages means flushing the live buffers into the outgoing page’s snapshot and loading the incoming one back into the same shared objects everything else already holds references to. Adding a new type of thing (as happened when Text/Callout became real objects) is a small, contained change — not a new subsystem.
The scroll-stitch reads window.scrollY after every scrollTo() call instead of trusting the requested position. Browsers clamp scrolling near the bottom, so using the actual position prevents the last strip from repeating. Fixed/sticky elements (navbars, footers) are captured separately at scroll=0, hidden during stitching, then composited back at their correct absolute positions.
Content script injection is guarded at three levels:
window.__snipwiseLoaded flag prevents re-registration of listenersexecuteScriptcaptureReady messages within 2 secondssnipwise/
├── manifest.json # MV3, no host permissions
├── popup.html/.js # Mode selector UI — capture modes + Blank Workspace
├── background.js # Service worker — captures, storage, tab routing
├── content.js # Page-level — full page stitch, SVG crop overlay
├── editor.html # Editor shell — loads editor/main.js as a module
├── editor/ # Modular editor — see "Editor architecture" above
├── pdf.html / pdf.js # Silent one-click PDF converter (the popup's
│ # "Save as PDF" fast path — separate from the
│ # in-editor Export PDF dialog)
├── jspdf.min.js # Bundled locally (no CDN, no CSP issues)
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
No build step. No npm. No dependencies to install. Edit files and reload the extension directly.
| Key | Tool |
|---|---|
V |
Move / Select |
P |
Pen |
A |
Arrow |
L |
Line |
R |
Rectangle |
C |
Circle |
T |
Text |
H |
Highlight |
S |
Step Marker |
O |
Callout Bubble |
B |
Blur / Redact |
E |
Emoji Stamp |
X |
Crop Canvas |
Shift (while resizing) |
Lock aspect ratio |
Delete / Backspace |
Remove selected layer |
| Arrow keys (Move tool) | Nudge selected layer (Shift = 10px) |
| Double-click (Move tool) | Edit a Text/Callout layer’s content |
Ctrl+Z |
Undo |
Ctrl+S |
Download PNG |
Ctrl+C |
Copy Image |
| Permission | Why |
|---|---|
activeTab |
Capture a screenshot of the current tab |
scripting |
Inject crop overlay and full-page scroll capture into the page |
tabs |
Open the editor and PDF exporter in new tabs |
storage |
Save theme preference and last capture reference locally |
downloads |
Save PNG and PDF files to the user’s Downloads folder |
No host permissions. No external requests. No “Proceed with caution” warning on install.
Snipwise is 100% local. No data ever leaves your device.
The only external resource is the Google Fonts stylesheet for editor typography — a standard browser request with no user identifiers.
Full privacy policy: https://adnantemur.github.io/snipwise/privacy
Both themes are CSS variables at the top of popup.html and editor.html — light is the default (:root), dark is opt-in (html.dark):
:root {
--accent: #6d4aff; /* primary accent */
--bg: #e7e9ec; /* canvas backdrop */
--surface: #ffffff; /* panels/toolbar */
}
html.dark {
--accent: #9583ff;
--bg: #18191c;
--surface: #232427;
}
editor/tools/your-tool.js exporting defineTool({ id, cursor, onPointerDown, onPointerMove, onPointerUp }) from tools/tool-base.js — look at editor/tools/rect.js for the simplest example (or text.js if it should be a movable object rather than raster).editor/tools/index.js.editor.html (data-icon="..." referencing an entry in editor/ui/icons.js, or add a new icon there first) and a shortcut key in editor/keyboard.js.Tools never touch document directly — they receive an injected ctx (scene, annotation canvas, history, and a few UI callbacks) and a canvas-space pointer position.
Full page capture uses only browser APIs — no external server involved. PDF conversion uses bundled jsPDF with no network calls.
Pull requests welcome. A few things to keep in mind:
core/ and render/ never import document; tools/* never touch it either (see “Editor architecture” above)editor/ui/icons.js instead (the Emoji Stamp tool’s own picker grid is the exception, since that’s user content)MIT — do whatever you want, attribution appreciated.