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.
180 lines
9.9 KiB
Racket
180 lines
9.9 KiB
Racket
#lang typed/racket/base
|
|
(require racket/list math/flonum)
|
|
(require "quads-typed.rkt" "utils-typed.rkt" "wrap-typed.rkt" "measure-typed.rkt" "world-typed.rkt" "logger-typed.rkt")
|
|
|
|
(define-type Block-Type (Listof Quad))
|
|
(define-type Multicolumn-Type (Listof Block-Type))
|
|
(define-type Multipage-Type (Listof Multicolumn-Type))
|
|
|
|
(define/typed (cons-reverse xs ys)
|
|
(All (A B) ((Listof A) (Listof B) -> (Pairof (Listof A) (Listof B))))
|
|
((inst cons (Listof A) (Listof B)) ((inst reverse A) xs) ys))
|
|
|
|
(define/typed+provide (input->nested-blocks i)
|
|
(Quad . -> . (Listof Multipage-Type))
|
|
(define-values (mps mcs bs b)
|
|
(for/fold ([multipages : (Listof Multipage-Type) empty]
|
|
[multicolumns : (Listof Multicolumn-Type) empty]
|
|
[blocks : (Listof Block-Type) empty]
|
|
[block-acc : Block-Type empty])
|
|
([q (in-list (split-quad i))])
|
|
(cond
|
|
[(page-break? q) (values (cons-reverse (cons-reverse (cons-reverse block-acc blocks) multicolumns) multipages) empty empty empty)]
|
|
[(column-break? q) (values multipages (cons-reverse (cons-reverse block-acc blocks) multicolumns) empty empty)]
|
|
[(block-break? q) (values multipages multicolumns (cons-reverse block-acc blocks) empty)]
|
|
[else (values multipages multicolumns blocks (cons q block-acc))])))
|
|
(reverse (cons-reverse (cons-reverse ((inst cons-reverse Quad Block-Type) b bs) mcs) mps)))
|
|
|
|
(define/typed+provide (merge-adjacent-within q)
|
|
(Quad . -> . Quad)
|
|
(quad (quad-name q) (quad-attrs q) (join-quads (cast (quad-list q) (Listof Quad)))))
|
|
|
|
(define/typed+provide (hyphenate-quad-except-last-word q)
|
|
(Quad . -> . Quad)
|
|
(log-quad-debug "last word will not be hyphenated")
|
|
(define-values (first-quads last-quad) ((inst split-last QuadListItem) (quad-list q)))
|
|
(quad (quad-name q) (quad-attrs q) (snoc ((inst map QuadListItem QuadListItem) hyphenate-quad first-quads) last-quad)))
|
|
|
|
(define/typed+provide (average-looseness lines)
|
|
((Listof Quad) . -> . Flonum)
|
|
(if (<= (length lines) 1)
|
|
0.0
|
|
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
|
|
(round-float (/ (foldl fl+ 0.0 ((inst map Flonum Quad) (λ(line) (cast (quad-attr-ref line world:line-looseness-key 0.0) Flonum)) lines-to-measure)) (- (fl (length lines)) 1.0))))))
|
|
|
|
|
|
(define/typed+provide (log-debug-lines lines)
|
|
((Listof Quad) . -> . (Listof String))
|
|
(log-quad-debug "line report:")
|
|
(for/list : (Listof String) ([(line idx) (in-indexed lines)])
|
|
(format "~a/~a: ~v ~a" idx
|
|
(length lines)
|
|
(quad->string line)
|
|
(quad-attr-ref line world:line-looseness-key))))
|
|
|
|
|
|
(define/typed+provide (block->lines b)
|
|
(Quad . -> . (Listof Quad)) ;; todo: introduce a Quad subtype where quad-list is guaranteed to be all Quads (no strings)
|
|
(define quality (cast (quad-attr-ref/parameter b world:quality-key) Real))
|
|
(define/typed (wrap-quads qs)
|
|
((Listof Quad) . -> . (Listof Quad))
|
|
(define wrap-proc (cond
|
|
[(>= quality world:max-quality) wrap-best]
|
|
[(<= quality world:draft-quality) wrap-first]
|
|
[else wrap-adaptive]))
|
|
(wrap-proc qs))
|
|
(log-quad-debug "wrapping lines")
|
|
(log-quad-debug "quality = ~a" quality)
|
|
(log-quad-debug "looseness tolerance = ~a" world:line-looseness-tolerance)
|
|
(define wrapped-lines-without-hyphens (wrap-quads (cast (quad-list b) (Listof Quad)))) ; 100/150
|
|
(log-quad-debug* (log-debug-lines wrapped-lines-without-hyphens))
|
|
(define avg-looseness (average-looseness wrapped-lines-without-hyphens))
|
|
(define gets-hyphenation? (and world:use-hyphenation?
|
|
(fl> avg-looseness world:line-looseness-tolerance)))
|
|
(log-quad-debug "average looseness = ~a" avg-looseness)
|
|
(log-quad-debug (if gets-hyphenation? "hyphenating" "no hyphenation needed"))
|
|
|
|
(define wrapped-lines (if gets-hyphenation?
|
|
(wrap-quads (split-quad (cast ((if world:allow-hyphenated-last-word-in-paragraph
|
|
hyphenate-quad
|
|
hyphenate-quad-except-last-word) (merge-adjacent-within b)) Quad)))
|
|
wrapped-lines-without-hyphens))
|
|
|
|
(when gets-hyphenation? (log-quad-debug* (log-debug-lines wrapped-lines)))
|
|
|
|
|
|
(log-quad-debug "final looseness = ~a" (average-looseness wrapped-lines))
|
|
(map insert-spacers-in-line
|
|
(for/list : (Listof Quad) ([line-idx (in-naturals)][line (in-list wrapped-lines)])
|
|
(quad-attr-set* line 'line-idx line-idx 'lines (length wrapped-lines)))))
|
|
|
|
|
|
(define/typed+provide (number-pages ps)
|
|
((Listof Quad) . -> . (Listof Quad))
|
|
(for/list ([i (in-naturals)][p (in-list ps)])
|
|
(quad (quad-name p) (merge-attrs (quad-attrs p) `(page ,i)) (quad-list p))))
|
|
|
|
(define/typed+provide (pages->doc ps)
|
|
((Listof Quad) . -> . Quad)
|
|
;; todo: resolve xrefs and other last-minute tasks
|
|
;; todo: generalize computation of widths and heights, recursively
|
|
(define (columns-mapper page)
|
|
(quad-map (compose1 add-vert-positions (λ(xs) (quad-map (λ(x) (compute-line-height (add-horiz-positions (fill (cast x Quad))))) (cast xs Quad)))) (cast page Quad)))
|
|
(define mapped-pages (map columns-mapper (number-pages ps)))
|
|
(define doc (quads->doc mapped-pages))
|
|
doc)
|
|
|
|
(require racket/class)
|
|
(require/typed csp
|
|
[problem% (Class (init-field)
|
|
(get-solution (-> HashTableTop)))])
|
|
(define/typed+provide (lines->columns lines)
|
|
((Listof Quad) . -> . (Listof Quad)) ; (lines? . -> . columns?)
|
|
(define prob (new problem%))
|
|
#;(define max-column-lines world:default-lines-per-column)
|
|
(define-values (columns ignored-return-value)
|
|
(for/fold ([columns : (Listof Quad) empty][lines-remaining : (Listof Quad) lines])
|
|
([col-idx : Nonnegative-Integer (stop-before (in-naturals) (λ(x) (empty? lines-remaining)))])
|
|
#;(log-quad-info "making column ~a" (add1 col-idx))
|
|
;; domain constraint is best way to simplify csp, because it limits the search space.
|
|
;; search from largest possible value to smallest.
|
|
;; largest possible is the minimum of the max column lines, or
|
|
;; the number of lines left (modulo minimum page lines) ...
|
|
#;(define viable-column-range
|
|
(range (min max-column-lines (max
|
|
(length lines-remaining)
|
|
(- (length lines-remaining) world:minimum-lines-per-column)))
|
|
;; ... and the smallest possible is 1, or the current minimum lines.
|
|
;; (sub1 insures that range is inclusive of last value.)
|
|
(sub1 (min 1 world:minimum-lines-per-column)) -1))
|
|
|
|
#;(log-quad-debug "viable number of lines for this column to start =\n~a" viable-column-range)
|
|
#;(send prob add-variable "column-lines" viable-column-range)
|
|
|
|
|
|
;; greediness constraint: leave enough lines for next page, or take all
|
|
#;(define (greediness-constraint pl)
|
|
(define leftover (- (length lines-remaining) pl))
|
|
(or (= leftover 0) (>= leftover world:minimum-lines-per-column)))
|
|
#;(send prob add-constraint greediness-constraint '("column-lines"))
|
|
|
|
#;(log-quad-debug "viable number of lines after greediness constraint =\n~a" (map (λ(x) (hash-ref x "column-lines")) (send prob get-solutions)))
|
|
|
|
;; last lines constraint: don't take page that will end with too few lines of last paragraph.
|
|
#;(define (last-lines-constraint pl)
|
|
(define last-line-of-page (list-ref lines-remaining (sub1 pl)))
|
|
(define lines-in-this-paragraph (quad-attr-ref last-line-of-page world:total-lines-key))
|
|
(define line-index-of-last-line (quad-attr-ref last-line-of-page world:line-index-key))
|
|
(define (paragraph-too-short-to-meet-constraint?)
|
|
(< lines-in-this-paragraph world:min-last-lines))
|
|
(or (paragraph-too-short-to-meet-constraint?)
|
|
(>= (add1 line-index-of-last-line) world:min-last-lines)))
|
|
#;(send prob add-constraint last-lines-constraint '("column-lines"))
|
|
|
|
#;(log-quad-debug "viable number of lines after last-lines constraint =\n~a" (map (λ(x) (hash-ref x "column-lines")) (send prob get-solutions)))
|
|
|
|
;; first lines constraint: don't take page that will leave too few lines at top of next page
|
|
#;(define (first-lines-constraint pl lines-remaining)
|
|
(define last-line-of-page (list-ref lines-remaining (sub1 pl)))
|
|
(define lines-in-this-paragraph (quad-attr-ref last-line-of-page world:total-lines-key))
|
|
(define line-index-of-last-line (quad-attr-ref last-line-of-page world:line-index-key))
|
|
(define lines-that-will-remain (- lines-in-this-paragraph (add1 line-index-of-last-line)))
|
|
(define (paragraph-too-short-to-meet-constraint?)
|
|
(< lines-in-this-paragraph world:min-first-lines))
|
|
(or (paragraph-too-short-to-meet-constraint?)
|
|
(= 0 lines-that-will-remain) ; ok to use all lines ...
|
|
(>= lines-that-will-remain world:min-first-lines))) ; but if any remain, must be minimum number.
|
|
#;(send prob add-constraint (λ(x) (first-lines-constraint x lines-remaining)) '("column-lines"))
|
|
|
|
#;(log-quad-debug "viable number of lines after first-lines constraint =\n~a" (map (λ(x) (hash-ref x "column-lines")) (send prob get-solutions)))
|
|
|
|
|
|
(define s (send prob get-solution))
|
|
(define how-many-lines-to-take (cast (hash-ref s "column-lines") Positive-Index))
|
|
(define-values (lines-to-take lines-to-leave) (split-at lines-remaining how-many-lines-to-take))
|
|
#;(log-quad-debug "taking ~a lines for column ~a:" how-many-lines-to-take (add1 col-idx))
|
|
#;(map (λ(idx line) (log-quad-debug "~a:~a ~v" (add1 col-idx) (add1 idx) (quad->string line))) (range how-many-lines-to-take) lines-to-take)
|
|
#;(send prob reset)
|
|
(values (cons (quad-attr-set (quads->column lines-to-take) world:column-index-key col-idx) columns) lines-to-leave)))
|
|
(reverse columns))
|