Configuration
Render options
Section titled “Render options”All of these live in src/content/exporter.js.
| Value | Default | Location | Effect |
|---|---|---|---|
| Wrapper width | 800px |
wrapper.style.cssText |
Layout width the conversation is re-flowed to before capture. |
html2canvas width / windowWidth |
800 |
generateImagePDF(), generateImage() |
Must match the wrapper width, or the capture is clipped or padded. |
html2canvas scale |
2 |
generateImagePDF(), generateImage() |
Device-pixel multiplier. 1 halves file size and sharpness; 3 triples memory use. |
| Content padding | 40px |
paddedContent.style.cssText |
White margin around the exported content. |
backgroundColor |
#ffffff |
both generators | Canvas background where the DOM is transparent. |
useCORS |
true |
both generators | Attempt to load cross-origin images. Images from hosts without permissive CORS headers are dropped from the capture regardless. |
foreignObjectRendering |
false |
both generators | Kept off — the foreignObject path is faster but renders Gmail’s DOM inconsistently. |
logging |
false |
both generators | Set to true to get html2canvas’ own trace in the console. |
| Settle delay | 500 ms |
exportEmail() |
await new Promise(r => setTimeout(r, 500)) before measuring and capturing, giving the browser time to lay out the clone and load images. Raise it for image-heavy threads. |
| Height measurement | paddedContent.scrollHeight + 50 |
exportEmail() |
The + 50 is slack so the last line is never clipped. |
PDF output
Section titled “PDF output”| Value | Default | Location |
|---|---|---|
| Image codec embedded in the PDF | JPEG at quality 0.95 |
canvas.toDataURL('image/jpeg', 0.95) |
| Page units | px |
new jsPDF({ unit: 'px' }) |
| Page format | [canvas.width / 2, canvas.height / 2] |
generateImagePDF() — the / 2 undoes scale: 2, so the page matches CSS pixels |
| Orientation | l if the canvas is wider than tall, otherwise p |
generateImagePDF() |
| Filename | <subject>.pdf via pdf.save() |
generateImagePDF() |
Switching the codec to canvas.toDataURL('image/png') and passing 'PNG' to pdf.addImage() gives
lossless output at a considerably larger file size.
Image output
Section titled “Image output”| Value | Default | Location |
|---|---|---|
| MIME type | image/png (image/jpeg when called with 'jpg') |
generateImage() |
| Quality argument | 1.0 |
canvas.toDataURL(mimeType, 1.0) — ignored for PNG |
| Download mechanism | a synthetic <a download> click |
generateImage() |
generateImage() already accepts 'jpg', but the injected menu only ever calls it with 'png'.
Export header
Section titled “Export header”exportEmail() prepends a header block to the clone:
| Value | Default | Location |
|---|---|---|
| Title text | filenameBase.substring(0, 100) — the conversation subject, truncated to 100 characters |
h1.innerText |
| Title style | 20px, bold |
h1.style.cssText |
| Timestamp | new Date().toLocaleString() — the export time, not the message date, rendered in the browser locale |
date.innerText |
| Timestamp style | 11px, #5f6368 |
date.style.cssText |
| Separator | 1px solid #ddd, 25px bottom margin |
header.style.cssText |
Font stack
Section titled “Font stack”forceJapaneseFont() applies one stack to the clone and to every descendant with !important:
"Hiragino Kaku Gothic Pro", "Meiryo", "Yu Gothic", "MS Gothic", "Noto Sans JP", sans-serifThis deliberately overrides whatever fonts the mail specified, to guarantee CJK glyph coverage. See the Fonts guide for how the stack resolves and how to add Noto Sans JP yourself.
Gmail selectors
Section titled “Gmail selectors”cleanArtifacts() holds the list of things stripped from the clone. This is the part most likely to
need maintenance, because Gmail’s obfuscated class names change without notice.
- Removed outright: the extension’s own UI, every
[role="toolbar"], and elements whosedata-tooltipcontainsReply,Forward,AntwortenorWeiterleiten. - Hidden via
display: none !important: a list ofaria-labelselectors (English and German),[role="alert"], roughly thirty Gmail class names (.hI,.fv,.at,.gE,.T-I,.bAo, …), and everything carrying adata-tooltipattribute. - Heuristics: short text nodes matching known interface phrases have their nearest
[role="button"]/.T-I/[data-tooltip]ancestor hidden;svgandpathelements inside a button-like ancestor are hidden unless they sit inside the message body (.a3s). - Colour normalisation: any computed background that is a light grey (channels within 10 of each
other and above 200) is forced to
#ffffff; the clone’s own text colour is forced to#202124.
Build configuration
Section titled “Build configuration”webpack.config.cjs:
| Setting | Value | Why |
|---|---|---|
mode |
production |
Minified output. |
entry.content |
./src/content/scanner.js |
The only bundled entry point. |
output.path |
dist/ |
What you load unpacked. |
LimitChunkCountPlugin maxChunks |
1 |
MV3 content scripts cannot load extra chunks at runtime, so everything must be in one file. |
| Font rule | asset/inline for `woff |
woff2 |
CopyPlugin patterns |
manifest.json, content/style.css, assets/, popup/, background/ |
Copied verbatim; the last three use noErrorOnMissing. |
performance.hints |
false |
The bundle is legitimately large (~770 KiB) because html2canvas and jsPDF are inlined. |
Manifest
Section titled “Manifest”src/manifest.json declares activeTab, scripting and downloads, plus the host permission
https://mail.google.com/* and a <all_urls> web_accessible_resources entry for assets/*. Only
the host permission is actually needed by the working code path — see
Privacy & Permissions.