///|
fn pi() -> Double {
4.0 * @math.atan(1.0)
}
///|
fn clamp01(x : Double) -> Double {
if x < 0.0 {
0.0
} else if x > 1.0 {
1.0
} else {
x
}
}
///|
fn hsv_to_rgb(h : Double, s : Double, v : Double) -> (Double, Double, Double) {
let h = h % 360.0
let s = clamp01(s)
let v = clamp01(v)
if s == 0.0 {
return (v, v, v)
}
let c = v * s
let x = c * (1.0 - (h / 60.0 % 2.0 - 1.0).abs())
let m = v - c
let (rp, gp, bp) = if h < 60.0 {
(c, x, 0.0)
} else if h < 120.0 {
(x, c, 0.0)
} else if h < 180.0 {
(0.0, c, x)
} else if h < 240.0 {
(0.0, x, c)
} else if h < 300.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
}
(rp + m, gp + m, bp + m)
}
///|
fn font_type1(base_font : String) -> @pdf.PdfObject {
Dictionary([
("/Type", Name(PdfName(b"/Font"))),
("/Subtype", Name(PdfName(b"/Type1"))),
("/BaseFont", Name(@pdf.PdfName::of_string("/" + base_font))),
])
}
///|
fn default_resources() -> @pdf.PdfObject {
Dictionary([
(
"/Font",
Dictionary([
("/F0", font_type1("Helvetica-Bold")),
("/F1", font_type1("Helvetica")),
("/F2", font_type1("Courier")),
]),
),
])
}
///|
fn a4_mediabox() -> @pdf.PdfObject {
@pdfpage.Page::blank(@pdfpaper.a4).mediabox
}
///|
fn ops_text(
x : Double,
y : Double,
font : String,
size : Double,
text : String,
rgb : (Double, Double, Double),
rotate_rad? : Double = 0.0,
) -> Array[@pdfops.Op] {
let (r, g, b) = rgb
let tr = if rotate_rad == 0.0 {
@pdftransform.Transform::new([Translate(x, y)]).to_matrix()
} else {
@pdftransform.Transform::new([
Rotate((0.0, 0.0), rotate_rad),
Translate(x, y),
]).to_matrix()
}
[Opq, Oprg(r, g, b), OpBT, OpTf(font, size), OpTm(tr), OpTj(text), OpET, OpQ]
}
///|
fn ops_fill_rect(
x : Double,
y : Double,
w : Double,
h : Double,
rgb : (Double, Double, Double),
) -> Array[@pdfops.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[@pdfops.Op] {
let (r, g, b) = rgb
[Opq, Opw(width), OpRG(r, g, b), Opre(x, y, w, h), OpS, OpQ]
}
///|
fn ops_poly_path(
points : Array[(Double, Double)],
close? : Bool = true,
) -> Array[@pdfops.Op] {
match points {
[] => []
[(x0, y0), .. rest] => {
let ops : Array[@pdfops.Op] = Array(capacity=points.length() + 2)
ops.push(Opm(x0, y0))
for point in rest {
let (x, y) = point
ops.push(Opl(x, y))
}
if close {
ops.push(Oph)
}
ops
}
}
}
///|
fn ops_fill_polygon(
points : Array[(Double, Double)],
rgb : (Double, Double, Double),
) -> Array[@pdfops.Op] {
let (r, g, b) = rgb
[Opq, Oprg(r, g, b)] + ops_poly_path(points) + [Opf, OpQ]
}
///|
fn ops_stroke_polyline(
points : Array[(Double, Double)],
rgb : (Double, Double, Double),
width : Double,
) -> Array[@pdfops.Op] {
let (r, g, b) = rgb
[Opq, Opw(width), OpRG(r, g, b)] +
ops_poly_path(points, close=false) +
[OpS, OpQ]
}
///|
fn circle_points(
cx : Double,
cy : Double,
r : Double,
n : Int,
) -> Array[(Double, Double)] {
let out = Array(capacity=n)
let two_pi = 2.0 * pi()
for i in 0.. Array[@pdfops.Op] {
let w = 595.0
let h = 842.0
let stripes = 28
let ops : Array[@pdfops.Op] = []
for i in 0.. (Double, Double) {
let r1 = 120.0
let r2 = 45.0
let d = 80.0
let a = r1 - r2
let k = a / r2
let x = a * @math.cos(t) + d * @math.cos(k * t)
let y = a * @math.sin(t) - d * @math.sin(k * t)
(x, y)
}
///|
fn spiro_ops() -> Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.98, 0.98, 0.985))[:])
ops.append(
ops_text(
54.0,
800.0,
"/F0",
24.0,
"Procedural Spirograph",
(0.1, 0.12, 0.16),
)[:],
)
let cx = 297.5
let cy = 420.0
let loops = 7.0
let steps = 1400
let dt = 2.0 * pi() * loops / steps.to_double()
for i in 0.. Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
// "Day" layer
ops.push(OpBDC("/OC", Name(PdfName(b"/DAY"))))
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.75, 0.9, 1.0))[:])
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 160.0, (0.9, 0.95, 0.98))[:])
ops.append(
ops_fill_polygon(circle_points(470.0, 700.0, 46.0, 40), (1.0, 0.86, 0.1))[:],
)
ops.append(
ops_stroke_rect(70.0, 320.0, 455.0, 240.0, (0.15, 0.25, 0.35), 2.0)[:],
)
ops.append(ops_text(90.0, 520.0, "/F0", 28.0, "DAY", (0.12, 0.18, 0.22))[:])
ops.append(
ops_text(
90.0,
480.0,
"/F1",
14.0,
"A light theme layer.",
(0.12, 0.18, 0.22),
)[:],
)
ops.push(OpEMC)
// "Night" layer
ops.push(OpBDC("/OC", Name(PdfName(b"/NIGHT"))))
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.05, 0.06, 0.1))[:])
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 160.0, (0.09, 0.1, 0.16))[:])
for i in 0..<120 {
let x = 30.0 + i.to_double() * 43.0 % 540.0
let y = 200.0 + i.to_double() * 97.0 % 610.0
let r = 0.8 + (i % 7).to_double() * 0.3
ops.append(
ops_fill_polygon(circle_points(x, y, r, 10), (0.95, 0.98, 1.0))[:],
)
}
ops.append(
ops_fill_polygon(circle_points(470.0, 700.0, 40.0, 40), (0.9, 0.92, 0.95))[:],
)
ops.append(
ops_fill_polygon(circle_points(485.0, 710.0, 40.0, 40), (0.05, 0.06, 0.1))[:],
)
ops.append(
ops_stroke_rect(70.0, 320.0, 455.0, 240.0, (0.8, 0.86, 0.95), 2.0)[:],
)
ops.append(ops_text(90.0, 520.0, "/F0", 28.0, "NIGHT", (0.9, 0.92, 0.95))[:])
ops.append(
ops_text(90.0, 480.0, "/F1", 14.0, "A dark theme layer.", (0.9, 0.92, 0.95))[:],
)
ops.push(OpEMC)
// Non-layer instructions (always visible)
ops.append(
ops_text(
54.0,
800.0,
"/F0",
24.0,
"Optional Content Groups (Layers)",
(0.98, 0.98, 0.985),
)[:],
)
ops.append(
ops_text(
54.0,
70.0,
"/F2",
11.0,
"Toggle DAY/NIGHT in the viewer's layers panel.",
(0.9, 0.92, 0.95),
)[:],
)
ops
}
///|
fn map_ops() -> Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.99, 0.985, 0.97))[:])
ops.append(
ops_text(54.0, 800.0, "/F0", 26.0, "Clickable Map", (0.12, 0.14, 0.18))[:],
)
ops.append(
ops_text(
54.0,
770.0,
"/F1",
13.0,
"These cards are backed by PDF link annotations (GoTo/URI).",
(0.25, 0.28, 0.32),
)[:],
)
let cards : Array[(Double, Double, String, String, (Double, Double, Double))] = [
(54.0, 620.0, "Go: Spirograph", "Page 2", (0.9, 0.3, 0.35)),
(310.0, 620.0, "Go: Layers", "Page 3", (0.25, 0.55, 0.9)),
(54.0, 440.0, "Go: Mini Comic", "Page 5", (0.45, 0.75, 0.35)),
(310.0, 440.0, "Go: Stickers", "Page 6", (0.95, 0.75, 0.2)),
(54.0, 260.0, "Go: Credits", "Page 7", (0.6, 0.35, 0.9)),
]
for card in cards {
let (x, y, title, subtitle, accent) = card
ops.append(ops_fill_rect(x, y, 231.0, 140.0, (1.0, 1.0, 1.0))[:])
ops.append(ops_stroke_rect(x, y, 231.0, 140.0, (0.2, 0.22, 0.26), 1.2)[:])
ops.append(ops_fill_rect(x, y + 134.0, 231.0, 6.0, accent)[:])
ops.append(
ops_text(x + 16.0, y + 92.0, "/F0", 16.0, title, (0.15, 0.17, 0.2))[:],
)
ops.append(
ops_text(x + 16.0, y + 66.0, "/F2", 12.0, subtitle, (0.35, 0.38, 0.42))[:],
)
ops.append(ops_text(x + 16.0, y + 24.0, "/F2", 10.0, "Click me", accent)[:])
}
ops
}
///|
fn comic_ops() -> Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (1.0, 1.0, 1.0))[:])
ops.append(
ops_text(54.0, 800.0, "/F0", 24.0, "Tiny PDF Comic", (0.12, 0.14, 0.18))[:],
)
let panels : Array[(Double, Double, Double, Double)] = [
(54.0, 520.0, 487.0, 220.0),
(54.0, 280.0, 487.0, 200.0),
(54.0, 80.0, 487.0, 160.0),
]
for panel in panels {
let (x, y, w, h) = panel
ops.append(ops_stroke_rect(x, y, w, h, (0.1, 0.1, 0.1), 2.0)[:])
}
// Speech bubble (panel 1)
ops.append(
ops_fill_polygon(circle_points(180.0, 680.0, 56.0, 40), (1.0, 1.0, 1.0))[:],
)
ops.append(
ops_stroke_polyline(
circle_points(180.0, 680.0, 56.0, 40),
(0.1, 0.1, 0.1),
1.5,
)[:],
)
ops.append(
ops_fill_polygon(
[(210.0, 642.0), (240.0, 612.0), (204.0, 624.0)],
(1.0, 1.0, 1.0),
)[:],
)
ops.append(
ops_stroke_polyline(
[(210.0, 642.0), (240.0, 612.0), (204.0, 624.0), (210.0, 642.0)],
(0.1, 0.1, 0.1),
1.5,
)[:],
)
ops.append(
ops_text(140.0, 688.0, "/F1", 12.0, "This is just ops.", (0.12, 0.14, 0.18))[:],
)
ops.append(
ops_text(144.0, 668.0, "/F1", 12.0, "No raster images.", (0.12, 0.14, 0.18))[:],
)
// Panel 2: a simple "burst"
let burst = Array(capacity=16)
for i in 0..<16 {
let t = 2.0 * pi() * (i.to_double() / 16.0)
let r = if i % 2 == 0 { 70.0 } else { 35.0 }
burst.push((300.0 + r * @math.cos(t), 380.0 + r * @math.sin(t)))
}
ops.append(ops_fill_polygon(burst, (1.0, 0.92, 0.2))[:])
ops.append(ops_stroke_polyline(burst + [burst[0]], (0.2, 0.18, 0.05), 2.0)[:])
ops.append(ops_text(250.0, 374.0, "/F0", 18.0, "BANG!", (0.2, 0.18, 0.05))[:])
// Panel 3: diagonal caption
ops.append(
ops_text(
90.0,
130.0,
"/F2",
12.0,
"Rotate text with OpTm",
(0.15, 0.17, 0.2),
rotate_rad=0.35,
)[:],
)
ops
}
///|
fn stickers_ops() -> Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.97, 0.98, 0.99))[:])
ops.append(
ops_text(54.0, 800.0, "/F0", 24.0, "Vector Stickers", (0.12, 0.14, 0.18))[:],
)
ops.append(
ops_text(
54.0,
770.0,
"/F1",
13.0,
"Everything here is rectangles, polylines, and polygons.",
(0.25, 0.28, 0.32),
)[:],
)
let centers : Array[(Double, Double, (Double, Double, Double))] = [
(150.0, 600.0, (0.95, 0.3, 0.35)),
(297.5, 600.0, (0.25, 0.55, 0.9)),
(445.0, 600.0, (0.45, 0.75, 0.35)),
(150.0, 380.0, (0.95, 0.75, 0.2)),
(297.5, 380.0, (0.6, 0.35, 0.9)),
(445.0, 380.0, (0.2, 0.7, 0.75)),
]
for c in centers {
let (cx, cy, accent) = c
let outline = circle_points(cx, cy, 78.0, 50)
ops.append(ops_fill_polygon(outline, (1.0, 1.0, 1.0))[:])
ops.append(
ops_stroke_polyline(outline + [outline[0]], (0.2, 0.22, 0.26), 1.5)[:],
)
ops.append(ops_fill_rect(cx - 78.0, cy + 70.0, 156.0, 8.0, accent)[:])
}
// Star sticker
let star = Array(capacity=10)
for i in 0..<10 {
let t = 2.0 * pi() * (i.to_double() / 10.0) - pi() / 2.0
let r = if i % 2 == 0 { 46.0 } else { 20.0 }
star.push((150.0 + r * @math.cos(t), 600.0 + r * @math.sin(t)))
}
ops.append(ops_fill_polygon(star, (0.95, 0.75, 0.2))[:])
// Heart sticker (two circles + triangle-ish)
ops.append(
ops_fill_polygon(circle_points(282.0, 615.0, 22.0, 30), (0.95, 0.3, 0.35))[:],
)
ops.append(
ops_fill_polygon(circle_points(314.0, 615.0, 22.0, 30), (0.95, 0.3, 0.35))[:],
)
ops.append(
ops_fill_polygon(
[(264.0, 612.0), (332.0, 612.0), (298.0, 564.0)],
(0.95, 0.3, 0.35),
)[:],
)
// Lightning bolt sticker
let bolt : Array[(Double, Double)] = [
(445.0, 632.0),
(420.0, 598.0),
(444.0, 598.0),
(416.0, 560.0),
(468.0, 604.0),
(444.0, 604.0),
]
ops.append(ops_fill_polygon(bolt, (0.25, 0.55, 0.9))[:])
ops
}
///|
fn credits_ops() -> Array[@pdfops.Op] {
let ops : Array[@pdfops.Op] = []
ops.append(ops_fill_rect(0.0, 0.0, 595.0, 842.0, (0.06, 0.07, 0.1))[:])
ops.append(
ops_text(54.0, 790.0, "/F0", 26.0, "Credits / Links", (0.95, 0.96, 0.98))[:],
)
ops.append(
ops_text(
54.0,
740.0,
"/F1",
14.0,
"Generated entirely with mbtpdf + PDF content stream operators.",
(0.8, 0.84, 0.9),
)[:],
)
ops.append(
ops_text(
54.0,
700.0,
"/F2",
12.0,
"Click: https://docs.moonbitlang.com",
(0.8, 0.84, 0.9),
)[:],
)
ops.append(
ops_text(
54.0,
90.0,
"/F2",
11.0,
"Tip: if you modify this demo, run: moon info && moon fmt && moon test",
(0.6, 0.64, 0.7),
)[:],
)
ops
}
///|
fn page_of_ops(ops : Array[@pdfops.Op]) -> @pdfpage.Page raise {
let stream = @pdfops.Op::stream_of_ops(ops)
Page(
content=[stream],
mediabox=a4_mediabox(),
resources=default_resources(),
rotate=Rotate0,
rest=Dictionary([]),
)
}
///|
fn set_catalog_entry(
pdf : @pdf.Pdf,
key : String,
value : @pdf.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_day_night_layers(pdf : @pdf.Pdf) -> @pdf.Pdf raise {
let ocg_day : @pdf.PdfObject = Dictionary([
("/Type", Name(PdfName(b"/OCG"))),
("/Name", PdfString("DAY")),
])
let ocg_night : @pdf.PdfObject = Dictionary([
("/Type", Name(PdfName(b"/OCG"))),
("/Name", PdfString("NIGHT")),
])
let day_num = pdf.addobj(ocg_day)
let night_num = pdf.addobj(ocg_night)
let default_dict : @pdf.PdfObject = Dictionary([
("/BaseState", Name(PdfName(b"/ON"))),
("/Intent", Name(PdfName(b"/View"))),
("/Name", PdfString("Default")),
("/OFF", PdfArray([Indirect(night_num)])),
("/ON", PdfArray([Indirect(day_num)])),
])
let ocprops : @pdf.PdfObject = Dictionary([
("/OCGs", PdfArray([Indirect(day_num), Indirect(night_num)])),
("/D", default_dict),
])
let ocprops_num = pdf.addobj(ocprops)
set_catalog_entry(pdf, "/OCProperties", Indirect(ocprops_num))
let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
if pages.length() < 3 {
return pdf
}
let layer_page = pages[2]
let props : @pdf.PdfObject = Dictionary([
("/DAY", Indirect(day_num)),
("/NIGHT", Indirect(night_num)),
])
let resources = layer_page.resources.add_entry("/Properties", props)
pages[2] = Page(
content=layer_page.content,
mediabox=layer_page.mediabox,
resources~,
rotate=layer_page.rotate,
rest=layer_page.rest,
)
@pdfpage.PdfPageDoc::new(pdf).change_pages(true, pages)
}
///|
fn add_bookmarks(pdf : @pdf.Pdf) -> @pdf.Pdf raise {
let refs = pdf.page_reference_numbers()
if refs.length() == 0 {
return pdf
}
let marks : Array[@pdfmarks.Bookmark] = []
let entries : Array[(String, Int)] = [
("Cover", 0),
("Procedural Spirograph", 1),
("DAY / NIGHT Layers", 2),
("Clickable Map", 3),
("Tiny PDF Comic", 4),
("Vector Stickers", 5),
("Credits / Links", 6),
]
for entry in entries {
let (title, idx) = entry
if idx < refs.length() {
marks.push({
level: 0,
text: title,
target: Fit(PageObject(refs[idx])),
isopen: true,
colour: (0.0, 0.0, 0.0),
flags: 0,
})
}
}
@pdfmarks.PdfMarks::new(pdf).add_bookmarks(marks)
}
///|
fn goto_action(refnum : Int) -> @pdf.PdfObject {
let dest = @pdfdest.Destination::Fit(PageObject(refnum)).to_pdf_object()
Dictionary([("/S", Name(PdfName(b"/GoTo"))), ("/D", dest)])
}
///|
fn uri_action(uri : String) -> @pdf.PdfObject {
Dictionary([("/S", Name(PdfName(b"/URI"))), ("/URI", PdfString(uri))])
}
///|
fn add_nav_links(pdf : @pdf.Pdf) -> @pdf.Pdf raise {
let refs = pdf.page_reference_numbers()
let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
if pages.length() == 0 {
return pdf
}
for i in 0.. 0 {
let prev_ref = refs[i - 1]
let rect = (54.0, footer_y, 54.0 + btn_w, footer_y + btn_h)
let annot : @pdfannot.Annotation = {
subtype: Link,
annot_contents: None,
subject: None,
rectangle: rect,
border: @pdfannot.Border::new(0.0),
colour: None,
annotrest: Dictionary([("/A", goto_action(prev_ref))]),
}
page = @pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}
// Next
if i + 1 < pages.length() {
let next_ref = refs[i + 1]
let rect = (
595.0 - 54.0 - btn_w,
footer_y,
595.0 - 54.0,
footer_y + btn_h,
)
let annot : @pdfannot.Annotation = {
subtype: Link,
annot_contents: None,
subject: None,
rectangle: rect,
border: @pdfannot.Border::new(0.0),
colour: None,
annotrest: Dictionary([("/A", goto_action(next_ref))]),
}
page = @pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}
// Cover: add one external link over the URL string
if i == pages.length() - 1 {
let rect = (54.0, 696.0, 340.0, 716.0)
let annot : @pdfannot.Annotation = {
subtype: Link,
annot_contents: None,
subject: None,
rectangle: rect,
border: @pdfannot.Border::new(0.0),
colour: None,
annotrest: Dictionary([
("/A", uri_action("https://docs.moonbitlang.com")),
]),
}
page = @pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}
pages[i] = page
}
// Map page: clickable cards -> internal navigation.
if pages.length() >= 7 {
let map_i = 3
let cards : Array[((Double, Double, Double, Double), Int)] = [
((54.0, 620.0, 285.0, 760.0), 1),
((310.0, 620.0, 541.0, 760.0), 2),
((54.0, 440.0, 285.0, 580.0), 4),
((310.0, 440.0, 541.0, 580.0), 5),
((54.0, 260.0, 285.0, 400.0), 6),
]
let mut page = pages[map_i]
for card in cards {
let (rect, target) = card
let annot : @pdfannot.Annotation = {
subtype: Link,
annot_contents: None,
subject: None,
rectangle: rect,
border: @pdfannot.Border::new(0.0),
colour: None,
annotrest: Dictionary([("/A", goto_action(refs[target]))]),
}
page = @pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}
pages[map_i] = page
}
@pdfpage.PdfPageDoc::new(pdf).change_pages(true, pages)
}
///|
pub async fn run(
out_file? : String = "showcase.pdf",
quiet? : Bool = false,
) -> Unit {
try {
let pages : Array[@pdfpage.Page] = [
page_of_ops(cover_ops()),
page_of_ops(spiro_ops()),
page_of_ops(layers_ops()),
page_of_ops(map_ops()),
page_of_ops(comic_ops()),
page_of_ops(stickers_ops()),
page_of_ops(credits_ops()),
]
let (pdf, pageroot) = @pdfpage.PdfPageDoc::new(@pdf.Pdf::empty()).add_pagetree(
pages,
)
let pdf = @pdfpage.PdfPageDoc::new(pdf).add_root(pageroot, [])
let pdf = add_day_night_layers(pdf)
let pdf = add_bookmarks(pdf)
let pdf = add_nav_links(pdf)
@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()
if args.length() == 2 {
run(out_file=args[1])
} else {
run()
}
}