luv

Workshop wiki

document.lisp

mupdf/document.lisp

system luv/mupdf · 23 definitions · on GitHub

Documents, pages, and what a page can be turned into.

Everything here owns a C object and is responsible for dropping it, so the objects are CLOS instances with explicit closers and WITH- macros rather than pointers handed around bare. MuPDF's own ownership rule is that a context outlives everything made from it, which is why the document keeps a reference to its context rather than the other way round.

in-package#:luv.mupdf
defclasscontext
pointer:initarg:pointer:accessorcontext-pointer
:documentation

A MuPDF context: its allocator, its store, and its error stack.

defunmake-context
&key
max-store
*25610241024

Open a MuPDF context with the document handlers registered.

let*
pointer
andversion
%fz-new-context-imp
cffi:null-pointer
cffi:null-pointer
max-storeversion
when
or
nullversion
nullpointer
cffi:null-pointer-ppointer
error'mupdf-error:operation'make-context:detail
formatnil

no context for claimed version ~S; ~ set luv.MUPDF:*mupdf-version* to the library's FZ_VERSION

version
%fz-register-document-handlerspointer
make-instance'context:pointerpointer
defunclose-context
alexandria:when-let
pointer
context-pointercontext
setf
context-pointercontext
nil
%fz-drop-contextpointer
context
defmacrowith-mupdf-context
variable&restoptions
&bodybody
`
let
unwind-protect
progn,@body

Documents

defclassdocument
context:initarg:context:readerdocument-context
pointer:initarg:pointer:accessordocument-pointer
pathname:initarg:pathname:readerdocument-pathname
:documentation

An open document and the context it was opened in.

defmethodprint-object
print-unreadable-object
documentstream:typet
formatstream"~A~@[ ~D page~:P~]"
file-namestring
document-pathnamedocument
defunpdf-file-p
pathname

True when pathname begins with a PDF header.

This is a guard, not a validation. MuPDF reports a broken file by longjmp to a handler this binding cannot install, and with none on the stack it takes the process with it -- so the cheapest thing that catches the ordinary mistake of handing it something that is not a PDF at all is worth doing.

ignore-errors
with-open-file
streampathname:element-type'
unsigned-byte8
let
header
make-array5:element-type'
unsigned-byte8
and
=5
read-sequenceheaderstream
equalpheader
#x25#x50#x44#x46#x2D
defunopen-document
pathname&keycontext

Open pathname. A context is made for the document unless one is given.

let
truename
or
probe-filepathname
error'mupdf-error:operation'open-document:detail"no such file"
unless
pdf-file-ptruename
error'mupdf-error:operation'open-document:detail"not a PDF: no %PDF- header"
let*
pointer
%fz-open-document
context-pointercontext
namestringtruename
when
cffi:null-pointer-ppointer
error'mupdf-error:operation'open-document:detailtruename
make-instance'document:contextcontext:pointerpointer:pathnametruename
defunclose-document
alexandria:when-let
pointer
document-pointerdocument
setf
document-pointerdocument
nil
%fz-drop-document
context-pointer
document-contextdocument
pointer
document
defmacrowith-document
variablepathname&restoptions
&bodybody
`
let
,variable
open-document,pathname,@options
unwind-protect
progn,@body
defundocument-page-count
%fz-count-pages
context-pointer
document-contextdocument
document-pointerdocument
defmacrowith-page
&bodybody

Load page NUMBER of document for the duration of body.

let
context
gensym"CONTEXT"
`
let*
,context
context-pointer
document-context,document
,variable
%fz-load-page,context
document-pointer,document
,number
unwind-protect
progn,@body
%fz-drop-page,context,variable

Measuring

defunpage-bounds
documentnumber

Page number's box in PDF points, as left, top, right, bottom.

with-page
cffi:with-foreign-object
rect'
:structfz-rect
let
value
%fz-bound-page
context-pointer
document-contextdocument
page
declare
ignorerect
values
getfvalue'x0
getfvalue'y0
getfvalue'x1
getfvalue'y1
defunpage-size
documentnumber

Page number's width and height in PDF points, which are 1/72 inch.

multiple-value-bind
x0y0x1y1
values
-x1x0
-y1y0

Pixels

defunpage-rgba-words
documentnumber&key

Render page number at scale and return height by width packed rgba words.

The words are alpha in the high byte and red in the third, which is what a CLIM pattern wants, so the result can be handed straight to MAKE-PATTERN.

let
context
context-pointer
document-contextdocument
with-page
let
pixmap
cffi:with-foreign-object
matrix'
:structfz-matrix
setf
cffi:foreign-slot-valuematrix'
:structfz-matrix
'a
cffi:foreign-slot-valuematrix'
:structfz-matrix
'b
0.0
cffi:foreign-slot-valuematrix'
:structfz-matrix
'c
0.0
cffi:foreign-slot-valuematrix'
:structfz-matrix
'd
cffi:foreign-slot-valuematrix'
:structfz-matrix
'e
0.0
cffi:foreign-slot-valuematrix'
:structfz-matrix
'f
0.0
%fz-new-pixmap-from-pagecontextpage
cffi:mem-refmatrix'
:structfz-matrix
%fz-device-rgbcontext
0
when
cffi:null-pointer-ppixmap
unwind-protect
let*
width
%fz-pixmap-widthcontextpixmap
height
%fz-pixmap-heightcontextpixmap
stride
%fz-pixmap-stridecontextpixmap
components
%fz-pixmap-componentscontextpixmap
samples
%fz-pixmap-samplescontextpixmap
words
make-array:element-type'
unsigned-byte32
dotimes
yheightwords
let
row
+
cffi:pointer-addresssamples
*ystride
dotimes
let
base
cffi:make-pointer
+row
*xcomponents
setf
arefwordsyx
logior
ash25524
ash
cffi:mem-refbase:unsigned-char0
16
ash
cffi:mem-refbase:unsigned-char1
8
cffi:mem-refbase:unsigned-char2
%fz-drop-pixmapcontextpixmap

Text

MuPDF serializes structured text to XML itself, so the positions come out of a documented format rather than out of struct offsets this system would otherwise have to grovel. The XML is regular enough to scan without a parser: what is wanted is the font size in force, and each character with the box it occupies.

defstruct
text-run
:constructormake-text-run
string"":typestring
x0.0:typesingle-float
y0.0:typesingle-float

Where the pen actually sits. X and Y are the line's box; a renderer setting the line from glyph outlines wants the baseline the document wrote it on, which is the first character's own origin.

baseline-x0.0:typesingle-float
baseline-y0.0:typesingle-float
width0.0:typesingle-float
height0.0:typesingle-float
size0.0:typesingle-float
fontnil
defunparse-decimal
string&key
start0
end
lengthstring

A decimal number out of string, without going through READ.

The values in this XML are all plain decimals, and READ on text that came out of a document is a wider door than this needs to open.

let
sign1
whole0
fraction0
indexstart
seennil
when
and
<indexend
member
charstringindex
'
#\-#\+
when
char=
charstringindex
#\-
setfsign-1
incfindex
loopwhile
and
<indexend
digit-char-p
charstringindex
do
setfwhole
+
*whole10
digit-char-p
charstringindex
seent
incfindex
when
and
<indexend
char=
charstringindex
#\.
incfindex
loopwhile
and
<indexend
digit-char-p
charstringindex
do
setffraction
+
*fraction10
digit-char-p
charstringindex
scaleseent
incfindex
whenseen
float
*sign
+whole
/fractionscale
1.0
defunparse-decimals
string

Every space-separated decimal in string, in order.

let
values'
start0
loopwhile
<startlength
do
let
end
or
position#\Spacestring:startstart
length
alexandria:when-let
value
parse-decimalstring:startstart:endend
pushvaluevalues
setfstart
1+end
nreversevalues
defunxml-attribute
textstartname

The value of attribute name in the tag beginning at start, or NIL.

MuPDF escapes the characters that would end a tag or a quoted value, so the end of the tag and the end of the value can both be found by looking for the literal character.

let*
tag-end
or
position#\>text:startstart
lengthtext
key
concatenate'string" "name"=\""
at
searchkeytext:start2start:end2tag-end
whenat
let*
from
+at
to
position#\"text:startfrom
when
andto
<=totag-end
subseqtextfromto
defunparse-character-reference
name

The code point in a reference body like "#8217" or "#x201c", or NIL.

let*
hex
and
>
lengthname
2
char-equal
charname1
#\x
digits
subseqname
ifhex21
radix
ifhex1610
value0
when
plusp
lengthdigits
dotimes
index
lengthdigits
value
let
weight
digit-char-p
chardigitsindex
radix
unlessweight
setfvalue
+
*valueradix
weight
defununescape-xml
string

The five escapes MuPDF's writer emits, turned back into characters.

if
find#\&string
with-output-to-string
out
let
index0
loopwhile
<indexlength
do
let
character
charstringindex
if
char=character#\&
let
semicolon
position#\;string:startindex
let
name
andsemicolon
subseqstring
1+index
semicolon
cond
nullname
write-charcharacterout
incfindex
t
write-string
cond
string=name"amp"
"&"
string=name"lt"
"<"
string=name"gt"
">"
string=name"quot"
"\""
string=name"apos"
"'"

Everything outside ASCII comes back as a numeric reference, which for a typeset document means every curly quote and dash in it.

and
>
lengthname
1
char=
charname0
#\#
alexandria:if-let
string
code-charcode
formatnil"&~A;"name
t
formatnil"&~A;"name
out
setfindex
1+semicolon
progn
write-charcharacterout
incfindex
string
defunpage-text-runs
documentnumber

Page number's text as one TEXT-RUN per typeset line.

Positions are in PDF points with the origin at the page's top left, which is the orientation MuPDF's structured text already uses and the one a drawing surface wants.

MuPDF writes the font after the line it applies to, so a line is held back until the font element that follows names its size.

let
runs'
pendingnil
let
index0
loop
let
line-at
search"<line "text:start2index
font-at
search"<font "text:start2index
cond
and
nullline-at
nullfont-at
or
nullfont-at
andline-at
<line-atfont-at
let
bbox
xml-attributetextline-at"bbox"
string
xml-attributetextline-at"text"
setfpending
when
andbboxstring
plusp
lengthstring
destructuring-bind
&optionalx0y0x1y1
wheny1
let
char-at
search"<char "text:start2line-at
make-text-run:string:xx0:yy0:width
-x1x0
:height
-y1y0
:baseline-x
or
andchar-at
x0
:baseline-y
or
andchar-at
y1
setfindex
1+line-at
t
whenpending
alexandria:when-let
size
xml-attributetextfont-at"size"
setf
text-run-sizepending
setf
text-run-fontpending
xml-attributetextfont-at"name"
pushpendingruns
setfpendingnil
setfindex
1+font-at
nreverseruns
defunpage-stext-xml
documentnumber

Page number's structured text, as MuPDF's own XML.

let
context
context-pointer
document-contextdocument
with-page
let
stext
%fz-new-stext-page-from-pagecontextpage
cffi:null-pointer
when
cffi:null-pointer-pstext
error'mupdf-error:operation'page-stext-xml
unwind-protect
let
buffer
%fz-new-buffercontext65536
unwind-protect
let
output
%fz-new-output-with-buffercontextbuffer
unwind-protect
progn
%fz-print-stext-page-as-xmlcontextoutputstextnumber
%fz-close-outputcontextoutput
cffi:with-foreign-object
data:pointer
let
length
%fz-buffer-storagecontextbufferdata
cffi:foreign-string-to-lisp
cffi:mem-refdata:pointer
:countlength:encoding:utf-8
%fz-drop-outputcontextoutput
%fz-drop-buffercontextbuffer
%fz-drop-stext-pagecontextstext