You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
typesetting/pitfall/pitfall/document.rkt

220 lines
6.8 KiB
Racket

6 years ago
#lang debug at-exp racket/base
(require
"helper.rkt"
"param.rkt"
"struct.rkt"
racket/class
racket/format
racket/contract
racket/list
sugar/unstable/class
sugar/unstable/js
sugar/unstable/dict
sugar/unstable/port
"reference.rkt"
"object.rkt"
"page.rkt"
"vector.rkt"
"color.rkt"
"fonts.rkt"
"text.rkt"
"images.rkt"
"annotations.rkt")
8 years ago
(provide PDFDocument)
8 years ago
(define mixed% (annotation-mixin (image-mixin (text-mixin (fonts-mixin (color-mixin (vector-mixin object%)))))))
8 years ago
8 years ago
(define-subclass mixed% (PDFDocument [options (mhash)])
8 years ago
(compress-streams? (hash-ref options 'compress #t))
6 years ago
(current-doc-offset 0)
8 years ago
6 years ago
(field [doc-byte-strings empty]
8 years ago
[_pageBuffer null]
8 years ago
[_offsets (mhash)] ; The PDF object store
[_ended #f]
8 years ago
[_root (ref this
(mhash 'Type "Catalog"
'Pages (ref this
(mhash 'Type "Pages"
'Count 0
'Kids empty))))] ; top object
[page #f] ; The current page
[x 0]
[y 0]
[info (mhash
8 years ago
'Producer "PITKIT"
'Creator "PITKIT"
8 years ago
'CreationDate (seconds->date (if (test-mode)
0
(current-seconds)) #f))] ; Initialize the metadata
8 years ago
[output-port #f]) ; for `pipe`
8 years ago
8 years ago
;; Initialize mixins
(· this initColor)
(· this initVector)
(· this initFonts)
(· this initText)
(· this initImages)
8 years ago
8 years ago
(as-methods
addPage
flushPages
ref
8 years ago
write
8 years ago
addContent
_refEnd
pipe
8 years ago
end)
8 years ago
8 years ago
(for ([(key val) (in-hash (hash-ref options 'info (hash)))]) ; if no 'info key, nothing will be copied from (hash)
(hash-set! info key val))
8 years ago
;; Write the header
6 years ago
(write this (format "%PDF-~a" (current-pdf-version))) ; PDF version
8 years ago
(write this (string-append "%" (list->string (map integer->char (make-list 4 #xFF))))) ; 4 binary chars, as recommended by the spec
8 years ago
8 years ago
;; Add the first page
8 years ago
(when (hash-ref options 'autoFirstPage #t) (addPage this)))
8 years ago
8 years ago
(define/contract (addPage this [options-arg (· this options)])
(() (hash?) . ->*m . object?)
;; end the current page if needed
(unless (hash-ref (· this options) 'bufferPages #f)
(send this flushPages))
;; create a page object
(set-field! page this (make-object PDFPage this options-arg))
8 years ago
(push-field! _pageBuffer this (· this page))
;; in Kids, store page dictionaries in correct order
;; this determines order in document
8 years ago
(define pages (· this _root payload Pages payload))
8 years ago
(hash-update! pages 'Kids (λ (val) (append val (list (· this page dictionary)))))
(hash-set! pages 'Count (length (hash-ref pages 'Kids)))
8 years ago
;; reset x and y coordinates
(set-field! x this (· this page margins left))
(set-field! y this (· this page margins top))
;; flip PDF coordinate system so that the origin is in
;; the top left rather than the bottom left
(set-field! _ctm this default-ctm-value)
(send this transform 1 0 0 -1 0 (· this page height))
this)
(define/contract (flushPages this)
8 years ago
(->m void?)
(define pb (· this _pageBuffer))
(for-each (λ (p) (· p end)) pb)
(set-field! _pageBuffer this empty))
8 years ago
;; every js function argument is 'undefined' by default
;; so even a function defined without default values
;; can be called without arguments
8 years ago
(define/contract (ref this [payload (mhash)])
8 years ago
(() (hash?) . ->*m . (is-a?/c PDFReference))
8 years ago
(define next-refid (add1 (length (hash-keys (· this _offsets)))))
(hash-set! (· this _offsets) next-refid 'missing-ref-offset)
8 years ago
(make-object PDFReference this next-refid payload))
8 years ago
8 years ago
(define/contract (write this x)
6 years ago
((or/c string? bytes?) . ->m . any/c)
(define bstr (if (not (bytes? x))
8 years ago
(newBuffer (string-append x "\n"))
x))
6 years ago
(push-field! doc-byte-strings this bstr)
6 years ago
(current-doc-offset (+ (current-doc-offset) (buffer-length bstr))))
8 years ago
(define/contract (addContent this data)
(any/c . ->m . object?)
(send (· this page) write data)
this)
8 years ago
(define/contract (offsets-missing? this)
(->m boolean?)
(positive? (length (filter (λ (v) (eq? 'missing-ref-offset v)) (hash-values (· this _offsets))))))
8 years ago
8 years ago
(define/contract (_refEnd this ref)
((is-a?/c PDFReference) . ->m . void?)
#;(report* (· ref id) (· this _offsets))
8 years ago
(hash-set! (· this _offsets) (· ref id) (· ref offset)))
8 years ago
(define/contract (pipe this port)
(port? . ->m . void?)
8 years ago
(set-field! output-port this port))
8 years ago
8 years ago
(define/contract (end this) ; called from source file to finish doc
(->m void?)
#;(report* 'start-end)
#;(report* (· this _offsets))
8 years ago
8 years ago
(flushPages this)
8 years ago
(define _info (ref this))
8 years ago
(for ([(key val) (in-hash (· this info))])
;; upgrade string literal to String struct
(hash-set! (· _info payload) key (if (string? val) (String val) val)))
8 years ago
#;(report* (· this _offsets))
8 years ago
(· _info end)
8 years ago
8 years ago
(for ([font (in-hash-values (· this _fontFamilies))])
(· font finalize))
8 years ago
#;(report* (· this _offsets))
8 years ago
(· this _root end)
#;(report* (· this _offsets))
8 years ago
(· this _root payload Pages end)
#;(report* (· this _offsets))
8 years ago
;; generate xref
6 years ago
(define xref-offset (current-doc-offset))
8 years ago
(with-method ([this-write (this write)])
(define sorted-offset-records (sort (hash->list (· this _offsets)) < #:key car)) ; sort by refid
(define this-offsets (map cdr sorted-offset-records))
(define this-idxs (map car sorted-offset-records))
8 years ago
(this-write "xref")
(this-write (format "0 ~a" (add1 (length this-offsets))))
(this-write "0000000000 65535 f ")
(let ([missing-offsets (for/list ([offset (in-list this-offsets)]
[idx (in-list this-idxs)]
#:unless (number? offset))
6 years ago
idx)])
(unless (empty? missing-offsets)
(raise-argument-error 'document:end "numerical offsets" missing-offsets)))
(for ([offset (in-list this-offsets)]
[idx (in-list this-idxs)])
(this-write @string-append{@(~r offset #:min-width 10 #:pad-string "0") 00000 n }))
8 years ago
(this-write "trailer") ;; trailer
(this-write (convert
(mhash 'Size (add1 (length this-offsets))
'Root (· this _root)
'Info _info)))
(this-write "startxref")
(this-write (number xref-offset))
(this-write "%%EOF"))
8 years ago
8 years ago
;; end the stream
;; in node you (@push null) which signals to the stream
;; to copy to its output port
;; here we'll do it manually
(define this-output-port (· this output-port))
(copy-port (open-input-bytes
6 years ago
(apply bytes-append (reverse (· this doc-byte-strings)))) this-output-port)
8 years ago
(close-output-port this-output-port))
8 years ago
(module+ test
(define d (new PDFDocument)))