1 ! Copyright (C) 2011-2012 John Benediktsson
2 ! See http://factorcode.org/license.txt for BSD license
3 USING: accessors assocs calendar combinators fonts formatting io
4 io.streams.string kernel literals make math math.order
5 namespaces pdf.canvas pdf.values pdf.wrap ranges sequences
6 sequences.extras sorting splitting ui.text xml.entities ;
7 FROM: pdf.canvas => draw-text ;
15 ! margin-left, margin-right += inset-width
18 ! margin-left, margin-right -= inset-width
23 ! TUPLE: spacer width height ;
26 ! TUPLE: image < span ;
29 ! Outlines (add to catalog):
31 ! /PageMode /UseOutlines
37 ! FIXME: spacing oddities if run multiple times
38 ! FIXME: make sure highlights text "in order"
39 ! FIXME: don't modify layout objects in pdf-render
40 ! FIXME: make sure unicode "works"
41 ! FIXME: only set style differences to reduce size?
42 ! FIXME: gadget. to take a "screenshot" into a pdf?
43 ! FIXME: compress each pdf object to reduce file size?
46 GENERIC: pdf-render ( canvas obj -- remain/f )
48 M: f pdf-render 2drop f ;
50 GENERIC: pdf-width ( canvas obj -- n )
54 : (pdf-layout) ( page obj -- page )
56 dupd [ pdf-render ] with-string-writer
57 '[ _ append ] [ change-stream ] curry dip
58 [ [ , <canvas> ] when ] keep
63 : pdf-layout ( seq -- pages )
66 dup stream>> empty? [ drop ] [ , ] if
70 TUPLE: div items style ;
75 [ style>> set-style ] keep
76 swap '[ _ pdf-render drop ] each f ;
79 [ style>> set-style ] keep
80 items>> [ dupd pdf-width ] map nip supremum ;
85 : convert-string ( str -- str' )
89 } escape-string-by [ 256 < ] filter ;
94 TUPLE: p string style ;
96 : <p> ( string style -- p )
97 [ convert-string ] dip p boa ;
100 [ style>> set-style ] keep
103 over [ font>> ] [ avail-width ] bi visual-wrap
104 over avail-lines bound cut
105 [ draw-text ] [ "" concat-as ] bi*
106 ] change-string dup string>> empty? [ drop f ] when ;
109 [ style>> set-style ] keep
110 [ font>> ] [ string>> ] bi* split-lines
111 [ dupd text-width ] map nip supremum ;
114 TUPLE: text string style ;
116 : <text> ( string style -- text )
117 [ convert-string ] dip text boa ;
119 ! FIXME: need to make links clickable, render text first, draw
120 ! box over text that is "link"
122 ! https://www.w3.org/WAI/WCAG21/Techniques/pdf/PDF11.html
125 [ style>> set-style ] keep
129 over [ font>> ] [ avail-width ] bi visual-wrap
130 unclip [ "" concat-as ] dip
131 ] [ over line-break f ] if
134 [ { } ] [ over [ font>> ] [ width ] bi visual-wrap ]
136 ] dip [ prefix ] when*
137 over avail-lines bound cut
138 [ draw-text ] [ "" concat-as ] bi*
139 ] change-string dup string>> empty? [ drop f ] when ;
142 [ style>> set-style ] keep
143 [ font>> ] [ string>> ] bi* split-lines
144 [ dupd text-width ] map nip supremum ;
154 [ dup 0 > pick avail-lines 0 > and ] [
155 over avail-width over min [ - ] keep [
156 overd [ draw-line ] [ inc-x ] 2bi
157 ] unless-zero dup 0 > [ over line-break ] when
159 ] change-width nip dup width>> 0 > [ drop f ] unless ;
171 over avail-lines 0 > [ drop ?break f ] [ nip ] if ;
179 : <pb> ( -- pb ) f pb boa ;
182 dup used?>> [ f >>used? drop f ] [ t >>used? ] if nip ;
189 CONSTANT: table-cell-padding 5
191 TUPLE: table-cell contents width ;
193 : <table-cell> ( contents -- table-cell )
196 M: table-cell pdf-render
198 [ width>> >>col-width 0 >>x drop ]
200 [ [ dupd pdf-render ] map nip ] change-contents
201 dup contents>> [ ] any? [ drop f ] unless
204 width>> table-cell-padding +
205 swap margin>> [ + ] change-left drop
209 TUPLE: table-row cells ;
211 C: <table-row> table-row
213 ! save y before rendering each cell
214 ! set y to max y after all renders
216 M: table-row pdf-render
227 over y>> max-y max max-y!
228 ] map swap max-y >>y drop
231 dup cells>> [ ] any? [ drop f ] unless
234 [ drop margin>> 54 >>left drop ]
236 drop dup width>> >>col-width
237 [ ?line-break ] [ table-cell-padding inc-y ] bi
241 : col-widths ( canvas cells -- widths )
245 [ [ dupd pdf-width ] [ 0 ] if* ] map supremum
250 :: max-col-widths ( canvas rows -- widths )
253 cells>> canvas swap col-widths
254 [ widths [ 0 or max ] change-at ] each-index
255 ] each widths >alist sort-keys values
260 ! special-case small first column
261 drop dup unclip-slice over sum swap
262 450 swap - swap / [ * ] curry map! drop
264 ! size down all columns
265 450 swap / [ * ] curry map
269 ! make last cell larger
270 450 swap [-] [ + ] curry dupd
271 sequences.extras:change-last
274 : set-col-widths ( canvas rows -- )
275 [ max-col-widths ] keep [
277 [ swap >>width drop ] [ drop ] if*
281 TUPLE: table rows widths? ;
283 : <table> ( rows -- table )
289 dup widths?>> [ 2drop ] [
290 t >>widths? rows>> set-col-widths
295 dup rows>> empty? [ t ] [
296 [ rows>> first dupd pdf-render ] keep swap
298 ] [ [ rest ] change-rows ] until nip
299 dup rows>> [ drop f ] [ drop ] if-empty
304 2drop 450 ; ! FIXME: hardcoded max-width
307 : pdf-object ( str n -- str' )
308 "%d 0 obj\n" sprintf "\nendobj" surround ;
310 : pdf-stream ( str -- str' )
311 [ length 1 + "<<\n/Length %d\n>>" sprintf ]
312 [ "\nstream\n" "\nendstream" surround ] bi append ;
314 : pdf-catalog ( -- str )
322 : pdf-pages ( n -- str )
326 "/MediaBox [ 0 0 612 792 ]" ,
327 [ "/Count %d" sprintf , ]
330 [ "%d 0 R " sprintf ] map concat
331 "/Kids [ " "]" surround ,
334 ] { } make join-lines ;
336 : pdf-page ( n -- page )
341 1 + "/Contents %d 0 R" sprintf ,
342 "/Resources << /Font <<" ,
343 "/F1 3 0 R /F2 4 0 R /F3 5 0 R" ,
344 "/F4 6 0 R /F5 7 0 R /F6 8 0 R" ,
345 "/F7 9 0 R /F8 10 0 R /F9 11 0 R" ,
346 "/F10 12 0 R /F11 13 0 R /F12 14 0 R" ,
349 ] { } make join-lines ;
351 : pdf-trailer ( objects -- str )
354 dup length 1 + "0 %d" sprintf ,
355 "0000000000 65535 f" ,
357 over "%010X 00000 n" sprintf , length 1 + +
361 dup length 1 + "/Size %d" sprintf ,
366 [ length 1 + ] map-sum 9 + "%d" sprintf ,
368 ] { } make join-lines ;
370 SYMBOLS: pdf-producer pdf-author pdf-creator ;
372 TUPLE: pdf-info title timestamp producer author creator ;
374 : <pdf-info> ( -- pdf-info )
377 pdf-producer get >>producer
378 pdf-author get >>author
379 pdf-creator get >>creator ;
381 M: pdf-info pdf-value
384 [ timestamp>> [ "/CreationDate " write pdf-write nl ] when* ]
385 [ producer>> [ "/Producer " write pdf-write nl ] when* ]
386 [ author>> [ "/Author " write pdf-write nl ] when* ]
387 [ title>> [ "/Title " write pdf-write nl ] when* ]
388 [ creator>> [ "/Creator " write pdf-write nl ] when* ]
390 ] with-string-writer ;
393 TUPLE: pdf-ref object revision ;
398 [ object>> ] [ revision>> ] bi "%d %d R" sprintf ;
401 TUPLE: pdf info pages fonts ;
409 :: pages>objects ( pdf -- objects )
411 pdf info>> pdf-value ,
413 { $ sans-serif-font $ serif-font $ monospace-font } {
414 [ [ f >>bold? f >>italic? pdf-value , ] each ]
415 [ [ t >>bold? f >>italic? pdf-value , ] each ]
416 [ [ f >>bold? t >>italic? pdf-value , ] each ]
417 [ [ t >>bold? t >>italic? pdf-value , ] each ]
419 pdf pages>> length pdf-pages ,
421 dup length 16 swap 2 range boa zip
422 [ pdf-page , , ] assoc-each
424 dup length [1..b] zip [ first2 pdf-object ] map ;
426 : objects>pdf ( objects -- str )
427 [ join-lines "\n" append "%PDF-1.4\n" ]
428 [ pdf-trailer ] bi surround ;
430 ! Rename to pdf>string, have it take a <pdf> object?
432 : pdf>string ( seq -- pdf )
433 <pdf> swap pdf-layout [
434 stream>> pdf-stream over pages>> push
435 ] each pages>objects objects>pdf ;
437 : write-pdf ( seq -- )