Skip to content

Commit 0c35c89

Browse files
authored
Release preparation (#98)
This pull request introduces major improvements to the syllabus PDF generation process, including the addition of a custom post-processing script for enhanced PDF output, and several documentation and dependency updates. The most significant changes are the new `pdf_postprocess.py` tool, updates to the Robot Framework documentation structure and content, and dependency upgrades for security and compatibility. **PDF Generation Enhancements:** * Added a new `pdf_postprocess.py` script that merges individual PDFs, adds a title page, table of contents with clickable links, custom footers, and rewrites internal links for the syllabus PDF using `reportlab` and `pypdf`. This script is now called from the Robot Framework automation (`gen_pdf.robot`). * Updated `gen_pdf.robot` to collect URLs for each PDF page, adjust margins, and call the new post-processing script, significantly improving the final PDF's structure and navigability. [[1]](diffhunk://#diff-33329e1fd56ec182e1cad4549a70a68cda2ffc289ec999ed2f0d938028c996f8R3-R8) [[2]](diffhunk://#diff-33329e1fd56ec182e1cad4549a70a68cda2ffc289ec999ed2f0d938028c996f8L23-R24) [[3]](diffhunk://#diff-33329e1fd56ec182e1cad4549a70a68cda2ffc289ec999ed2f0d938028c996f8L49-R73) **Documentation Structure and Content Updates:** * Updated all chapter `_category_.json` files to provide more descriptive labels, set positions, and add overview links, improving navigation and clarity in the documentation. [[1]](diffhunk://#diff-639c8509fc4d433aae825ceb3689be8c21ee7e6cb8c52c041eca1c6d0a3d8800L2-R7) [[2]](diffhunk://#diff-70a4cb9a0396ea98808a98abd959b5f7df56172cf62964f6aee38ba8a0c52cb6L2-R7) [[3]](diffhunk://#diff-bdab0e4fd10d944bfe182e79709cbbd3db1b8fa3dda84d204b8e32059218358bL2-R7) [[4]](diffhunk://#diff-41d5d40393a231c6885158ddb8697b0c18b0a53774fbdbf12f08859c6631867bL2-R7) [[5]](diffhunk://#diff-31891eeadbfecee7c7bab9a03ab1b40c79c958f3c298dcb832f3c8655efadc3fL2-R7) * Improved terminology and consistency in documentation, such as clarifying the definition of execution artifacts, updating references to output files, and correcting minor language issues. [[1]](diffhunk://#diff-268002b99f593cf3d9dc01249a09284688cd3a23be4e5cc8e8dce0e4c39907aeL88-R92) [[2]](diffhunk://#diff-268002b99f593cf3d9dc01249a09284688cd3a23be4e5cc8e8dce0e4c39907aeL174-R174) [[3]](diffhunk://#diff-aa76ffed2c19ba7bc109d2ed4da2debe16d0e3808dc9bd1e0e3a3a6440938ad4L439-R439) [[4]](diffhunk://#diff-4886556024df14b9ca945113d5e45990257feec880ed7b14728885d1f32f7a5eL53-R53) [[5]](diffhunk://#diff-aa6cfafec175fc942a1ef49a8fa6bb6fa55b58ed3a6f26ab0ea9ea20c234b5cbL5-R5) **Dependency Upgrades:** * Upgraded several npm dependencies (`express`, `body-parser`, `qs`) in `package-lock.json` to the latest versions, addressing potential security and compatibility issues. [[1]](diffhunk://#diff-999051457a1934c824a5f8395589ea5e749a581e9f70a43585c885319cb56e64L7483-R7485) [[2]](diffhunk://#diff-999051457a1934c824a5f8395589ea5e749a581e9f70a43585c885319cb56e64L7496-R7496) [[3]](diffhunk://#diff-999051457a1934c824a5f8395589ea5e749a581e9f70a43585c885319cb56e64L10057-R10064) [[4]](diffhunk://#diff-999051457a1934c824a5f8395589ea5e749a581e9f70a43585c885319cb56e64L10083-R10083) [[5]](diffhunk://#diff-999051457a1934c824a5f8395589ea5e749a581e9f70a43585c885319cb56e64L17945-R17947) **Content Fixes*** * Fixed two typos in American vs British English * Fixes #97 --------- Signed-off-by: René <snooz@posteo.de>
1 parent 3cc04d2 commit 0c35c89

66 files changed

Lines changed: 5906 additions & 75 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ results
66
.vscode
77
.devcontainer
88
.venv
9-
Syllabus.pdf
9+
Syllabus.pdf
10+
website/static/fonts/OCRAEXT.ttf

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
robotframework-browser-batteries
22
pypdf
3+
reportlab
4+
fonttools

tools/gen_pdf.robot

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
*** Settings ***
22
Library Browser
3+
Library Collections
34
Library Process
45

56

67
*** Variables ***
7-
@{EXCLUDE_FROM_PDF} Example Questions
8+
@{EXCLUDE_FROM_PDF} Example Exam Download Syllabus PDF
89
${BROWSER} chromium
910
${HEADLESS} ${True}
1011

@@ -20,7 +21,7 @@ Gen Syllabus
2021

2122
*** Keywords ***
2223
Build Docusaurus
23-
Process.Run Process npm run build cwd=website
24+
Process.Run Process npm run build:production cwd=website
2425
Process.Start Process npm run serve alias=docusaurus cwd=website
2526

2627
Open Syllabus
@@ -46,24 +47,27 @@ Expand Menues
4647

4748
Generate Syllabus.pdf
4849
${pages} Get Elements .theme-doc-sidebar-item-link
49-
${writer} Evaluate pypdf.PdfWriter()
50+
VAR ${syllabus_version} ${{json.load(open('website/versions.json'))[0]}}
51+
VAR @{pdf_files}
5052
FOR ${page} IN @{pages}
53+
${title} Get Text ${page}
54+
IF $title in $EXCLUDE_FROM_PDF CONTINUE
5155
Click ${page}
52-
${title} Get Title
53-
IF $title.split('|', 1)[0].strip() in $EXCLUDE_FROM_PDF CONTINUE
5456
Scroll To vertical=bottom behavior=smooth
5557
sleep 1s
5658
${title} Get Title then value.split("|")[0]
5759
${file} Save Page As Pdf
5860
... pdfs/${title.replace('/', '_').strip()}.pdf
59-
... displayHeaderFooter=True
61+
# ... displayHeaderFooter=False
6062
... format=A4
6163
... outline=True
62-
... margin={'top': '20px', 'right': '20px', 'bottom': '20px', 'left': '20px'}
64+
... margin={'top': '20px', 'right': '60px', 'bottom': '80px', 'left': '20px'}
6365
... printBackground=True
6466
... tagged=True
6567
... scale=0.8
68+
${url} Get Url
6669
Log To Console ${file}
67-
Evaluate $writer.append($file)
70+
Append To List ${pdf_files} ${{($file, $url)}}
6871
END
69-
Evaluate $writer.write("Syllabus.pdf")
72+
Evaluate __import__('sys').path.insert(0, 'tools')
73+
Evaluate __import__('pdf_postprocess').postprocess($pdf_files, "website/static/pdfs/RFCP-Syllabus-${syllabus_version}.pdf")

tools/pdf_postprocess.py

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
"""Post-processing for the merged Syllabus PDF.
2+
3+
Adds a title page, table of contents, and footers using reportlab + pypdf.
4+
Called from gen_pdf.robot after individual page PDFs have been generated.
5+
"""
6+
7+
import json
8+
import io
9+
from pathlib import Path
10+
from urllib.parse import urlparse
11+
12+
from reportlab.lib.pagesizes import A4
13+
from reportlab.lib.units import cm, mm
14+
from reportlab.pdfgen import canvas
15+
from reportlab.lib.colors import HexColor
16+
from reportlab.pdfbase import pdfmetrics
17+
from reportlab.pdfbase.ttfonts import TTFont
18+
from pypdf import PdfReader, PdfWriter
19+
from pypdf.annotations import Link
20+
from pypdf.generic import Fit
21+
22+
23+
TEAL = HexColor("#008682")
24+
PAGE_WIDTH, PAGE_HEIGHT = A4
25+
26+
FONTS_DIR = Path(__file__).resolve().parent.parent / "website" / "static" / "fonts"
27+
28+
29+
def _register_fonts():
30+
ocra_ttf = FONTS_DIR / "OCRAEXT.ttf"
31+
if not ocra_ttf.exists():
32+
from fontTools.ttLib import TTFont as FTFont
33+
woff = FONTS_DIR / "OCRAEXT.woff"
34+
ft = FTFont(str(woff))
35+
ft.flavor = None
36+
ft.save(str(ocra_ttf))
37+
pdfmetrics.registerFont(TTFont("OCRA", str(ocra_ttf)))
38+
39+
40+
_register_fonts()
41+
42+
43+
def get_version() -> str:
44+
versions_path = Path(__file__).resolve().parent.parent / "website" / "versions.json"
45+
with open(versions_path) as f:
46+
versions = json.load(f)
47+
return versions[0]
48+
49+
50+
def create_title_page(version: str) -> PdfReader:
51+
buf = io.BytesIO()
52+
c = canvas.Canvas(buf, pagesize=A4)
53+
54+
title = "Robot Framework®"
55+
subtitle = "Certified Professional (RFCP®)"
56+
doc_type = "Syllabus"
57+
copyright_text = "© Robot Framework ry"
58+
version_text = f"Version {version}"
59+
60+
y = PAGE_HEIGHT * 0.62
61+
62+
c.setFont("OCRA", 28)
63+
c.setFillColor(TEAL)
64+
c.drawCentredString(PAGE_WIDTH / 2, y, title)
65+
66+
y -= 42
67+
c.setFont("OCRA", 20)
68+
c.drawCentredString(PAGE_WIDTH / 2, y, subtitle)
69+
70+
y -= 60
71+
c.setFont("OCRA", 26)
72+
c.setFillColor(HexColor("#333333"))
73+
c.drawCentredString(PAGE_WIDTH / 2, y, doc_type)
74+
75+
y -= 50
76+
c.setFont("OCRA", 16)
77+
c.setFillColor(TEAL)
78+
c.drawCentredString(PAGE_WIDTH / 2, y, version_text)
79+
80+
# Decorative line
81+
y -= 30
82+
c.setStrokeColor(TEAL)
83+
c.setLineWidth(2)
84+
line_half = 100
85+
c.line(PAGE_WIDTH / 2 - line_half, y, PAGE_WIDTH / 2 + line_half, y)
86+
87+
# Copyright at bottom
88+
c.setFont("Helvetica", 10)
89+
c.setFillColor(HexColor("#666666"))
90+
c.drawCentredString(PAGE_WIDTH / 2, 2.5 * cm, copyright_text)
91+
92+
c.save()
93+
buf.seek(0)
94+
return PdfReader(buf)
95+
96+
97+
def create_toc_page(
98+
toc_entries: list[tuple[str, int]], version: str
99+
) -> tuple[PdfReader, list[tuple[int, tuple[float, float, float, float], int]]]:
100+
"""Create TOC pages with link metadata.
101+
102+
Returns (pdf_reader, links) where links is a list of
103+
(toc_page_index, rect, target_page_index) for each entry.
104+
rect is (x1, y1, x2, y2) in PDF coordinates.
105+
"""
106+
buf = io.BytesIO()
107+
c = canvas.Canvas(buf, pagesize=A4)
108+
links: list[tuple[int, tuple[float, float, float, float], int]] = []
109+
110+
left_margin = 2.5 * cm
111+
right_margin = PAGE_WIDTH - 2.5 * cm
112+
y = PAGE_HEIGHT - 3 * cm
113+
toc_page_idx = 0
114+
115+
c.setFont("OCRA", 20)
116+
c.setFillColor(TEAL)
117+
c.drawString(left_margin, y, "Table of Contents")
118+
119+
y -= 14
120+
c.setStrokeColor(TEAL)
121+
c.setLineWidth(1.5)
122+
c.line(left_margin, y, right_margin, y)
123+
124+
y -= 30
125+
c.setFillColor(HexColor("#000000"))
126+
127+
for title, page_num in toc_entries:
128+
if y < 3 * cm:
129+
c.showPage()
130+
toc_page_idx += 1
131+
y = PAGE_HEIGHT - 3 * cm
132+
133+
c.setFont("Helvetica", 11)
134+
c.drawString(left_margin, y, title)
135+
136+
page_str = str(page_num)
137+
c.drawRightString(right_margin, y, page_str)
138+
139+
title_width = c.stringWidth(title, "Helvetica", 11)
140+
page_width = c.stringWidth(page_str, "Helvetica", 11)
141+
dot_start = left_margin + title_width + 5
142+
dot_end = right_margin - page_width - 5
143+
if dot_end > dot_start:
144+
c.setFont("Helvetica", 9)
145+
dots = " . " * 80
146+
c.saveState()
147+
p = c.beginPath()
148+
p.rect(dot_start, y - 3, dot_end - dot_start, 14)
149+
c.clipPath(p, stroke=0)
150+
c.drawString(dot_start, y, dots)
151+
c.restoreState()
152+
153+
rect = (left_margin, y - 3, right_margin, y + 13)
154+
links.append((toc_page_idx, rect, page_num))
155+
156+
y -= 20
157+
158+
c.save()
159+
buf.seek(0)
160+
return PdfReader(buf), links
161+
162+
163+
def create_footer_overlay(page_num: int, total_pages: int, version: str) -> PdfReader:
164+
"""Create a single-page PDF with just the footer text, to overlay onto a content page."""
165+
buf = io.BytesIO()
166+
c = canvas.Canvas(buf, pagesize=A4)
167+
168+
left_margin = 2 * cm
169+
right_margin = PAGE_WIDTH - 2 * cm
170+
footer_y = 1.2 * cm
171+
172+
# Footer line
173+
line_y = footer_y + 10
174+
c.setStrokeColor(TEAL)
175+
c.setLineWidth(0.75)
176+
c.line(left_margin, line_y, right_margin, line_y)
177+
178+
# Left: document name + version
179+
c.setFont("Helvetica", 8)
180+
c.setFillColor(HexColor("#444444"))
181+
c.drawString(left_margin, footer_y, f"RFCP Syllabus — Version {version}")
182+
183+
# Center: copyright
184+
c.drawCentredString(PAGE_WIDTH / 2, footer_y, "© Robot Framework ry")
185+
186+
# Right: page number
187+
c.drawRightString(right_margin, footer_y, f"{page_num} / {total_pages}")
188+
189+
c.save()
190+
buf.seek(0)
191+
return PdfReader(buf)
192+
193+
194+
def _rewrite_internal_links(writer: PdfWriter, url_to_page: dict[str, int]) -> None:
195+
"""Rewrite localhost URI annotations to internal GoTo links."""
196+
from pypdf.generic import (
197+
ArrayObject,
198+
DictionaryObject,
199+
NameObject,
200+
NumberObject,
201+
)
202+
203+
for page_idx in range(len(writer.pages)):
204+
page = writer.pages[page_idx]
205+
annots = page.get("/Annots")
206+
if not annots:
207+
continue
208+
for annot_ref in annots:
209+
annot = annot_ref.get_object()
210+
if annot.get("/Subtype") != "/Link":
211+
continue
212+
action = annot.get("/A")
213+
if not action:
214+
continue
215+
action = action.get_object()
216+
if str(action.get("/S")) != "/URI":
217+
continue
218+
uri = str(action.get("/URI", ""))
219+
parsed = urlparse(uri)
220+
if parsed.hostname != "localhost":
221+
continue
222+
path = parsed.path.rstrip("/")
223+
target_page_idx = url_to_page.get(path)
224+
if target_page_idx is None:
225+
continue
226+
dest = ArrayObject([
227+
writer.pages[target_page_idx].indirect_reference,
228+
NameObject("/Fit"),
229+
])
230+
annot[NameObject("/Dest")] = dest
231+
del annot["/A"]
232+
233+
234+
def postprocess(
235+
pdf_files: list[tuple[str, str]] | list[str], output_path: str
236+
) -> str:
237+
"""Merge individual PDFs, prepend title page + TOC, add footers, and rewrite internal links."""
238+
version = get_version()
239+
240+
# Normalize input: accept both (path, url) tuples and plain paths
241+
entries: list[tuple[str, str | None]] = []
242+
for item in pdf_files:
243+
if isinstance(item, (list, tuple)):
244+
entries.append((str(item[0]), str(item[1])))
245+
else:
246+
entries.append((str(item), None))
247+
248+
# First pass: merge all content pages and collect TOC entries + URL mapping
249+
content_writer = PdfWriter()
250+
toc_entries: list[tuple[str, int]] = []
251+
url_to_content_page: dict[str, int] = {}
252+
current_page = 1
253+
254+
for pdf_path, url in entries:
255+
reader = PdfReader(pdf_path)
256+
title = Path(pdf_path).stem
257+
toc_entries.append((title, current_page))
258+
if url:
259+
path = urlparse(url).path.rstrip("/")
260+
url_to_content_page[path] = current_page
261+
for page in reader.pages:
262+
content_writer.add_page(page)
263+
current_page += 1
264+
265+
total_content_pages = len(content_writer.pages)
266+
267+
# Create front matter
268+
title_reader = create_title_page(version)
269+
title_page_count = len(title_reader.pages)
270+
271+
toc_reader, _ = create_toc_page(toc_entries, version)
272+
toc_page_count = len(toc_reader.pages)
273+
front_matter_pages = title_page_count + toc_page_count
274+
275+
adjusted_entries = [(title, page + front_matter_pages) for title, page in toc_entries]
276+
toc_reader, toc_links = create_toc_page(adjusted_entries, version)
277+
278+
# Adjust URL mapping to account for front matter (0-based page index)
279+
url_to_page: dict[str, int] = {
280+
path: (page_num - 1) + front_matter_pages
281+
for path, page_num in url_to_content_page.items()
282+
}
283+
284+
total_pages = front_matter_pages + total_content_pages
285+
286+
# Final assembly
287+
writer = PdfWriter()
288+
289+
for page in title_reader.pages:
290+
writer.add_page(page)
291+
292+
for page in toc_reader.pages:
293+
writer.add_page(page)
294+
295+
for i, page in enumerate(content_writer.pages):
296+
page_num = front_matter_pages + i + 1
297+
footer = create_footer_overlay(page_num, total_pages, version)
298+
page.merge_page(footer.pages[0])
299+
writer.add_page(page)
300+
301+
# Add clickable links on TOC pages
302+
for toc_page_idx, rect, target_page_num in toc_links:
303+
writer_page_idx = title_page_count + toc_page_idx
304+
target_page_idx = target_page_num - 1
305+
link = Link(
306+
rect=rect,
307+
target_page_index=target_page_idx,
308+
fit=Fit.fit(),
309+
border=[0, 0, 0],
310+
)
311+
writer.add_annotation(page_number=writer_page_idx, annotation=link)
312+
313+
# Rewrite internal localhost links to in-document GoTo links
314+
_rewrite_internal_links(writer, url_to_page)
315+
316+
writer.write(output_path)
317+
return output_path

website/docs/chapter-01/05_organization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Key objectives of the foundation include:
5050

5151
- **Funding of Ecosystem Projects**: Whenever possible, the foundation finances open-source projects that are proposed by community members, aiming to support broader ecosystem development and innovation.
5252

53-
As a non-profit association, all funds are directed towards the development and promotion of the Robot Framework, ensuring that it remains accessible to all users without commercial restrictions.
53+
As a non-profit association, all funds are directed toward the development and promotion of the Robot Framework, ensuring that it remains accessible to all users without commercial restrictions.
5454

5555
More information, including a list of foundation members, is available at **[robotframework.org/foundation](https://robotframework.org/foundation)**.
5656

0 commit comments

Comments
 (0)