///|
type PdfObject = @pdf.PdfObject
///|
type Op = @pdfops.Op
///|
const SLIDE_W : Double = 960.0
///|
const SLIDE_H : Double = 540.0
///|
const MARGIN_X : Double = 56.0
///|
const HEADER_H : Double = 44.0
///|
const FOOTER_H : Double = 32.0
///|
fn theme_bg() -> (Double, Double, Double) {
(0.99, 0.99, 0.995)
}
///|
fn theme_ink() -> (Double, Double, Double) {
(0.12, 0.14, 0.18)
}
///|
fn theme_muted() -> (Double, Double, Double) {
(0.45, 0.48, 0.52)
}
///|
fn theme_accent() -> (Double, Double, Double) {
(0.18, 0.48, 0.86)
}
///|
fn theme_accent2() -> (Double, Double, Double) {
(0.92, 0.33, 0.36)
}
///|
fn font_type1(base_font : String) -> PdfObject {
Dictionary([
("/Type", Name(PdfName(b"/Font"))),
("/Subtype", Name(PdfName(b"/Type1"))),
("/BaseFont", Name(@pdf.PdfName::of_string("/" + base_font))),
])
}
///|
fn slide_resources() -> PdfObject {
Dictionary([
(
"/Font",
Dictionary([
("/F0", font_type1("Helvetica-Bold")),
("/F1", font_type1("Helvetica")),
("/F2", font_type1("Courier")),
]),
),
])
}
///|
fn slide_paper() -> @pdfpaper.Paper {
@pdfpaper.Paper::new(unit=PdfPoint, width=SLIDE_W, height=SLIDE_H)
}
///|
fn slide_mediabox() -> PdfObject {
slide_paper().rectangle()
}
///|
fn ops_fill_rect(
x : Double,
y : Double,
w : Double,
h : Double,
rgb : (Double, Double, Double),
) -> Array[Op] {
let (r, g, b) = rgb
[Opq, Oprg(r, g, b), Opre(x, y, w, h), Opf, OpQ]
}
///|
fn ops_stroke_rect(
x : Double,
y : Double,
w : Double,
h : Double,
rgb : (Double, Double, Double),
width : Double,
) -> Array[Op] {
let (r, g, b) = rgb
[Opq, Opw(width), OpRG(r, g, b), Opre(x, y, w, h), OpS, OpQ]
}
///|
fn ops_line(
x0 : Double,
y0 : Double,
x1 : Double,
y1 : Double,
rgb : (Double, Double, Double),
width : Double,
) -> Array[Op] {
let (r, g, b) = rgb
[Opq, Opw(width), OpRG(r, g, b), Opm(x0, y0), Opl(x1, y1), OpS, OpQ]
}
///|
fn ops_text(
x : Double,
y : Double,
font : String,
size : Double,
text : String,
rgb : (Double, Double, Double),
) -> Array[Op] {
let (r, g, b) = rgb
let tr = @pdftransform.Transform::new([Translate(x, y)]).to_matrix()
[Opq, Oprg(r, g, b), OpBT, OpTf(font, size), OpTm(tr), OpTj(text), OpET, OpQ]
}
///|
fn ops_code_block(
x : Double,
y_top : Double,
lines : Array[String],
) -> Array[Op] {
let ops : Array[Op] = []
let line_h = 15.0
let w = SLIDE_W - 2.0 * MARGIN_X
let h = line_h * lines.length().to_double() + 26.0
let y = y_top - h
ops.append(ops_fill_rect(MARGIN_X, y, w, h, (0.965, 0.97, 0.98))[:])
ops.append(ops_stroke_rect(MARGIN_X, y, w, h, (0.78, 0.8, 0.84), 1.0)[:])
let mut i = 0
for line in lines {
let yy = y_top - 18.0 - i.to_double() * line_h
ops.append(ops_text(x, yy, "/F2", 11.0, line, (0.12, 0.14, 0.18))[:])
i = i + 1
}
ops
}
///|
fn header_ops(section : String, title : String) -> Array[Op] {
let ops : Array[Op] = []
let accent = theme_accent()
ops.append(
ops_fill_rect(0.0, SLIDE_H - HEADER_H, SLIDE_W, HEADER_H, accent)[:],
)
ops.append(
ops_text(MARGIN_X, SLIDE_H - 28.0, "/F1", 13.0, section, (1.0, 1.0, 1.0))[:],
)
ops.append(
ops_text(MARGIN_X, SLIDE_H - 18.0, "/F0", 18.0, title, (1.0, 1.0, 1.0))[:],
)
ops
}
///|
fn footer_ops(
page_index_1 : Int,
total_pages : Int,
progress0_1 : Double,
) -> Array[Op] {
let ops : Array[Op] = []
let bar = (0.12, 0.14, 0.18)
ops.append(ops_fill_rect(0.0, 0.0, SLIDE_W, FOOTER_H, bar)[:])
ops.append(
ops_fill_rect(
0.0,
FOOTER_H - 3.0,
SLIDE_W * progress0_1,
3.0,
theme_accent2(),
)[:],
)
let label = page_index_1.to_string() + " / " + total_pages.to_string()
ops.append(
ops_text(
SLIDE_W - MARGIN_X - 72.0,
12.0,
"/F2",
10.0,
label,
(0.85, 0.88, 0.92),
)[:],
)
ops.append(
ops_text(MARGIN_X, 12.0, "/F2", 10.0, "Prev", (0.85, 0.88, 0.92))[:],
)
ops.append(
ops_text(
SLIDE_W - MARGIN_X - 28.0,
12.0,
"/F2",
10.0,
"Next",
(0.85, 0.88, 0.92),
)[:],
)
ops
}
///|
fn notes_ops(note : String) -> Array[Op] {
let ops : Array[Op] = []
let x = MARGIN_X
let w = SLIDE_W - 2.0 * MARGIN_X
let y = FOOTER_H + 10.0
let h = 54.0
ops.push(OpBDC("/OC", Name(PdfName(b"/NOTES"))))
ops.append(ops_fill_rect(x, y, w, h, (1.0, 1.0, 0.9))[:])
ops.append(ops_stroke_rect(x, y, w, h, (0.88, 0.84, 0.4), 1.0)[:])
ops.append(
ops_text(
x + 10.0,
y + 36.0,
"/F2",
11.0,
"NOTES (layer): " + note,
(0.2, 0.18, 0.05),
)[:],
)
ops.push(OpEMC)
ops
}
///|
fn slide_ops(
section : String,
title : String,
body : Array[Op],
note : String?,
page_index_1 : Int,
total_pages : Int,
) -> Array[Op] {
let ops : Array[Op] = []
ops.append(ops_fill_rect(0.0, 0.0, SLIDE_W, SLIDE_H, theme_bg())[:])
ops.append(header_ops(section, title)[:])
let progress = page_index_1.to_double() / total_pages.to_double()
ops.append(footer_ops(page_index_1, total_pages, progress)[:])
for op in body {
ops.push(op)
}
match note {
None => ()
Some(n) => ops.append(notes_ops(n)[:])
}
ops
}
///|
fn bullet_slide_body(
x : Double,
y_top : Double,
bullets : Array[String],
shown : Int,
) -> Array[Op] {
let ops : Array[Op] = []
let line_h = 26.0
let mut i = 0
for b in bullets {
i = i + 1
if i > shown {
continue
}
let y = y_top - (i - 1).to_double() * line_h
let color = if i == shown { theme_accent2() } else { theme_ink() }
ops.append(ops_text(x, y, "/F1", 18.0, "- " + b, color)[:])
}
ops
}
///|
fn title_slide_body(title : String, subtitle : String) -> Array[Op] {
let ops : Array[Op] = []
ops.append(ops_text(MARGIN_X, 340.0, "/F0", 46.0, title, theme_ink())[:])
ops.append(ops_text(MARGIN_X, 300.0, "/F1", 20.0, subtitle, theme_muted())[:])
ops.append(
ops_text(
MARGIN_X,
230.0,
"/F2",
12.0,
"Generated by mbtpdf using PDF content-stream operators.",
theme_muted(),
)[:],
)
ops.append(
ops_text(
MARGIN_X,
210.0,
"/F2",
12.0,
"Tip: open your viewer's Layers panel and enable NOTES.",
theme_muted(),
)[:],
)
// Decorative diagonal
ops.append(ops_line(0.0, 140.0, SLIDE_W, 500.0, theme_accent(), 6.0)[:])
ops.append(ops_line(0.0, 120.0, SLIDE_W, 480.0, theme_accent2(), 3.0)[:])
ops
}
///|
fn section_slide_body(section : String) -> Array[Op] {
let ops : Array[Op] = []
ops.append(ops_text(MARGIN_X, 320.0, "/F0", 40.0, section, theme_ink())[:])
ops.append(
ops_text(
MARGIN_X,
280.0,
"/F1",
18.0,
"Beamer-style layout: header, footer, progress bar, navigation.",
theme_muted(),
)[:],
)
ops
}
///|
fn toc_slide_body(
entries : Array[(String, Int)],
) -> (Array[Op], Array[(Int, (Double, Double, Double, Double))]) {
let ops : Array[Op] = []
ops.append(ops_text(MARGIN_X, 420.0, "/F0", 30.0, "Outline", theme_ink())[:])
ops.append(
ops_text(
MARGIN_X,
392.0,
"/F2",
12.0,
"Click a section title to jump.",
theme_muted(),
)[:],
)
let mut y = 340.0
let rects : Array[(Int, (Double, Double, Double, Double))] = []
for entry in entries {
let (name, page_i) = entry
ops.append(ops_text(MARGIN_X, y, "/F1", 22.0, name, theme_accent())[:])
rects.push((page_i, (MARGIN_X, y - 6.0, MARGIN_X + 420.0, y + 22.0)))
y = y - 40.0
}
(ops, rects)
}
///|
fn page_of_ops(ops : Array[Op]) -> @pdfpage.Page raise {
let stream = @pdfops.Op::stream_of_ops(ops)
Page(
content=[stream],
mediabox=slide_mediabox(),
resources=slide_resources(),
rotate=Rotate0,
rest=Dictionary([]),
)
}
///|
fn set_catalog_entry(
pdf : @pdf.Pdf,
key : String,
value : PdfObject,
) -> Unit raise {
let rootnum = pdf.root
let root = pdf.lookup_obj(rootnum)
let updated_root = root.add_entry(key, value)
pdf.addobj_given_num((rootnum, updated_root))
pdf.trailerdict = pdf.trailerdict.add_entry("/Root", Indirect(rootnum))
}
///|
fn add_notes_layer(pdf : @pdf.Pdf) -> Int raise {
let ocg_notes : PdfObject = Dictionary([
("/Type", Name(PdfName(b"/OCG"))),
("/Name", PdfString("NOTES")),
])
let notes_num = pdf.addobj(ocg_notes)
let default_dict : PdfObject = Dictionary([
("/BaseState", Name(PdfName(b"/OFF"))),
("/Intent", Name(PdfName(b"/View"))),
("/Name", PdfString("Default")),
])
let ocprops : PdfObject = Dictionary([
("/OCGs", PdfArray([Indirect(notes_num)])),
("/D", default_dict),
])
let ocprops_num = pdf.addobj(ocprops)
set_catalog_entry(pdf, "/OCProperties", Indirect(ocprops_num))
notes_num
}
///|
fn add_notes_properties_to_pages(
pdf : @pdf.Pdf,
notes_num : Int,
) -> @pdf.Pdf raise {
let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
for i in 0.. PdfObject {
let dest = @pdfdest.Destination::Fit(PageObject(refnum)).to_pdf_object()
Dictionary([("/S", Name(PdfName(b"/GoTo"))), ("/D", dest)])
}
///|
fn add_link(
pdf : @pdf.Pdf,
page : @pdfpage.Page,
rect : (Double, Double, Double, Double),
action : PdfObject,
) -> @pdfpage.Page raise {
let annot : @pdfannot.Annotation = {
subtype: Link,
annot_contents: None,
subject: None,
rectangle: rect,
border: @pdfannot.Border::new(0.0),
colour: None,
annotrest: Dictionary([("/A", action)]),
}
@pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}
///|
fn add_nav_links(
pdf : @pdf.Pdf,
toc_links : Array[(Int, (Double, Double, Double, Double))],
) -> @pdf.Pdf raise {
let refs = pdf.page_reference_numbers()
let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
for i in 0.. 0 {
page = add_link(
pdf,
page,
(MARGIN_X - 8.0, 6.0, MARGIN_X + 48.0, 28.0),
goto_action(refs[i - 1]),
)
}
if i + 1 < pages.length() {
page = add_link(
pdf,
page,
(SLIDE_W - MARGIN_X - 20.0, 6.0, SLIDE_W - MARGIN_X + 52.0, 28.0),
goto_action(refs[i + 1]),
)
}
pages[i] = page
}
// TOC: links on slide 2 (index 1).
if pages.length() > 1 {
let mut toc_page = pages[1]
for item in toc_links {
let (target_index_1, rect) = item
let target_i = target_index_1 - 1
if target_i >= 0 && target_i < refs.length() {
toc_page = add_link(pdf, toc_page, rect, goto_action(refs[target_i]))
}
}
pages[1] = toc_page
}
@pdfpage.PdfPageDoc::new(pdf).change_pages(true, pages)
}
///|
fn add_bookmarks(
pdf : @pdf.Pdf,
section_starts : Array[(String, Int)],
) -> @pdf.Pdf raise {
let refs = pdf.page_reference_numbers()
let marks : Array[@pdfmarks.Bookmark] = []
marks.push({
level: 0,
text: "Slides",
target: Fit(PageObject(refs[0])),
isopen: true,
colour: (0.0, 0.0, 0.0),
flags: 2,
})
marks.push({
level: 1,
text: "Outline",
target: Fit(PageObject(refs[1])),
isopen: true,
colour: (0.0, 0.0, 0.0),
flags: 0,
})
for entry in section_starts {
let (name, page_i_1) = entry
let idx = page_i_1 - 1
if idx >= 0 && idx < refs.length() {
marks.push({
level: 1,
text: name,
target: Fit(PageObject(refs[idx])),
isopen: true,
colour: (0.0, 0.0, 0.0),
flags: 0,
})
}
}
@pdfmarks.PdfMarks::new(pdf).add_bookmarks(marks)
}
///|
priv enum SlideBody {
Title(String, String)
Toc
SectionDivider(String)
Bullets(Array[String], Int)
PipelineDiagram
CodeSnippet
Takeaways(Array[String])
Thanks
}
///|
priv struct SlideDef {
section : String
title : String
note : String?
body : SlideBody
}
///|
fn build_deck() -> (
Array[@pdfpage.Page],
Array[(String, Int)],
Array[(Int, (Double, Double, Double, Double))],
) raise {
// Section start indices are 1-based slide indices (for TOC/bookmarks).
let section_starts : Array[(String, Int)] = []
let toc_entries : Array[(String, Int)] = []
let slides : Array[SlideDef] = []
fn push_slide(
slides : Array[SlideDef],
section : String,
title : String,
note : String?,
body : SlideBody,
) -> Unit {
slides.push({ section, title, note, body, })
}
fn begin_section(
slides : Array[SlideDef],
section_starts : Array[(String, Int)],
toc_entries : Array[(String, Int)],
name : String,
note : String?,
) -> Unit {
let start = slides.length() + 1
section_starts.push((name, start))
toc_entries.push((name, start))
push_slide(slides, name, name, note, SectionDivider(name))
}
push_slide(
slides,
"mbtpdf",
"Beamer-ish Slides",
Some("We fake 'beamer overlays' by generating multiple slides."),
Title("Beamer-ish Slides", "Made with pdfops + pdfpage"),
)
push_slide(
slides,
"mbtpdf",
"Outline",
Some("Click section titles; use Prev/Next footer links."),
Toc,
)
begin_section(
slides,
section_starts,
toc_entries,
"Motivation",
Some("This is a section divider slide."),
)
push_slide(
slides,
"Motivation",
"Why generate slides as PDF?",
Some("PDF is a universal container: vector, text, links, layers."),
Bullets(
[
"No LaTeX toolchain required", "Precise control over content streams", "Interactive features: links, bookmarks, layers",
],
3,
),
)
let bullets = [
"PDF ops are composable (Array[Op])", "You can post-process existing PDFs", "You can add structure (bookmarks) and UI (links)",
]
for shown in 1..<=3 {
push_slide(
slides,
"Motivation",
"Three things we can do",
Some("Each slide reveals one more bullet."),
Bullets(bullets, shown),
)
}
begin_section(
slides,
section_starts,
toc_entries,
"Pipeline",
Some("The 'mental model' section."),
)
push_slide(
slides,
"Pipeline",
"Read → Ops → Transform → Write",
Some("This is the mental model slide."),
PipelineDiagram,
)
begin_section(
slides,
section_starts,
toc_entries,
"Code",
Some("Concrete API shape."),
)
push_slide(
slides,
"Code",
"A slide is just operators",
Some("Build ops, then stream_of_ops, then attach to a Page."),
CodeSnippet,
)
begin_section(
slides,
section_starts,
toc_entries,
"Wrap-up",
Some("Closing thoughts."),
)
push_slide(
slides,
"Wrap-up",
"Takeaways",
Some("Next: add images, gradients, and layout helpers."),
Takeaways([
"Beamer-ish decks are feasible with mbtpdf", "Links + bookmarks already feel presentation-like",
"OCG layers can emulate speaker notes",
]),
)
push_slide(
slides,
"Wrap-up",
"Thanks",
Some("Try extending: miniframes, code highlighting, SVG-to-ops."),
Thanks,
)
let total = slides.length()
let pages : Array[@pdfpage.Page] = Array(capacity=total)
let mut toc_links : Array[(Int, (Double, Double, Double, Double))] = []
for i in 0.. title_slide_body(t, s)
SectionDivider(name) => section_slide_body(name)
Bullets(bs, shown) => bullet_slide_body(MARGIN_X, 420.0, bs, shown)
PipelineDiagram => {
let ops : Array[Op] = []
ops.append(
ops_text(
MARGIN_X,
420.0,
"/F1",
18.0,
"A tiny pipeline diagram:",
theme_muted(),
)[:],
)
let y = 290.0
let box_w = 180.0
let box_h = 74.0
let gap = 22.0
let x0 = MARGIN_X
fn box(
ops : Array[Op],
x : Double,
y : Double,
label : String,
) -> Unit {
ops.append(ops_fill_rect(x, y, 180.0, 74.0, (1.0, 1.0, 1.0))[:])
ops.append(
ops_stroke_rect(x, y, 180.0, 74.0, (0.2, 0.22, 0.26), 1.5)[:],
)
ops.append(
ops_text(x + 16.0, y + 44.0, "/F0", 16.0, label, theme_ink())[:],
)
}
box(ops, x0, y, "Read/Parse")
box(ops, x0 + (box_w + gap) * 1.0, y, "Ops")
box(ops, x0 + (box_w + gap) * 2.0, y, "Transform")
box(ops, x0 + (box_w + gap) * 3.0, y, "Write")
let ay = y + box_h / 2.0
for k in 0..<3 {
let xa = x0 + (box_w + gap) * (k + 1).to_double() - gap
let x1 = xa + gap
ops.append(ops_line(xa, ay, x1, ay, theme_accent2(), 3.0)[:])
}
ops
}
CodeSnippet => {
let ops : Array[Op] = []
ops.append(
ops_text(
MARGIN_X,
420.0,
"/F1",
18.0,
"This is the rough shape of the API:",
theme_muted(),
)[:],
)
ops.append(
ops_code_block(MARGIN_X + 14.0, 360.0, [
"let ops : Array[@pdfops.Op] = [ ... ]", "let stream = @pdfops.Op::stream_of_ops(ops)",
"let page = @pdfpage.Page(", " content=[stream],", " mediabox=slide_mediabox(),",
" resources=slide_resources(),", " rotate=Rotate0,", " rest=Dictionary([]),",
")",
])[:],
)
ops
}
Takeaways(items) =>
bullet_slide_body(MARGIN_X, 420.0, items, items.length())
Thanks => {
let ops : Array[Op] = []
ops.append(
ops_text(MARGIN_X, 340.0, "/F0", 44.0, "Thanks!", theme_ink())[:],
)
ops.append(
ops_text(
MARGIN_X,
300.0,
"/F1",
18.0,
"mbtpdf can generate presentations.",
theme_muted(),
)[:],
)
ops.append(
ops_text(
MARGIN_X,
250.0,
"/F2",
12.0,
"Run: moon run cmd/pdfbeamer -- tmp/deck.pdf",
theme_muted(),
)[:],
)
ops
}
Toc => {
let (ops, links) = toc_slide_body(toc_entries)
toc_links = links
ops
}
}
let full_ops = slide_ops(
slide.section,
slide.title,
body_ops,
slide.note,
i + 1,
total,
)
pages.push(page_of_ops(full_ops))
}
(pages, section_starts, toc_links)
}
///|
pub async fn run(
out_file? : String = "deck.pdf",
quiet? : Bool = false,
) -> Unit {
try {
let (pages, section_starts, toc_links) = build_deck()
let (pdf, pageroot) = @pdfpage.PdfPageDoc::new(@pdf.Pdf::empty()).add_pagetree(
pages,
)
let pdf = @pdfpage.PdfPageDoc::new(pdf).add_root(pageroot, [])
let notes_num = add_notes_layer(pdf)
let pdf = add_notes_properties_to_pages(pdf, notes_num)
let pdf = add_bookmarks(pdf, section_starts)
let pdf = add_nav_links(pdf, toc_links)
@pdfwritefs.PdfWriteFs::new().pdf_to_file(pdf, out_file)
} catch {
err =>
if !quiet {
println("Failed to produce output.")
println(err)
}
}
}
///|
async fn main {
let args = @env.args()
match args {
[_, out] => run(out_file=out)
_ => run()
}
}