1 Commits

Author SHA1 Message Date
nopeitsnothing f667d020d5 Fix PDF build in CI
Added workflow for building PDF. Progress.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-12 04:51:12 -04:00
11 changed files with 172 additions and 437 deletions
+4 -14
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.13" python-version: "3.12"
- name: Install MkDocs Material - name: Install MkDocs Material
run: pip install mkdocs-material run: pip install mkdocs-material
@@ -44,22 +44,12 @@ jobs:
- name: Build PDF - name: Build PDF
env: env:
CI: true CI: true
run: python scripts/build_guide_pdf.py --both run: python scripts/build_guide_pdf.py
- name: Upload PDF artifact - name: Upload PDF artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: light-pdf name: guide-pdf
path: export/thgtoa.pdf path: export/guide.pdf
archive: false
if-no-files-found: error
retention-days: 90
- name: Upload PDF artifact (Dark Mode)
uses: actions/upload-artifact@v7
with:
name: dark-pdf
path: export/thgtoa-dark.pdf
archive: false
if-no-files-found: error if-no-files-found: error
retention-days: 90 retention-days: 90
-33
View File
@@ -1,33 +0,0 @@
name: '🦠 VirusTotal Scan'
on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: '📦 Checkout'
uses: actions/checkout@v6
- name: '📦 Set up Go'
uses: actions/setup-go@v6
with:
go-version: '1.26.2'
- run: go version
- name: '🦠 Scan PDF files using VT'
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
update_release_body: true
files: |
./export/thgtoa.pdf
./export/thgtoa-dark.pdf
-1
View File
@@ -20,4 +20,3 @@ site/
_site/ _site/
_site_test/ _site_test/
export/ export/
build/
+2 -7
View File
@@ -7,16 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs (`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively).
- Restored previous VT scans workflow **VirusTotal Scan** (`.github/workflows/vt-scan.yml`): submit files to VT for malware scanning. Links will be published on the site.
## [1.2.1] - 2026-04-11 ## [1.2.1] - 2026-04-11
### Added ### Added
- GitHub Actions workflow **Build PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests. - GitHub Actions workflow **Build guide PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests.
- `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF. - `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF.
- `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling. - `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling.
@@ -28,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print. - Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print.
- `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`. - `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`.
- `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images. - `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images.
- `README.md`: note the **Build PDF** GitHub Actions workflow and the `guide-pdf` artifact. - `README.md`: note the **Build guide PDF** GitHub Actions workflow and the `guide-pdf` artifact.
### Fixed ### Fixed
+2 -2
View File
@@ -18,9 +18,9 @@ This guide is an open-source non-profit initiative, [licensed](LICENSE.html) und
python scripts/build_guide_pdf.py python scripts/build_guide_pdf.py
``` ```
This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/thgtoa.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`. This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/guide.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`.
On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`thgtoa.pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF****Run workflow**). On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`guide-pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF****Run workflow**).
- **OpenDocument (ODT):** not produced by this repository (previous hosted export removed). - **OpenDocument (ODT):** not produced by this repository (previous hosted export removed).
- **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md) - **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md)
+2 -2
View File
@@ -27,9 +27,9 @@ schema:
!!! Note "PDF export (single file)" !!! Note "PDF export (single file)"
The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) on the [**source repository**](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`thgtoa`** and **`thgtoa-dark`** artifacts. You can start a fresh build anytime (**Actions** → **Build guide PDF****Run workflow**). The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) on the [**thgtoa** source repository](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`guide-pdf`** artifact. You can start a fresh build anytime (**Actions** → **Build guide PDF****Run workflow**).
To produce the same file locally, clone the repository and run `python3 scripts/build_guide_pdf.py --both` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide). To produce the same file locally, clone the repository and run `python scripts/build_guide_pdf.py` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
!!! Note "Our official git mirrors" !!! Note "Our official git mirrors"
-130
View File
@@ -1,130 +0,0 @@
/* Generate dark mode PDF of the HTML at guide/index.html */
/*
DARK_MODE_PDF.CSS
Use this stylesheet when generating a PDF from HTML.
*/
:root {
/* Color Palette */
--bg-color: #121212; /* Deep dark grey (easier on eyes than pure black) */
--text-primary: #e0e0e0; /* Off-white for readability */
--text-secondary: #a0a0a0; /* Grey for captions/metadata */
--accent-color: #bb86fc; /* Light purple accent (optional) */
--border-color: #333333; /* Subtle borders */
/* Fonts - System fonts ensure best rendering across PDF engines */
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* --- RESET & BASE STYLES --- */
* {
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
line-height: 1.6;
margin: 0;
padding: 0;
}
/* --- TYPOGRAPHY & HEADINGS --- */
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary);
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
p {
margin-bottom: 1rem;
color: var(--text-secondary); /* Slightly dimmer text for body copy */
}
a {
color: var(--accent-color);
text-decoration: underline;
}
/* --- CONTAINER & LAYOUT --- */
.container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
/* Cards / Sections with dark backgrounds */
.card {
background-color: #1e1e1e;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
/* --- TABLES (Common in PDFs) --- */
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
color: var(--text-primary);
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: #2c2c2c;
font-weight: bold;
}
tr:last-child td {
border-bottom: none;
}
/* --- IMAGES --- */
img {
max-width: 100%;
height: auto;
display: block;
margin: 20px 0;
/* This ensures high contrast images don't get washed out */
filter: brightness(1.1);
}
/* --- CRITICAL FOR PDF GENERATORS --- */
/* Forces the browser/PDF engine to print background colors and graphics */
@media print {
@page {
size: A4; /* Change to 'Letter' if preferred */
margin: 20mm;
}
body {
background-color: var(--bg-color) !important;
color: var(--text-primary) !important;
}
.card, table th {
-webkit-print-color-adjust: exact !important; /* Chrome/Safari */
print-color-adjust: exact !important; /* Firefox/Standard */
background-color: #1e1e1e !important;
color: var(--text-primary) !important;
}
/* Prevent page breaks in the middle of a sentence or card if possible */
.card {
break-inside: avoid;
}
/* Hide elements you don't want in PDF (like navigation bars) */
nav, footer, button {
display: none !important;
}
}
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -122,4 +122,4 @@ markdown_extensions:
toc_depth: 3 toc_depth: 3
copyright: | copyright: |
&copy; 2023-2026 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a> &copy; 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
+22 -108
View File
@@ -1,16 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extended version of build_guide_pdf.py with dark mode support. """Build the MkDocs site, then render docs/guide/ to a single PDF via a Chromium-based browser.
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium. Uses headless Chrome/Edge print-to-PDF (embeds images). WeasyPrint-based mkdocs-with-pdf is
omitted here because it needs GTK/Pango (awkward on Windows).
Usage: Usage (from repo root):
python scripts/build_guide_pdf_extended.py # Generate light mode PDF only python scripts/build_guide_pdf.py
python scripts/build_guide_pdf_extended.py --dark-mode # Generate dark mode PDF only python scripts/build_guide_pdf.py --site-dir build/html --pdf export/guide.pdf
python scripts/build_guide_pdf_extended.py --both # Generate both light and dark mode PDFs
Examples:
python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf
python scripts/build_guide_pdf_extended.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
""" """
from __future__ import annotations from __future__ import annotations
@@ -68,22 +64,15 @@ def run_mkdocs(site_dir: Path) -> None:
) )
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool = False) -> Path: def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
"""Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows """Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows
does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead. does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead.
Args:
browser: Path to Chromium executable
html_file: Path to HTML file to convert
pdf_out: Output PDF path
dark_mode: If True, use dark mode color scheme via --prefers-color-scheme flag
""" """
pdf_out.parent.mkdir(parents=True, exist_ok=True) pdf_out.parent.mkdir(parents=True, exist_ok=True)
partial = pdf_out.parent / f".{pdf_out.name}.writing" partial = pdf_out.parent / f".{pdf_out.name}.writing"
partial.unlink(missing_ok=True) partial.unlink(missing_ok=True)
uri = html_file.resolve().as_uri() uri = html_file.resolve().as_uri()
# Chromium headless print; allow time for fonts/images on very large pages. # Chromium headless print; allow time for fonts/images on very large pages.
cmd = [str(browser)] cmd = [str(browser)]
if os.environ.get("CI"): if os.environ.get("CI"):
@@ -93,22 +82,13 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool
"--disable-setuid-sandbox", "--disable-setuid-sandbox",
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
] ]
cmd += [ cmd += [
"--headless=new", "--headless=new",
"--disable-gpu", "--disable-gpu",
"--no-pdf-header-footer", "--no-pdf-header-footer",
]
# Add dark mode preference if requested
if dark_mode:
cmd.append("--prefers-color-scheme=dark")
cmd += [
f"--print-to-pdf={partial.resolve()}", f"--print-to-pdf={partial.resolve()}",
uri, uri,
] ]
subprocess.run(cmd, check=True, timeout=600) subprocess.run(cmd, check=True, timeout=600)
deadline = time.time() + 120 deadline = time.time() + 120
while time.time() < deadline: while time.time() < deadline:
@@ -132,77 +112,27 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool
return pdf_out return pdf_out
def generate_dark_mode_html(html_file: Path, output_file: Path, dark_css_path: Path) -> None:
"""Create a temporary HTML file with dark mode stylesheet applied.
This is used when we need to force dark mode rendering via CSS rather than browser flags.
"""
try:
from bs4 import BeautifulSoup
# Read the original HTML
html_content = html_file.read_text(encoding='utf-8')
soup = BeautifulSoup(html_content, 'html.parser')
# Add dark mode stylesheet link if not present
existing_links = [link.get('href', '') for link in soup.find_all('link', rel='stylesheet')]
if not any(dark_css_path.name in link for link in existing_links):
head = soup.head or soup.new_tag('head')
link_tag = soup.new_tag('link', rel='stylesheet', href=str(dark_css_path))
if soup.head:
soup.head.append(link_tag)
else:
# Create a new head section
new_head = soup.new_tag('head')
new_head.append(link_tag)
soup.insert(0, new_head)
# Write the modified HTML
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(str(soup), encoding='utf-8')
except ImportError:
print("BeautifulSoup not available. Skipping CSS injection.")
def main() -> int: def main() -> int:
root = repo_root() root = repo_root()
ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF (light and/or dark mode).") ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF.")
ap.add_argument( ap.add_argument(
"--site-dir", "--site-dir",
type=Path, type=Path,
default=root / "build" / "html", default=root / "site",
help="MkDocs output directory (default: ./build/html)", help="MkDocs output directory (default: ./site)",
) )
ap.add_argument( ap.add_argument(
"--pdf-light", "--pdf",
type=Path, type=Path,
default=root / "export" / "thgtoa.pdf", default=root / "export" / "guide.pdf",
help="Output PDF path for light mode (default: ./export/guide.pdf)", help="Output PDF path (default: ./export/guide.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Output PDF path for dark mode (default: ./export/guide-dark.pdf)",
) )
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.") ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
ap.add_argument("--dark-mode", action="store_true", help="Generate dark mode PDF only")
ap.add_argument("--both", action="store_true", help="Generate both light and dark mode PDFs")
args = ap.parse_args() args = ap.parse_args()
# Determine which modes to generate
if args.dark_mode:
modes = ["dark"]
elif args.both:
modes = ["light", "dark"]
else:
modes = ["light"]
guide_html = args.site_dir / "guide" / "index.html" guide_html = args.site_dir / "guide" / "index.html"
if not args.skip_mkdocs:
if not args.skip_mkdocs or any(mode == "light" for mode in modes):
run_mkdocs(args.site_dir) run_mkdocs(args.site_dir)
if not guide_html.is_file(): if not guide_html.is_file():
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr) print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
return 1 return 1
@@ -216,30 +146,14 @@ def main() -> int:
) )
return 1 return 1
dark_css_path = root / "docs" / "stylesheets" / "dark-extra.css" out = print_to_pdf(browser, guide_html, args.pdf)
size_kb = out.stat().st_size // 1024
# Generate light mode PDF (default) print(f"Wrote {out.resolve()} ({size_kb} KiB)")
if "light" in modes: if out.resolve() != args.pdf.resolve():
out_light = print_to_pdf(browser, guide_html, args.pdf_light, dark_mode=False) print(
size_kb = out_light.stat().st_size // 1024 f"Note: {args.pdf.name} was in use; close it and rename or replace with the file above.",
print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]") file=sys.stderr,
if out_light.resolve() != args.pdf_light.resolve(): )
print(
f"Note: {args.pdf_light.name} was in use; close it and rename or replace with the file above.",
file=sys.stderr,
)
# Generate dark mode PDF
if "dark" in modes:
out_dark = print_to_pdf(browser, guide_html, args.pdf_dark, dark_mode=True)
size_kb = out_dark.stat().st_size // 1024
print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]")
if out_dark.resolve() != args.pdf_dark.resolve():
print(
f"Note: {args.pdf_dark.name} was in use; close it and rename or replace with the file above.",
file=sys.stderr,
)
return 0 return 0