#lang debug racket/base (require "draw.rkt" "log.rkt" xml txexpr) (provide (all-defined-out)) (module inner racket/base (provide (all-defined-out)) (struct $renderer (doc-start-func doc-end-func page-start-func page-end-func text-func set-font-func move-func return-func) #:transparent) (define-syntax-rule (check-arity PROCNAME [PROC ARITY] ...) (begin (unless (or (eq? (procedure-arity PROC) ARITY) (equal? void PROC)) (raise-argument-error PROCNAME (format "procedure of arity ~a for ~a" ARITY 'PROC) PROC)) ...)) (define (make-renderer #:doc-start-func [doc-start-func void] #:doc-end-func [doc-end-func void] #:page-start-func [page-start-func void] #:page-end-func [page-end-func void] #:text-func [text-func void] #:set-font-func [set-font-func void] #:move-func [move-func void] #:return-func [return-func void]) (check-arity 'make-renderer [doc-start-func 0] [doc-end-func 0] [page-start-func 2] [page-end-func 0] [text-func 1] [set-font-func 1] [move-func 2] [return-func 0]) ($renderer doc-start-func doc-end-func page-start-func page-end-func text-func set-font-func move-func return-func))) (require 'inner) (define null-renderer (make-renderer)) (define current-renderer (make-parameter null-renderer)) (define text-renderer ;; scan over the instructions and record where the chars want to go (let ([char-pos-table (make-hasheqv)] [current-loc 0+0i] [xmax 0] [ymax 0] [results null]) (make-renderer #:page-start-func (λ (width height) (set! xmax width) (set! ymax height)) #:page-end-func (λ () ;; fill in a character grid (define str (string-join (for/list ([y (in-range ymax)]) (list->string (map integer->char (for/list ([x (in-range xmax)]) (hash-ref char-pos-table (make-rectangular x y) (char->integer #\space)))))) "\n")) (set! results (cons str results))) #:text-func (λ (str) (hash-set! char-pos-table current-loc str)) #:move-func (λ (x y) (set! current-loc (make-rectangular x y))) #:return-func (λ () #;(unless (pair? results) (error 'text-renderer-failed)) (for-each displayln results))))) (require racket/gui) (define drr-renderer ;; scan over the instructions and record where the chars want to go (let ([targets null] [dc #f] [current-loc 0+0i] [current-font #false]) (make-renderer #:page-start-func (let ([my-face (match (get-face-list 'mono) [(? null?) (error 'no-mono-font-available)] [(cons face _) face])]) (λ (width height) (define target (make-bitmap width height)) (set! targets (cons target targets)) (set! dc (new bitmap-dc% [bitmap target])) (send dc set-font (make-font #:size 1 #:face my-face)) (send dc set-text-foreground "black"))) #:text-func (λ (charint) (when dc (send dc draw-text (string (integer->char charint)) (real-part current-loc) (imag-part current-loc)))) #:set-font-func (λ (ps) ;; racket/draw can't load arbitrary user fonts from a path ;; https://github.com/racket/racket/issues/1348 ;; TODO: font substitution? but this would require ;; holding & propagating Panose-like metadata about the font ;; but it would allow slightly more accurate rendering for contexts ;; that don't support fonts by path (log-quad2-warning (format "can't load font ~a" ps))) #:move-func (λ (x y) (set! current-loc (make-rectangular x y))) #:return-func (λ () (for-each displayln (map (λ (target) (make-object image-snip% target)) targets)))))) (define (html-renderer html-file) (let ([xmax 0] [ymax 0] [page-quads null] [current-loc 0+0i] [pages null] [fonts (make-hasheqv)] [current-font ""]) (make-renderer #:page-start-func (λ (width height) (set! page-quads null) (set! xmax width) (set! ymax height)) #:page-end-func (λ () (set! pages (cons `(div ((class "page") (style ,(format "position: relative;width:~apx;height:~apx;border:1px solid black;background:white" xmax ymax))) ,@(reverse page-quads)) pages)) (set! page-quads null)) #:text-func (λ (charint) (set! page-quads (cons `(div ((style ,(format "position: absolute;left:~apx;top:~apx;font-family:~a;font-size:~apx" (real-part current-loc) (imag-part current-loc) current-font 12))) ,(string (integer->char charint))) page-quads))) #:set-font-func (λ (ps) (set! current-font (hash-ref! fonts ps (λ () (gensym 'font))))) #:move-func (λ (x y) (set! current-loc (make-rectangular x y))) #:return-func (λ () (with-output-to-file html-file #:exists 'replace (λ () (displayln "") (display-xml/content (xexpr->xml `(html (head (style ((type "text/css")) ,(string-join (for/list ([(ps fontsym) (in-hash fonts)]) (format "@font-face { font-family: \"~a\";\nsrc: url(\"~a\");}" fontsym ps))))) (body ((style "background: #ddd")) ,@pages)))))))))) (define-syntax (cond-eq stx) (syntax-case stx (else) [(MAC ARG [SYM . BODY] ... [else ELSEBODY]) #'(cond [(eq? ARG SYM) . BODY] ... [else ELSEBODY])] [(MAC ARG CLAUSE ...) #'(MAC ARG CLAUSE ... [else (void)])])) (define (render inst-str #:using [renderer (current-renderer)]) (match-define ($renderer doc-start-func doc-end-func page-start-func page-end-func text-func set-font-func move-func return-func) renderer) (let/ec exit (for/fold ([stack null] #:result (void)) ([tok (in-port read (open-input-string inst-str))]) (cond-eq tok ['doc-start (doc-start-func) stack] ['doc-end (exit (doc-end-func)) (error 'should-never-reach)] ['page-start (match-define (list* x y tail) stack) (page-start-func x y) tail] ['page-end (page-end-func) stack] ['text (match-define (cons charint tail) stack) (text-func charint) tail] ['set-font (match-define (cons path-string tail) stack) (set-font-func (symbol->string path-string)) tail] ['move (match-define (list* x y tail) stack) (move-func x y) tail] [else (cons tok stack)]))) (return-func))