Repository navigation
Screenshot Pipeline
This page follows a screenshot from the popup button to the annotated image you download, copy or upload: the three capture modes, the full-page scroll-and-stitch, storage, and the editor. Read it before changing screenshot.ts, page-scripts.ts, annotate.ts or screenshot-editor.tsx. The Architecture page covers the messages between the popup and the background worker, and the Upload Pipeline page covers uploads. The Screenshots page describes the same features from the user's side.
A screenshot passes through these stages:
-
Request. You click one of the popup's three screenshot buttons. The popup finds the active tab, turns the screenshot quality setting into a
maxWidth, sends ascreenshotmessage to the background worker and closes. -
Capture.
takeScreenshotin the worker captures the tab in the requested mode:visible,areaorfull. -
Resize.
limitWidthscales the image down tomaxWidth, keeping its shape. AmaxWidthof 0, or an image no wider thanmaxWidth, passes through untouched. -
Store.
addScreenshotwrites the PNG to IndexedDB, with the page's title and URL. -
Edit. The worker opens
app.html#/screenshots/<id>in a new tab next to the captured one, and the editor loads it.
Capturing runs in the background worker because the popup closes itself straight after sending the message, and a full-page capture can take about a minute. The worker uses OffscreenCanvas for all its image work, since a service worker has no DOM.
Screen Tools has no host permissions at install. Clicking the toolbar popup grants activeTab for the current tab, which allows browser.tabs.captureVisibleTab and browser.scripting.executeScript on that tab. The same works in Chrome and Firefox. Chrome refuses both on its own pages, the Chrome Web Store and some others. Firefox refuses them on about: pages, addons.mozilla.org, reader view and some others.
When a capture fails, takeScreenshot shows a notification titled "Can't capture this page", with the browser's own wording from platform.copy.blockedScreenshotMessage. isBlockedPage matches the error messages either browser gives for pages extensions can't touch, including Firefox's "Missing host permission for the tab" and "Missing activeTab permission". It logs those with console.warn, so they don't appear as extension errors on Chrome's extensions page. Any other error goes to console.error.
captureVisible calls browser.tabs.captureVisibleTab for the window as PNG and converts the data URL to a blob. The other two modes build on this call.
captureArea injects selectArea from page-scripts.ts into the tab. selectArea puts a full-viewport overlay at the top z-index, inside a closed shadow root so the page's CSS can't restyle it, and lets you drag out a rectangle:
- The overlay shows the selection's size in CSS pixels as you drag.
- A click or a drag under 5 px resets the overlay instead of capturing a sliver.
- Escape removes the overlay and resolves
null, and the worker then stops without storing anything. - On release, the overlay removes itself and waits two animation frames before resolving, so it's gone from the screen when the capture happens.
The worker then captures the visible area and crops it to the selection. The scale is the captured image width divided by the viewport width in CSS pixels, which accounts for both screen density and page zoom.
captureFullPage scrolls the page one viewport at a time, captures each position and stitches the captures onto one canvas. It injects four functions from page-scripts.ts:
| Function | Does |
|---|---|
preparePage |
Hides the scrollbar, turns off smooth scrolling, returns the page height, viewport size and scroll position |
scrollPage |
Scrolls to a y, waits two animation frames and 100 ms for the page to paint, returns where it landed |
hideFixedElements |
Sets visibility: hidden !important on every fixed or sticky element |
restorePage |
Undoes the other three and scrolls back to where you were |
The stitch runs as follows:
-
preparePagemeasures the page. The step count is the page height divided by the viewport height, rounded up and capped at 100 steps (about a minute of scrolling). - Each step sets the toolbar badge to
<step>/<count>and scrolls to the next viewport. The last step is clamped to the bottom of the page. - Before the second capture,
hideFixedElementshides fixed and sticky elements, so a sticky header appears once at the top instead of in every slice. - Captures are spaced at least 550 ms apart, because Chrome allows two
captureVisibleTabcalls a second. Firefox has no such limit, and keeps the same gap. - The first capture sizes the canvas: the capture's width by the page height, both in captured pixels. If that would break a canvas limit (32 767 px a side or about 268 million pixels), the whole image is scaled down to fit.
- Each capture is drawn at the position
scrollPagereported, so the browser's clamping on the last step doesn't misplace it. - A
finallyblock runsrestorePageand clears the badge, whether the capture succeeded or failed.
browser.scripting.executeScript({ func }) sends each function's source text to the page, so the functions in page-scripts.ts can't import anything or use anything outside their own body. A change that adds an import or a module-level constant to one of them breaks it at run time in the page, not at build time.
db.ts stores each screenshot as one row in the screenshots store, with two image blobs:
-
blobholds the image with any edits flattened in. -
originalholds the image as captured, so you can revert.
Both are PNG. The row also holds the width and height, the page title and URL, and two flags: edited once annotated or cropped, and saved once downloaded or copied. addScreenshot deletes the oldest rows beyond the latest 10 (MAX_SCREENSHOTS). Deleting a screenshot also deletes its upload rows.
The Screenshots list (screenshots.tsx) can download or copy a stored screenshot without opening the editor, and upload it when a service is connected. A JPG download or upload is re-encoded from the stored PNG at quality 0.92.
annotate.ts represents the edits as a Doc: a list of shapes and an optional crop, all in image pixels. Every edit replaces the whole Doc, so undo and redo move through a list of Doc values.
| Tool | Shape | Drawn as |
|---|---|---|
| Rectangle, Ellipse |
from, to
|
Outline in the chosen colour and size |
| Arrow |
from, to
|
Line with a filled head 4 × the stroke size; the line stops inside the head so its round cap doesn't show |
| Pen | points |
Freehand line |
| Highlighter | points |
Freehand line at 35 % opacity, 6 × the stroke size, with square ends |
| Text |
at, text
|
Semi-bold system font with an outline in white, or black for white text |
| Blur |
from, to
|
Pixelated blocks of the original image, at least 8 px each |
| Crop |
crop on the Doc
|
Limits the visible area |
The blur tool pixelates instead of blurring, because a blur can be partly undone to reveal what's underneath and pixelation can't. The editor offers 6 colours and 3 stroke sizes (thin, medium and thick). Each size is 1, 2 or 4 times a base stroke of the image width divided by 400, at least 2 px, so strokes look the same weight on small and large screenshots.
render draws the image and shapes onto a canvas the size of the visible area, shifting by the crop's offset. flatten does the same onto an OffscreenCanvas and returns a PNG or JPEG blob. Both draw the shapes in order, so a later shape covers an earlier one.
screenshot-editor.tsx is the ScreenshotEditor component on the /screenshots/<id> route. It loads the stored blob as an ImageBitmap and redraws the whole canvas on every change.
Pointer positions are converted to image pixels from the canvas's on-screen size, so shapes land in the right place at any display size. A drag under 4 image pixels counts as a stray click and adds nothing. The crop tool dims everything outside the rectangle being dragged and keeps the crop inside the current visible area, so cropping again narrows the crop further.
The text tool opens an input over the canvas where you clicked, styled to match the final text. Enter or clicking away commits the text, and Escape drops it.
⌘Z or Ctrl+Z undoes, and ⇧⌘Z, Ctrl+Shift+Z, ⌘Y or Ctrl+Y redoes, unless focus is in a text field. While there are unsaved edits, the browser asks before you close the tab.
fitWidth scales the canvas to the largest size that fits the space below the toolbar. An image more than twice as tall as it is wide counts as a full-page shot: the editor fits its width and lets the stage scroll, instead of shrinking it to fit the height.
-
Download flattens the edits in the chosen format (PNG, or JPG at quality 0.92) and saves it with a name like
screenshot-<yyyy>-<mm>-<dd>-<hhmmss>.png. The format choice is stored as theimageFormatsetting. -
Copy writes a flattened PNG to the clipboard. The editor passes the unresolved
flattenpromise toClipboardItem, which keeps the click's user activation while the image encodes. -
Upload appears once a service or upload server is connected. It opens
UploadDialogfromupload-dialog.tsx, which flattens the edits in theimageFormatformat and uploads them. The screenshot's uploads are listed under the editor. See Upload Pipeline. - After any of these, the editor stores a flattened PNG back in the row's
bloband setssaved, pluseditedif there are shapes or a crop. The editor keeps drawing on the image it loaded, so further edits stack on the sameDochistory. -
Revert to original appears once a screenshot is edited. It copies
originalback intoblob, clearsedited, and remounts the editor with an empty history.
Using Screen Tools
- Installation and Updating
- Firefox
- Recording
- Screenshots
- Uploading
- Settings
- Troubleshooting and FAQ
- Privacy
How it works
Contributing