11 Commits
Author SHA1 Message Date
github-actions[bot] 69bc8ab079 chore(export): update PDFs, hashes and signatures [skip ci] 2026-10-24 14:36:15 +00:00
nopeitsnothing 63bb3a6868 docs(changelog): fixing broken link
Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-10-24 10:20:31 -04:00
nopeitsnothing 927f5aac98 docs(changelog): add v1.2.6 release notes
Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-10-24 10:16:04 -04:00
nopeitsnothing 23bc196e80 feat(pdf): add page numbers and ToC page references
Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-10-24 09:54:56 -04:00
nopeitsnothing 53d17ca29e v1.2.6
Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 22:29:53 -04:00
nopeitsnothing d6992a3b80 fix(css): oops in search icon pt2
Turns out the culprit may be a gh-pages deploy mutating the CSS

Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 22:08:30 -04:00
nopeitsnothing 4ff619f422 fix(css): oops in search icon
Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 21:36:03 -04:00
nopeitsnothing 11510a863b fix(css): oops, flat ToC rendering
Adjusted a few ToC and navigation settings to fix flat rendering.
Also removed `toc.integrate`
Fixed margin-left
Altered changelog admonition types
Fixed admonition title rendering bug (overlap)

Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 03:39:18 -04:00
nopeitsnothing 40e88da30a release: v1.2.6-pre1
- Various website improvements (CSS)
 - Adjust keyring path use the new relative path: pgp/anonymousplanet.asc
 - 02-sign.yml: use a fpr match to lock a specific key
 - 02-sign.yml: quick mkdir -p repo/export before copying files to the dir ^_^
 - Moved the Acknowledgements section to the bottom
 - COM Class Identifiers (CLSID/GUID) (NEW)
 - Windows Global Device Identifier (GDID) (NEW)
 - USB bus telemetry (device enumeration history) (NEW)
 - Glossary (`docs/includes/glossary.md`) and removed inline expansions where applicable

Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 01:16:16 -04:00
github-actions[bot] 444b5e416e chore(export): update PDFs, hashes and signatures [skip ci] 2026-07-12 01:16:04 -04:00
nopeitsnothing d9d144e563 chore(build): adjust keyring path
use the new relative path: pgp/anonymousplanet.asc

Signed-off-by: nopeitsnothing <no@anonymousplanet.net>
2026-07-12 01:00:43 -04:00
26 changed files with 84433 additions and 77520 deletions

No files matched your search

+2 -2
View File
@@ -71,8 +71,8 @@ jobs:
- name: 🖨️ Build PDFs
env:
CI: true
run: python scripts/build_guide_pdf.py --${{ steps.mode.outputs.build_mode }}
run: python scripts/build_guide_pdf.py --${{ steps.mode.outputs.build_mode }} 2>/dev/null
# ignore stderr (DEPRECATED_ENDPOINT, etc.)
- name: 📤 Upload PDF artifacts
uses: actions/upload-artifact@v4
with:
+35 -7
View File
@@ -1,19 +1,47 @@
name: 🚀 Publish docs via GitHub Pages
on:
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yml"
- "requirements.txt"
- ".github/workflows/publish.yml"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
name: Publish the Hitchhiker's Guide
name: Build and deploy the Hitchhiker's Guide
runs-on: ubuntu-latest
steps:
- name: Checkout main
- name: 🛠️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: anonymousplanet.net
- name: 🐍 Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"
- name: 📦 Install pinned dependencies
run: pip install -r requirements.txt
- name: 🔍 Sanity-check the build (fail fast on broken markup/links)
run: mkdocs build --strict
- name: 🚀 Deploy to gh-pages
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
mkdocs gh-deploy --force --clean
+1
View File
@@ -27,3 +27,4 @@ export/thgtoa-dark.pdf.sha256
export/thgtoa.pdf.b2sum
export/thgtoa-dark.pdf.b2sum
export/*.asc
export/.*.body.pdf
+1
View File
@@ -2,6 +2,7 @@
title: "Anonymous Planet"
description: "We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space."
hide:
- navigation
- toc
schema:
"@context": https://schema.org
+26 -14
View File
@@ -1,6 +1,8 @@
---
title: "Changelog"
description: "Release notes for the Hitchhiker's Guide to Online Anonymity."
hide:
- navigation
schema:
"@context": https://schema.org
"@type": Organization
@@ -30,37 +32,47 @@ schema:
<!-- Change nothing below this line, please. -->
---
## [v1.2.6-pre1]
## [v1.2.6]
!!! Note "Meta"
!!! abstract "Meta"
- Released 2026-10-24
- Various website improvements (CSS)
- PDF build overhaul: larger text, page numbers, tighter layout, and a new cover
!!! Note "Changed"
!!! info "Changed"
- Adjust keyring path use the new relative path: pgp/anonymousplanet.asc
- 02-sign.yml: use a fpr match to lock a specific key
- 02-sign.yml: quick mkdir -p repo/export before copying files to the dir
- Moved the Acknowledgements section to the bottom
- PDF body text enlarged to 13pt, with headings, code, and tables scaled to match
- PDF page margins reduced to 12 mm (16 mm at the bottom) and spacing tightened to cut the page count; the cover and Table of Contents use the same margins
- PDF tables, code blocks, and admonitions can now break across pages, and table header rows repeat on each page
- PDF Table of Contents shrinks slightly (down to 85%) when needed to avoid a nearly empty last page
- Glossary abbreviations are no longer underlined in the PDF; the website keeps the underline and tooltips
- PDF cover redesigned with Fira Code monospace, green and amber accents, in both light and dark builds
!!! Note "Added"
!!! tip "Added"
- COM Class Identifiers (CLSID/GUID) (NEW)
- Windows Global Device Identifier (GDID) (NEW)
- USB bus telemetry (device enumeration history) (NEW)
- Page numbers on every page of both PDFs (bottom right)
- Page numbers in the PDF Table of Contents
!!! Note "Improved"
!!! info "Improved"
- Glossary (`docs/includes/glossary.md`) and removed inline expansions where applicable
## [v1.2.5]
!!! Note "Meta"
!!! abstract "Meta"
- Website theme customization
- License change to accomodate our fiscal host Open Source Collective
!!! Note "Changed"
!!! info "Changed"
- Comprehensive updates throughout the guide reflecting the transition to Whonix 18.x as the newest version
- Replaced outdated Whonix.org wiki docs links with GitHub releases mirror and Archive.org backups
@@ -74,14 +86,14 @@ schema:
- Added comprehensive upgrade path guidance with backup procedures
- Updated 196! Wikipedia reference links throughout the entire guide
!!! Note "Added"
!!! tip "Added"
- All Whonix.org/wiki links now have Archive.org mirror backups for availability
- Upgrade path documented: Whonix 17 to 18 with automated release-upgrade support
- Detailed Qubes OS compatibility notes for both Whonix versions
- You can now get the Anonymous Planet PGP keyring from the site (copy/paste or download)
!!! Note "Improved"
!!! info "Improved"
- Virtualbox hardening section reorganized
- AppArmor configuration guidance added where applicable (Whonix 18.x)
@@ -91,11 +103,11 @@ schema:
## [v1.2.4]
!!! Note "Meta"
!!! abstract "Meta"
- Rename workflows (GH - now we can know the order)
!!! Note "Changed"
!!! info "Changed"
- Change the repo URL for our tor mirror
- Fix recommended reading admonition
@@ -115,7 +127,7 @@ schema:
CI/CD pipeline split into independent stages, dark PDF quality improved, release signing automated, and the changelog now updates itself on every build. Skipping v1.2.2 which was a placeholder and contained broken Python unsuitable for a tag/release.
???+ tip "Added"
!!! tip "Added"
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM on large documents.
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
@@ -151,7 +163,7 @@ CI/CD pipeline split into independent stages, dark PDF quality improved, release
First automated PDF build and the start of the CI pipeline.
???+ tip "Added"
!!! tip "Added"
- `scripts/build_guide_pdf.py`: builds the MkDocs site and renders the full guide to a single PDF via headless Chromium (Chrome or Edge). Supports `--dark`, `--light`, and `--both` modes.
- GitHub Actions workflow that installs Chromium, runs the build script, and uploads `export/thgtoa.pdf` as an artifact on every push to `main` or manual dispatch.
@@ -170,7 +182,7 @@ First automated PDF build and the start of the CI pipeline.
---
[v1.2.6-pre1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.6-pre1
[v1.2.6]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.6
[v1.2.5]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.5
[v1.2.4]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.4
[v1.2.3]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.3
+1
View File
@@ -3,6 +3,7 @@ title: "Content Contributions"
description: "How to contribute to the Hitchhiker's Guide — building, signing, releasing, and writing."
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": Organization
+1 -8
View File
@@ -3,21 +3,14 @@ title: "A Constitution"
description: "The rules, values, and principles governing the Anonymous Planet initiative and PSA community."
hide:
- toc
- navigation
---
<div class="hero-block">
<div class="hero-eyebrow">Applicable to all projects of the initiative.</div>
<h1 class="hero-title">A Constitution<span class="hero-subtitle">The rules we hold ourselves to.</span></h1>
<div class="hero-cta-row">
<a href="#principles" class="hero-cta hero-cta--primary">Our Principles</a>
<a href="#goals" class="hero-cta hero-cta--secondary">Core Goals</a>
</div>
</div>
---
## Principles { #principles }
<div class="index-grid">
<div class="index-card">
+2 -3
View File
@@ -2,7 +2,7 @@
title: Contribute & Donate
description: "Support the Anonymous Planet project through crypto donations or content contributions. No fiat. No tracking. No profit."
hide:
- toc
- navigation
---
<div class="hero-block">
@@ -172,5 +172,4 @@ All donations and spendings are recorded here. <span style="color:var(--status-e
| 2022-07-11 | 0.503232 XMR | 1984.is VPS (12 mo) | <span class="status-indicator status-warning">Ended</span> |
| 2022-09-19 | 0.345024 XMR | VPS RAM/disk upgrade | <span class="status-indicator status-warning">Ended</span> |
!!! note "About lost funds"
Entries marked **Lost** represent funds spent on services that are no longer available.
**Entries marked <span class="status-indicator status-error">Lost</span> represent funds spent on services that are no longer available.**
+20 -20
View File
@@ -1,6 +1,8 @@
---
title: "The Hitchhiker's Guide"
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
hide:
- navigation
schema:
"@context": https://schema.org
"@type": Organization
@@ -578,33 +580,29 @@ Whether it is Android, iOS, Windows, macOS, or even Ubuntu. Most popular Operati
Here are good overviews of what is being collected by those five popular OSes in their last versions:
- Android/Google:
**Android/Google:**
- Just have a read at their privacy policy <https://policies.google.com/privacy> <sup>[[Archive.org]](https://web.archive.org/web/https://policies.google.com/privacy)</sup>
- Just have a read at their privacy policy <https://policies.google.com/privacy> <sup>[[Archive.org]](https://web.archive.org/web/https://policies.google.com/privacy)</sup>
- School of Computer Science & Statistics, Trinity College Dublin, Ireland Mobile Handset Privacy: Measuring The Data iOS and Android Send to Apple And Google <https://www.scss.tcd.ie/doug.leith/apple_google.pdf> <sup>[[Archive.org]](https://web.archive.org/web/https://www.scss.tcd.ie/doug.leith/apple_google.pdf)</sup>
- School of Computer Science & Statistics, Trinity College Dublin, Ireland Mobile Handset Privacy: Measuring The Data iOS and Android Send to Apple And Google <https://www.scss.tcd.ie/doug.leith/apple_google.pdf> <sup>[[Archive.org]](https://web.archive.org/web/https://www.scss.tcd.ie/doug.leith/apple_google.pdf)</sup>
**IOS/Apple:**
- IOS/Apple:
- More information at <https://www.apple.com/legal/privacy/en-ww/> <sup>[[Archive.org]](https://web.archive.org/web/https://www.apple.com/legal/privacy/en-ww/)</sup> and <https://support.apple.com/en-us/HT202100> <sup>[[Archive.org]](https://web.archive.org/web/https://support.apple.com/en-us/HT202100)</sup>
- School of Computer Science & Statistics, Trinity College Dublin, Ireland Mobile Handset Privacy: Measuring The Data iOS and Android Send to Apple And Google <https://www.scss.tcd.ie/doug.leith/apple_google.pdf> <sup>[[Archive.org]](https://web.archive.org/web/https://www.scss.tcd.ie/doug.leith/apple_google.pdf)</sup>
- Apple does claim[^109] that they anonymize this data using differential privacy[^110] but you will have to trust them on that.
- More information at <https://www.apple.com/legal/privacy/en-ww/> <sup>[[Archive.org]](https://web.archive.org/web/https://www.apple.com/legal/privacy/en-ww/)</sup> and <https://support.apple.com/en-us/HT202100> <sup>[[Archive.org]](https://web.archive.org/web/https://support.apple.com/en-us/HT202100)</sup>
**Windows/Microsoft:**
- School of Computer Science & Statistics, Trinity College Dublin, Ireland Mobile Handset Privacy: Measuring The Data iOS and Android Send to Apple And Google <https://www.scss.tcd.ie/doug.leith/apple_google.pdf> <sup>[[Archive.org]](https://web.archive.org/web/https://www.scss.tcd.ie/doug.leith/apple_google.pdf)</sup>
- Full list of required diagnostic data: <https://docs.microsoft.com/en-us/windows/privacy/required-windows-diagnostic-data-events-and-fields-2004> <sup>[[Archive.org]](https://web.archive.org/web/https://docs.microsoft.com/en-us/windows/privacy/required-windows-diagnostic-data-events-and-fields-2004)</sup>
- Full list of optional diagnostic data: <https://docs.microsoft.com/en-us/windows/privacy/windows-diagnostic-data> <sup>[[Archive.org]](https://web.archive.org/web/https://docs.microsoft.com/en-us/windows/privacy/windows-diagnostic-data)</sup>
- Apple does claim[^109] that they anonymize this data using differential privacy[^110] but you will have to trust them on that.
**macOS:**
- Windows/Microsoft:
- More details on <https://support.apple.com/guide/mac-help/share-analytics-information-mac-apple-mh27990/mac> <sup>[[Archive.org]](https://web.archive.org/web/https://support.apple.com/guide/mac-help/share-analytics-information-mac-apple-mh27990/mac)</sup>
- Full list of required diagnostic data: <https://docs.microsoft.com/en-us/windows/privacy/required-windows-diagnostic-data-events-and-fields-2004> <sup>[[Archive.org]](https://web.archive.org/web/https://docs.microsoft.com/en-us/windows/privacy/required-windows-diagnostic-data-events-and-fields-2004)</sup>
**Ubuntu:**
- Full list of optional diagnostic data: <https://docs.microsoft.com/en-us/windows/privacy/windows-diagnostic-data> <sup>[[Archive.org]](https://web.archive.org/web/https://docs.microsoft.com/en-us/windows/privacy/windows-diagnostic-data)</sup>
- macOS:
- More details on <https://support.apple.com/guide/mac-help/share-analytics-information-mac-apple-mh27990/mac> <sup>[[Archive.org]](https://web.archive.org/web/https://support.apple.com/guide/mac-help/share-analytics-information-mac-apple-mh27990/mac)</sup>
- Ubuntu:
- Ubuntu despite being a Linux distribution also collects Telemetry Data nowadays. This data however is quite limited compared to the others. More details on <https://ubuntu.com/desktop/statistics> <sup>[[Archive.org]](https://web.archive.org/web/https://ubuntu.com/desktop/statistics)</sup>
- Ubuntu despite being a Linux distribution also collects Telemetry Data nowadays. This data however is quite limited compared to the others. More details on <https://ubuntu.com/desktop/statistics> <sup>[[Archive.org]](https://web.archive.org/web/https://ubuntu.com/desktop/statistics)</sup>
Not only are Operating Systems gathering telemetry services but so are Apps themselves like Browsers, Mail Clients, and Social Networking Apps installed on your system.
@@ -1762,13 +1760,15 @@ There are some forums and subreddits (like r/phoneverification/) where users wil
To this date, we do not know any reputable service that would offer this service and accept cash payments (by post for instance) like some VPN providers. But a few services are providing online phone numbers and do accept Monero which could be reasonably anonymous (yet less recommended than that physical way in the earlier chapter) that you could consider:
- **Recommended**: Do not require any identification (even e-mail):
- **Recommended**: No name, no e-mail, and no KYC:
- (Iceland based, accepts Monero) <https://crypton.sh> <sup>[[Tor Mirror]](http://cryptonx6nsmspsnpicuihgmbbz3qvro4na35od3eht4vojdo7glm6yd.onion)</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://crypton.sh/)</sup>
- (Ukraine based, accepts Monero) <https://virtualsim.net/> <sup>[[Archive.org]](https://web.archive.org/web/https://virtualsim.net/)</sup>
- Do require identification (valid e-mail):
- (180+ countries, accepts crypto) <https://cypheresim.com/esim-with-number> <sup>[[Archive.org]](https://web.archive.org/web/https://cypheresim.com/esim-with-number)</sup>
- Require valid e-mail:
- (US California based, accepts Monero) <https://mobilesms.io> <sup>[[Archive.org]](https://web.archive.org/web/https://mobilesms.io/)</sup>
+3
View File
@@ -1,6 +1,9 @@
---
title: "Home"
description: "The Hitchhiker's Guide to Online Anonymity"
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": WebPage
+7
View File
@@ -3,6 +3,7 @@ title: "Mirrors"
description: "Every place you can read or download the Hitchhiker's Guide."
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": Organization
@@ -117,4 +118,10 @@ schema:
<a href="http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg" class="index-card__link">Onion only</a>
</div>
<div class="index-card">
<h3 class="index-card__title">Tor Project</h3>
<p class="index-card__body">We are not affiliated with Tor Project, but we do love them.</p>
<a href="https://gitlab.torproject.org/anonymousplanet/the-hitchhikers-guide" class="index-card__link">Tor Project</a>
</div>
</div>
+2 -2
View File
@@ -25,8 +25,8 @@
<div class="data-text off">On</div>
<div class="data-text on">Off</div>
</div>
<div class="status-indicator off"></div>
<div class="status-indicator on"></div>
<div class="toggle-status-dot off"></div>
<div class="toggle-status-dot on"></div>
<div class="interface-lines">
<div class="interface-line"></div>
<div class="interface-line"></div>
+1 -1
View File
@@ -2,7 +2,7 @@
title: "PGP"
description: "Import our GPG keys to verify our releases and signed content."
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": Organization
+76 -4
View File
@@ -36,6 +36,24 @@
font-weight: 600;
}
/* Secondary TOC: restore depth differentiation the global .md-nav__link override removed */
.md-nav--secondary .md-nav__item .md-nav__link {
padding-left: 0.6rem;
opacity: 0.85;
}
.md-nav--secondary .md-nav__item .md-nav__item .md-nav__link {
padding-left: 1.2rem;
font-size: 0.55rem;
opacity: 0.7;
}
.md-nav--secondary .md-nav__item .md-nav__item .md-nav__item .md-nav__link {
padding-left: 1.8rem;
font-size: 0.52rem;
opacity: 0.6;
}
.md-nav__link--active,
.md-nav__link[aria-current] {
color: var(--accent-green) !important;
@@ -65,6 +83,38 @@
animation: none;
}
.md-header__inner {
flex-wrap: nowrap;
}
.md-header__button,
.md-header__source,
.toggle-container--header {
flex-shrink: 0;
}
.md-search {
flex-shrink: 1;
min-width: 0;
}
@media screen and (max-width: 76.1875em) {
.toggle-container--header {
width: 56px;
margin: 0 4px;
}
.toggle-container--header .toggle-wrap {
height: 28px;
}
}
@media screen and (max-width: 44.9375em) {
.toggle-container--header {
width: 44px;
}
}
.md-typeset {
max-width: none;
}
@@ -90,8 +140,30 @@
border-top: 1px solid var(--border-color);
}
@media screen and (min-width: 76.25em) {
.md-main__inner {
flex-direction: row-reverse;
}
.md-top {
background-color: var(--bg-card) !important;
color: var(--crt-green) !important;
border: 1px solid var(--border-color) !important;
left: 50% !important;
margin-left: 0 !important;
transform: translate(-50%, 0) !important;
}
.md-top svg,
.md-top .md-icon {
fill: var(--crt-green) !important;
color: var(--crt-green) !important;
}
.md-top:hover {
background-color: var(--bg-card) !important;
color: var(--accent-green) !important;
border-color: var(--accent-green) !important;
}
.md-top:hover svg,
.md-top:hover .md-icon {
fill: var(--accent-green) !important;
color: var(--accent-green) !important;
}
+59 -21
View File
@@ -42,8 +42,8 @@
background: #ffffff !important;
color: #1a1a1a !important;
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif !important;
font-size: 11pt !important;
line-height: 1.65 !important;
font-size: 13pt !important;
line-height: 1.45 !important;
margin: 0 !important;
padding: 0 !important;
}
@@ -58,11 +58,11 @@
@page {
size: A4;
margin: 25mm 28mm 28mm 28mm;
margin: 12mm 12mm 16mm 12mm;
}
@page :first {
margin-top: 30mm;
margin-top: 12mm;
}
*, *::before, *::after {
@@ -127,7 +127,7 @@
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 0 0 12mm !important;
padding: 0 !important;
float: none !important;
}
@@ -150,13 +150,13 @@
content: none !important;
}
h1, .md-typeset h1 { font-size: 20pt !important; margin: 18pt 0 6pt !important; }
h2, .md-typeset h2 { font-size: 15pt !important; margin: 14pt 0 5pt !important; }
h3, .md-typeset h3 { font-size: 12pt !important; margin: 10pt 0 4pt !important; }
h4, .md-typeset h4 { font-size: 11pt !important; margin: 8pt 0 3pt !important; }
h1, .md-typeset h1 { font-size: 23pt !important; margin: 20pt 0 7pt !important; }
h2, .md-typeset h2 { font-size: 18pt !important; margin: 16pt 0 6pt !important; }
h3, .md-typeset h3 { font-size: 14.5pt !important; margin: 12pt 0 5pt !important; }
h4, .md-typeset h4 { font-size: 13pt !important; margin: 9pt 0 4pt !important; }
h5, h6,
.md-typeset h5,
.md-typeset h6 { font-size: 10pt !important; margin: 6pt 0 2pt !important; }
.md-typeset h6 { font-size: 12pt !important; margin: 7pt 0 3pt !important; }
p, li, dt, dd, blockquote p,
.md-typeset p,
@@ -165,20 +165,49 @@
.md-typeset dd {
color: #1a1a1a !important;
font-family: 'EB Garamond', Georgia, serif !important;
font-size: 11pt !important;
line-height: 1.65 !important;
font-size: 13pt !important;
line-height: 1.45 !important;
orphans: 3;
widows: 3;
}
.md-typeset p,
.md-typeset ul,
.md-typeset ol,
.md-typeset dl {
margin-top: 0.55em !important;
margin-bottom: 0.55em !important;
}
.md-typeset li {
margin-bottom: 0.15em !important;
}
a, .md-typeset a {
color: #00487a !important;
text-decoration: underline !important;
}
/* Glossary abbreviations are stripped from the PDF */
abbr, abbr[title],
.md-typeset abbr,
.md-typeset abbr[title] {
border-bottom: none !important;
text-decoration: none !important;
cursor: auto !important;
}
abbr::before, abbr::after,
abbr[title]::before, abbr[title]::after,
.md-typeset abbr::before, .md-typeset abbr::after,
.md-typeset abbr[title]::before, .md-typeset abbr[title]::after {
content: none !important;
display: none !important;
}
code, .md-typeset code {
font-family: 'Fira Code VF', 'Fira Code', 'Courier New', monospace !important;
font-size: 9pt !important;
font-size: 10.5pt !important;
background: #f4f4f4 !important;
color: #1a1a1a !important;
border: 1px solid #cccccc !important;
@@ -188,7 +217,7 @@
pre, .md-typeset pre {
font-family: 'Fira Code VF', 'Fira Code', 'Courier New', monospace !important;
font-size: 8.5pt !important;
font-size: 10pt !important;
background: #f8f8f8 !important;
color: #1a1a1a !important;
border: 1px solid #cccccc !important;
@@ -196,8 +225,8 @@
padding: 0.75em 1em !important;
white-space: pre-wrap !important;
word-break: break-all !important;
page-break-inside: avoid;
break-inside: avoid;
page-break-inside: auto;
break-inside: auto;
}
pre code, .md-typeset pre code {
@@ -210,23 +239,32 @@
table, .md-typeset table {
border-collapse: collapse !important;
width: 100% !important;
page-break-inside: auto;
break-inside: auto;
font-size: 11.5pt !important;
}
tr, .md-typeset tr {
page-break-inside: avoid;
break-inside: avoid;
font-size: 10pt !important;
}
thead, .md-typeset thead {
display: table-header-group;
}
th, .md-typeset th {
background: #eeeeee !important;
color: #1a1a1a !important;
border: 1px solid #aaaaaa !important;
padding: 5pt 8pt !important;
padding: 3pt 6pt !important;
font-weight: 700 !important;
}
td, .md-typeset td {
color: #1a1a1a !important;
border: 1px solid #cccccc !important;
padding: 4pt 8pt !important;
padding: 2.5pt 6pt !important;
}
tr:nth-child(even) td {
@@ -240,8 +278,8 @@
border-left: 4px solid #555555 !important;
border: 1px solid #cccccc !important;
color: #1a1a1a !important;
page-break-inside: avoid;
break-inside: avoid;
page-break-inside: auto;
break-inside: auto;
}
.admonition-title,
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -2,7 +2,7 @@
title: "Verify"
description: "Verify the authenticity and integrity of Anonymous Planet releases."
hide:
- toc
- navigation
schema:
"@context": https://schema.org
"@type": Organization
+2 -2
View File
@@ -1,2 +1,2 @@
ed1fff860d113609cbeaf38beef5342b473d5f09b9263020ef0a82f42ba9c8d4d17ad2743267a76b0f315c4719f9866d3e2018bc8f4eb64a46d0fcbec0c6ee2f thgtoa.pdf
1bd77081bf7f2962f8a52a141e286fc7cb571e17b1b551d1baab5fd892cf874a1ace903301f263d7073251a1c53a4d141aa346fb0c87ea0af0b96703127f5133 thgtoa-dark.pdf
5e9375ebd0a30fc9a105957965b8c46c1929db91cae3816b9510d990b0b9d0543c2fb004612e655e4fdafbf1b9fca5a310b7221bdb11f8c0bf66cbf40cd47d88 thgtoa.pdf
0a88b526f1be34d0b8b7d581a372b63935aa9fa6b1b7acc342a76e500fbd4cc6b1e03f25101ec0ff89b520dcfe5548511c0188bc367e74b7b1bd8ca59bdf0296 thgtoa-dark.pdf
+2 -2
View File
@@ -1,2 +1,2 @@
85e8824dcda6176ac4444fdc92ff5272527065aabb2c240d3e2f87418aa255b3 thgtoa.pdf
28675b585406f0e0543e4f63d5cff6507a064bf4d91db9ff82e4a826e8c8959d thgtoa-dark.pdf
ce3568f3966ed9802ffdf4b83542f46c1941405dd3acb8f94c2dc25064aad75a thgtoa.pdf
e36e5fb1f74bf2f80ba2822d13632d153a9a98666195270c6767e1397defad1c thgtoa-dark.pdf
+55805 -54448
View File
File diff suppressed because it is too large. Load diff
+27475 -22260
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -24,7 +24,6 @@ theme:
- navigation.tabs
- navigation.sections
- navigation.path
- toc.integrate
- navigation.top
- search.suggest
- search.highlight
+2
View File
@@ -0,0 +1,2 @@
mkdocs==1.6.1
mkdocs-material[imaging]==9.7.2
+232 -47
View File
@@ -3,7 +3,9 @@
Usage:
python scripts/build_guide_pdf.py # Light PDF only (clean light theme)
python scripts/build_guide_pdf.py --dark # Dark PDF only (requires light PDF to exist)
python scripts/build_guide_pdf.py --dark # Dark PDF only (requires a body-only
# cache from a prior light build in
# this checkout, e.g. via --both)
python scripts/build_guide_pdf.py --both # Light PDF, then dark PDF
Examples:
@@ -17,10 +19,12 @@ import argparse
import html as _html_mod
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import unicodedata
from pathlib import Path
@@ -160,6 +164,76 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
return pdf_out
# Page numbers (stamped with qpdf; no extra Python dependencies)
def stamp_page_numbers(pdf_path: Path) -> None:
"""Stamp a right-aligned page number at the bottom of every page, in place.
Chromium's --print-to-pdf can't draw page numbers when the default
header/footer is disabled, and @page margin boxes need Chrome >= 131
(CI pins 120). So build a tiny overlay PDF by hand (standard Times font,
no dependencies) and merge it with `qpdf --overlay`.
Called on the body-only PDF, before the cached copy is taken and before
the cover + ToC are prepended, so the body starts at page 1 in both the
light and dark builds.
"""
n_pages = int(
subprocess.run(
["qpdf", "--show-npages", str(pdf_path)],
check=True, capture_output=True, text=True,
).stdout.strip()
)
width, height = 595.28, 841.89 # A4 in points (matches @page size)
font_size = 10
baseline_y = 28 # ~10 mm from the bottom edge (inside the 16 mm margin)
digit_w = 0.5 * font_size # Times-Roman digits are 500/1000 em
right_x = width - 12 * 72 / 25.4 # right text edge (12 mm page margin)
objs: list[bytes] = [b""] * (3 + 2 * n_pages)
kids = " ".join(f"{4 + 2 * i} 0 R" for i in range(n_pages))
objs[0] = b"<< /Type /Catalog /Pages 2 0 R >>"
objs[1] = f"<< /Type /Pages /Kids [{kids}] /Count {n_pages} >>".encode()
objs[2] = b"<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>"
for i in range(n_pages):
label = str(i + 1)
x = right_x - digit_w * len(label)
stream = f"BT /F1 {font_size} Tf 0.33 g {x:.2f} {baseline_y} Td ({label}) Tj ET".encode()
objs[3 + 2 * i] = (
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] "
f"/Resources << /Font << /F1 3 0 R >> >> /Contents {5 + 2 * i} 0 R >>"
).encode()
objs[4 + 2 * i] = (
f"<< /Length {len(stream)} >>\nstream\n".encode() + stream + b"\nendstream"
)
out = bytearray(b"%PDF-1.4\n")
offsets: list[int] = []
for num, body in enumerate(objs, start=1):
offsets.append(len(out))
out += f"{num} 0 obj\n".encode() + body + b"\nendobj\n"
xref_pos = len(out)
out += f"xref\n0 {len(objs) + 1}\n".encode()
out += b"0000000000 65535 f \n"
for off in offsets:
out += f"{off:010d} 00000 n \n".encode()
out += (
f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\n"
f"startxref\n{xref_pos}\n%%EOF\n"
).encode()
with tempfile.TemporaryDirectory() as tmp:
overlay = Path(tmp) / "page-numbers.pdf"
overlay.write_bytes(bytes(out))
stamped = Path(tmp) / "stamped.pdf"
subprocess.run(
["qpdf", str(pdf_path), "--overlay", str(overlay), "--", str(stamped)],
check=True,
)
shutil.move(str(stamped), str(pdf_path))
# ---------------------------------------------------------------------------
# Front matter: ToC parsing + HTML generation
# ---------------------------------------------------------------------------
@@ -198,7 +272,13 @@ def parse_toc_headings(guide_md: Path) -> list[tuple[int, str]]:
return headings
def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool) -> str:
def create_toc_html(
tmp_dir: str,
headings: list[tuple[int, str]],
pages: list[int | None],
is_dark: bool,
scale: float = 1.0,
) -> str:
"""Write a research-style Table of Contents HTML file and return its path.
H2 entries: full-width row with dot leader.
@@ -219,33 +299,31 @@ def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool
h3color = "#444466"
rows: list[str] = []
for level, title in headings:
for (level, title), page in zip(headings, pages):
safe = _html_mod.escape(title)
if level == 2:
rows.append(
f'<div class="toc-h2">'
f'<span class="toc-title">{safe}</span>'
f'<span class="toc-dots"></span>'
f"</div>"
)
else:
rows.append(f'<div class="toc-h3">{safe}</div>')
page_html = f'<span class="toc-page">{page}</span>' if page else ""
cls = "toc-h2" if level == 2 else "toc-h3"
rows.append(
f'<div class="{cls}">'
f'<span class="toc-title">{safe}</span>'
f'<span class="toc-dots"></span>'
f"{page_html}"
f"</div>"
)
body = "\n".join(rows)
css = (
"* { box-sizing: border-box; margin: 0; padding: 0; }\n"
"@page { size: A4; margin: 0; }\n"
"@page { size: A4; margin: 12mm 12mm 16mm 12mm; }\n"
"html, body {\n"
f" width: 210mm;\n"
f" background: {bg};\n"
f" color: {fg};\n"
" font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;\n"
"}\n"
".page {\n"
" width: 210mm;\n"
" min-height: 297mm;\n"
" padding: 25mm 28mm 28mm 28mm;\n"
" padding: 0;\n"
f" zoom: {scale};\n"
"}\n"
".toc-heading {\n"
" font-size: 18pt;\n"
@@ -266,10 +344,14 @@ def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool
" margin-bottom: 1.5pt;\n"
"}\n"
".toc-title {\n"
" white-space: nowrap;\n"
" overflow: hidden;\n"
" flex: 0 1 auto;\n"
" max-width: 88%;\n"
"}\n"
".toc-page {\n"
" flex-shrink: 0;\n"
" max-width: 85%;\n"
" padding-left: 4pt;\n"
" font-style: normal;\n"
" font-variant-numeric: tabular-nums;\n"
"}\n"
".toc-dots {\n"
" flex: 1;\n"
@@ -280,6 +362,8 @@ def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool
" min-width: 8pt;\n"
"}\n"
".toc-h3 {\n"
" display: flex;\n"
" align-items: baseline;\n"
" font-size: 9pt;\n"
f" color: {h3color};\n"
" padding-left: 10mm;\n"
@@ -304,6 +388,76 @@ def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool
return html_path
# ---------------------------------------------------------------------------
# Locating headings in the built body PDF (for ToC page numbers)
# ---------------------------------------------------------------------------
def locate_heading_pages(
pdf_path: Path, headings: list[tuple[int, str]]
) -> list[int | None]:
"""Return the 1-based page each (level, title) heading starts on in pdf_path.
Uses pdftotext (poppler, already required by convert.py) and matches each
heading, in document order, against a whole line (or up to 3 consecutive
wrapped lines), so a passing mention in body text can't be mistaken for
the heading. Headings that can't be found get None: the ToC row then shows
dots only, and a warning is printed.
"""
def norm(s: str) -> str:
s = unicodedata.normalize("NFKC", s).replace("­", "")
s = (s.replace("‘", "'").replace("’", "'")
.replace("“", '"').replace("”", '"')
.replace("–", "-").replace("—", "-"))
return " ".join(s.split()).casefold()
raw = subprocess.run(
["pdftotext", "-enc", "UTF-8", str(pdf_path), "-"],
check=True, capture_output=True,
).stdout.decode("utf-8", "replace")
lines: list[tuple[int, str]] = []
for page_no, page_text in enumerate(raw.split("\f"), start=1):
for line in page_text.splitlines():
n = norm(line)
if n:
lines.append((page_no, n))
result: list[int | None] = []
missing: list[str] = []
cursor = 0
for _level, title in headings:
target = norm(title)
hit: tuple[int, int] | None = None
for j in range(cursor, len(lines)):
joined = lines[j][1]
if not target.startswith(joined):
continue
for k in range(1, 4):
if k > 1:
if j + k - 1 >= len(lines):
break
joined = joined + " " + lines[j + k - 1][1]
if not target.startswith(joined):
break
if joined == target:
hit = (j, k)
break
if hit:
break
if hit:
result.append(lines[hit[0]][0])
cursor = hit[0] + hit[1]
else:
result.append(None)
missing.append(title)
found = len(headings) - len(missing)
print(f" Located {found}/{len(headings)} headings in the body PDF.", flush=True)
for t in missing:
print(f" Warning: no page found for heading: {t!r}", file=sys.stderr)
return result
# ---------------------------------------------------------------------------
# Shared Chromium render helper
# ---------------------------------------------------------------------------
@@ -331,7 +485,12 @@ def _render_html_to_pdf(
# Front matter assembly: cover + ToC prepended to body PDF
# ---------------------------------------------------------------------------
def prepend_front_matter(browser: Path, pdf_path: Path, is_dark: bool) -> Path:
def prepend_front_matter(
browser: Path,
pdf_path: Path,
is_dark: bool,
page_source: Path | None = None,
) -> Path:
"""Render cover + ToC and prepend them to pdf_path in-place.
- Cover HTML is sourced from convert.py (single definition).
@@ -350,6 +509,10 @@ def prepend_front_matter(browser: Path, pdf_path: Path, is_dark: bool) -> Path:
n_h3 = sum(1 for l, _ in headings if l == 3)
print(f" Found {len(headings)} headings ({n_h2} H2, {n_h3} H3).", flush=True)
# Page numbers come from a text-bearing body PDF: pdf_path itself for the
# light build; the cached light body for dark (its pages are rasterised).
pages = locate_heading_pages(page_source or pdf_path, headings)
with tempfile.TemporaryDirectory() as tmp:
print(" Rendering cover page...", flush=True)
cover_html = create_cover_page(tmp, is_dark=is_dark)
@@ -357,9 +520,26 @@ def prepend_front_matter(browser: Path, pdf_path: Path, is_dark: bool) -> Path:
_render_html_to_pdf(browser, cover_html, cover_pdf, ci=ci)
print(" Rendering ToC page...", flush=True)
toc_html = create_toc_html(tmp, headings, is_dark=is_dark)
toc_pdf = os.path.join(tmp, "toc.pdf")
_render_html_to_pdf(browser, toc_html, toc_pdf, ci=ci)
toc_pdf = os.path.join(tmp, "toc.pdf")
def _render_toc(scale: float) -> int:
html_path = create_toc_html(tmp, headings, pages, is_dark=is_dark, scale=scale)
_render_html_to_pdf(browser, html_path, toc_pdf, ci=ci)
return int(subprocess.run(
["qpdf", "--show-npages", toc_pdf],
check=True, capture_output=True, text=True,
).stdout.strip())
# If the ToC spills onto an extra page, shrink it a little at a time
# (down to 85%) until it fits in one page fewer; otherwise keep 100%.
toc_pages = _render_toc(1.0)
if toc_pages > 1:
for scale in (0.97, 0.94, 0.91, 0.88, 0.85):
if _render_toc(scale) < toc_pages:
print(f" ToC scaled to {scale:.0%} to save a page.", flush=True)
break
else:
_render_toc(1.0)
merged = str(pdf_path.parent / f".{pdf_path.name}.with-front")
subprocess.run(
@@ -373,19 +553,15 @@ def prepend_front_matter(browser: Path, pdf_path: Path, is_dark: bool) -> Path:
return pdf_path
# ---------------------------------------------------------------------------
# Dark PDF build (delegates to convert.py which adds its own front matter)
# ---------------------------------------------------------------------------
def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path:
"""Convert the light PDF to dark mode using scripts/convert.py."""
def build_dark_pdf(browser: Path, body_pdf: Path, dark_pdf: Path) -> Path:
"""Convert a body-only PDF to dark mode, then prepend fresh front matter."""
convert_script = repo_root() / "scripts" / "convert.py"
print(f"Converting {light_pdf.name} -> {dark_pdf.name} (dark mode)...")
print(f"Converting {body_pdf.name} -> {dark_pdf.name} (dark mode)...")
subprocess.run(
[sys.executable, str(convert_script), str(light_pdf), str(dark_pdf)],
[sys.executable, str(convert_script), str(body_pdf), str(dark_pdf)],
check=True,
)
return dark_pdf
return prepend_front_matter(browser, dark_pdf, is_dark=True, page_source=body_pdf)
# ---------------------------------------------------------------------------
@@ -423,13 +599,23 @@ def main() -> int:
mode = ap.add_mutually_exclusive_group()
mode.add_argument("--dark", action="store_true",
help="Dark PDF only (light PDF must already exist)")
help="Dark PDF only (requires a body-only cache from a prior light build)")
mode.add_argument("--both", action="store_true",
help="Build light PDF, then dark PDF")
args = ap.parse_args()
build_light = not args.dark
build_dark = args.dark or args.both
body_cache = args.pdf_light.parent / f".{args.pdf_light.stem}.body.pdf"
browser = find_chromium_executable()
if not browser:
print(
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
file=sys.stderr,
)
return 1
# --- Light PDF ---
if build_light:
@@ -442,31 +628,30 @@ def main() -> int:
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
return 1
browser = find_chromium_executable()
if not browser:
print(
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
file=sys.stderr,
)
return 1
out_light = print_to_pdf(browser, guide_html, args.pdf_light)
print(" Stamping page numbers...", flush=True)
stamp_page_numbers(out_light)
import shutil as _shutil # noqa: PLC0415
body_cache.parent.mkdir(parents=True, exist_ok=True)
_shutil.copy2(out_light, body_cache)
out_light = prepend_front_matter(browser, out_light, is_dark=False)
size_kb = out_light.stat().st_size // 1024
print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]")
# --- Dark PDF ---
if build_dark:
if not args.pdf_light.exists():
if not body_cache.exists():
print(
f"Light PDF not found at {args.pdf_light}. "
"Run without --dark first, or use --both.",
f"No body-only PDF cached at {body_cache}. "
"Run with --both (or a plain light build first) so a fresh "
"body-only copy exists before converting to dark mode.",
file=sys.stderr,
)
return 1
out_dark = build_dark_pdf(args.pdf_light, args.pdf_dark)
out_dark = build_dark_pdf(browser, body_cache, args.pdf_dark)
size_kb = out_dark.stat().st_size // 1024
print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]")
+85 -95
View File
@@ -58,9 +58,9 @@ def apply_dark_theme(
Remap a white-background page image to a dark theme.
Strategy:
- Near-white pixels (page background) → bg color
- Dark, low-saturation pixels (text) → text color
- Blue-dominant dark pixels (links) → link color
- Near-white pixels (page background) → bg color
- Dark, low-saturation pixels (text) → text color
- Blue-dominant dark pixels (links) → link color
- High-saturation pixels (photos/images) → preserved exactly as-is
(no dark remapping; images stay at natural colors)
@@ -181,17 +181,36 @@ def _check_dependencies() -> None:
def create_cover_page(tmp_dir: str, is_dark: bool) -> str:
"""Create a research-style text-only cover page for Chromium rendering."""
"""Create the hacker-theme text cover page for Chromium rendering."""
# Hacker theme: monospace type with green/amber accents, drawn directly on
# the page so the cover matches the body pages in both builds.
if is_dark:
bg_color = '#1f1f31'
text_color = '#e0e0e0'
rule_color = '#4a4a6a'
meta_color = '#a0a0c0'
bg_color = '#1f1f31'
green = '#33ff66'
amber = '#ffb000'
muted = '#a8b0c0'
glow = '0 0 6px rgba(51, 255, 102, 0.45)'
else:
bg_color = '#ffffff'
text_color = '#1a1a1a'
rule_color = '#cccccc'
meta_color = '#555555'
bg_color = '#ffffff'
green = '#005c2e'
amber = '#8a5a00'
muted = '#555555'
glow = 'none'
# Embed Fira Code so the cover matches the site regardless of what the
# machine running Chromium has installed.
import base64
font_path = (Path(__file__).resolve().parent.parent / 'docs' / 'fonts'
/ 'fira-code' / 'fira-code-latin-wght-normal.woff2')
font_face = ''
if font_path.is_file():
b64 = base64.b64encode(font_path.read_bytes()).decode('ascii')
font_face = (
"@font-face { font-family: 'Fira Code Cover'; "
f"src: url(data:font/woff2;base64,{b64}) format('woff2'); "
"font-weight: 300 700; font-style: normal; }"
)
mono = "'Fira Code Cover', 'Fira Code', 'DejaVu Sans Mono', Consolas, 'Courier New', monospace"
html_path = os.path.join(tmp_dir, 'cover.html')
with open(html_path, 'w', encoding='utf-8') as f:
@@ -200,6 +219,7 @@ def create_cover_page(tmp_dir: str, is_dark: bool) -> str:
<head>
<meta charset="UTF-8">
<style>
{font_face}
* {{
box-sizing: border-box;
margin: 0;
@@ -213,8 +233,9 @@ html, body {{
width: 210mm;
height: 297mm;
background: {bg_color};
color: {text_color};
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;
font-family: {mono};
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}}
.page {{
width: 210mm;
@@ -223,41 +244,51 @@ html, body {{
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30mm 25mm;
padding: 12mm 12mm 16mm 12mm;
text-align: center;
}}
.title {{
font-size: 28pt;
font-weight: normal;
font-size: 26pt;
font-weight: 700;
line-height: 1.25;
letter-spacing: 0.01em;
margin-bottom: 10mm;
letter-spacing: -0.01em;
color: {green};
text-shadow: {glow};
margin-bottom: 12mm;
}}
.rule {{
width: 80mm;
height: 1px;
background: {rule_color};
margin: 0 auto 10mm auto;
.cursor {{
display: inline-block;
width: 0.55em;
height: 0.95em;
margin-left: 0.15em;
background: {green};
vertical-align: -0.1em;
}}
.subtitle {{
font-size: 13pt;
font-weight: normal;
font-style: italic;
color: {meta_color};
font-size: 12pt;
line-height: 1.6;
color: {muted};
margin-bottom: 14mm;
}}
.meta {{
font-size: 11pt;
color: {meta_color};
line-height: 1.9;
.subtitle::before {{
content: "// ";
color: {amber};
}}
.meta strong {{
color: {text_color};
font-weight: normal;
.info {{
align-self: flex-end;
text-align: right;
margin-right: 13mm;
position: relative;
top: 10mm;
}}
.meta {{
font-size: 11.5pt;
line-height: 1.9;
color: {green};
}}
.version {{
font-size: 11pt;
color: {meta_color};
font-size: 11.5pt;
color: {amber};
margin-top: 12mm;
letter-spacing: 0.05em;
}}
@@ -265,15 +296,16 @@ html, body {{
</head>
<body>
<div class="page">
<p class="title">The Hitchhiker&#8217;s Guide<br>to Online Anonymity</p>
<div class="rule"></div>
<h1 class="title">The Hitchhiker's Guide<br>to Online Anonymity</h1>
<p class="subtitle">The comprehensive guide for online anonymity and OpSec.</p>
<div class="meta">
<p><strong>Author</strong> &nbsp; Anonymous Planet</p>
<p><strong>License</strong> &nbsp; Creative Commons BY-SA 4.0</p>
<p><strong>Source</strong> &nbsp; https://anonymousplanet.net</p>
<div class="info">
<div class="meta">
<p>Anonymous Planet</p>
<p>Creative Commons BY-SA 4.0</p>
<p>https://anonymousplanet.net</p>
</div>
<p class="version">v1.2.6 &mdash; October 24 2026</p>
</div>
<p class="version">v1.2.5 &mdash; June 2026</p>
</div>
</body>
</html>""")
@@ -296,7 +328,9 @@ def convert_pdf_to_dark(
avoid OOM, then merged with qpdf. Falls back to single-pass Pillow save
if qpdf is not available (fine for small documents).
Adds a cover page at the start with title/subtitle/version info.
Does not add a cover page or ToC. Pass a body-only PDF and prepend
front matter separately (build_guide_pdf.py does this for both the
light and dark builds so it's generated exactly once per PDF).
"""
input_path = str(input_path)
output_path = str(output_path)
@@ -329,50 +363,15 @@ def convert_pdf_to_dark(
if out_dir:
os.makedirs(out_dir, exist_ok=True)
# Determine theme from filename for cover page
# 2. Determine theme from filename for downstream use by callers
# (front matter (cover + ToC) is prepended by build_guide_pdf.py,
# not here, so it's generated exactly once regardless of light/dark).
pdf_output = Path(output_path)
pdf_name = pdf_output.stem.lower()
is_dark = 'dark' in pdf_name
print(f" Theme: {'Going dark' if is_dark else 'Light mode'}")
# 2. Build front matter: cover + ToC
# Import ToC helpers from build_guide_pdf.py (single source of truth).
scripts_dir = str(Path(__file__).resolve().parent)
if scripts_dir not in sys.path:
sys.path.insert(0, scripts_dir)
from build_guide_pdf import parse_toc_headings, create_toc_html # noqa: PLC0415
cover_html_path = create_cover_page(tmp, is_dark)
browser = find_chromium_executable()
if not browser:
raise RuntimeError(
"No Chromium-based browser found; needed for cover page rendering.\n"
"Install Chrome, Edge, or add Chromium to PATH."
)
cover_pdf = os.path.join(tmp, 'cover.pdf')
cmd = [str(browser), "--headless=new", "--disable-gpu",
"--no-pdf-header-footer", "--print-background", "--no-margins",
f"--print-to-pdf={cover_pdf}", cover_html_path]
subprocess.run(cmd, check=True, capture_output=True)
guide_md = Path(__file__).resolve().parent.parent / 'docs' / 'guide' / 'index.md'
if guide_md.is_file():
print(" Building ToC...", flush=True)
headings = parse_toc_headings(guide_md)
toc_html_path = create_toc_html(tmp, headings, is_dark)
toc_pdf = os.path.join(tmp, 'toc.pdf')
cmd_toc = [str(browser), "--headless=new", "--disable-gpu",
"--no-pdf-header-footer", "--print-background", "--no-margins",
f"--print-to-pdf={toc_pdf}", toc_html_path]
subprocess.run(cmd_toc, check=True, capture_output=True)
front_pages = [cover_pdf, toc_pdf]
else:
print(" Warning: guide/index.md not found, skipping ToC.", file=sys.stderr)
front_pages = [cover_pdf]
# 3. Process pages with theme remapping
use_batches = total > batch_size and _check_qpdf()
@@ -396,10 +395,10 @@ def convert_pdf_to_dark(
_save_images_as_pdf(dark, batch_path)
batch_files.append(batch_path)
# Merge batches with front matter using qpdf
print(" Merging batches and front matter...", flush=True)
# Merge batches into the final output using qpdf
print(" Merging batches...", flush=True)
subprocess.run(
['qpdf', '--empty', '--pages'] + front_pages + batch_files + ['--', output_path],
['qpdf', '--empty', '--pages'] + batch_files + ['--', output_path],
check=True,
)
@@ -413,15 +412,6 @@ def convert_pdf_to_dark(
_save_images_as_pdf(dark_pages, output_path)
# Prepend front matter to the single-pass output
if not use_batches:
tmp_body = os.path.join(tmp, 'body_only.pdf')
os.rename(output_path, tmp_body)
subprocess.run(
['qpdf', '--empty', '--pages'] + front_pages + [tmp_body] + ['--', output_path],
check=True,
)
size_mb = os.path.getsize(output_path) / 1024 / 1024
print(f" Saved → {output_path} ({size_mb:.1f} MB)")
+12 -2
View File
@@ -143,8 +143,18 @@ def categorise(messages: list[str]) -> dict[str, list[str]]:
return buckets
# Distinct admonition type per bucket so icons/colors differ (not all blue "note")
ADMONITION_TYPE: dict[str, str] = {
"Meta": "abstract",
"Added": "tip",
"Changed": "info",
"Fixed": "success",
}
def format_admonition(bucket: str, items: list[str]) -> str:
lines = [f'!!! Note "{bucket}"', ""]
adm_type = ADMONITION_TYPE.get(bucket, "note")
lines = [f'!!! {adm_type} "{bucket}"', ""]
for item in items:
lines.append(f" - {item}")
lines.append("")
@@ -154,7 +164,7 @@ def format_admonition(bucket: str, items: list[str]) -> str:
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
lines = [f"## [{version}]", ""]
lines.append('!!! Note "Meta"')
lines.append('!!! abstract "Meta"')
lines.append("")
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
lines.append("")