Skip to the content
W07NotesEngines from nothing

Quire, a PDF reader written from nothing

Quire reads a PDF file, finds its pages and draws them on a canvas, and reads their text back out. It was writ­ten from an empty file and the spec­i­fi­ca­tion, and it stands on two of the en­gines before it: Thaw in­flates its com­pressed streams, and Glyph reads the TrueType fonts em­bed­ded in the files. Below, it draws a page as slowly as you like, and the num­bers fur­ther down hold it against pdf.js, the reader in Firefox.

PostedKindNotesSeriesEngines from nothing

·

Page ·

·

Everything on the canvas is drawn by Quire: the paths, the images, the gra­di­ents and the let­ters of every em­bed­ded TrueType and Type3 font. Where a font’s out­lines are in a format Quire does not read, the browser draws the let­ters in a font like it, and the panel says so. A file you open is read in this page and not sent any­where.

Read from the end

A PDF is a heap of num­bered ob­jects: dic­tio­nar­ies, arrays, names, strings, num­bers and streams of bytes. Their places are listed only in the last lines of the file. There, after startxref, is the place of the cross ref­er­ence table, which gives the byte offset of every object, and the trailer, which names the cat­a­log the pages hang from. So a reader starts at the end of the file and works back­wards.

A file that has been edited and saved again keeps its old bytes and adds the changes after them, with a new table that points back to the one before. Newer files pack their table into a com­pressed stream and many small ob­jects into object streams. Quire reads all of these. When the table is wrong, which hap­pens in real files more than it should, Quire scans the whole file for obj and builds the table again, as every reader has to.

    
            
    ·

    These are the ob­jects of the file drawn above, printed from what Quire parsed and not from the bytes. A ref­er­ence is a button that goes to its object. A stream shows its dic­tio­nary and then its con­tents, in­flated by Thaw where it was com­pressed, so a page’s con­tent stream reads as the pro­gram it is.

    A page is a program

    The con­tent of a page is a list of op­er­a­tors in re­verse Polish no­ta­tion: the operands first, then the op­er­a­tor. re adds a rec­tan­gle to the path, f fills it, cm changes the co­or­di­nate system, q and Q save and re­store the whole graph­ics state. Text is the same: BT starts it, Tf sets the font and size, Tj shows a string, and a text matrix moves along as each char­ac­ter is placed.

    Quire runs that pro­gram against a canvas. Its ma­tri­ces mul­ti­ply in the order the spec­i­fi­ca­tion writes them, which is the op­po­site of the order canvas code is usu­ally writ­ten in, and the most common bug while writ­ing Quire was a page drawn mir­rored, upside down or a page away. A form is a small pro­gram of its own that a page can call, with its own re­sources, and a trans­parency group is drawn on a canvas of its own first and laid onto the page with its opac­ity. The slider above stops the pro­gram after any op­er­a­tor, so you can watch a page being built in the order its writer wrote it.

    Four kinds of font

    A font in a PDF is a dic­tio­nary that maps its codes to glyphs, gives the width of each glyph, and often con­tains the font pro­gram itself. A TrueType font goes to Glyph, the ren­derer from the note before last, which al­ready reads out­lines: Quire asks it for the out­line of each glyph and fills it as a path. Fonts cut down for a PDF lack tables a font on its own must have, so Glyph learned a loose mode for them. A Type3 font is glyphs writ­ten as small con­tent streams, and Quire draws each as a pro­gram, like a form.

    Getting the text back out is a dif­fer­ent ques­tion from draw­ing it. A code in a string is only a number. A ToUnicode map, when the file has one, says which char­ac­ters it stands for. When it has none, Quire goes through the names of the glyphs, the stan­dard en­cod­ings and the font’s own tables, and, for a font with none of these, reads the font’s char­ac­ter map back­wards. The check­box above lays what it read over the page, a box for each char­ac­ter.

    Held against pdf.js

    Four files from three dif­fer­ent writ­ers, the sam­ples above, drawn by Quire and by pdf.js in the same Chromium at the same scale. The pixels are com­pared one by one, and the text of each page is com­pared as a count of each char­ac­ter, so a dif­fer­ent order does not count.

    $ node web/notes/quire/held.mjs
    4 PDFs from three writers, every page drawn by Quire and by pdf.js 4.10.38 in the same Chromium at scale 1.5
    
    pixels differ when a channel is off by more than a quarter of its range; "as paths" is pdf.js set to draw
    its fonts as outlines, the way Quire does, instead of handing them to the browser's text engine
    
    file                   pages  characters  differ      pixels   differ  as paths    Quire   pdf.js
    chromium.pdf               2       3,080       3   2,260,032    0.71%     0.02%    84 ms    95 ms
    libreoffice.pdf            1         602       0   1,090,584    0.31%     0.00%    14 ms    49 ms
    reportlab-plain.pdf        1         385       3   1,127,859    0.01%     1.17%    45 ms    63 ms
    reportlab.pdf              2         425       3   2,255,718    0.01%     0.76%    25 ms    62 ms
    
    characters: 4,489 of the 4,495 pdf.js read, Quire read the same
    pixels: 0.29% differ, and 0.46% with pdf.js drawing its fonts as paths

    Most of the pixels that differ are the edges of let­ters. pdf.js hands a font to the browser’s own text engine, which smooths edges its own way. Told to draw its fonts as paths, as Quire does, it dif­fers from Quire on far fewer pixels for the files from Chromium and LibreOffice. For re­port­lab it is the other way round, be­cause its stan­dard fonts are not em­bed­ded and both read­ers ask the browser for a font like them, which is what pdf.js stops doing when it is told to draw paths. The only char­ac­ters that differ are the same lig­a­ture in three of the files, which pdf.js un­folds into the two let­ters it is made of.

    Held against pdf.js’s own tests

    pdf.js keeps hun­dreds of PDFs in its repos­i­tory to catch its own mis­takes, broken and odd ones on pur­pose among them. The com­mand fetches every one from the tag of the pdf.js ver­sion in use, and both read­ers read the text of every page.

    $ node web/notes/quire/suite.mjs
    pdf.js 4.10.38's test suite: 577 files in its repository, read by Quire and by pdf.js in 23 s
    pdf.js read 577; of those, Quire read 571 and refused 6
    pages: Quire found 1,097, pdf.js 1,097
    text, in the first fifty pages of each: of the 458,821 characters pdf.js read, Quire read 451,347 the same (98.37%), and every one in 508 files
    the reasons for each refusal:
         6  The file is encrypted, and Quire does not decrypt

    Every file Quire could not open is en­crypted, and Quire does not de­crypt. Much of the text that dif­fers is in fonts that keep their char­ac­ters only inside a font pro­gram Quire does not read, where pdf.js reads the names out of the font itself. Running this suite found most of what is in this note: a trailer whose cat­a­log is the wrong object, text that stands out­side the page, code page de­coded as Latin one by the de­coder in Node, and a page whose crop box was all that told three pages apart.

    What it does not have

    Quire does not de­crypt, so a file with a pass­word, or one locked against copy­ing, is re­fused. It reads no Type1 or CFF out­lines and lets the browser draw those let­ters in a sim­i­lar font. It de­codes JPEG through the browser and leaves JPEG , JBIG2 and fax images out. It leaves out soft masks and the shad­ings made of meshes, and text used as a clip­ping path is drawn but does not clip. It draws the page and not what lies on it: an­no­ta­tions, form fields, links and the out­line are not read. Each thing it leaves out on a page is named in the panel beside it.

    The source is in the port­fo­lio’s repos­i­tory under web/notes/quire/: pdf.js for the file, its ob­jects and its fil­ters, content.js for the painter, fonts.js and encodings.js for fonts and their char­ac­ters, held.mjs and suite.mjs for the com­mands, and corpus.mjs for how the sam­ples were writ­ten.