REBOL [ Library: [ level: 'advanced platform: 'all type: 'dialect domain: [dialects graphics printing] tested-under: none support: none license: none see-also: none ] Title: "PDF Maker" Purpose: { A dialect to create PDF files from REBOL. } Author: "Gabriele Santilli" EMail: giesse@rebol.it Comments: { Thanks to Volker Nitsch for the AFM parser. } File: %pdf-maker.r License: { Copyright (c) 2001-2006, Gabriele Santilli All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of Gabriele Santilli may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. } Date: 3-Aug-2006 Version: 1.27.1 ; majorv.minorv.status ; status: 0: unfinished; 1: testing; 2: stable History: [ 15-Jul-2001 1.1.0 "History start" 15-Jul-2001 1.2.0 "Added some comments; this will hopefully be appreciated :)" 15-Jul-2001 1.3.0 "Added graphics system and some primitives" 15-Jul-2001 1.4.0 "Added coordinate transformations" 16-Jul-2001 1.5.0 "Added circle (approx) and bezier" 16-Jul-2001 1.6.0 "(Hopefully) fixed buggy XREF handling" 17-Jul-2001 1.7.0 "Added paths; now graphics lacks only images" 17-Jul-2001 1.8.0 "Better decimal handling in PDF-FORM" 17-Jul-2001 1.9.0 "Added images (not efficient, but works!)" 21-Jul-2001 1.10.0 "Added font metrics information (THANKS VOLKER!)" 22-Jul-2001 1.11.0 "Text sizing experiments..." 26-Jul-2001 1.12.0 "Added text typesetter (alpha version, only justification)" 26-Jul-2001 1.13.0 "Fixed bugs in justification" 26-Jul-2001 1.14.0 "Finished typesetting engine" 27-Jul-2001 1.15.0 "Fixed various bugs" 31-Jul-2001 1.16.0 "Fixed a nasty bug (layout-pdf wasn't clearing pdf-spec)" 1-Aug-2001 1.17.0 "Added the ability to disable wrapping" 9-Aug-2001 1.18.0 "Changed the behaviour of the newline command in textboxes" 9-Aug-2001 1.19.0 "Changed path-rule a bit; now allows circles in a path too" 25-Aug-2001 1.20.0 "traslation -> translation (fixed the spelling)" 10-Jan-2002 1.21.0 { Fixed a bug: if a page ended with graphic commands, and the following page started with graphic commands, these were rendered in the previous page. Added support for the Euro char (€) in font metrics . } 24-Feb-2003 1.22.0 {Uploaded new version with temporary PRECALC-TEXTBOX function.} 20-Jun-2003 1.23.0 { Fixed a bug that caused an error when layouting a string starting with a space or a newline } 23-Jul-2003 1.24.0 { Small change to PRECALC-TEXBOX, now should add enough space for letters that expand below the baseline (as pqg etc.); it is just an hack, and don't expect it to really be precise. (That is, the typesetter is too stupid right now to take this kind of things into account. Maybe I'll change it, or you'll just have to wait for version 2.) } 11-Aug-2005 1.25.1 "Port to View 1.3; backported some changes from version 2" 2-Jun-2006 1.26.1 "Fixed a bug with images" 3-Aug-2006 1.27.1 "Fixed a bug (make-images returning none if there where no images)" ] ] context [ ; font metrics information for the 14 standard fonts (THANKS VOLKER!) ; now handles € (it was somehow left out, don't ask me why :) metrics: load decompress 64#{ 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 u1/3qzdXfje0nfK774zvkBcX/x+JkUJfi50BAA== } ; guess what are these? ; note: we will actually emit a 1.4 version PDF file, tough ; we'll use the key in the root catalog to state the real version; ; this way we should stay compatible with 1.3 (i.e. Acrobat 4) pdf-start: "%PDF-1.3^/" pdf-end: "%%EOF" ; form a decimal value avoiding scientific format etc. form-decimal: func [ "Form a decimal number" num [number!] /local str sign float ip fp ] [ if zero? num [return copy "0"] sign: either negative? num [ num: abs num "-" ] [""] str: make string! 20 num: form multiply power 10 negate float: to-integer log-10 num to-decimal num ip: first num fp: copy skip num 2 ; understanding this is left as an exercise to the reader. >:-> insert/dup insert/part insert insert/dup insert insert str sign either float < 0 ["0."] [""] #"0" -1 - float ip fp either float < 0 [tail fp] [float] #"0" float - length? fp if all [float >= 0 float < length? fp] [ insert insert tail str #"." skip fp float ] str ] ; valid characters in strings pdf-string-valid: complement charset "()\" ; this converts REBOL values to PDF values; it's way from perfect but works. pdf-form: func ["REBOL to PDF" value /only /local result mrk1 mrk2] [ result: make string! 256 if block? :value [ if empty? value [return copy "[]"] if only [insert result "["] foreach element value [ insert insert tail result pdf-form/only element #" " ] either only [change back tail result "]"] [remove back tail result] return result ] if char? :value [ return head insert result reduce [ #"(" either find pdf-string-valid value [""] [#"\"] value #")" ] ] if string? :value [ insert result "(" parse/all value [ some [ mrk1: some pdf-string-valid mrk2: ( insert/part tail result mrk1 mrk2 ) | mrk1: skip ( insert insert tail result #"\" mrk1/1 ) ] ] insert tail result ")" return result ] if decimal? :value [return form-decimal value] ; issues are used for tricks. ;-) if issue? :value [return form value] ; other values simply molded currently. mold :value ] ; this will hold the document's xref table xref: [] ; this will hold the document itself contents: #{} ; LOWLEVEL PDF DIALECT ; (this is what people on the ml were looking for. :) pdf-words: context [ ; creates an object obj: func [ id "Object id (generation will always be 0)" data "A block of data (will use PDF-FORM above)" ] [ insert tail xref compose/deep [(id) [(-1 + index? tail contents)]] insert tail contents reduce [ id " 0 obj^/" pdf-form data "^/endobj^/" ] ] ; creates a stream stream: func [ id "Object id (generation will always be 0)" data "Block (will use PDF-FORM) or any-string" ] [ insert tail xref compose/deep [(id) [(-1 + index? tail contents)]] if block? data [data: pdf-form data] insert tail contents reduce [ id " 0 obj^/" pdf-form compose [ #<< /Length (length? data) #>> ] "^/stream^/" data "^/endstream^/endobj^/" ] ] ; creates an Image XObject ; now has full support for the alpha channel (PDF 1.4) ; you are required to supply the ID for the SoftMask image: func [ id "Object id for the image (generation will always be 0)" aid "Object id for the SoftMask (generation will always be 0)" img [image!] "Image data" /local rgb alpha ] [ insert tail xref compose/deep [(id) [(-1 + index? tail contents)]] ; requires View 1.3 rgb: img/rgb alpha: img/alpha insert tail contents reduce [ id " 0 obj^/" pdf-form compose [ #<< /Type /XObject /Subtype /Image /Width (img/size/x) /Height (img/size/y) /ColorSpace /DeviceRGB /BitsPerComponent 8 /Interpolate true /SMask (aid) 0 R /Length (length? rgb) #>> ] "^/stream^/" rgb "^/endstream^/endobj^/" ] insert tail xref compose/deep [(aid) [(-1 + index? tail contents)]] ; NOTE: I'm not using the Matte key, i.e. I'm assuming that the image ; is not preblended. handling all that would go far beyond the scope of ; the PDF Maker. if you need to use preblended images you could apply the ; inverse formula on the image before passing it to the PDF Maker, or you could ; hack it here adding /Matte for your own purpose... :) insert tail contents reduce [ aid " 0 obj^/" pdf-form compose [ #<< /Type /XObject /Subtype /Image /Width (img/size/x) /Height (img/size/y) /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate true ; REBOL's alpha channel is inverted with respect to PDF's /Decode [1 0] /Length (length? alpha) #>> ] "^/stream^/" alpha "^/endstream^/endobj^/" ] ] ] ; guess what's this? :) zero-padded: func [val n] [ val: form val head insert insert/dup make string! n #"0" n - length? val val ] ; makes the xref table for the document make-xref: has [pos xref' lastfree firstfree cur] [ pos: tail contents sort/skip xref 2 xref': clear [] firstfree: lastfree: 0 repeat i pick tail xref -2 [ either cur: select xref i [ insert/only tail xref' reduce [cur/1 'n] ] [ either firstfree = 0 [firstfree: i] [xref'/:lastfree/1: i] lastfree: i insert/only tail xref' copy [0 f] ] ] insert pos reduce [ "xref^/0 " 1 + length? xref' "^/" zero-padded firstfree 10 " 65535 f ^/" ] foreach item xref' [ insert tail pos reduce [ zero-padded item/1 10 " 00000 " item/2 " ^/" ] ] insert tail pos reduce [ "trailer^/" pdf-form compose [ #<< /Size (1 + length? xref') /Root 1 0 R ; this assumes root will always be 1 #>> ] "^/startxref^/" -1 + index? pos newline ] ] ; THIS IS THE LOWLEVEL FUNCTION ; use this to make a PDF file using the three lowlevel commands defined above ; (OBJ, STREAM and IMAGE) set 'make-pdf func [spec [block!]] [ clear xref clear contents insert contents pdf-start do bind spec in pdf-words 'self make-xref copy head insert tail contents pdf-end ] ; high level dialect begins here... ; this will hold the pages etc. pages: [] used-fonts: [] font-resources: [] ; this will hold the spec then passed to MAKE-PDF pdf-spec: [] ; default page object default-page: context [ size: [211 297] ; mm. (ISO A4) offset: [0 0] rotation: 0 contents: [] ] ; default textbox object default-textbox: context [ bbox: [10 17 191 263] ; default font is Helvetica 4.23 (12pt) font-name: 'Helvetica font-size: 4.23 ; last used font (to avoid setting it each time) last-font: [none none] ; line height handling max-size: 0 linefactor: 1.1 lineheight: none ; last used line height (to avoid setting it each time) last-lh: none left: right: 0 ; margins last-offset: 0 ; current x text offset ; this is the amount of space a text line can consume ; before being wrapped fuel: bbox/3 - left - right ; text width wrappers: charset "+-\/" no-wrap?: no ; set to yes to disable wrapping in-para?: no mode: 'justify ; 'left 'right 'center 'as-is ; justify: word spacing vs char spacing factor word-spacing: 0.5 ; buffer holding each rendered line linebuff: [] ; buffer holding the entire text text: [] ; text color (default is black) color: 0.0.0 last-color: none ; current y position of text (actually, this is a sort of ; temporary text-height; text-height gets the maximum value ; reached by this word) (needed for automatic page breaks) current-y-pos: 0 ; actual height of text (textbox autosizing, automatic page breaks) text-height: 0 ; space between paragraphs para-skip: 5 to-pdf: does [ compose [ q (bbox/1) (bbox/2) (bbox/3) (bbox/4) re W n BT (bbox/1) (bbox/2 + bbox/4) Td (text) ET Q ] ] ] ; default space object default-space: context [ translate: none ; [x y] scale: none ; [sx sy] rotate: none ; angle skew: none ; [alpha beta] contents: [] to-pdf: has [result] [ result: make block! 256 insert result 'q ; apply transformations... if translate [ insert tail result reduce [1 0 0 1 translate/1 translate/2 'cm] ] if rotate [ insert tail result reduce [ cosine rotate sine rotate negate sine rotate cosine rotate 0 0 'cm ] ] if scale [ insert tail result reduce [scale/1 0 0 scale/2 0 0 'cm] ] if skew [ insert tail result reduce [1 tangent skew/1 tangent skew/2 1 0 0 'cm] ] ; handle contents foreach object contents [ insert tail result object/to-pdf ] head insert tail result 'Q ] ] ; default graphics object default-gfx: context [ contents: [] to-pdf: does [ contents ] ] ; this is a "context" stack; it is used to make spaces work stack: [] push: func [thing] [insert tail stack thing] but: func [a [any-type!] b [any-type!]] [:a] pop: does [if not empty? stack [but last stack remove back tail stack]] ; this creates the document's root objects make-docroot: func [pages] [ insert tail pdf-spec [ obj 1 compose [ #<< /Type /Catalog /Version /1.4 /Outlines 2 0 R /Pages (pages) 0 R #>> ] obj 2 [ #<< /Type /Outlines /Count 0 #>> ] obj 3 [ ; ProcSet to use in pages [/PDF /Text /ImageC] ] ] ] new: val1: val2: txtb: gfx: none gfx-emit: func [data] [ if not gfx [insert tail new/contents gfx: make default-gfx []] insert tail gfx/contents reduce data ] ; TEXT TYPESETTER typeset-text: none emit-line: none context [ sum: chset: widths: kern: prev: char: buff: wbuff: wstr: invalid: wrappers: none ; emit first char in a line emit-veryfirst: func [char] [ wbuff: reduce [wstr: to-string char] sum: pick widths prev: 1 + to-integer char emit-char: :emit-other ] ; emit first char in a word emit-first: func [char /local k] [ clear wbuff either k: select/case pick kern prev char [ sum: k insert insert tail wbuff negate k wstr: to-string char ] [ sum: 0 insert tail wbuff wstr: to-string char ] sum: sum + pick widths prev: 1 + to-integer char emit-char: :emit-other ] ; emit any other char emit-other: func [char /local k] [ either k: select/case pick kern prev char [ sum: sum + k insert insert tail wbuff negate k wstr: to-string char ] [ insert tail wstr char ] sum: sum + pick widths prev: 1 + to-integer char ] emit-char: :emit-veryfirst ; handles spaces at the end of a word; they should not be ; rendered if we are at the end of the line old-spaces: [0 0 0 [""]] spaces: [0 0 0 []] emit-space: has [k] [ if all [prev k: select/case pick kern prev #" "] [ spaces/3: spaces/3 + k spaces/2: spaces/2 - k ] spaces/1: spaces/1 + 1 spaces/3: spaces/3 + pick widths prev: 33 spaces/4: buff ] ; this actually assumes #"?" is available in any font... char-rule: [char: chset (emit-char char/1 bc) | invalid (emit-char #"?" bc)] wrapper-rule: [char: wrappers (emit-char char/1 bc)] word-rule: [ [some wrapper-rule any char-rule | some char-rule] opt wrapper-rule opt [#" " (emit-space) any [#" " (if txtb/mode = 'as-is [emit-space])]] ] ; needed for justification word-chars: word-spaces: 0 line-chars: line-spaces: 0 bc: does [word-chars: word-chars + 1] bs: does [word-spaces: word-spaces + 1] reset-margin: does [ if txtb/left <> txtb/last-offset [ insert tail txtb/text reduce [txtb/left - txtb/last-offset 0 'Td] txtb/last-offset: txtb/left ] ] set 'emit-line has [lh ofs] [ if txtb/max-size = 0 [ return empty-line ] lh: any [txtb/lineheight txtb/max-size * txtb/linefactor] if txtb-vskip lh [ ;print "overflow" return false ] if lh <> txtb/last-lh [ insert insert tail txtb/text lh 'TL ] txtb/last-lh: lh insert tail txtb/text 'T* switch txtb/mode [ justify [ reset-margin ; no space should be added after the last char! line-chars: line-chars - 1 either line-spaces > 0 [ if line-chars > 0 [ insert tail txtb/text reduce [ txtb/fuel * txtb/word-spacing / line-spaces 'Tw 1 - txtb/word-spacing * txtb/fuel / line-chars 'Tc ] ] ] [ if line-chars > 0 [ insert tail txtb/text reduce [ txtb/fuel / line-chars 'Tc ] ] ] ] right [ ofs: txtb/left + txtb/fuel insert tail txtb/text reduce [ofs - txtb/last-offset 0 'Td] txtb/last-offset: ofs ] center [ ofs: txtb/left + txtb/fuel / 2 insert tail txtb/text reduce [ofs - txtb/last-offset 0 'Td] txtb/last-offset: ofs ] left [ reset-margin ] as-is [ reset-margin ] ] insert tail txtb/text txtb/linebuff txtb/fuel: txtb/bbox/3 - txtb/left - txtb/right txtb/max-size: 0 clear txtb/linebuff emit-char: :emit-veryfirst old-spaces: [0 0 0 [""]] line-spaces: line-chars: 0 insert tail txtb/linebuff reduce [buff: copy/deep [""] 'TJ] true ] ; render an empty line empty-line: does [ if txtb-vskip any [txtb/last-lh 0] [ return false ] insert tail txtb/text 'T* true ] emit-word: does [ sum: sum * txtb/font-size / 1000 old-spaces/3: old-spaces/3 * txtb/font-size / 1000 either any [txtb/no-wrap? sum + old-spaces/3 <= txtb/fuel line-chars = 0] [ ; let's render spaces we did not render before if old-spaces/2 <> 0 [ insert insert tail old-spaces/4 old-spaces/2 copy "" ] insert/dup tail last old-spaces/4 #" " old-spaces/1 line-spaces: line-spaces + old-spaces/1 + word-spaces line-chars: line-chars + old-spaces/1 + word-chars insert tail buff either integer? wbuff/1 [ wbuff ] [ insert tail last buff wbuff/1 next wbuff ] txtb/fuel: txtb/fuel - sum - old-spaces/3 ] [ emit-line spaces/4: buff if integer? wbuff/1 [wbuff: next wbuff] insert tail last buff wbuff/1 insert tail buff next wbuff txtb/fuel: txtb/fuel - sum txtb/max-size: txtb/font-size line-spaces: word-spaces line-chars: word-chars ] emit-char: :emit-first old-spaces: spaces spaces: copy [0 0 0 [""]] word-spaces: word-chars: 0 ] set 'typeset-text func [text /local wrp] [ if empty? text [exit] replace/all text newline #" " txtb/max-size: max txtb/max-size txtb/font-size set [widths kern chset] get in metrics txtb/font-name if txtb/last-font <> reduce [txtb/font-name txtb/font-size] [ used-fonts: union used-fonts reduce [txtb/font-name] insert tail txtb/linebuff reduce [to-refinement txtb/font-name txtb/font-size 'Tf] txtb/last-font/1: txtb/font-name txtb/last-font/2: txtb/font-size ] if txtb/last-color <> txtb/color [ txtb/last-color: txtb/color insert tail txtb/linebuff reduce [ c2d txtb/color/1 c2d txtb/color/2 c2d txtb/color/3 'rg ] ] either all [not empty? txtb/linebuff 'TJ = last txtb/linebuff] [ buff: pick tail txtb/linebuff -2 ] [ insert tail txtb/linebuff reduce [buff: copy/deep [""] 'TJ] ] chset: exclude make bitset! chset wrp: union wrappers: txtb/wrappers charset " ^/" invalid: exclude complement chset wrp emit-char: :emit-veryfirst spaces: copy [0 0 0 [""]] parse/all text [ opt [ #" " (emit-space) any [#" " (if txtb/mode = 'as-is [emit-space])] (old-spaces: spaces spaces: copy [0 0 0 [""]]) ] some [word-rule (emit-word) | newline (empty-line)] ] ] ] ; sets the current font; notice that the line height is set to ; size * 1.1 as a reasonable default. use-font: func [name size] [ txtb/font-name: name txtb/font-size: size ] txtb-vskip: func [amount] [ txtb/current-y-pos: txtb/current-y-pos + amount txtb/text-height: max txtb/text-height txtb/current-y-pos txtb/text-height > txtb/bbox/4 ] ; dialect rules endp: does [ if txtb/in-para? [ emit-last txtb-vskip txtb/para-skip append txtb/text compose [0 (negate txtb/para-skip) Td] txtb/in-para?: no ] ] end-para: [opt 'end ['p | 'paragraph] (endp)] set-wrappers: [ 'wrap (txtb/no-wrap?: no) opt ['on set val1 string! (txtb/wrappers: charset val1)] | 'don't 'wrap (txtb/no-wrap?: yes) ] set-margins: [opt 'with [ 'left 'margin set val1 number! (txtb/left: val1) | 'right 'margin set val1 number! (txtb/right: val1) ]] set-para: [ set val1 ['justify | 'left 'align | 'right 'align | 'center | 'as-is] ( endp txtb/mode: val1 ) any [ set-margins | opt ['with 'word] 'spacing opt 'factor set val1 number! (txtb/word-spacing: val1) ] (txtb/fuel: txtb/bbox/3 - txtb/left - txtb/right) ] font-def: ['font set val1 word! set val2 number! (use-font val1 val2)] set-lead: ['line [ 'height set val1 number! (txtb/lineheight: val1) | 'factor set val1 number! (txtb/lineheight: none txtb/linefactor: val1) ]] set-para-skip: [ 'space 'after opt 'paragraphs set val1 number! (txtb/para-skip: val1) ] draw-text: [set val1 string! ( txtb/in-para?: yes either txtb/mode = 'as-is [ val1: parse/all val1 "^/" if not empty? val1 [ typeset-text val1/1 foreach text next val1 [ emit-line typeset-text text ] ] ] [typeset-text val1] )] ; 0-255 -> 0.0-1.0 c2d: func [val] [divide any [val 0] 255] set-color: [set val1 tuple! (txtb/color: val1)] vspace: [opt ['vertical] 'space set val1 number! (txtb-vskip val1 append txtb/text reduce [0 negate val1 'Td])] emit-last: does [ either txtb/mode = 'justify [ txtb/mode: 'left append txtb/text [0 Tc 0 Tw] emit-line txtb/mode: 'justify ] [ emit-line ] ] textbox-rule: [ some [ font-def | 'newline (emit-line) | vspace | end-para | set-para | set-lead | draw-text | set-color | set-wrappers | set-para-skip ] end (emit-last) ] gfxstate-words: context [ butt: 0 round: 1 square: 2 miter: 0 bevel: 2 ] gfxstate-rule: [ 'width set val1 number! (gfx-emit [val1 'w]) | 'cap set val1 ['butt | 'round | 'square] ( gfx-emit [get in gfxstate-words val1 'J] ) | 'join set val1 ['miter | 'round | 'bevel] ( gfx-emit [get in gfxstate-words val1 'j] ) | 'miter 'limit set val1 number! (gfx-emit [val1 'M]) | 'dash [ 'solid (gfx-emit [[] 0 'd]) | set val1 into [some number!] set val2 number! (gfx-emit [val1 val2 'd]) ] ] color-rule: [opt ['color] set val1 tuple! (gfx-emit [c2d val1/1 c2d val1/2 c2d val1/3])] sc-rule: [color-rule (gfx-emit ['RG])] fc-rule: [color-rule (gfx-emit ['rg])] box-rule: [ copy val1 4 number! ( gfx-emit [val1/1 val1/2 val1/3 val1/4 're] ) ] lineopt-rule: [any [gfxstate-rule | sc-rule]] boxopt-rule: [any ['line gfxstate-rule | sc-rule]] sboxopt-rule: [any ['edge gfxstate-rule | 'edge sc-rule | fc-rule]] circle-rule: [ copy val1 3 number! ( ; approximates a circle gfx-emit [ val1/1 + val1/3 val1/2 'm val1/1 + val1/3 val1/3 * 0.552 + val1/2 val1/3 * 0.552 + val1/1 val1/2 + val1/3 val1/1 val1/2 + val1/3 'c -0.552 * val1/3 + val1/1 val1/2 + val1/3 val1/1 - val1/3 val1/3 * 0.552 + val1/2 val1/1 - val1/3 val1/2 'c val1/1 - val1/3 -0.552 * val1/3 + val1/2 -0.552 * val1/3 + val1/1 val1/2 - val1/3 val1/1 val1/2 - val1/3 'c 0.552 * val1/3 + val1/1 val1/2 - val1/3 val1/1 + val1/3 -0.552 * val1/3 + val1/2 val1/1 + val1/3 val1/2 'c 'h ] ) ] move-to: ['move opt 'to] line-to: ['line opt 'to] boxpath-rule: ['box copy val1 4 number! (gfx-emit [val1/1 val1/2 val1/3 val1/4 're])] path-rule: [some [boxpath-rule | 'circle circle-rule | shape-rule]] shape-rule: [ opt move-to copy val1 2 number! (gfx-emit [val1/1 val1/2 'm]) some [ opt line-to copy val1 2 number! (gfx-emit [val1/1 val1/2 'l]) | move-to copy val1 2 number! (gfx-emit [val1/1 val1/2 'm]) | 'bezier copy val1 6 number! (gfx-emit [val1/1 val1/2 val1/3 val1/4 val1/5 val1/6 'c]) | 'bezier 'to copy val1 4 number! (gfx-emit [val1/1 val1/2 val1/3 val1/4 'v]) | 'bezier 'from copy val1 4 number! (gfx-emit [val1/1 val1/2 val1/3 val1/4 'y]) | 'close (gfx-emit ['h]) ] ] contents-rule: [ any [ 'textbox (gfx: none insert tail new/contents txtb: make default-textbox []) opt [copy val1 4 number! (change txtb/bbox val1 txtb/fuel: val1/3 - txtb/left - txtb/right)] into textbox-rule | 'apply ( push new gfx: none insert tail new/contents new: make default-space [] ) any [ 'translation copy val1 2 number! (new/translate: val1) | 'rotation set val1 number! (new/rotate: val1) | 'scaling copy val1 2 number! (new/scale: val1) | 'skew copy val1 2 number! (new/skew: val1) ] into contents-rule (new: pop gfx: none) | 'line lineopt-rule opt [ copy val1 4 number! (gfx-emit [val1/1 val1/2 'm val1/3 val1/4 'l 'S]) ] | 'bezier lineopt-rule copy val1 8 number! ( gfx-emit [ val1/1 val1/2 'm val1/3 val1/4 val1/5 val1/6 val1/7 val1/8 'c 'S ] ) | 'box boxopt-rule box-rule (gfx-emit ['S]) | 'solid 'box sboxopt-rule box-rule (gfx-emit ['B]) | 'circle boxopt-rule circle-rule (gfx-emit ['S]) | 'solid 'circle sboxopt-rule circle-rule (gfx-emit ['B]) | 'stroke boxopt-rule into path-rule (gfx-emit ['S]) | 'fill (val2: 'f) any [fc-rule | 'even-odd (val2: 'f*)] opt [into path-rule (gfx-emit [val2])] | 'paint (val2: 'B) any [ 'edge gfxstate-rule | 'edge sc-rule | fc-rule | 'even-odd (val2: 'B*) ] into path-rule (gfx-emit [val2]) | 'clip opt 'to (val2: 'W) opt ['even-odd (val2: 'W*)] into path-rule (gfx-emit [val2 'n]) | 'image ( push new gfx: none insert tail new/contents new: make default-space [] ) opt 'at copy val1 2 number! (new/translate: val1) opt 'size copy val1 2 number! (new/scale: val1) any [ 'rotated set val1 number! (new/rotate: val1) | 'skew copy val1 2 number! (new/skew: val1) ] set val1 [image! | file! | word!] ( if word? val1 [val1: get val1] if file? val1 [val1: load val1] insert insert tail used-images val2: join "Img" length? used-images val1 gfx-emit [to-refinement val2 'Do] new: pop gfx: none ) ] ] page-rule: [ (insert tail pages new: make default-page [] gfx: none) opt ['page any [ 'size set val1 number! set val2 number! (new/size: reduce [val1 val2]) | 'rotation set val1 integer! (new/rotation: val1) | 'offset set val1 number! set val2 number! (new/offset: reduce [val1 val2]) ]] contents-rule ] ; dialect parser parse-spec: func [spec] [ parse spec [some [into page-rule]] ] ; this creates the font objects in the PDF file ; only the 14 standard PDF fonts supported currently make-fonts: has [i] [ i: 4 clear font-resources foreach font used-fonts [ insert tail font-resources reduce [to-refinement font i 0 'R] insert tail pdf-spec compose/deep [ obj (i) [ #<< /Type /Font /Subtype /Type1 /BaseFont (to-refinement font) /Encoding /WinAnsiEncoding #>> ] ] i: i + 1 ] i ] image-resources: [] used-images: [] ; this creates the Image XObjects in the PDF file make-images: func [i] [ clear image-resources foreach [name image] used-images [ insert tail image-resources reduce [to-refinement name i 0 'R] insert tail pdf-spec compose/deep [ image (i) (i + 1) (image) ] i: i + 2 ] i ] ; guess what's this? ;) mm2pt: func [mm] compose [mm * (72 / 25.4)] ; this creates the page objects make-pages: func [i /local kids mediabox stream pid] [ kids: clear [] pid: (2 * length? pages) + i foreach page pages [ insert tail kids reduce [i 0 'R] mediabox: reduce [0 0 mm2pt page/size/1 mm2pt page/size/2] stream: clear [] insert tail stream compose [(mm2pt 1) 0 0 (mm2pt 1) (mm2pt page/offset/1) (mm2pt page/offset/2) cm] foreach object page/contents [ insert tail stream object/to-pdf ] insert tail pdf-spec compose/deep [ obj (i) [ #<< /Type /Page /Parent (pid) 0 R /MediaBox [(mediabox)] /Rotate (page/rotation) /Contents (i + 1) 0 R /Resources #<< /ProcSet 3 0 R (either empty? font-resources [] [compose [/Font #<< (font-resources) #>>]]) (either empty? image-resources [] [compose [/XObject #<< (image-resources) #>>]]) #>> #>> ] stream (i + 1) [ (stream) ] ] i: i + 2 ] insert tail pdf-spec compose/deep [ obj (i) [ #<< /Type /Pages /Kids [(kids)] /Count (length? pages) #>> ] ] i + 1 ] ; MAIN FUNCTION - takes a dialect block and returns a binary set 'layout-pdf func [ "Layout a PDF file (based on the provided spec)" spec [block!] "PDF contents, see documentation for details" /local pgs ] [ clear pages clear used-fonts clear used-images clear pdf-spec parse-spec spec pgs: make-pages make-images make-fonts make-docroot pgs - 1 make-pdf pdf-spec ] ; quick hack to allow the creation of tables and so that things like ; MDP will be able to use the PDF Maker set 'precalc-textbox func [ "Precalculate a textbox, to get its vertical space" width [number!] "Width of the textbox" spec [block!] "Textbox spec" ] [ txtb: make default-textbox [ bbox/3: width fuel: width - left - right ] parse spec textbox-rule but (any [txtb/last-lh 0]) * 0.1818 + txtb/text-height txtb: none ] ]