gowkhtmltopdf

A page of text becomes a page you can print

Imagine you wrote a tiny web page. A heading that says Invoice. A blue card that says Hello. You want a PDF. A PDF is a file a viewer draws, like a still picture of a page. This program draws that picture itself. It does not open Chrome. It does not call another program named wkhtmltopdf.

Words I will use, once, so they stop being mystery:

The letter we start with

Save this as sample.html.

<h1>Invoice</h1>
<div class="card">Hello</div>

/* in a style block */
h1 { font-size: 18pt; }
.card { background-color: blue; color: white; padding: 12pt; }

blue is four letters. It is not paint yet. By the end it must be a filled rectangle on paper.

Nine jobs, in order

Think of a kitchen line. Each station does one job and passes a tray. Nobody skips ahead to drawing until the tray has sizes on it.

  1. load

    Get the bytes

    Read the file. Do not look at tags yet. If the letters are not UTF-8, stop. UTF-8 is the usual way computers store text.

  2. parse

    Build a family tree

    Walk the text once. Open tags go on a stack, like plates. Close tags take a plate off. You get a tree. The CSS is still just writing stuck under <style>.

  3. collect

    Copy the style notes

    Find the style block. Split it into two notes. One for h1. One for .card. A class is a name you pin on a tag, here card.

  4. match

    Who gets which note

    The h1 note only sticks to the heading. The .card note only sticks to the div. The heading never hears the word blue.

  5. winner map

    One whiteboard per node

    Each node gets a map. A map is a list of "this property, this winning text." On the heading the font size is still the string "18pt". On the card, color is still the string "blue".

  6. numbers

    Turn words into amounts

    Look up blue in a color table. You get 0, 0, 255. Store that as 0, 0, 1. Look up white. You get 1, 1, 1. 12pt is already 12. That 12 is padding. Padding is empty space inside the box, like bubble wrap. It is not paint.

  7. layout

    Measure the furniture

    Give each box an x, a y, a width, a height. Y starts at the top and grows down, like reading a page. Write a to-do list of drawing jobs. One job is "fill a blue rectangle." One job is "write Hello in white."

  8. paint

    Copy onto paper that counts from the floor

    A PDF page counts Y from the bottom. Flip the box. Write tiny commands. rg sets the crayon. re traces a rectangle. f fills it.

  9. write

    Zip the drawing into a file

    Put a %PDF-1.4 header on top. Compress the drawing. Add a table of contents for the file itself so a viewer can find page 1. If you asked for an archive stamp, add that stamp here. The drawing jobs did not change.

Load and parse: internal/convert/prepare/prepare.go. Tree builder: internal/html/html.go:118. Color table: internal/css/values.go:666. Flip: internal/layout/paint.go:853.

The tree, with plates

Parse starts a fake root named #document. Then it pushes tags. sample.html becomes:

#document
  html
    body
      h1          "Invoice"
      div.card    "Hello"
internal/html/html.go:118

A script tag would sit here as dead text. Nothing runs it. If a page is an empty Angular shell, the PDF is empty too. There is no little browser inside.

Two whiteboards, not one

The fight over which style note stays is called the cascade. The function is cascadeRaw. It runs once per node. That is why the heading keeps 18pt and never turns blue.

heading whiteboard

h1

The program already had a default note for headings. Size 2em. Bold. Block. Your file said 18pt. 18pt replaces 2em. Bold stays, because you never mentioned weight.

Then a later pass reads "18pt" and stores the number 18.

card whiteboard

div.card

Three notes land as text. background-color: "blue". color: "white". Padding split into four sides of "12pt".

A later pass walks that same list. Blue becomes fill. White becomes letter color. Padding shrinks the inside of the box. Hello sits in that inside. Padding never becomes a crayon command.

cascadeRaw: internal/layout/style_cascade.go:344. Walking the map: applyRestProps at style_cascade.go:666. Color names: ParseColor in internal/css/values.go:182. Padding inset: contentBox in internal/layout/layout_flow.go:11.

Who wins when two notes argue

Each matching note gets a score of three numbers: (a, b, c). Compare them left to right, like a sports ranking.

NumberIn EnglishIn sample.html
aHow many ids, like #box0 and 0. No ids.
bHow many classes, like .card.card has 1. h1 has 0.
cHow many tag names, like h1h1 has 1. .card has 0.

So the heading match is (0, 0, 1). The card match is (0, 1, 0). Class beats tag. That only matters if both notes try to set the same property on the same node. They do not. Different whiteboards.

If you later write h1 { color: black } and .card { color: white }, the div is still white. The heading never matched .card.

Above that score there is a ranking:

  1. A note marked important wins over a normal note.
  2. Your file beats the program's defaults. Defaults are scored (0, 0, 0).
  3. Then a, then b, then c.
  4. If still tied, the later note in the file wins.
The three-number score: internal/css/css.go:1733. The fight: applyCascadeWin in internal/layout/style_cascade.go:535. Defaults go in with order -1 at cascadeRaw line 360.

Blue, white, and the 12-point air

The color table is a dictionary. blue is 0 red, 0 green, 255 blue. Computers often store that as 0 to 1, so 255 becomes 1. White is 1, 1, 1.

NoteBecomesJob
background-color bluefill 0, 0, 1Paint the card's rectangle.
color whiteletter fill 1, 1, 1Paint the letters of Hello. Not a rectangle.
padding 12ptfour sides of 12Push Hello inward. No crayon.

Three notes. Three jobs. One walk of the whiteboard.

namedColorTable "blue" and "white": internal/css/values.go:664-666. Padding used as geometry: internal/layout/layout_flow.go:11-18.

The drawing list

Layout does not draw yet. It writes orders.

OrderMeans
write Invoice at 18pt, blackFrom the heading.
fill a rectangle, color 0, 0, 1From the card background.
write Hello, whiteFrom the card text, sitting in the 12-point air.

The fill is tucked under the text on purpose, so you see letters on blue, not blue on letters.

How x y w h get onto the paper

Layout and PDF do not share the same zero for Y. Layout puts zero at the top of the writing area and counts downward, like line numbers on a page. PDF puts zero at the bottom of the paper and counts upward. Same blue box on screen. Two different numbers for its place.

A PDF rectangle command looks like x y w h re. That is not the word red. re means rectangle. It wants the bottom-left corner, then width, then height.

The fill job stored the top-left. So the code adds the height first, to get the bottom. Then it flips.

x = leftMargin + layoutX
y = pageHeight - topMargin - (layoutY + height)
w = width
h = height

then: x y w h re
then: f          (fill that shape)
canvasToPDF: internal/layout/paint.go:853-856. drawFill passes op.Y+op.H at paint.go:862. Rect writes " re": internal/pdf/content.go:288. Fill writes "f": content.go:293. Color writes " rg": content.go:228.

A4 paper is about 595 by 842 points. Default empty border around the page is 10 millimetres, about 28 points. If a teaching box sat at layout X=0, Y=40, width 500, height 36:

x = 28 + 0
y = 842 - 28 - (40 + 36) = 738
28 738 500 36 re

Your real Invoice heading will push that Y down a bit. The sums stay the same kind of sums.

Letters are different. Their Y is already the line they sit on, not the top of a box. The flip still happens. The extra "+ height" does not.

Blue is gone as a word. The stream says 0 0 1 rg (set fill color to blue), then x y w h re (trace the rectangle; re means rectangle, not red), then f (fill that shape with the color). Without f you would have a path and a color and still see no blue card. White Hello later says 1 1 1 rg and draws the letters.

A stamp, or no stamp (Compliance)

The drawing is the same for an invoice and for an archive file. The last station can glue on extra labels.

no stamp

Normal invoice

The file starts %PDF-1.4. A viewer draws it. A checker that wants an official archive mark will say no. That is the default.

stamp

Archive or accessible

You pass a profile flag. The writer adds extra packets. Archive wants embedded fonts and a color recipe. Accessible wants a title, picture descriptions, and a map of "this is a heading, this is a background so do not read it aloud." Changing only the version number on line one is not that stamp.

If you skip the stamp, the blue rectangle is still drawn. If you ask for accessible, that same rectangle is marked "background, ignore." Hello is still Hello.

To Summerize

A file of tags becomes a tree. Style notes stick to matching nodes. Each node keeps one winning text per property. Those texts become numbers. Numbers become box sizes and a list of drawing jobs. Jobs become crayon commands on a page that counts from the floor. The file around those commands can carry a stamp, or not.

If you can say that out loud, you have the html to pdf converter.