happy birthday

main
Matthew Butterick 10 years ago
parent 86bc94ef6c
commit 46c49c7bbc

@ -0,0 +1,28 @@
#lang racket
(require hyphenate "quads.rkt" "world.rkt" "render.rkt" "main.rkt" "utils.rkt")
(define (make-test-blocks string)
(let ([string string])
(add-between (list
(block '(quality 100 x-align justify) string)
; (block '(quality 100 x-align justify) string)
) (block-break))))
(define test-block (block '(font "Equity Text B" measure 265 leading 8 size 10 x-align-last-line left) (apply block #f
(make-test-blocks (hyphenate "“This is a PDF generated from my typesetting system, which is called Quad. Im writing this in a source file in DrRacket. When I click “Run”, a PDF pops out. Not bad — and no LaTeX needed. Quad, however, does use the fancy linebreaking algorithm developed for TeX. (It also includes a faster linebreaking algorithm for when speed is more important than quality.) This tutorial provides a brief introduction to the Racket programming language by using one of its picture-drawing libraries. Even if you dont intend to use Racket for your artistic endeavours, the picture library supports interesting and enlightening examples. After all, a picture is worth five hundred “hello world”s.")))))
;(block-break) (block #f (~a (random)))))
(define ti5 (block '(font "Equity Text B" x-align justify x-align-last-line left) (block #f (block '(x-align-last-line center weight bold font "Equity Caps B") "Greetings" (block-break) "Matthew & Robby!") (block-break) (block #f " This is a PDF generated from my Racket typesetting language, which is called " (word '(style italic) "Quad.") " Im writing this in a source file in DrRacket. When I click “Run”, a PDF pops out. Not bad — and no LaTeX needed." (block-break) (box '(width 15)) (word '(font "Concourse T2") "Quad takes some of the good ideas from TeX, like its fancy algorithm for breaking paragraphs into lines. Though respectfully to Prof. Knuth, I believe Ive even improved it.") (block-break) (box '(width 15)) "Of course, Quad can also handle " (word '(font "Avenir") "different fonts,") (word '(style italic) " styles, ") (word '(size 14 weight bold) "and sizes") " within the same line. As you can see, it can also justify paragraphs. (This sample also uses the new fractional point sizes.)" (block-break) (box '(width 15)) "Truly, the combination of Lisp and typesetting is an unprecedented confluence of geekery. I hope that Quad can become a useful part of the Racket ecosystem." (block-break) (word '(x-align-last-line center weight bold font "Equity Caps B") "thank you for your help" (block-break) "mb" )))))
(define ti6 (block #f (apply block '(column-count 3 column-gutter 15 measure 170) (add-between (map (λ(r) (quad-attr-set* ti5 'size r 'leading (* 1.25 r))) (range 8.5 13 .5)) (column-break)))
(page-break)
(apply block '(column-count 2 column-gutter 25 measure 240) (add-between (map (λ(r) (quad-attr-set* ti5 'size r 'leading (* 1.25 r))) (range 10 14 .5)) (column-break)))
(page-break)
(apply block '(column-count 1 column-gutter 15 measure 360) (add-between (map (λ(r) (quad-attr-set* ti5 'size r 'leading (* 1.25 r))) (range 15 18)) (column-break)))))
(parameterize ([world:quality-default world:max-quality]
[world:paper-width-default 792]
[world:paper-height-default 612])
(send (new pdf-renderer%) render-to-file (time (typeset ti6)) "foo.pdf")
)

@ -0,0 +1,75 @@
#lang racket/base
(require ffi/unsafe
ffi/unsafe/define
ffi/unsafe/alloc
setup/dirs
racket/draw/private/libs
racket/draw/private/utils)
(define-runtime-lib fontconfig-lib
[(unix) (ffi-lib "libfontconfig" '("1" ""))]
[(macosx)
(ffi-lib "libpng16.16.dylib")
(ffi-lib "libexpat.1.dylib")
(ffi-lib "libfreetype.6.dylib")
(ffi-lib "libfontconfig.1.dylib")]
[(windows)
(ffi-lib "zlib1.dll")
(ffi-lib "libintl-8.dll")
(ffi-lib "libpng16-16.dll")
(ffi-lib "libexpat-1.dll")
(ffi-lib "libfreetype-6.dll")
(ffi-lib "libfontconfig-1.dll")])
(define-syntax-rule (_pfun spec ...)
(_fun #:in-original-place? #t spec ...))
(define-ffi-definer define-fc fontconfig-lib
#:provide provide)
;; datatype information from
;; http://www.freedesktop.org/software/fontconfig/fontconfig-devel/x31.html
(define FcConfig (_cpointer 'FcConfig))
(define FcPattern (_cpointer 'FcPattern))
(define FcPattern-pointer (_cpointer FcPattern))
(define FcObjectSet (_cpointer 'FcObjectSet))
(define FcBool _bool)
(define FcChar8 _bytes)
(define-cstruct _FcFontSet
([nfont _int]
[sfont _int]
[fonts FcPattern-pointer])) ;; ?? spec says "FcPattern **fonts" but I don't know how this translates to ffi
;; function information from
;; http://www.freedesktop.org/software/fontconfig/fontconfig-devel/x102.html
(define-fc FcGetVersion (_pfun -> _int))
(define-fc FcConfigCreate (_pfun -> FcConfig))
(define-fc FcInitLoadConfig (_pfun -> FcConfig))
(define-fc FcConfigAppFontAddFile (_pfun FcConfig FcChar8 -> FcBool))
(define-fc FcConfigHome (_pfun -> FcChar8))
(define-fc FcConfigGetSysRoot(_pfun FcConfig -> FcChar8))
(define-fc FcFontList(_pfun FcConfig FcPattern FcObjectSet -> _FcFontSet))
(define-fc FcPatternCreate (_pfun -> FcPattern))
(define-fc FcFontSetCreate (_pfun -> _FcFontSet))
(define-fc FcObjectSetCreate (_pfun -> FcObjectSet))
(define-fc FcPatternPrint (_pfun FcPattern -> _void))
(define-fc FcPatternEqual (_pfun FcPattern FcPattern -> FcBool))
;; attempting to replicate font-loading workaround shown at
;; https://bugzilla.gnome.org/show_bug.cgi?id=347237#c25
(define cfg (FcConfigCreate)) ; workaround step 1
(define path (string->bytes/utf-8 "/Users/MB/Desktop/reporter.otf"))
(FcConfigAppFontAddFile cfg path) ; workaround step 2
(define fcp (FcPatternCreate))
(define fcos (FcObjectSetCreate))
(define fs (FcFontList cfg fcp fcos))
(define pat (FcFontSet-fonts fs))
;; this crashes DrRacket, prob because I have mangled the _FcFontSet definition
;; (FcPatternPrint pat)

@ -0,0 +1,357 @@
#lang racket/base
(require ffi/unsafe
ffi/unsafe/define
ffi/unsafe/alloc
racket/draw/unsafe/glib
racket/draw/unsafe/cairo
racket/draw/private/utils
racket/draw/private/libs)
(define-runtime-lib pango-lib
[(unix) (ffi-lib "libpango-1.0" '("0" ""))]
[(macosx) (ffi-lib "libpango-1.0.0.dylib")]
[(windows) (ffi-lib "libpango-1.0-0.dll")])
(define-runtime-lib pangowin32-lib
[(unix) #f]
[(macosx)]
[(windows)
(ffi-lib "libpangowin32-1.0-0.dll")])
(define-runtime-lib pangocairo-lib
[(unix) (ffi-lib "libpangocairo-1.0" '("0" ""))]
[(macosx)
(ffi-lib "libharfbuzz.0.dylib")
(ffi-lib "libpangoft2-1.0.0.dylib")
(ffi-lib "libpangocairo-1.0.0.dylib")]
[(windows)
(ffi-lib "libintl-8.dll")
(ffi-lib "libpangowin32-1.0-0.dll")
(ffi-lib "libexpat-1.dll")
(ffi-lib "libfreetype-6.dll")
(ffi-lib "libfontconfig-1.dll")
(ffi-lib "libharfbuzz-0.dll")
(ffi-lib "libpangoft2-1.0-0.dll")
(ffi-lib "libpangocairo-1.0-0.dll")])
(define-ffi-definer define-pango pango-lib
#:provide provide)
(define-ffi-definer define-pangocairo pangocairo-lib
#:provide provide)
(define-ffi-definer define-pangowin32 pangowin32-lib
#:provide provide)
;; Pango's Core Text back-end can somehow go wrong if we're going to eventually
;; use AppKit but don't load AppKit it before using functions such as
;; `pango_cairo_font_map_get_default'. So, force AppKit now for the platform
;; where the Core Text back-end is used:
(when (equal? "x86_64-macosx/3m"
(path->string (system-library-subpath)))
(void (ffi-lib (format "/System/Library/Frameworks/AppKit.framework/AppKit"))))
;; ALLOCATION NOTE: since Pango calls into Cairo, it has the same
;; allocation constraints on arguments as Cairo functions; see
;; "cairo.rkt".
(define PangoContext (_cpointer 'PangoContext))
(define PangoLayout (_cpointer 'PangoLayout))
(define PangoFontDescription (_cpointer 'PangoFontDescription))
(define PangoFontFamily (_cpointer 'PangoFontFamily))
(define PangoFontFace (_cpointer 'PangoFontFace))
(define PangoFont (_cpointer 'PangoFont))
(define PangoFontMap (_cpointer 'PangoFontMap))
(define PangoFontMetrics (_cpointer 'PangoFontMetrics))
(define PangoAttrList (_cpointer 'PangoAttrList))
(define PangoAttribute (_cpointer 'PangoAttribute))
(define PangoLanguage (_cpointer 'PangoLanguage))
(define PangoCoverage (_cpointer 'PangoCoverage))
(define PangoLayoutIter (_cpointer 'PangoLayoutIter))
(define PangoLayoutLine (_cpointer 'PangoLayoutLine))
(define-cstruct _PangoRectangle ([x _int]
[y _int]
[width _int]
[height _int])
#:malloc-mode 'atomic-interior)
(provide make-PangoRectangle
PangoRectangle-x
PangoRectangle-y
PangoRectangle-width
PangoRectangle-height)
(define-cstruct _PangoItem
([offset _int]
[length _int]
[num_chars _int]
;; Inline PangoAnalysis:
[shape_engine _pointer]
[lang_engine _pointer]
[font (_or-null PangoFont)]
[level _uint8]
[gravity _uint8]
[flags _uint8]
[script _uint8]
[language _pointer]
[extra_attrs _pointer]))
(provide (struct-out PangoItem)
_PangoItem _PangoItem-pointer)
(define-cstruct _PangoGlyphInfo
([glyph _uint32]
[width _uint32]
[dx _uint32]
[dy _uint32]
[is_cluster_start _uint]))
(provide (struct-out PangoGlyphInfo)
_PangoGlyphInfo _PangoGlyphInfo-pointer)
(define-cstruct _PangoGlyphString
([num_glyphs _int]
[glyphs _pointer]
[log_clusters _pointer])
#:malloc-mode 'atomic-interior)
(provide (struct-out PangoGlyphString)
_PangoGlyphString)
(define-cstruct _PangoGlyphItem ([item _PangoItem-pointer]
[glyphs _PangoGlyphString-pointer]))
(provide (struct-out PangoGlyphItem))
;; As of Pango 1.28, Pango is not thread-safe at the C level, which
;; means that it isn't place-safe in Racket. Also, for some reason,
;; when parts of Pango are initialized in a non-main place under
;; Windows, then font operations start to fail when that place exits.
;; Run all Pango calls in the original place, which synchronizes them
;; and avoids Windows problems.
(define-syntax-rule (_pfun spec ...)
(_fun #:in-original-place? #t spec ...))
(provide g_object_unref g_free)
(define-gobj g_object_unref (_pfun _pointer -> _void)
#:wrap (deallocator))
(define-glib g_free (_pfun _pointer -> _void)
#:wrap (deallocator))
;; For working around a Win32 Pango bug (see `unref-font-map'):
(define _GQueue (_cpointer 'GQueue))
(define-cstruct _PangoWin32FontMap ([type-instance _pointer]
[ref_count _uint]
[qdata _pointer]
[font_cache _pointer]
[freed_fonts _GQueue]))
(define-glib g_queue_foreach (_pfun _GQueue _fpointer #;(_fun _pointer -> _void) _pointer -> _void))
(define-glib g_queue_free (_pfun _GQueue -> _void))
(define-glib g_queue_new (_pfun -> _GQueue))
(define-gobj raw_g_object_unref _fpointer #:c-id g_object_unref)
(define (unref-font-map v)
(when (eq? (system-type) 'windows)
;; For version 1.28 of Pango, reported as Bug 649293:
;; Under Windows, PangoWin32FontMap holds a queue of freed
;; fonts, and the fonts hold a weak link back to the map.
;; Unreffing the font map drops the weak links and *then*
;; tries to release the freed fonts, which leads to failures
;; releasing the fonts. Work around the bug by manually
;; flushing the queue of freed fonts before the font map is
;; unreffed.
(let ([fm (cast v _pointer _PangoWin32FontMap-pointer)])
(g_queue_foreach (PangoWin32FontMap-freed_fonts fm) raw_g_object_unref #f)
(g_queue_free (PangoWin32FontMap-freed_fonts fm))
(set-PangoWin32FontMap-freed_fonts! fm (g_queue_new))))
(g_object_unref v))
(define-pangocairo pango_cairo_font_map_get_default (_pfun -> PangoFontMap)) ;; not an allocator
(define-pangocairo pango_cairo_font_map_new (_pfun -> PangoFontMap)
#:wrap (allocator unref-font-map))
(define-pango pango_context_new (_pfun -> PangoContext)
#:wrap (allocator g_object_unref))
;; pango_font_map_create_context() is in 1.22 and later
(provide pango_font_map_create_context)
(define (pango_font_map_create_context fm)
(let ([c (pango_context_new)])
(pango_context_set_font_map c fm)
c))
(define-pangocairo pango_cairo_update_context (_pfun _cairo_t PangoContext -> _void))
;; The convenince function pango_cairo_create_context() is in 1.22 and later
(provide pango_cairo_create_context)
(define (pango_cairo_create_context cr)
(let ([ctx (pango_font_map_create_context
(pango_cairo_font_map_get_default))])
(pango_cairo_update_context cr ctx)
ctx))
(define-pangocairo pango_cairo_create_layout (_pfun _cairo_t -> PangoLayout)
#:wrap (allocator g_object_unref))
(define-pangocairo pango_cairo_update_layout (_pfun _cairo_t PangoLayout -> _void))
(define-pango pango_layout_set_text (_pfun PangoLayout [s : _string] [_int = -1] -> _void))
(define-pangocairo pango_cairo_show_layout (_pfun _cairo_t PangoLayout -> _void))
(define-pangocairo pango_cairo_show_layout_line (_pfun _cairo_t PangoLayoutLine -> _void))
(define-pangocairo pango_cairo_show_glyph_string (_pfun _cairo_t PangoFont _PangoGlyphString-pointer -> _void))
(define-pangocairo pango_cairo_layout_line_path (_pfun _cairo_t PangoLayoutLine -> _void))
(define-pango pango_layout_iter_free (_pfun PangoLayoutIter -> _void)
#:wrap (deallocator))
(define-pango pango_layout_get_iter (_pfun PangoLayout -> PangoLayoutIter)
#:wrap (allocator pango_layout_iter_free))
(define-pango pango_layout_iter_get_baseline (_pfun PangoLayoutIter -> _int))
(define-pango pango_layout_iter_next_run (_pfun PangoLayoutIter -> _bool))
(define-pango pango_layout_iter_get_run (_pfun PangoLayoutIter -> (_or-null _PangoGlyphItem-pointer)))
(define-pango pango_layout_iter_get_run_readonly (_pfun PangoLayoutIter -> (_or-null _PangoGlyphItem-pointer))
#:fail (lambda () pango_layout_iter_get_run))
(define-pango pango_layout_get_line (_pfun PangoLayout _int -> PangoLayoutLine))
(define-pango pango_layout_get_line_readonly (_pfun PangoLayout _int -> PangoLayoutLine)
#:fail (lambda () pango_layout_get_line))
(define-pango pango_layout_get_context (_pfun PangoLayout -> PangoContext)) ;; not an allocator
;(define-pango pango_layout_get_extents (_pfun PangoLayout _pointer _PangoRectangle-pointer -> _void))
(define-pango pango_layout_get_baseline (_pfun PangoLayout -> _int)
;; The convenince function pango_layout_get_baseline() is in 1.22 and later
#:fail (lambda ()
(lambda (layout)
(let ([iter (pango_layout_get_iter layout)])
(begin0
(pango_layout_iter_get_baseline iter)
(pango_layout_iter_free iter))))))
(define-pango pango_layout_get_spacing (_pfun PangoLayout -> _int))
(define-pango pango_layout_new (_pfun PangoContext -> PangoLayout)
#:wrap (allocator g_object_unref))
(define-pangocairo pango_cairo_context_get_font_options (_pfun PangoContext -> _cairo_font_options_t)) ;; not an allocator
(define-pangocairo pango_cairo_context_set_font_options (_pfun PangoContext _cairo_font_options_t -> _void)) ;; makes a copy
(define-pango pango_layout_set_font_description (_pfun PangoLayout PangoFontDescription -> _void)) ;; makes a copy
(define-pango pango_context_get_font_map (_pfun PangoContext -> PangoFontMap)) ;; not an allocator
(define-pango pango_context_set_font_map (_pfun PangoContext PangoFontMap -> _void))
(define-pango pango_font_family_get_name (_pfun PangoFontFamily -> _string)) ;; not an allocator
(define-pango pango_font_family_is_monospace (_pfun PangoFontFamily -> _bool))
(define-pango pango_language_get_default (_pfun -> PangoLanguage)
;; not available before 1.16
#:fail (lambda () (lambda () #f)))
(define-pango pango_font_map_load_font (_pfun PangoFontMap PangoContext PangoFontDescription -> (_or-null PangoFont)))
(define-pango pango_coverage_unref (_pfun PangoCoverage -> _void)
#:wrap (deallocator))
(define-pango pango_font_get_coverage (_pfun PangoFont PangoLanguage -> PangoCoverage)
#:wrap (allocator pango_coverage_unref))
(define-pango pango_coverage_get (_pfun PangoCoverage _int -> _int))
(define-pango pango_font_metrics_unref (_pfun PangoFontMetrics -> _void)
#:wrap (deallocator))
(define-pango pango_font_get_metrics (_pfun PangoFont (_or-null PangoLanguage) -> PangoFontMetrics)
#:wrap (allocator pango_font_metrics_unref))
(define-pango pango_font_metrics_get_approximate_char_width (_pfun PangoFontMetrics -> _int))
(define-pango pango_font_metrics_get_ascent (_pfun PangoFontMetrics -> _int))
(define-pango pango_font_metrics_get_descent (_pfun PangoFontMetrics -> _int))
(define-pango pango_layout_get_unknown_glyphs_count (_pfun PangoLayout -> _int)
;; not available in old versions:
#:fail (lambda () (lambda (lo) 0)))
(define-pango pango_attr_list_unref (_pfun PangoAttrList -> _void)
#:wrap (deallocator))
(define-pango pango_attr_list_new (_pfun -> PangoAttrList)
#:wrap (allocator pango_attr_list_unref))
(define-pango pango_attr_list_insert (_pfun PangoAttrList PangoAttribute -> _void)
;; takes ownership of the attribute
#:wrap (deallocator cadr))
(define-pango pango_attribute_destroy (_pfun PangoAttribute -> _void)
#:wrap (deallocator))
(define-pango pango_attr_underline_new (_pfun _int -> PangoAttribute)
#:wrap (allocator pango_attribute_destroy))
(define-pango pango_attr_fallback_new (_pfun _bool -> PangoAttribute)
#:wrap (allocator pango_attribute_destroy))
(define-pango pango_layout_set_attributes (_pfun PangoLayout PangoAttrList -> _void))
(define-pango pango_font_map_list_families (_pfun PangoFontMap
(fams : (_ptr o _pointer))
(len : (_ptr o _int))
-> _void
-> (begin0
(for/list ([i (in-range len)])
(ptr-ref fams PangoFontFamily i))
(g_free fams))))
(define-pango pango_font_family_list_faces (_pfun PangoFontFamily
(faces : (_ptr o _pointer))
(len : (_ptr o _int))
-> _void
-> (begin0
(for/list ([i (in-range len)])
(ptr-ref faces PangoFontFace i))
(g_free faces))))
(define-pango pango_font_face_get_face_name (_pfun PangoFontFace -> _string))
(define-pango pango_font_description_free (_pfun PangoFontDescription -> _void)
#:wrap (deallocator))
(define-pango pango_font_description_new (_pfun -> PangoFontDescription)
#:wrap (allocator pango_font_description_free))
(define-pango pango_font_description_from_string (_pfun _string -> PangoFontDescription)
#:wrap (allocator pango_font_description_free))
(define-pango pango_font_description_set_family (_pfun PangoFontDescription _string -> _void))
(define-pango pango_font_description_set_style (_pfun PangoFontDescription _int -> _void))
(define-pango pango_font_description_set_weight (_pfun PangoFontDescription _int -> _void))
(define-pango pango_font_description_set_size (_pfun PangoFontDescription _int -> _void))
(define-pango pango_font_description_set_absolute_size (_pfun PangoFontDescription _double* -> _void))
(define-pango pango_font_description_get_family (_pfun PangoFontDescription -> _string))
(define _PangoWin32FontCache (_cpointer 'PangoWin32FontCache))
(define _HFONT (_cpointer 'HFONT))
(define _LOGFONT-pointer _pointer)
(define-pangowin32 pango_win32_font_map_for_display (_pfun -> PangoFontMap)
#:make-fail make-not-available)
(define-pangowin32 pango_win32_font_logfont (_pfun PangoFont -> _LOGFONT-pointer)
#:make-fail make-not-available
#:wrap (allocator g_free))
(define-pangowin32 pango_win32_font_description_from_logfont (_pfun _LOGFONT-pointer -> PangoFontDescription)
#:make-fail make-not-available
#:wrap (allocator pango_font_description_free))
(define-pangowin32 pango_win32_font_cache_unload (_pfun _PangoWin32FontCache _HFONT -> _void)
#:make-fail make-not-available)
(define-pangowin32 pango_win32_font_cache_load (_pfun _PangoWin32FontCache _LOGFONT-pointer -> _HFONT)
#:make-fail make-not-available)
(define-pangowin32 pango_win32_font_cache_new (_pfun -> _PangoWin32FontCache)
#:make-fail make-not-available)
(define-enum
0
PANGO_STYLE_NORMAL
PANGO_STYLE_OBLIQUE
PANGO_STYLE_ITALIC)
(define-enum
0
PANGO_UNDERLINE_NONE
PANGO_UNDERLINE_SINGLE
PANGO_UNDERLINE_DOUBLE
PANGO_UNDERLINE_LOW
PANGO_UNDERLINE_ERROR)
(define/provide PANGO_WEIGHT_LIGHT 300)
(define/provide PANGO_WEIGHT_MEDIUM 500)
(define/provide PANGO_WEIGHT_BOLD 700)
(define/provide PANGO_SCALE 1024)
(define-pango pango_layout_set_width (_pfun PangoLayout _int -> _void))
(define-pango pango_layout_get_width (_pfun PangoLayout -> _int))
(define-pango pango_layout_get_character_count (_pfun PangoLayout -> _int))
(define-pango pango_layout_copy (_pfun PangoLayout -> PangoLayout))
(define-pango pango_layout_get_serial (_pfun PangoLayout -> _uint))
(define-pango pango_layout_get_text (_pfun PangoLayout -> _string/utf-8))
(define-pango pango_layout_get_attributes (_pfun PangoLayout -> PangoAttrList))
(define-pango pango_layout_get_font_description (_pfun PangoLayout -> PangoFontDescription))
(define-pango pango_font_description_get_size (_pfun PangoFontDescription -> _int))
(define-pango pango_layout_get_extents (_pfun PangoLayout _PangoRectangle-pointer _PangoRectangle-pointer -> _void))
(define-pango pango_layout_get_pixel_extents (_pfun PangoLayout _PangoRectangle-pointer _PangoRectangle-pointer -> _void))

File diff suppressed because one or more lines are too long

@ -0,0 +1,77 @@
#lang racket/base
(require (for-syntax racket/base racket/syntax) sugar/coerce racket/contract)
(require "world.rkt" racket/match sugar/debug racket/date racket/list)
(module+ test (require rackunit))
(provide (all-defined-out))
(define-syntax-rule (define-orphan-logger name)
(begin
(define remember-cl (current-logger))
(define dummy-cl (make-logger))
(current-logger dummy-cl)
(define-logger name)
(current-logger remember-cl)))
(define levels '(none fatal error warning info debug))
(define/contract (log-level> lev1 lev2)
(symbol? symbol? . -> . coerce/boolean?)
(member lev1 (cdr (member lev2 levels))))
(define/contract (log-level>= lev1 lev2)
(symbol? symbol? . -> . coerce/boolean?)
(member lev1 (member lev2 levels)))
(define (log-level< lev1 lev2)
(log-level> lev2 lev1))
(define (log-level<= lev1 lev2)
(log-level>= lev2 lev1))
(module+ test
(check-true (log-level< 'none 'error))
(check-true (log-level<= 'none 'none))
(check-false (log-level< 'none 'none))
(check-true (log-level> 'warning 'error))
(check-true (log-level>= 'debug 'debug))
(check-false (log-level> 'fatal 'debug)))
(define-logger quad)
(define-syntax-rule (activate-logger logger)
(begin
(define logger-receiver (make-log-receiver logger (world:logging-level)))
(define log-file (build-path (current-directory) (format "~a.txt" 'logger)))
(with-output-to-file log-file #:exists 'truncate void)
(void (thread
(λ ()
(let loop ()
(match (sync logger-receiver)
[(vector event-level event-message event-value name)
(define msg (format "[~a] ~a\n" event-level event-message))
; (eprintf msg)
(flush-output)
(with-output-to-file log-file #:exists 'append (λ () (display msg)))])
(loop))))
(log-quad-info "started at ~a" (date->string (current-date) #t)))))
(define-syntax-rule (log-quad-debug-report x)
(begin
(log-quad-debug "~a = ~a" 'x x)
x))
(define-syntax-rule (log-quad-debug* xs)
(when (equal? (world:logging-level) 'debug)
(map (λ(x) (log-quad-debug x)) xs)))
(module+ main
(activate-logger quad-logger)
(log-quad-fatal "Exterminate! Exterminate!")
(log-quad-error "~a" (time (apply + (range 1000))))
(log-quad-debug "What's the red button for?"))

@ -0,0 +1,225 @@
#lang racket/base
(require racket/list sugar racket/contract racket/function math/flonum)
(require "quads.rkt" "utils.rkt" "wrap.rkt" "measure.rkt" "world.rkt" "logger.rkt")
(provide typeset)
(define+provide/contract (input->multipages i)
(input? . -> . multipages?)
(define exploded-input (split-quad i))
(map quads->multipage (split-on-page-breaks exploded-input)))
(define/contract (multipage->multicolumns mp)
(multipage? . -> . multicolumns?)
(map quads->multicolumn (split-on-column-breaks (quad-list mp))))
(define+provide/contract (multicolumn->blocks mc)
(multicolumn? . -> . blocks?)
;; segfault happens in next line
(map quads->block (split-on-block-breaks (quad-list mc))))
(define+provide/contract (merge-adjacent-within q)
(quad? . -> . quad?)
(quad (quad-name q) (quad-attrs q) (join-quads (quad-list q))))
(define (hyphenate-quad-except-last-word q)
(log-quad-debug "last word will not be hyphenated")
(define-values (first-quads last-quad) (split-last (quad-list q)))
(quad (quad-name q) (quad-attrs q) (snoc (map hyphenate-quad first-quads) last-quad)))
(define+provide/contract (average-looseness lines)
(lines? . -> . flonum?)
(if (<= (length lines) 1)
0.0
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
(round-float (fl/ (fold-fl+ (map (λ(line) (quad-attr-ref line world:line-looseness-key 0.0)) lines-to-measure)) (fl- (fl (length lines)) 1.0))))))
(define (log-debug-lines lines)
(log-quad-debug "line report:")
(for/list ([(line idx) (in-indexed lines)])
(format "~a/~a: ~v ~a" idx
(length lines)
(quad->string line)
(quad-attr-ref line world:line-looseness-key))))
(require racket/trace)
(define+provide/contract (block->lines b-in)
(block? . -> . lines?)
(define b (if (ormap string? (quad-list b-in))
(quads->block (split-quad b-in))
b-in))
(define quality (quad-attr-ref/parameter b world:quality-key))
(define (wrap-quads qs)
(define wrap-proc (if (> quality world:draft-quality) wrap-best wrap-first))
(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 (quad-list b))) ; 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 ((if world:allow-hyphenated-last-word-in-paragraph
hyphenate-quad
hyphenate-quad-except-last-word) (merge-adjacent-within b))))
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 ([line-idx (in-naturals)][line (in-list wrapped-lines)])
(quad-attr-set* line 'line-idx line-idx 'lines (length wrapped-lines)))))
(define+provide/contract (number-pages ps)
(pages? . -> . pages?)
(for/list ([i (in-naturals)][p (in-list ps)])
(quad (quad-name p) (merge-attrs (quad-attrs p) `(page ,i)) (quad-list p))))
(define+provide/contract (pages->doc ps)
(pages? . -> . doc?)
(map quad-attrs (quad-list (first ps)))
;; 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 (curry quad-map (compose1 compute-line-height add-horiz-positions fill))) page))
(define mapped-pages (map columns-mapper (number-pages ps)))
(define doc (quads->doc mapped-pages))
doc)
(require racket/class csp)
(define+provide/contract (lines->columns lines)
(lines? . -> . columns?)
(define prob (new problem%))
(define max-column-lines world:default-lines-per-column)
(define-values (columns ignored-return-value)
(for/fold ([columns null][lines-remaining lines])([col-idx (in-naturals)] #:break (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 (curryr hash-ref "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 (curryr hash-ref "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 (curryr first-lines-constraint lines-remaining) '("column-lines"))
(log-quad-debug "viable number of lines after first-lines constraint =\n~a" (map (curryr hash-ref "column-lines") (send prob get-solutions)))
(define s (send prob get-solution))
(define how-many-lines-to-take (hash-ref s "column-lines"))
(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))
(define/contract (columns->pages cols)
(columns? . -> . pages?)
(define columns-per-page (quad-attr-ref/parameter (car cols) world:column-count-key))
(define column-gutter (quad-attr-ref/parameter (car cols) world:column-gutter-key))
;; don't use default value here. If the col doesn't have a measure key,
;; it deserves to be an error, because that means the line was composed incorrectly.
(when (not (quad-has-attr? (car cols) world:measure-key))
(error 'columns->pages "column attrs contain no measure key: ~a ~a" (quad-attrs (car cols)) (quad-car (car cols))))
(define column-width (quad-attr-ref (car cols) world:measure-key))
(define width-of-printed-area (+ (* columns-per-page column-width) (* (sub1 columns-per-page) column-gutter)))
(define result-pages
(map (λ(cols) (quads->page cols))
(for/list ([page-cols (in-list (slice-at cols columns-per-page))])
(define-values (last-x cols)
(for/fold ([current-x (/ (- (world:paper-width-default) width-of-printed-area) 2)][cols empty]) ([col (in-list page-cols)][idx (in-naturals)])
(values (+ current-x column-width column-gutter) (cons (quad-attr-set* col 'x current-x 'y 40 world:column-index-key idx) cols))))
(reverse cols))))
result-pages)
(define/contract (typeset x)
(coerce/input? . -> . doc?)
(cond
[(input? x) (load-text-cache-file)
(define multipages (input->multipages x)) ; 170
(define pages (append-map typeset multipages)) ; 2370
(define doc (typeset pages)) ; 370
(update-text-cache-file)
doc]
[(multipage? x) (define multicolumns (multipage->multicolumns x)) ; 77
(define columns (append-map typeset multicolumns)) ; 2420
(define pages (typeset columns)) ; 0
pages]
[(multicolumn? x) (define blocks (multicolumn->blocks x)) ; 85
(define lines (append-map typeset blocks)) ; 2422
(define columns (typeset lines)) ; 4
columns]
[(lines? x) (map typeset (lines->columns x))] ; 10
[(pages? x) (typeset (pages->doc x))] ; 370
[(columns? x) (map typeset (columns->pages x))] ; 1
[(block? x) (map typeset (block->lines x))] ; about 2/3 of running time
[else x]))
(define (para ht . xs)
(apply box ht `(,(block-break) ,@xs ,(block-break))))
(module+ main
(require "render.rkt" racket/class profile)
(require "samples.rkt")
(activate-logger quad-logger)
(parameterize ([world:quality-default world:max-quality]
[world:paper-width-default 412]
[world:paper-height-default 600])
(define to (begin (time (typeset (jude)))))
(time (send (new pdf-renderer%) render-to-file to "foo.pdf"))))

@ -0,0 +1,67 @@
#lang racket/base
(require math/flonum racket/draw racket/class racket/contract sugar/debug sugar/list racket/list sugar/cache racket/serialize racket/file)
(provide measure-text measure-ascent round-float update-text-cache-file load-text-cache-file)
(define precision 4.0)
(define base (flexpt 10.0 precision))
(define-syntax-rule (round-float x)
(fl/ (flround (fl* base (fl x))) base))
(define dc (new record-dc%))
(define max-size 1000.0)
(define/caching (make-font/caching font weight style)
(make-font #:size max-size #:style style #:weight weight #:face font))
(define (get-cache-file-path)
(build-path "font.cache"))
(define (update-text-cache-file)
(when (current-text-cache-changed?)
(write-to-file (serialize (current-text-cache)) (get-cache-file-path) #:exists 'replace)
(current-text-cache-changed? #f)))
(define (load-text-cache-file)
(define cache-file-path (get-cache-file-path))
(current-text-cache (if (file-exists? cache-file-path)
(deserialize (file->value cache-file-path))
(make-hash))))
(define current-text-cache (make-parameter (make-hash)))
(define current-text-cache-changed? (make-parameter #f))
(define current-font-cache (make-parameter (make-hash)))
(define/caching (measure-max-size text font [weight 'normal] [style 'normal])
;((string? string?) (symbol? symbol?) . ->* . number?)
(define font-instance (hash-ref! (current-font-cache) (list font weight style) (λ() (make-font #:size max-size #:style style #:weight weight #:face font))))
;; combine boolean only makes a difference for two or more chars
(hash-ref! (current-text-cache) (list text font weight style) (λ() (current-text-cache-changed? #t)
(values->list (send dc get-text-extent text font-instance (>= (string-length text) 1))))))
(define (width x) (first x))
(define (height x) (second x))
(define (descent x) (third x))
(define (extra x) (fourth x))
(define (measure-text-max-size text font [weight 'normal] [style 'normal])
(width (measure-max-size text font weight style)))
(define/contract (measure-text text size font [weight 'normal] [style 'normal])
((string? flonum? string?) (symbol? symbol?) . ->* . flonum?)
;; Native function only accepts integers, so get max-size and scale down to size needed.
(define raw-measure (measure-text-max-size text font weight style))
(round-float (fl/ (fl* (fl raw-measure) size) max-size)))
(define (measure-ascent-max-size text font [weight 'normal] [style 'normal])
(define result-list (measure-max-size text font weight style))
(- (height result-list) (descent result-list)))
(define/contract (measure-ascent text size font [weight 'normal] [style 'normal])
((string? flonum? string?) (symbol? symbol?) . ->* . flonum?)
;; Native function only accepts integers, so get max-size and scale down to size needed.
(define raw-baseline-distance (measure-ascent-max-size text font weight style))
(round-float (fl/ (fl* (fl raw-baseline-distance) size) max-size)))

@ -0,0 +1,434 @@
#lang racket/base
(require (for-syntax racket/base racket/syntax))
(require racket/list sugar/debug rackunit racket/function racket/vector sugar/cache "logger.rkt")
(define-logger ocm)
;(activate-logger ocm-logger)
#|
Totally monotone matrix searching algorithms.
The offline algorithm in ConcaveMinima is from Agarwal, Klawe, Moran,
Shor, and Wilbur, Geometric applications of a matrix searching algorithm,
Algorithmica 2, pp. 195-208 (1987).
The online algorithm in OnlineConcaveMinima is from Galil and Park,
A linear time algorithm for concave one-dimensional dynamic programming,
manuscript, 1989, which simplifies earlier work on the same problem
by Wilbur (J. Algorithms 1988) and Eppstein (J. Algorithms 1990).
D. Eppstein, March 2002, significantly revised August 2005
|#
(provide smawky? make-ocm reduce reduce2 (prefix-out ocm- (combine-out min-value min-index)))
(define (select-elements xs is)
(map (curry list-ref xs) is))
(define (odd-elements xs)
(select-elements xs (range 1 (length xs) 2)))
(define (vector-odd-elements xs)
(for/vector ([i (in-range (vector-length xs))] #:when (odd? i))
(vector-ref xs i)))
(define (even-elements xs)
(select-elements xs (range 0 (length xs) 2)))
;; Wrapper for the matrix procedure
;; that automatically maintains a hash cache of previously-calculated values
;; because the minima operations tend to hit the same values.
;; Assuming here that (matrix i j) is invariant
;; and that the matrix function is more expensive than the cache lookup.
(define-syntax-rule (vector-append-item xs value)
(vector-append xs (vector value)))
(define-syntax-rule (vector-set vec idx val)
(begin
(vector-set! vec idx val)
vec))
(define-syntax-rule (vector-cdr vec)
(vector-drop vec 1))
(define-syntax-rule (vector-empty? vec)
(= 0 (vector-length vec)))
(define (integers? x) (and (list? x) (andmap integer? x)))
;; Reduce phase: make number of rows at most equal to number of cols
(define (reduce row-indices col-indices matrix-proc value->integer)
;(vector? vector? procedure? procedure? . -> . vector?)
(log-ocm-debug "starting reduce phase with")
(log-ocm-debug "row-indices = ~a" row-indices)
(log-ocm-debug "col-indices = ~a" col-indices)
(define (process-stack stack row-idx)
(log-ocm-debug "row stack = ~a" stack)
(let ([last-stack-idx (sub1 (vector-length stack))])
(cond
[(and (>= (vector-length stack) 1)
(log-ocm-debug "comparing row values at column ~a" (vector-ref col-indices last-stack-idx))
(log-ocm-debug "end of row stack (~a) value at column ~a = ~a" (vector-ref stack last-stack-idx) (vector-ref col-indices last-stack-idx) (value->integer (matrix-proc (vector-ref stack last-stack-idx) (vector-ref col-indices last-stack-idx))))
(log-ocm-debug "challenger row (~a) value at column ~a = ~a" row-idx (vector-ref col-indices last-stack-idx) (value->integer (matrix-proc row-idx (vector-ref col-indices last-stack-idx))))
(> (value->integer (matrix-proc (vector-ref stack last-stack-idx) (vector-ref col-indices last-stack-idx)))
(value->integer (matrix-proc row-idx (vector-ref col-indices last-stack-idx)))))
(log-ocm-debug "challenger row (~a) wins with a new minimum ~a, so end of row stack (~a) is removed" row-idx (value->integer (matrix-proc row-idx (vector-ref col-indices last-stack-idx))) (vector-ref stack last-stack-idx))
(process-stack (vector-drop-right stack 1) row-idx)]
[else
(log-ocm-debug (if (< (vector-length stack) 1)
(format "row stack too short for challenge, pushing row ~a" row-idx)
(format "challenger row (~a) loses to end of row stack (~a), so ~a joins stack" row-idx (vector-ref stack last-stack-idx) row-idx)))
stack])))
(define reduced-row-indexes
(for/fold ([stack (vector)]) ([row-idx (in-vector row-indices)])
(let ([stack (process-stack stack row-idx)])
(if (= (vector-length stack) (vector-length col-indices))
stack
(vector-append stack (vector row-idx))))))
(log-ocm-debug "finished reduce. row indexes = ~v" reduced-row-indexes)
reduced-row-indexes)
(define (reduce2 row-indices col-indices matrix-proc value->integer)
(let find-survivors ([rows row-indices][survivors empty])
(cond
[(vector-empty? rows) (list->vector (reverse survivors))]
[else
(define challenger-row (vector-ref rows 0))
(cond
;; no survivors yet, so push first row and keep going
[(empty? survivors) (find-survivors (vector-cdr rows) (cons challenger-row survivors))]
[else
(define index-of-last-survivor (sub1 (length survivors)))
(define col-head (vector-ref col-indices index-of-last-survivor))
(define-syntax-rule (test-function r) (value->integer (matrix-proc r col-head)))
(cond
;; this is the challenge: is the head cell of challenger a new minimum?
;; use < not <=, so the recorded winner is the earliest row with the new minimum, not the latest row
;; if yes, challenger wins. pop element from stack, and let challenger try again (= leave rows alone)
[(< (test-function challenger-row) (test-function (car survivors))) (find-survivors rows (cdr survivors))]
;; if not, challenger lost.
;; If we're in the last column, ignore the loser by recurring on the same values
[(= col-head (vector-last col-indices)) (find-survivors (vector-cdr rows) survivors)]
;; otherwise challenger lost and we're not in last column,
;; so add challenger to survivor stack
[else (find-survivors (vector-cdr rows) (cons challenger-row survivors))])])])))
(define (make-minimum value row-idx)
(define ht (make-hash))
(! ht 'value value)
(! ht 'row-idx row-idx)
ht)
;; Interpolate phase: in the minima hash, add results for even rows
(define-syntax-rule (vector-last v)
(vector-ref v (sub1 (vector-length v))))
(define (interpolate minima row-indices col-indices matrix-proc value->integer)
;(hash? vector? vector? procedure? procedure? . -> . hash?)
(for ([col-idx (in-range 0 (vector-length col-indices) 2)]) ;; even-col-indices
(define col (vector-ref col-indices col-idx))
(define idx-of-last-row
(if (= col-idx (sub1 (vector-length col-indices)))
(vector-last row-indices)
(: (hash-ref minima (vector-ref col-indices (add1 col-idx))) 'row-idx)))
(define smallest-value-entry
(vector-argmin (compose1 value->integer car)
(for/vector ([row-idx (in-list (dropf-right (vector->list row-indices) (negate (curry = idx-of-last-row))))])
(list (matrix-proc row-idx col) row-idx))))
(! minima col (apply make-minimum smallest-value-entry)))
minima)
(define (interpolate2 minima row-indices col-indices matrix-proc value->integer)
(define idx-of-last-col (sub1 (vector-length col-indices)))
(define (smallest-value-entry col idx-of-last-row)
(argmin (compose1 value->integer car)
(for/list ([row-idx (stop-after (in-vector row-indices) (curry = idx-of-last-row))])
(list (matrix-proc row-idx col) row-idx))))
(for ([(col col-idx) (in-indexed col-indices)] #:when (even? col-idx))
(define idx-of-last-row (if (= col-idx idx-of-last-col)
(vector-last row-indices)
(: (hash-ref minima (vector-ref col-indices (add1 col-idx))) 'row-idx)))
(! minima col (apply make-minimum (smallest-value-entry col idx-of-last-row))))
minima)
#|
Search for the minimum value in each column of a matrix.
The return value is a dictionary mapping ColIndices to pairs
(value,rowindex). We break ties in favor of earlier rows.
The matrix is defined implicitly as a function, passed
as the third argument to this routine, where Matrix(i,j)
gives the matrix value at row index i and column index j.
The matrix must be concave, that is, satisfy the property
Matrix(i,j) > Matrix(i',j) => Matrix(i,j') > Matrix(i',j')
for every i<i' and j<j'; that is, in every submatrix of
the input matrix, the positions of the column minima
must be monotonically nondecreasing.
The rows and columns of the matrix are labeled by the indices
given in order by the first two arguments. In most applications,
these arguments can simply be integer ranges.
|#
;; The return value `minima` is a hash:
;; the keys are col-indices (integers)
;; the values are pairs of (value row-index).
(require rackunit)
(define (concave-minima row-indices [col-indices null] [matrix-proc (make-caching-proc identity)] [value->integer identity])
;((vector?) ((or/c #f vector?) procedure? procedure?) . ->* . hash?)
(define reduce-proc reduce2)
(define interpolate-proc interpolate2)
(if (= 0 (vector-length col-indices))
(make-hash)
(let ([row-indices (reduce-proc row-indices col-indices matrix-proc value->integer)])
(define odd-column-minima (concave-minima row-indices (vector-odd-elements col-indices) matrix-proc value->integer))
(interpolate-proc odd-column-minima row-indices col-indices matrix-proc value->integer))))
#|
Online concave minimization algorithm of Galil and Park.
OnlineConcaveMinima(Matrix,initial) creates a sequence of pairs
(self.value(j),self.index(j)), where
self.value(0) = initial,
self.value(j) = min { Matrix(i,j) | i < j } for j > 0,
and where self.index(j) is the value of j that provides the minimum.
Matrix(i,j) must be concave, in the same sense as for ConcaveMinima.
We never call Matrix(i,j) until value(i) has already been computed,
so that the Matrix function may examine previously computed values.
Calling value(i) for an i that has not yet been computed forces
the sequence to be continued until the desired index is reached.
Calling iter(self) produces a sequence of (value,index) pairs.
Matrix(i,j) should always return a value, rather than raising an
exception, even for j larger than the range we expect to compute.
If j is out of range, a suitable value to return that will not
violate concavity is Matrix(i,j) = -i. It will not work correctly
to return a flag value such as None for large j, because the ties
formed by the equalities among such flags may violate concavity.
|#
;; Online Concave Minima object
;(struct $ocm (values indices finished matrix-proc base tentative) #:transparent #:mutable)
;; State used by self.value(), self.index(), and iter(self) =
;; $ocm-values, $ocm-indices, $ocm-finished
#|
State used by the internal algorithm:
$ocm-matrix, $ocm-base, $ocm-tentative
We allow self._values to be nonempty for indices > finished,
keeping invariant that
(1) self._values[i] = Matrix(self._indices[i], i),
(2) if the eventual correct value of self.index(i) < base,
then self._values[i] is nonempty and correct.
In addition, we keep a column index self._tentative, such that
(3) if i <= tentative, and the eventual correct value of
self.index(i) <= finished, then self._values[i] is correct.
|#
(define no-value 'none)
(define-syntax-rule (: hashtable key)
(hash-ref hashtable key))
(define-syntax-rule (! hashtable key value)
(hash-set! hashtable key value))
(define (make-ocm matrix-proc [initial-value 0][value->integer identity])
(log-ocm-debug "making new ocm")
(define ocm (make-hash))
(! ocm 'min-values (vector initial-value))
(! ocm 'min-row-indices (vector no-value))
(! ocm 'finished 0)
(! ocm 'matrix-proc (make-caching-proc matrix-proc))
(! ocm 'value->integer value->integer) ; for converting matrix values to an integer
(! ocm 'base 0)
(! ocm 'tentative 0)
ocm)
;; Return min { Matrix(i,j) | i < j }.
(define (min-value ocm j)
(if (< (: ocm 'finished) j)
(begin (advance! ocm) (min-value ocm j))
(vector-ref (: ocm 'min-values) j)))
;; Return argmin { Matrix(i,j) | i < j }.
(define (min-index ocm j)
(if (< (: ocm 'finished) j)
(begin (advance! ocm) (min-index ocm j))
(vector-ref (: ocm 'min-row-indices) j)))
;; Finish another value,index pair.
(define (advance! ocm)
(define next (add1 (: ocm 'finished)))
(log-ocm-debug "advance! ocm to next = ~a" (add1 (: ocm 'finished)))
(cond
;; First case: we have already advanced past the previous tentative
;; value. We make a new tentative value by applying ConcaveMinima
;; to the largest square submatrix that fits under the base.
[(> next (: ocm 'tentative))
(log-ocm-debug "advance: first case because next (~a) > tentative (~a)" next (: ocm 'tentative))
(define rows (list->vector (range (: ocm 'base) next)))
(! ocm 'tentative (+ (: ocm 'finished) (vector-length rows)))
(define cols (list->vector (range next (add1 (: ocm 'tentative)))))
(define minima (concave-minima rows cols (: ocm 'matrix-proc) (: ocm 'value->integer)))
(for ([col (in-vector cols)])
(cond
[(>= col (vector-length (: ocm 'min-values)))
(! ocm 'min-values (vector-append-item (: ocm 'min-values) (: (: minima col) 'value)))
(! ocm 'min-row-indices (vector-append-item (: ocm 'min-row-indices) (: (: minima col) 'row-idx)))]
[(< ((: ocm 'value->integer) (: (: minima col) 'value)) ((: ocm 'value->integer) (vector-ref (: ocm 'min-values) col)))
(! ocm 'min-values (vector-set (: ocm 'min-values) col (: (: minima col) 'value)))
(! ocm 'min-row-indices (vector-set (: ocm 'min-row-indices) col (: (: minima col) 'row-idx)))]))
(! ocm 'finished next)]
[else
;; Second case: the new column minimum is on the diagonal.
;; All subsequent ones will be at least as low,
;; so we can clear out all our work from higher rows.
;; As in the fourth case, the loss of tentative is
;; amortized against the increase in base.
(define diag ((: ocm 'matrix-proc) (sub1 next) next))
(cond
[(< ((: ocm 'value->integer) diag) ((: ocm 'value->integer) (vector-ref (: ocm 'min-values) next)))
(log-ocm-debug "advance: second case because column minimum is on the diagonal")
(! ocm 'min-values (vector-set (: ocm 'min-values) next diag))
(! ocm 'min-row-indices (vector-set (: ocm 'min-row-indices) next (sub1 next)))
(! ocm 'base (sub1 next))
(! ocm 'tentative next)
(! ocm 'finished next)]
;; Third case: row i-1 does not supply a column minimum in
;; any column up to tentative. We simply advance finished
;; while maintaining the invariant.
[(>= ((: ocm 'value->integer) ((: ocm 'matrix-proc) (sub1 next) (: ocm 'tentative)))
((: ocm 'value->integer) (vector-ref (: ocm 'min-values) (: ocm 'tentative))))
(log-ocm-debug "advance: third case because row i-1 does not suppply a column minimum")
(! ocm 'finished next)]
;; Fourth and final case: a new column minimum at self._tentative.
;; This allows us to make progress by incorporating rows
;; prior to finished into the base. The base invariant holds
;; because these rows cannot supply any later column minima.
;; The work done when we last advanced tentative (and undone by
;; this step) can be amortized against the increase in base.
[else
(log-ocm-debug "advance: fourth case because new column minimum")
(! ocm 'base (sub1 next))
(! ocm 'tentative next)
(! ocm 'finished next)])]))
(define (print ocm)
(displayln (: ocm 'min-values))
(displayln (: ocm 'min-row-indices)))
(define (smawky? m)
(define (position-of-minimum xs)
;; put each element together with its list index
(let ([xs (map cons (range (length xs)) xs)])
;; find the first one with the min value, and grab the list index
(car (argmin cdr (filter (compose1 not negative? cdr) xs)))))
;; tests if penalty matrix is monotone for non-negative values.
(define increasing-minima? (apply <= (map position-of-minimum m)))
(define monotone?
(for*/and ([ridx (in-range 1 (length m))]
[cidx (in-range (sub1 (length (car m))))])
(let* ([prev-row (list-ref m (sub1 ridx))]
[row (list-ref m ridx)]
[a (list-ref prev-row cidx)]
[b (list-ref prev-row (add1 cidx))]
[c (list-ref row cidx)]
[d (list-ref row (add1 cidx))])
(if (andmap (compose1 not negative?) (list a b c d)) ;; smawk disregards negative values
(cond
[(< c d) (if (< a b) #t (error (format "Submatrix ~a not monotone in ~a" (list (list a b) (list c d)) m)))]
[(= c d) (if (<= a b) #t (error (format "Submatrix ~a not monotone in ~a" (list (list a b) (list c d)) m)))]
[else #t])
#t))))
(and increasing-minima? monotone?))
(module+ test
(require rackunit)
(define m '((25 42 57 78 90 103 123 142 151)
(21 35 48 65 76 85 105 123 130)
(13 26 35 51 58 67 86 100 104)
(10 20 28 42 48 56 75 86 88)
(20 29 33 44 49 55 73 82 80)
(13 21 24 35 39 44 59 65 59)
(19 25 28 38 42 44 57 61 52)
(35 37 40 48 48 49 62 62 49)
(37 36 37 42 39 39 51 50 37)
(41 39 37 42 35 33 44 43 29)
(58 56 54 55 47 41 50 47 29)
(66 64 61 61 51 44 52 45 24)
(82 76 72 70 56 49 55 46 23)
(99 91 83 80 63 56 59 46 20)
(124 116 107 100 80 71 72 58 28)
(133 125 113 106 86 75 74 59 25)
(156 146 131 120 97 84 80 65 31)
(178 164 146 135 110 96 92 73 39)))
(define m2 (apply map list m))
(check-true (smawky? m))
(check-true (smawky? m2))
;; proc must return a value even for out-of-bounds i and j
(define (simple-proc i j) (with-handlers [(exn:fail? (λ(exn) (* -1 i)))]
(list-ref (list-ref m i) j)))
(define (simple-proc2 i j) (with-handlers [(exn:fail? (λ(exn) (* -1 i)))]
(list-ref (list-ref m2 i) j)))
(check-equal? (simple-proc 0 2) 57) ; 0th row, 2nd col
(check-equal? (simple-proc2 2 0) 57) ; flipped
(define o (make-ocm simple-proc))
(define row-indices (list->vector (range (length m))))
(define col-indices (list->vector (range (length (car m)))))
(define result (concave-minima row-indices col-indices simple-proc identity))
(check-equal?
(for/list ([j (in-vector col-indices)])
(define h (hash-ref result j))
(list (hash-ref h 'value) (hash-ref h 'row-idx)))
'((10 3) (20 3) (24 5) (35 5) (35 9) (33 9) (44 9) (43 9) (20 13))) ; checked against SMAWK.py
(check-equal?
(for/list ([j (in-vector col-indices)])
(list (min-value o j) (min-index o j)))
'((0 none) (42 0) (48 1) (51 2) (48 3) (55 4) (59 5) (61 6) (49 7))) ; checked against SMAWK.py
(define o2 (make-ocm simple-proc2))
(define row-indices2 (list->vector (range (length m2))))
(define col-indices2 (list->vector (range (length (car m2)))))
(define result2 (concave-minima row-indices2 col-indices2 simple-proc2 identity))
(check-equal?
(for/list ([j (in-vector col-indices2)])
(define h (hash-ref result2 j))
(list (hash-ref h 'value) (hash-ref h 'row-idx)))
'((25 0) (21 0) (13 0) (10 0) (20 0) (13 0) (19 0) (35 0) (36 1) (29 8) (29 8) (24 8) (23 8) (20 8) (28 8) (25 8) (31 8) (39 8))) ; checked against SMAWK.py
(check-equal?
(for/list ([j (in-vector col-indices2)])
(list (min-value o2 j) (min-index o2 j)))
'((0 none) (21 0) (13 0) (10 0) (20 0) (13 0) (19 0) (35 0) (36 1) (29 8) (-9 9) (-10 10) (-11 11) (-12 12) (-13 13) (-14 14) (-15 15) (-16 16))) ; checked against SMAWK.py
)

@ -0,0 +1,192 @@
"""SMAWK.py
Totally monotone matrix searching algorithms.
The offline algorithm in ConcaveMinima is from Agarwal, Klawe, Moran,
Shor, and Wilbur, Geometric applications of a matrix searching algorithm,
Algorithmica 2, pp. 195-208 (1987).
The online algorithm in OnlineConcaveMinima is from Galil and Park,
A linear time algorithm for concave one-dimensional dynamic programming,
manuscript, 1989, which simplifies earlier work on the same problem
by Wilbur (J. Algorithms 1988) and Eppstein (J. Algorithms 1990).
D. Eppstein, March 2002, significantly revised August 2005
"""
def ConcaveMinima(RowIndices,ColIndices,Matrix):
"""
Search for the minimum value in each column of a matrix.
The return value is a dictionary mapping ColIndices to pairs
(value,rowindex). We break ties in favor of earlier rows.
The matrix is defined implicitly as a function, passed
as the third argument to this routine, where Matrix(i,j)
gives the matrix value at row index i and column index j.
The matrix must be concave, that is, satisfy the property
Matrix(i,j) > Matrix(i',j) => Matrix(i,j') > Matrix(i',j')
for every i<i' and j<j'; that is, in every submatrix of
the input matrix, the positions of the column minima
must be monotonically nondecreasing.
The rows and columns of the matrix are labeled by the indices
given in order by the first two arguments. In most applications,
these arguments can simply be integer ranges.
"""
# Base case of recursion
if not ColIndices: return {}
# Reduce phase: make number of rows at most equal to number of cols
stack = []
for r in RowIndices:
while len(stack) >= 1 and \
Matrix(stack[-1], ColIndices[len(stack)-1]) \
> Matrix(r, ColIndices[len(stack)-1]):
stack.pop()
if len(stack) != len(ColIndices):
stack.append(r)
RowIndices = stack
# Recursive call to search for every odd column
minima = ConcaveMinima(RowIndices,
[ColIndices[i] for i in range(1,len(ColIndices),2)],
Matrix)
# Go back and fill in the even rows
r = 0
for c in range(0,len(ColIndices),2):
col = ColIndices[c]
row = RowIndices[r]
if c == len(ColIndices) - 1:
lastrow = RowIndices[-1]
else:
lastrow = minima[ColIndices[c+1]][1]
pair = (Matrix(row,col),row)
while row != lastrow:
r += 1
row = RowIndices[r]
pair = min(pair,(Matrix(row,col),row))
minima[col] = pair
return minima
class OnlineConcaveMinima:
"""
Online concave minimization algorithm of Galil and Park.
OnlineConcaveMinima(Matrix,initial) creates a sequence of pairs
(self.value(j),self.index(j)), where
self.value(0) = initial,
self.value(j) = min { Matrix(i,j) | i < j } for j > 0,
and where self.index(j) is the value of j that provides the minimum.
Matrix(i,j) must be concave, in the same sense as for ConcaveMinima.
We never call Matrix(i,j) until value(i) has already been computed,
so that the Matrix function may examine previously computed values.
Calling value(i) for an i that has not yet been computed forces
the sequence to be continued until the desired index is reached.
Calling iter(self) produces a sequence of (value,index) pairs.
Matrix(i,j) should always return a value, rather than raising an
exception, even for j larger than the range we expect to compute.
If j is out of range, a suitable value to return that will not
violate concavity is Matrix(i,j) = -i. It will not work correctly
to return a flag value such as None for large j, because the ties
formed by the equalities among such flags may violate concavity.
"""
def __init__(self,Matrix,initial):
"""Initialize a OnlineConcaveMinima object."""
# State used by self.value(), self.index(), and iter(self)
self._values = [initial] # tentative solution values...
self._indices = [None] # ...and their indices
self._finished = 0 # index of last non-tentative value
# State used by the internal algorithm
#
# We allow self._values to be nonempty for indices > finished,
# keeping invariant that
# (1) self._values[i] = Matrix(self._indices[i], i),
# (2) if the eventual correct value of self.index(i) < base,
# then self._values[i] is nonempty and correct.
#
# In addition, we keep a column index self._tentative, such that
# (3) if i <= tentative, and the eventual correct value of
# self.index(i) <= finished, then self._values[i] is correct.
#
self._matrix = Matrix
self._base = 0
self._tentative = 0
def __str__(self):
return "%s" % self._values
def __iter__(self):
"""Loop through (value,index) pairs."""
i = 0
while True:
yield self.value(i),self.index(i)
i += 1
def value(self,j):
"""Return min { Matrix(i,j) | i < j }."""
while self._finished < j:
self._advance()
return self._values[j]
def index(self,j):
"""Return argmin { Matrix(i,j) | i < j }."""
while self._finished < j:
self._advance()
return self._indices[j]
def _advance(self):
"""Finish another value,index pair."""
# First case: we have already advanced past the previous tentative
# value. We make a new tentative value by applying ConcaveMinima
# to the largest square submatrix that fits under the base.
i = self._finished + 1
if i > self._tentative:
rows = range(self._base,self._finished+1)
self._tentative = self._finished+len(rows)
cols = range(self._finished+1,self._tentative+1)
minima = ConcaveMinima(rows,cols,self._matrix)
for col in cols:
if col >= len(self._values):
self._values.append(minima[col][0])
self._indices.append(minima[col][1])
elif minima[col][0] < self._values[col]:
self._values[col],self._indices[col] = minima[col]
self._finished = i
return
# Second case: the new column minimum is on the diagonal.
# All subsequent ones will be at least as low,
# so we can clear out all our work from higher rows.
# As in the fourth case, the loss of tentative is
# amortized against the increase in base.
diag = self._matrix(i-1,i)
if diag < self._values[i]:
self._values[i] = diag
self._indices[i] = self._base = i-1
self._tentative = self._finished = i
return
# Third case: row i-1 does not supply a column minimum in
# any column up to tentative. We simply advance finished
# while maintaining the invariant.
if self._matrix(i-1,self._tentative) >= self._values[self._tentative]:
self._finished = i
return
# Fourth and final case: a new column minimum at self._tentative.
# This allows us to make progress by incorporating rows
# prior to finished into the base. The base invariant holds
# because these rows cannot supply any later column minima.
# The work done when we last advanced tentative (and undone by
# this step) can be amortized against the increase in base.
self._base = i-1
self._tentative = self._finished = i
return

Binary file not shown.

@ -0,0 +1,66 @@
import SMAWK
m1 = [
[25, 42, 57, 78, 90, 103, 123, 142, 151],
[21, 35, 48, 65, 76, 85, 105, 123, 130],
[13, 26, 35, 51, 58, 67, 86, 100, 104],
[10, 20, 28, 42, 48, 56, 75, 86, 88],
[20, 29, 33, 44, 49, 55, 73, 82, 80],
[13, 21, 24, 35, 39, 44, 59, 65, 59],
[19, 25, 28, 38, 42, 44, 57, 61, 52],
[35, 37, 40, 48, 48, 49, 62, 62, 49],
[37, 36, 37, 42, 39, 39, 51, 50, 37],
[41, 39, 37, 42, 35, 33, 44, 43, 29],
[58, 56, 54, 55, 47, 41, 50, 47, 29],
[66, 64, 61, 61, 51, 44, 52, 45, 24],
[82, 76, 72, 70, 56, 49, 55, 46, 23],
[99, 91, 83, 80, 63, 56, 59, 46, 20],
[124, 116, 107, 100, 80, 71, 72, 58, 28],
[133, 125, 113, 106, 86, 75, 74, 59, 25],
[156, 146, 131, 120, 97, 84, 80, 65, 31],
[178, 164, 146, 135, 110, 96, 92, 73, 39]]
num_rows = len(m1)
row_indices = range(num_rows)
num_cols = len(m1[0])
col_indices = range(num_cols)
def matrix_func1(i, j):
try:
return m1[i][j]
except:
return -1 * i
def matrix_func2(i, j):
try:
return m1[j][i]
except:
return -1 * i
cm_hash = SMAWK.ConcaveMinima(row_indices, col_indices, matrix_func1)
cm_hash2 = SMAWK.ConcaveMinima(col_indices, row_indices, matrix_func2)
print cm_hash2
'''
{0: (10, 3), 1: (20, 3), 2: (24, 5), 3: (35, 5), 4: (35, 9), 5: (33, 9), 6: (44, 9), 7: (43, 9), 8: (20, 13)}
'''
ocm = SMAWK.OnlineConcaveMinima(matrix_func1, 0)
ocm2 = SMAWK.OnlineConcaveMinima(matrix_func2, 0)
ocm_hash = dict()
for j in col_indices:
ocm_hash[j] = (ocm.value(j), ocm.index(j))
ocm_hash2 = dict()
for j in row_indices:
ocm_hash2[j] = (ocm2.value(j), ocm2.index(j))
print ocm_hash2
'''
{0: (0, None), 1: (42, 0), 2: (48, 1), 3: (51, 2), 4: (48, 3), 5: (55, 4), 6: (59, 5), 7: (61, 6), 8: (49, 7)}'''

@ -0,0 +1,241 @@
#lang racket/base
(require (for-syntax racket/base racket/syntax racket/string) racket/string racket/contract racket/serialize sugar/list racket/format racket/list sugar/debug sugar/coerce racket/bool racket/function sugar/string)
(require "world.rkt")
(provide (all-defined-out))
;; struct implementation
(serializable-struct quad (name attrs list) #:transparent
#:methods gen:custom-write
[(define write-proc (λ(b port mode)
(display (format "(~a)" (string-join (filter-not void? (list
(~a (quad-name b))
(if (and (hash? (quad-attrs b)) (> (length (hash-keys (quad-attrs b))) 0)) (~v (flatten (hash->list (quad-attrs b)))) (void))
(if (> (length (quad-list b)) 0) (~a (string-join (map ~v (quad-list b)) "")) (void)))) " ")) port)))]
#:property prop:sequence (λ(q) (quad-list q)))
;; vector implementation
#|
(define (quad-name q) (vector-ref q 0))
(define (quad-attrs q) (vector-ref q 1))
(define (quad-list q) (vector-ref q 2))
(define (quad? x)
(and (vector? x)
(symbol? (quad-name x))
(or (false? (quad-attrs x)) (hash? (quad-attrs x)))
(list? (quad-list x))))
(define (quad name attrs xs)
(vector name attrs xs))
|#
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; hash implementation
#|
(define (quad-name q) (hash-ref q 'name))
(define (quad-attrs q) (hash-ref q 'attrs))
(define (quad-list q) (hash-ref q 'list))
(define (quad? x)
(and (hash? x)
(andmap (λ(k) (hash-has-key? x k)) (list 'name 'attrs 'list))
(symbol? (quad-name x))
(ormap (λ(pred) (pred (quad-attrs x))) (list false? hash?))
(list? (quad-list x))))
(define (quad name attrs xs)
(hash 'name name 'attrs attrs 'list xs))
|#
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (quad-name? x) (symbol? x))
(define (hashable-list? x) (and (list? x) (even? (length x))))
(define (quad-attrs? x) (or (false? x) (hash? x)))
(define (quad-list? x) (and (list? x) (andmap (λ(xi) (or (quad? xi) (string? xi))) x)))
(define (quads? x) (and (list? x) (andmap quad? x)))
(define (lists-of-quads? x) (and (list? x) (andmap quads? x)))
(define quad= equal?)
(define token? quad?)
(define (quad/c x) (λ(x) (and (quad? x) (symbol? (quad-name x)) (hash? (quad-attrs x))
(andmap (λ(xi) (or (quad/c xi) (string? xi))) (quad-list x)))))
(define quad-attr-ref
(case-lambda
[(q key)
(if (quad-attrs q)
(hash-ref (quad-attrs q) key)
(error 'quad-attr-ref (format "no attrs in quad ~a" q)))]
[(q key default)
(if (quad-attrs q)
(hash-ref (quad-attrs q) key default)
default)]))
(define-syntax (quad-attr-ref/parameter stx)
(syntax-case stx ()
[(_ q key)
(with-syntax ([world:key-default (format-id stx "~a-default" (string-trim (symbol->string (syntax->datum #'key)) "-key"))])
#'(quad-attr-ref q key (world:key-default)))]))
(define (quad-has-attr? q key)
(define qa (quad-attrs q))
(and qa (hash-has-key? qa key)))
(define-syntax (define-quad-list-function stx)
(syntax-case stx ()
[(_ proc)
(with-syntax ([quad-proc (format-id stx "quad-~a" #'proc)])
#'(define (quad-proc q) (proc (quad-list q))))]))
(define-quad-list-function first)
(define-quad-list-function car)
(define-quad-list-function cdr)
(define-quad-list-function last)
(define (quad-cons item q)
(quad (quad-name q) (quad-attrs q) (cons item (quad-list q))))
(define-syntax-rule (quad-ref q r)
(list-ref (quad-list q) r))
(define/contract (quad-ends-with? q str)
(quad? string? . -> . boolean?)
(cond
[(not (empty? (quad-list q)))
(define last-item (last (quad-list q)))
(cond
[(string? last-item) (ends-with? last-item str)]
[(quad? last-item) (quad-ends-with? last-item str)])]
[else #f]))
(define/contract (quad-append q new-item)
(quad? (or/c quad? string?) . -> . quad?)
(quad (quad-name q) (quad-attrs q) (append (quad-list q) (list new-item))))
(define/contract (quad->string x)
(quad? . -> . string?)
(cond
[(quad? x) (string-append* (map quad->string (quad-list x)))]
[(string? x) x]
[else ""]))
(define-syntax-rule (report-quadstring q)
(begin
(report (quad->string q) 'q)
q))
(define cannot-be-common-attrs '(width x y page)) ;; todo: how to specify these better? this-* prefix?
;; make this a macro because qs-in is often huge
;; and the macro avoids allocation + garbage collection
(define-syntax-rule (gather-common-attrs qs-in)
(let ([qs qs-in])
(and (quad-attrs (car qs))
(let ([attr-missing (gensym)])
(let loop ([qs (cdr qs)]
[common-attrs (for/list ([kv-pair (in-hash-pairs (quad-attrs (car qs)))]
#:unless (member (car kv-pair) cannot-be-common-attrs))
kv-pair)])
(cond
[(empty? common-attrs) #f]
[(empty? qs) (flatten common-attrs)]
[else (define reference-quad (car qs))
(loop (cdr qs)
(filter (λ(ca) (let ([v (quad-attr-ref reference-quad (car ca) attr-missing)])
(equal? v (cdr ca))))
common-attrs))]))))))
(define-syntax (define-box-type stx)
(syntax-case stx ()
[(_ id)
(with-syntax ([id? (format-id #'id "~a?" #'id)]
[ids? (format-id #'id "~as?" #'id)]
[lists-of-ids? (format-id #'id "list-of-~as?" #'id)]
[quads->id (format-id #'id "quads->~a" #'id)]
[inline/quads->id (format-id #'id "inline/quads->~a" #'id)])
#'(begin
;; quad predicate - ok to be relaxed here if we're strict when making the struct
(define (id? x)
(and (quad? x) (equal? (quad-name x) 'id)))
;; quad constructor
;; put contract here rather than on struct, because this is the main interface
;; and this contract is more liberal.
;; but don't put a separate contract on struct, because it's superfluous.
(define/contract (id [attrs #f] . xs)
(() ((or/c quad-attrs? hashable-list?)) #:rest quad-list? . ->* . id?)
(quad 'id (and attrs (if (hash? attrs) attrs (apply hash attrs))) xs))
;; quad list predicate and list-of-list predicate.
;; These are faster than the listof contract combinator.
(define (ids? x)
(and (list? x) (andmap id? x)))
(define (lists-of-ids? x)
(and (list? x) (andmap ids? x)))
;; quad converter macro
(define (quads->id qs)
(apply id (gather-common-attrs qs) qs))))]))
;; do not treat empty string as whitespace.
;; throws off tests that rely on adjacency to positive whitespace.
(define/contract (whitespace? x [nbsp? #f])
((any/c)(boolean?) . ->* . coerce/boolean?)
(cond
[(quad? x) (whitespace? (quad-list x) nbsp?)]
[(string? x) (or (and (regexp-match #px"\\p{Zs}" x) ; Zs = unicode whitespace category
(or nbsp? (not (regexp-match #px"\u00a0" x)))))] ; 00a0: nbsp
[(list? x) (and (not (empty? x)) (andmap (curryr whitespace? nbsp?) x))] ; andmap returns #t for empty lists
[else #f]))
(define (whitespace/nbsp? x)
(whitespace? x #t))
(define-syntax (define-break-type stx)
(syntax-case stx ()
[(_ id)
(with-syntax ([split-on-id-breaks (format-id #'id "split-on-~a-breaks" #'id)]
[id-break (format-id #'id "~a-break" #'id)]
[id-break? (format-id #'id "~a-break?" #'id)]
[multi-id (format-id #'id "multi~a" #'id)]
[multi-id? (format-id #'id "multi~a?" #'id)]
[quads->multi-id (format-id #'id "quads->multi~a" #'id)])
#'(begin
(define-box-type id)
(define-box-type id-break)
(define-box-type multi-id)
;; breaker
(define/contract (split-on-id-breaks x)
(quads? . -> . lists-of-quads?)
;; omit leading & trailing whitespace, because they're superfluous next to a break
(map (curryr trimf whitespace?) (filter-split x id-break?)))))]))
(define-box-type box)
(define-break-type word)
(define (word-string c) (car (quad-list c)))
(define-box-type spacer)
(define-box-type kern)
(define-box-type optical-kern)
(define-box-type flag)
(define-box-type doc)
(define-box-type input)
(define-box-type piece)
(define-box-type run)
(define-break-type page)
(define-break-type column)
(define-break-type block)
(define-break-type line)
(define (->input q) (input #f q))
(define coerce/input? (make-coercion-contract input))

@ -0,0 +1,105 @@
#lang racket/base
(require racket/class racket/contract sugar/debug sugar/cache racket/list racket/file racket/draw data/gvector)
(require "utils.rkt" "quads.rkt" "world.rkt")
(provide (all-defined-out))
(define/contract abstract-renderer%
(class/c [render (quad? . ->m . any/c)]
[render-element (quad? . ->m . quad?)])
(class object%
(super-new)
(define renderable-quads '(word box))
;; hash implementation
(define/public (render0 doc-quad)
(finalize
(let ([rendering-input (flatten-quad (setup doc-quad))])
(define page-quad-hash (make-hash))
(for ([q (in-list rendering-input)])
(when (member (quad-name q) renderable-quads)
(hash-update! page-quad-hash (quad-attr-ref q world:page-key) (λ(v) (cons q v)) null)))
(map (λ(k) (render-page (hash-ref page-quad-hash k))) (sort (hash-keys page-quad-hash) <)))))
;; gvector implementation
(define/public (render doc-quad)
(finalize
(let ([rendering-input (flatten-quad (setup doc-quad))])
(define page-quad-vector (make-gvector))
(for ([q (in-list rendering-input)] #:when (member (quad-name q) renderable-quads))
(define p (quad-attr-ref q world:page-key))
(gvector-set! page-quad-vector p (cons q (gvector-ref page-quad-vector p null))))
(for/list ([pq (in-gvector page-quad-vector)])
(render-page pq)))))
(define/public (render-element q)
(cond
[(word? q) (render-word q)]
[else q]))
(define/public (setup q) q)
(abstract render-page)
(abstract render-word)
(define/public (finalize q) q)))
(define-syntax-rule (map/send method xs)
(map (λ(x) (method x)) xs))
(define pdf-renderer%
(class abstract-renderer%
(super-new)
(send* (current-ps-setup) (set-margin 0 0) (set-scaling 1.0 1.0))
(define dc-output-port (open-output-bytes))
(define dc (new pdf-dc% [interactive #f][use-paper-bbox #f][as-eps #f]
[output dc-output-port]
[width (world:paper-width-default)][height (world:paper-height-default)]))
(define/override (setup tx)
(send* dc
(start-doc "boing")
(set-pen "black" 1 'solid)
(set-brush "black" 'transparent)) ; no fill by default
tx)
(inherit render-element)
(define/caching (make-font/caching font size style weight)
(make-font #:face font #:size size #:style style #:weight weight))
(define/override-final (render-word w)
(define word-font (quad-attr-ref/parameter w world:font-name-key))
(define word-size (quad-attr-ref/parameter w world:font-size-key))
(define word-style (quad-attr-ref/parameter w world:font-style-key))
(define word-weight (quad-attr-ref/parameter w world:font-weight-key))
(define word-color (quad-attr-ref/parameter w world:font-color-key))
(define word-background (quad-attr-ref/parameter w world:font-background-key))
(send dc set-font (make-font/caching word-font word-size word-style word-weight))
(send dc set-text-foreground (send the-color-database find-color word-color))
(define background-color (send the-color-database find-color word-background))
(if background-color ; all invalid color-string values will return #f
(send* dc (set-text-mode 'solid) (set-text-background background-color))
(send dc set-text-mode 'transparent))
(define word-text (quad-car w))
(send dc draw-text word-text (quad-attr-ref w world:x-position-key)
;; we want to align by baseline rather than top of box
;; thus, subtract ascent from y to put baseline at the y coordinate
(- (quad-attr-ref w world:y-position-key) (quad-attr-ref w world:ascent-key 0)) #t))
(define/override-final (render-page elements)
(send dc start-page)
(map/send render-element (filter-not whitespace/nbsp? elements))
(send dc end-page))
(define/override-final (finalize xs)
(send dc end-doc)
(get-output-bytes dc-output-port))
(define/public (render-to-file doc-quad path)
(define result-bytes (send this render doc-quad))
(display-to-file result-bytes path #:exists 'replace #:mode 'binary))))

@ -0,0 +1,28 @@
#lang racket/base
(require "quads.rkt" racket/file racket/string racket/function racket/list)
(provide (all-defined-out))
;(define ti (block '(measure 54 leading 18) "Meg is an ally."))
(define (ti2) (block '(leading 10 measure 400 size 13 x-align left x-align-last-line left font "Equity Text B") (block #f "Foo-d" (word '(size 13) "og ") "and " (box) "Zu" (word-break '(nb "c" bb "k-")) "kermans. Instead of a circle, the result is a picture of the code that, if it were used as an expression, would produce a circle. In other words, code is not a function, but instead a " (block '(style italic) "new syntactic form") " for creating pictures; the bit between the opening " (block '(weight bold) "parenthesis") " with code is not an expression, but instead manipulated by the code syntactic form. " (word '(font "Triplicate T4" size 22.5 color "Orchid" background "Yellow") "Bangazoom!") " This helps explain what we meant in the previous section when we said that racket provides require and the function-calling syntax.") (block-break) (block #f "Libraries are not restricted to exporting values, such as functions; they can also define new syntactic forms. In this sense, Racket isnt exactly a language at all; its more of an idea for how to structure a language so that you can extend it or create entirely new ones.")))
(define (ti3) (block '(measure 54 leading 18) "Meg is an ally."))
(define (ti4) (block '(measure 300 x-align justify x-align-last-line right leading 18) "In this Madagascarian hoo-ha, Racket isnt exactly a language at all"))
(define (ti5) (block '(measure 240 font "Equity Text B" leading 16 size 13.5 x-align justify x-align-last-line left) (box '(width 15)) (block #f (block '(weight bold font "Equity Caps B") "Hotdogs, My Fellow Americans.") " This " (block '(no-break #t) "is some truly") " bullshit generated from my typesetting system, which is called Quad. Im writing this in a source file in DrRacket. When I click [Run], a PDF pops out. Not bad\u200a\u200aand no LaTeX needed. Quad, however, does use the fancy linebreaking algorithm developed for TeX. (It also includes a faster linebreaking algorithm for when speed is more important than quality.) Of course, it can also handle " (block '(font "Triplicate C4") "different fonts,") (block '(style italic) " styles, ") (word '(size 14 weight bold) "and sizes-") " within the same line. As you can see, it can also justify paragraphs." (block-break) (box '(width 15)) (block #f "“Each horizontal row represents an OS-level thread, and the colored dots represent important events in the execution of the program (they are color-coded to distinguish one event type from another). The upper-left blue dot in the timeline represents the futures creation. The future executes for a brief period (represented by a green bar in the second line) on thread 1, and then pauses to allow the runtime thread to perform a future-unsafe operation.") (block-break) (box '(width 15))(block #f "In the Racket implementation, future-unsafe operations fall into one of two categories. A blocking operation halts the evaluation of the future, and will not allow it to continue until it is touched. After the operation completes within touch, the remainder of the futures work will be evaluated sequentially by the runtime thread. A synchronized operation also halts the future, but the runtime thread may perform the operation at any time and, once completed, the future may continue running in parallel. Memory allocation and JIT compilation are two common examples of synchronized operations."))))
(define (ti6) (block '(font "Equity Text B" measure 210 leading 14 size 20 x-align justify x-align-last-line left)
"Firstlinerhere" (column-break) "Secondlinerhere" (column-break) "Thirdlinerhere"))
(define (make-jude jude-text)
(define jude-blocks (map (λ(s) (regexp-replace* #rx"\n" s " ")) (string-split (file->string jude-text) "\n\n")))
(apply block '(font "Equity Text B" measure 360 leading 14 column-count 1 column-gutter 10 size 11.5 x-align justify x-align-last-line left) (add-between (map (λ(jb) (block #f (box '(width 10)) (optical-kern) jb)) jude-blocks) (block-break))))
(define (jude) (make-jude "texts/jude.txt"))
(define (jude0) (make-jude "texts/jude0.txt"))
(define (judebig) (make-jude "texts/judebig.txt"))
(define (jude1) (block '(font "Equity Text B" measure 150 leading 14 column-count 4 size 11 x-align justify x-align-last-line left) "this—is—a—test—of—em—dashes—breakable—or—not?"))

File diff suppressed because one or more lines are too long

@ -0,0 +1,194 @@
#lang racket
(require math/statistics sugar racket/serialize plot)
(require (except-in "quads.rkt" line) "utils.rkt" "wrap.rkt" "world.rkt" "measure.rkt" "logger.rkt" "main.rkt")
(define+provide (make-wrap-proc-bps
#:make-pieces-proc make-pieces-proc
#:measure-quad-proc measure-quad-proc
#:compose-line-proc compose-line-proc
#:fast-measure-pieces-proc [fast-measure-pieces-proc (compose1 measure-quad-proc compose-line-proc)]
#:find-breakpoints-proc find-breakpoints-proc)
(λ(qs [measure #f])
(let* ([measure (or measure (quad-attr-ref/parameter (car qs) world:measure-key))]
[qs (if (quad-has-attr? (car qs) world:measure-key)
qs
(map (curryr quad-attr-set world:measure-key measure) qs))])
(log-quad-debug "wrapping on measure = ~a" measure)
(define pieces (make-pieces-proc qs))
(log-quad-debug "avg piece length for breakpoints = ~a"
(/ (for/sum ([p (in-list pieces)])
(for/sum ([q (in-list (quad-list p))])
(define str (quad->string q))
(if (equal? str "")
(quad-attr-ref q world:width-key 0)
(apply measure-text (quad->string q) (font-attributes-with-defaults q)))))
(length pieces)))
(define bps (find-breakpoints-proc (list->vector pieces) (+ 0.0 measure)))
(values pieces bps (map (curryr compose-line-proc measure-quad-proc) (break-at pieces bps))))))
;; wrap proc based on greedy proc
(define wrap-first-bps (make-wrap-proc-bps
#:make-pieces-proc (make-caching-proc make-pieces)
#:measure-quad-proc quad-width
#:compose-line-proc pieces->line
#:fast-measure-pieces-proc measure-potential-line
#:find-breakpoints-proc first-fit-proc))
;; wrap proc based on penalty function
(define wrap-best-bps (make-wrap-proc-bps
#:make-pieces-proc (make-caching-proc make-pieces)
#:measure-quad-proc quad-width
#:compose-line-proc pieces->line
#:fast-measure-pieces-proc measure-potential-line
#:find-breakpoints-proc best-fit-proc))
(define ti (block '(measure 54 leading 18) "Meg is an ally."))
(define (looseness-spread lines)
(if (<= (length lines) 1)
0
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
(define measures (map (λ(line) (quad-attr-ref line world:line-looseness-key 0)) lines-to-measure))
(round-float (- (apply max measures) (apply min measures))))))
(define (geometric-mean lines)
(if (<= (length lines) 1)
0
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
(define measures (map (λ(line) (quad-attr-ref line world:line-looseness-key 0)) lines-to-measure))
(expt (apply * measures) (/ 1 (length measures))))))
(define (looseness-stddev lines)
(if (<= (length lines) 1)
0
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
(define measures (map (λ(line) (quad-attr-ref line world:line-looseness-key 0)) lines-to-measure))
(stddev measures))))
(define (looseness-var lines)
(if (<= (length lines) 1)
0
(let ([lines-to-measure (drop-right lines 1)]) ; exclude last line from looseness calculation
(define measures (map (λ(line) (quad-attr-ref line world:line-looseness-key 0)) lines-to-measure))
(variance measures))))
(define (list->hash0 . xs)
(define mh (make-hash))
(for ([(k v) (in-hash (apply hash xs))])
(hash-set! mh k v))
mh)
(define (piece-length p)
(apply + (map quad-width-fast (quad-list p))))
(define (trial wrap-proc ti measure)
(match-define-values (result time _ _) (time-apply wrap-proc (append (list ti) (list measure))))
(match-define (list pieces bps lines) result)
(define line-count (length lines))
(define piece-count (length pieces))
(define avg-piece-length (/ (apply + (map piece-length pieces)) (length pieces)))
(define avg-looseness (average-looseness lines))
(define looseness-sd (looseness-stddev lines))
(define looseness-variance (looseness-var lines))
(define spread (looseness-spread lines))
(define looses (map (λ(line) (quad-attr-ref line world:line-looseness-key 0)) lines))
(hash 'bps bps 'time time 'line-count line-count 'looses looses
'piece-count piece-count 'avg-piece-length avg-piece-length
'avg-looseness avg-looseness 'looseness-spread spread
'looseness-sd looseness-sd
'looseness-variance looseness-variance))
(define (improved? h1 h2)
(define h1-bps (if (hash? h1) (hash-ref h1 'bps) h1))
(define h2-bps (if (hash? h2) (hash-ref h2 'bps) h2))
(define min-length (min (length h1-bps) (length h2-bps)))
(apply + (map abs (map - (take h1-bps min-length) (take h2-bps min-length)))))
(define (trial-set measure ti)
(define text (quad->string ti))
(define ti-unhyphenated (split-quad ti))
(define fu (trial wrap-first-bps ti-unhyphenated measure))
(define bu (trial wrap-best-bps ti-unhyphenated measure))
(define ti-hyphenated (split-quad (hyphenate-quad ti)))
(define fh (trial wrap-first-bps ti-hyphenated measure))
(define bh (trial wrap-best-bps ti-hyphenated measure))
(hash 'fu fu 'bu bu 'fh fh 'bh bh
'text text
'measure measure
'fh-improved (improved? fu fh)
'bu-improved (improved? fu bu)
'bh-improved (improved? fh bh)))
(define (make-blocks textfile)
(define strings (filter (λ(s) (> (string-length s) 10)) (map (λ(s) (regexp-replace* #rx"\n" s " ")) (map string-trim (string-split (file->string textfile) "\n\n")))))
(map (λ(t) (block '(font "Equity Text B" leading 14 column-count 1 column-gutter 10 size 11.5 x-align justify x-align-last-line left) t)) strings))
(define (refresh [filename "jude0.txt"] [measures '(150 180 210 240 270 300 330 360 390)])
(define paragraphs (make-blocks filename))
;; only use paragraphs > 2 lines because last line is disregarded for looseness spread calculation
(define results (filter
(λ(rec) (> (hash-ref (hash-ref rec 'fu) 'line-count) 2)) (append-map (compose1 (λ(m) (map (λ(p) (trial-set m p)) paragraphs)) (λ(m) (report m))) measures)))
(write-to-file (serialize results) "stats-data.txt" #:exists 'replace))
(define (load-data-records)
(deserialize (file->value "stats-data.txt")))
(define recs (load-data-records))
(define (plot-lists xs ys zs kx ky kz)
(parameterize ([plot-width 700]
[plot-height 700]
[plot-x-label kx]
[plot-y-label ky]
[plot-z-label kz])
(plot3d
;#:x-max 2 #:x-min -2
#:y-min 140 #:y-max 400
#:z-max 1.5
#:angle 0
#:altitude 32
(points3d (map vector xs ys zs)
#:sym 'fullcircle7
#:alpha 0.1
#:color 42))))
(define (fu-formula rec)
(define pieces-per-line (/ (hash-ref (hash-ref rec 'fu) 'piece-count)
(hash-ref (hash-ref rec 'fu) 'line-count) 1.0))
(+ 2.2 (log (abs (hash-ref (hash-ref rec 'fu) 'looseness-sd)))
(* 0.09 pieces-per-line)))
(define (geo-mean rec)
(define looses (hash-ref (hash-ref rec 'fu) 'looses))
(expt (abs (apply * looses)) (/ 1 (length looses))))
(define (magic2 rec)
(define looses (map abs (hash-ref (hash-ref rec 'fu) 'looses)))
(expt (log (+ 1 (/ (variance looses) (expt (mean looses) 2)))) 0.5))
(define (plot-it)
(define-values (helped or-not) (partition (λ(rec) (< 0 (hash-ref rec 'bu-improved))) recs))
(define-values (fu-pos fu-neg) (partition (λ(rec) (> (fu-formula rec) 0)) recs))
(report* (length helped) (length or-not) (length fu-pos) (length fu-neg))
(let ([recs recs])
(plot-lists
(map fu-formula recs)
(map (λ(rec) (hash-ref rec 'measure)) recs)
(map (λ(rec) (if (= 0 (hash-ref rec 'bu-improved)) 0 1)) recs) "fu-formula" "measure" "improve?")))
(plot-it)
(define (looseness-improvement rec)
(/ (abs (- (hash-ref (hash-ref rec 'bu) 'avg-looseness) (hash-ref (hash-ref rec 'fu) 'avg-looseness)))
(hash-ref (hash-ref rec 'bu) 'avg-looseness)))
(define zs (filter positive? (sort (map looseness-improvement recs) <)))

@ -0,0 +1,79 @@
#lang racket/base
(require racket/list racket/function rackunit "ocm.rkt" sugar)
(define (vector-range n)
(build-vector n identity))
(define (random-ascending len start finish)
(if (= len 0)
null
(let ([r (+ start (random (- finish start)))])
(cons r (random-ascending (sub1 len) r finish)))))
(define (concave-list len min [lowval 0][highval 100])
(append (reverse (random-ascending min lowval highval)) (random-ascending (- len min) lowval highval)))
(define (make-matrix2 rows cols)
(define row-minima-indexes (random-ascending rows 0 cols))
(reverse (for/list ([row-minima-index (in-list row-minima-indexes)])
(concave-list cols row-minima-index 20 400))))
(define (make-matrix rows cols)
(define seed (for/list ([i (in-range (max rows cols))])
(random 100)))
(for/list ([i (in-range rows)])
(for/list ([j (in-range cols)])
(if (< i j)
(apply + (sublist seed i (add1 j)))
(apply + (sublist seed j (add1 i)))))))
(define (make-matrix-proc m [is (range (length m))] [js (range (length (car m)))])
(let ([ipairs (apply hash (flatten (map cons is (range (length is)))))]
[jpairs (apply hash (flatten (map cons js (range (length js)))))])
(λ(i j)
(define my-i (hash-ref ipairs i))
(define my-j (hash-ref jpairs j))
(with-handlers [(exn:fail? (λ(exn) (* -1 i)))]
(list-ref (list-ref m my-i) my-j)))))
(define (compare-reductions m)
(check-equal?
(reduce2 (vector-range (length m)) (vector-range (length (car m))) (make-matrix-proc m) identity)
(reduce (vector-range (length m)) (vector-range (length (car m))) (make-matrix-proc m) identity)))
(define (do-it x)
(repeat x
(define rows (+ 2 (random 40)))
(define cols (+ 2 (random rows)))
(define m (make-matrix rows cols))
(check-true (smawky? m))
(compare-reductions m)))
(define me '((25 21 13 10 20 13 19 35 37 41 58 66 82 99 124 133 156 178) (42 35 26 20 29 21 25 37 36 39 56 64 76 91 116 125 146 164) (57 48 35 28 33 24 28 40 37 37 54 61 72 83 107 113 131 146) (78 65 51 42 44 35 38 48 42 42 55 61 70 80 100 106 120 135) (90 76 58 48 49 39 42 48 39 35 47 51 56 63 80 86 97 110) (103 85 67 56 55 44 44 49 39 33 41 44 49 56 71 75 84 96) (123 105 86 75 73 59 57 62 51 44 50 52 55 59 72 74 80 92) (142 123 100 86 82 65 61 62 50 43 47 45 46 46 58 59 65 73) (151 130 104 88 80 59 52 49 37 29 29 24 23 20 28 25 31 39)))
(define (bug-test bugmatrix bugrows bugcols)
(define bugproc (make-matrix-proc bugmatrix bugrows bugcols))
(check-equal? (reduce (list->vector bugrows) (list->vector bugcols) bugproc identity)
(reduce2 (list->vector bugrows) (list->vector bugcols) bugproc identity)))
(bug-test '((19496.0 14025.0 7134.0 5027.0) (108793.0 102427.0 93819.0 90268.0) (101409.0 93357.0 81509.0 75236.0) (106662.0 93357.0 71417.0 56665.0))
'(0 1 2 3)
'(4 5 6 7))
(bug-test '((25 42 57 78 90 103 123 142 151) (21 35 48 65 76 85 105 123 130) (13 26 35 51 58 67 86 100 104) (10 20 28 42 48 56 75 86 88) (20 29 33 44 49 55 73 82 80) (13 21 24 35 39 44 59 65 59) (19 25 28 38 42 44 57 61 52) (35 37 40 48 48 49 62 62 49) (37 36 37 42 39 39 51 50 37) (41 39 37 42 35 33 44 43 29) (58 56 54 55 47 41 50 47 29) (66 64 61 61 51 44 52 45 24) (82 76 72 70 56 49 55 46 23) (99 91 83 80 63 56 59 46 20) (124 116 107 100 80 71 72 58 28) (133 125 113 106 86 75 74 59 25) (156 146 131 120 97 84 80 65 31) (178 164 146 135 110 96 92 73 39))
'(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17)
'(0 1 2 3 4 5 6 7 8))
(do-it 10)

@ -0,0 +1,93 @@
#lang racket/base
(require "utils.rkt" "wrap.rkt" "quads.rkt" "world.rkt" racket/list racket/format)
(require rackunit)
(check-equal? (join-attrs (list (box '(width 10)) (box #f "foobar") (hash 'x 10) (list 'width 20)))
(list (cons 'width 10) (cons 'x 10) (cons 'width 20)))
(check-equal? (flatten-attrs (hash 'foo 'bar) (hash 'x 10)) (apply hash '(foo bar x 10)))
(check-equal? (flatten-attrs (hash 'x -5) (hash 'x 10)) (apply hash '(x 5)))
(check-equal? (merge-attrs (hash 'x -5) (hash 'x 10)) (apply hash '(x 10)))
(check-equal? (gather-common-attrs (list (box '(foo bar)) (box '(foo bar goo bar zam zino)) (box '(foo bar)))) '(foo bar))
(check-equal? (gather-common-attrs (list (box) (box '(foo bar goo bar zam zino)) (box '(foo bar)))) #f)
(check-equal? (gather-common-attrs (list (box '(width bar)) (box '(width bar)) (box '(width bar)))) #f)
(define b1 (box '(x 10) "1st" (box '(foo bar) "2nd") "3rd"))
(define b1-flattened (list (box '(x 10) "1st") (box '(x 10 foo bar) "2nd") (box '(x 10) "3rd")))
(define b3 (box #f (word) (line) (page)))
(check-true (sequence? b3))
;(check-equal? (for/list ([i (in-quad b3)]) i) (list (word) (line) (page)))
(check-true (quad= (flatten-quad b1) b1-flattened))
(define b2 (box '(x 10) (spacer) (box '(x 15) (spacer) (spacer)) (spacer)))
(define b2-flattened (list (spacer '(x 10)) (spacer '(x 25)) (spacer '(x 25)) (spacer '(x 10))))
(check-true (quad= (flatten-quad b2) b2-flattened))
(check-true (quad= (split-quad b2) b2-flattened))
(check-true (quad= (flatten-quad (box '(foo 10) (spacer) (box) (spacer))) (list (spacer '(foo 10)) (box '(foo 10)) (spacer '(foo 10)))))
(check-equal? (compute-absolute-positions (page '(x 100 y 100) (line '(x 10 y 10) (word '(x 1 y 1) "hello")
(word '(x 2 y 2) "world"))))
(page '(y 100.0 x 100.0) (line '(y 110.0 x 110.0) (word '(y 111.0 x 111.0) "hello")(word '(y 112.0 x 112.0) "world"))))
(define b2-exploded (list (word '(x 10) "1") (word '(x 10) "s") (word '(x 10) "t") (word '(x 10 foo bar) "2") (word '(x 10 foo bar) "n") (word '(x 10 foo bar) "d") (word '(x 10) "3") (word '(x 10) "r") (word '(x 10) "d")))
(check-true (quad= (split-quad b1) b2-exploded))
(let ([world:minimum-last-line-chars 0])
(check-equal? (map (compose1 quad-list last quad-list) (make-pieces (split-quad (block #f "Foo-dog and " (box) " mas\u00adsachu.")))) '(("o") ("g") ("d") () ("s") ("."))))
(check-false (quad-has-attr? (box) 'foo))
(check-true (quad-has-attr? (box '(foo bar)) 'foo))
(check-equal? (quad-attr-set (box '(foo bar)) 'foo 'zam) (box '(foo zam)))
(check-equal? (quad-attr-set (box #f) 'foo 'zam) (box '(foo zam)))
(check-equal? (quad-attr-set* (box #f) 'foo 'zam 'bar 'boo) (box '(foo zam bar boo)))
(check-equal? (quad-attr-set* (box '(foo bar)) 'foo 'zam 'bar 'boo) (box '(foo zam bar boo)))
(check-equal? (quad-attr-remove (box '(foo bar zim zam)) 'foo) (box '(zim zam)))
(check-equal? (quad-attr-remove (box #f) 'zim) (box))
(check-equal? (quad-attr-remove* (box '(foo bar zim zam ding dong)) 'foo 'ding) (box '(zim zam)))
(check-equal? (quad-attr-remove* (box #f) 'zim) (box))
(check-true (quad-ends-with? (box #f "foo") "foo"))
(check-false (quad-ends-with? (box #f "foo") "food"))
(check-false (quad-ends-with? (box #f (box #f "foo")) "food"))
(check-true (quad-ends-with? (box #f (box #f "foo")) "foo"))
(check-true (quad-ends-with? (box #f (box #f "foo")) "o"))
(check-true (quad-ends-with? (box #f (box #f (box #f (box #f (box #f "foo-"))))) "-"))
(check-equal? (quad-append (box #f "foo") "bar") (box #f "foo" "bar"))
(check-equal? (quad-append (box #f "foo") (box #f "bar")) (box #f "foo" (box #f "bar")))
(check-equal? (quad-last-char (box #f (box #f "foo") "food")) "d")
(check-equal? (quad-last-char (box #f (box #f "foo") "")) "o")
(check-equal? (quad-last-char (box #f "foo")) "o")
(check-false (quad-last-char (box)))
(check-equal? (quad-first-char (box #f (box #f "foo") "bar")) "f")
(check-equal? (quad-first-char (box #f "" (box #f "foo") "bar")) "f")
(check-equal? (quad-first-char (box #f "foo")) "f")
(check-false (quad-first-char (box)))
(check-equal? (quad->string (box '(width 100) "foo")) "foo")
(check-equal? (quad->string (box '(width 100) "foo" (box '(width 100) "bar"))) "foobar")
(check-equal? (quad->string (box '(width 100) "foo" (box '(width 100) "bar") "ino")) "foobarino")
(check-equal? (quad->string (box '(width 100) (box '(width 100)))) "")
(check-false (whitespace? (~a #\u00A0)))
(check-true (whitespace/nbsp? (~a #\u00A0)))
(check-true (whitespace/nbsp? (word #f (~a #\u00A0))))
(check-false (whitespace? (format " ~a " #\u00A0)))
(check-true (whitespace/nbsp? (format " ~a " #\u00A0)))
(define funny-unicode-spaces (map ~a (list #\u2000 #\u2007 #\u2009 #\u200a #\u202f)))
(check-true (andmap whitespace? funny-unicode-spaces))
(check-true (andmap whitespace/nbsp? funny-unicode-spaces))

@ -0,0 +1,235 @@
The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it. The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house. Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it.

@ -0,0 +1,429 @@
The schoolmaster was leaving the village, and everybody seemed sorry.
The miller at Cresscombe lent him the small white tilted cart and
horse to carry his goods to the city of his destination, about twenty
miles off, such a vehicle proving of quite sufficient size for the
departing teachers effects. For the schoolhouse had been partly
furnished by the managers, and the only cumbersome article possessed
by the master, in addition to the packing-case of books, was a
cottage piano that he had bought at an auction during the year in
which he thought of learning instrumental music. But the enthusiasm
having waned he had never acquired any skill in playing, and the
purchased article had been a perpetual trouble to him ever since in
moving house.
The rector had gone away for the day, being a man who disliked the
sight of changes. He did not mean to return till the evening, when
the new school-teacher would have arrived and settled in, and
everything would be smooth again.
The blacksmith, the farm bailiff, and the schoolmaster himself were
standing in perplexed attitudes in the parlour before the instrument.
The master had remarked that even if he got it into the cart he
should not know what to do with it on his arrival at Christminster,
the city he was bound for, since he was only going into temporary
lodgings just at first.
A little boy of eleven, who had been thoughtfully assisting in the
packing, joined the group of men, and as they rubbed their chins he
spoke up, blushing at the sound of his own voice: “Aunt have got a
great fuel-house, and it could be put there, perhaps, till youve
found a place to settle in, sir.”
“A proper good notion,” said the blacksmith.
It was decided that a deputation should wait on the boys aunt—an
old maiden resident—and ask her if she would house the piano till
Mr. Phillotson should send for it. The smith and the bailiff started
to see about the practicability of the suggested shelter, and the boy
and the schoolmaster were left standing alone.
“Sorry I am going, Jude?” asked the latter kindly.
Tears rose into the boys eyes, for he was not among the regular day
scholars, who came unromantically close to the schoolmasters life,
but one who had attended the night school only during the present
teachers term of office. The regular scholars, if the truth must
be told, stood at the present moment afar off, like certain historic
disciples, indisposed to any enthusiastic volunteering of aid.
The boy awkwardly opened the book he held in his hand, which Mr.
Phillotson had bestowed on him as a parting gift, and admitted that
he was sorry.
“So am I,” said Mr. Phillotson.
“Why do you go, sir?” asked the boy.
“Ah—that would be a long story. You wouldnt understand my reasons,
Jude. You will, perhaps, when you are older.”
“I think I should now, sir.”
“Well—dont speak of this everywhere. You know what a university
is, and a university degree? It is the necessary hallmark of a man
who wants to do anything in teaching. My scheme, or dream, is to be
a university graduate, and then to be ordained. By going to live at
Christminster, or near it, I shall be at headquarters, so to speak,
and if my scheme is practicable at all, I consider that being on the
spot will afford me a better chance of carrying it out than I should
have elsewhere.”
The smith and his companion returned. Old Miss Fawleys fuel-house
was dry, and eminently practicable; and she seemed willing to give
the instrument standing-room there. It was accordingly left in
the school till the evening, when more hands would be available for
removing it; and the schoolmaster gave a final glance round.
The boy Jude assisted in loading some small articles, and at nine
oclock Mr. Phillotson mounted beside his box of books and other
impedimenta, and bade his friends good-bye.
“I shant forget you, Jude,” he said, smiling, as the cart moved off.
“Be a good boy, remember; and be kind to animals and birds, and read
all you can. And if ever you come to Christminster remember you hunt
me out for old acquaintance sake.”
The cart creaked across the green, and disappeared round the corner
by the rectory-house. The boy returned to the draw-well at the edge
of the greensward, where he had left his buckets when he went to help
his patron and teacher in the loading. There was a quiver in his lip
now and after opening the well-cover to begin lowering the bucket he
paused and leant with his forehead and arms against the framework,
his face wearing the fixity of a thoughtful childs who has felt the
pricks of life somewhat before his time. The well into which he was
looking was as ancient as the village itself, and from his present
position appeared as a long circular perspective ending in a shining
disk of quivering water at a distance of a hundred feet down.
There was a lining of green moss near the top, and nearer still the
harts-tongue fern.
He said to himself, in the melodramatic tones of a whimsical boy,
that the schoolmaster had drawn at that well scores of times on a
morning like this, and would never draw there any more. “Ive seen
him look down into it, when he was tired with his drawing, just as I
do now, and when he rested a bit before carrying the buckets home!
But he was too clever to bide here any longer—a small sleepy place
like this!”
A tear rolled from his eye into the depths of the well. The morning
was a little foggy, and the boys breathing unfurled itself as
a thicker fog upon the still and heavy air. His thoughts were
interrupted by a sudden outcry:
“Bring on that water, will ye, you idle young harlican!”
It came from an old woman who had emerged from her door towards the
garden gate of a green-thatched cottage not far off. The boy quickly
waved a signal of assent, drew the water with what was a great effort
for one of his stature, landed and emptied the big bucket into his
own pair of smaller ones, and pausing a moment for breath, started
with them across the patch of clammy greensward whereon the well
stood—nearly in the centre of the little village, or rather hamlet
of Marygreen.
It was as old-fashioned as it was small, and it rested in the lap of
an undulating upland adjoining the North Wessex downs. Old as it
was, however, the well-shaft was probably the only relic of the local
history that remained absolutely unchanged. Many of the thatched
and dormered dwelling-houses had been pulled down of late years, and
many trees felled on the green. Above all, the original church,
hump-backed, wood-turreted, and quaintly hipped, had been taken
down, and either cracked up into heaps of road-metal in the lane, or
utilized as pig-sty walls, garden seats, guard-stones to fences, and
rockeries in the flower-beds of the neighbourhood. In place of it
a tall new building of modern Gothic design, unfamiliar to English
eyes, had been erected on a new piece of ground by a certain
obliterator of historic records who had run down from London and back
in a day. The site whereon so long had stood the ancient temple to
the Christian divinities was not even recorded on the green and level
grass-plot that had immemorially been the churchyard, the obliterated
graves being commemorated by eighteen-penny cast-iron crosses
warranted to last five years.
II
Slender as was Jude Fawleys frame he bore the two brimming
house-buckets of water to the cottage without resting. Over the door
was a little rectangular piece of blue board, on which was painted
in yellow letters, “Drusilla Fawley, Baker.” Within the little lead
panes of the window—this being one of the few old houses left—were
five bottles of sweets, and three buns on a plate of the willow
pattern.
While emptying the buckets at the back of the house he could hear an
animated conversation in progress within-doors between his great-aunt,
the Drusilla of the sign-board, and some other villagers. Having
seen the school-master depart, they were summing up particulars of
the event, and indulging in predictions of his future.
“And whos he?” asked one, comparatively a stranger, when the boy
entered.
“Well ye med ask it, Mrs. Williams. Hes my great-nephew—come since
you was last this way.” The old inhabitant who answered was a tall,
gaunt woman, who spoke tragically on the most trivial subject, and
gave a phrase of her conversation to each auditor in turn. “He come
from Mellstock, down in South Wessex, about a year ago—worse luck
for n, Belinda” (turning to the right) “where his father was living,
and was took wi the shakings for death, and died in two days, as you
know, Caroline” (turning to the left). “It would ha been a blessing
if Goddy-mighty had took thee too, wi thy mother and father, poor
useless boy! But Ive got him here to stay with me till I can see
whats to be done with un, though I am obliged to let him earn any
penny he can. Just now hes a-scaring of birds for Farmer Troutham.
It keeps him out of mischty. Why do ye turn away, Jude?” she
continued, as the boy, feeling the impact of their glances like slaps
upon his face, moved aside.
The local washerwoman replied that it was perhaps a very good plan of
Miss or Mrs. Fawleys (as they called her indifferently) to have him
with her—”to kip ee company in your loneliness, fetch water, shet
the winder-shetters o nights, and help in the bit o baking.”
Miss Fawley doubted it.... “Why didnt ye get the schoolmaster to
take ee to Christminster wi un, and make a scholar of ee,” she
continued, in frowning pleasantry. “Im sure he couldnt ha took a
better one. The boy is crazy for books, that he is. It runs in our
family rather. His cousin Sue is just the same—so Ive heard; but
I have not seen the child for years, though she was born in this
place, within these four walls, as it happened. My niece and her
husband, after they were married, didn get a house of their own for
some year or more; and then they only had one till—Well, I wont go
into that. Jude, my child, dont you ever marry. Tisnt for the
Fawleys to take that step any more. She, their only one, was like
a child o my own, Belinda, till the split come! Ah, that a little
maid should know such changes!”
Jude, finding the general attention again centering on himself, went
out to the bakehouse, where he ate the cake provided for his
breakfast. The end of his spare time had now arrived, and emerging
from the garden by getting over the hedge at the back he pursued a
path northward, till he came to a wide and lonely depression in the
general level of the upland, which was sown as a corn-field. This
vast concave was the scene of his labours for Mr Troutham the farmer,
and he descended into the midst of it.
The brown surface of the field went right up towards the sky all
round, where it was lost by degrees in the mist that shut out the
actual verge and accentuated the solitude. The only marks on the
uniformity of the scene were a rick of last years produce standing
in the midst of the arable, the rooks that rose at his approach, and
the path athwart the fallow by which he had come, trodden now by he
hardly knew whom, though once by many of his own dead family.
“How ugly it is here!” he murmured.
The fresh harrow-lines seemed to stretch like the channellings in
a piece of new corduroy, lending a meanly utilitarian air to the
expanse, taking away its gradations, and depriving it of all history
beyond that of the few recent months, though to every clod and stone
there really attached associations enough and to spare—echoes of
songs from ancient harvest-days, of spoken words, and of sturdy
deeds. Every inch of ground had been the site, first or last,
of energy, gaiety, horse-play, bickerings, weariness. Groups of
gleaners had squatted in the sun on every square yard. Love-matches
that had populated the adjoining hamlet had been made up there
between reaping and carrying. Under the hedge which divided the
field from a distant plantation girls had given themselves to lovers
who would not turn their heads to look at them by the next harvest;
and in that ancient cornfield many a man had made love-promises to
a woman at whose voice he had trembled by the next seed-time after
fulfilling them in the church adjoining. But this neither Jude nor
the rooks around him considered. For them it was a lonely place,
possessing, in the one view, only the quality of a work-ground, and
in the other that of a granary good to feed in.
The boy stood under the rick before mentioned, and every few seconds
used his clacker or rattle briskly. At each clack the rooks left off
pecking, and rose and went away on their leisurely wings, burnished
like tassets of mail, afterwards wheeling back and regarding him
warily, and descending to feed at a more respectful distance.
He sounded the clacker till his arm ached, and at length his heart
grew sympathetic with the birds thwarted desires. They seemed, like
himself, to be living in a world which did not want them. Why should
he frighten them away? They took upon more and more the aspect of
gentle friends and pensioners—the only friends he could claim as
being in the least degree interested in him, for his aunt had often
told him that she was not. He ceased his rattling, and they alighted
anew.
“Poor little dears!” said Jude, aloud. “You SHALL have some dinner—
you shall. There is enough for us all. Farmer Troutham can afford
to let you have some. Eat, then my dear little birdies, and make a
good meal!”
They stayed and ate, inky spots on the nut-brown soil, and Jude
enjoyed their appetite. A magic thread of fellow-feeling united his
own life with theirs. Puny and sorry as those lives were, they much
resembled his own.
His clacker he had by this time thrown away from him, as being a mean
and sordid instrument, offensive both to the birds and to himself
as their friend. All at once he became conscious of a smart blow
upon his buttocks, followed by a loud clack, which announced to his
surprised senses that the clacker had been the instrument of offence
used. The birds and Jude started up simultaneously, and the dazed
eyes of the latter beheld the farmer in person, the great Troutham
himself, his red face glaring down upon Judes cowering frame, the
clacker swinging in his hand.
“So its Eat my dear birdies, is it, young man? Eat, dear
birdies, indeed! Ill tickle your breeches, and see if you say,
Eat, dear birdies, again in a hurry! And youve been idling at the
schoolmasters too, instead of coming here, hant ye, hey? Thats
how you earn your sixpence a day for keeping the rooks off my corn!”
Whilst saluting Judes ears with this impassioned rhetoric, Troutham
had seized his left hand with his own left, and swinging his slim
frame round him at arms-length, again struck Jude on the hind parts
with the flat side of Judes own rattle, till the field echoed with
the blows, which were delivered once or twice at each revolution.
“Dont ee, sir—please dont ee!” cried the whirling child, as
helpless under the centrifugal tendency of his person as a hooked
fish swinging to land, and beholding the hill, the rick, the
plantation, the path, and the rooks going round and round him in an
amazing circular race. “I—I sir—only meant that—there was a good
crop in the ground—I saw em sow it—and the rooks could have a
little bit for dinner—and you wouldnt miss it, sir—and Mr.
Phillotson said I was to be kind to em—oh, oh, oh!”
This truthful explanation seemed to exasperate the farmer even more
than if Jude had stoutly denied saying anything at all, and he still
smacked the whirling urchin, the clacks of the instrument continuing
to resound all across the field and as far as the ears of distant
workers—who gathered thereupon that Jude was pursuing his business
of clacking with great assiduity—and echoing from the brand-new
church tower just behind the mist, towards the building of which
structure the farmer had largely subscribed, to testify his love for
God and man.
Presently Troutham grew tired of his punitive task, and depositing
the quivering boy on his legs, took a sixpence from his pocket and
gave it him in payment for his days work, telling him to go home and
never let him see him in one of those fields again.
Jude leaped out of arms reach, and walked along the trackway
weeping—not from the pain, though that was keen enough; not from the
perception of the flaw in the terrestrial scheme, by which what was
good for Gods birds was bad for Gods gardener; but with the awful
sense that he had wholly disgraced himself before he had been a year
in the parish, and hence might be a burden to his great-aunt for
life.
With this shadow on his mind he did not care to show himself in the
village, and went homeward by a roundabout track behind a high hedge
and across a pasture. Here he beheld scores of coupled earthworms
lying half their length on the surface of the damp ground, as
they always did in such weather at that time of the year. It was
impossible to advance in regular steps without crushing some of them
at each tread.
Though Farmer Troutham had just hurt him, he was a boy who could not
himself bear to hurt anything. He had never brought home a nest of
young birds without lying awake in misery half the night after, and
often reinstating them and the nest in their original place the next
morning. He could scarcely bear to see trees cut down or lopped,
from a fancy that it hurt them; and late pruning, when the sap was up
and the tree bled profusely, had been a positive grief to him in his
infancy. This weakness of character, as it may be called, suggested
that he was the sort of man who was born to ache a good deal before
the fall of the curtain upon his unnecessary life should signify that
all was well with him again. He carefully picked his way on tiptoe
among the earthworms, without killing a single one.
On entering the cottage he found his aunt selling a penny loaf to a
little girl, and when the customer was gone she said, “Well, how do
you come to be back here in the middle of the morning like this?”
“Im turned away.”
“What?”
“Mr. Troutham have turned me away because I let the rooks have a few
peckings of corn. And theres my wages—the last I shall ever hae!”
He threw the sixpence tragically on the table.
“Ah!” said his aunt, suspending her breath. And she opened upon him
a lecture on how she would now have him all the spring upon her hands
doing nothing. “If you cant skeer birds, what can ye do? There!
dont ye look so deedy! Farmer Troutham is not so much better than
myself, come to that. But tis as Job said, Now they that are
younger than I have me in derision, whose fathers I would have
disdained to have set with the dogs of my flock. His father was my
fathers journeyman, anyhow, and I must have been a fool to let ee
go to work for n, which I shouldnt ha done but to keep ee out of
mischty.”
More angry with Jude for demeaning her by coming there than for
dereliction of duty, she rated him primarily from that point of view,
and only secondarily from a moral one.
“Not that you should have let the birds eat what Farmer Troutham
planted. Of course you was wrong in that. Jude, Jude, why didstnt
go off with that schoolmaster of thine to Christminster or somewhere?
But, oh no—poor ornary child—there never was any sprawl on thy
side of the family, and never will be!”
“Where is this beautiful city, Aunt—this place where Mr. Phillotson
is gone to?” asked the boy, after meditating in silence.
“Lord! you ought to know where the city of Christminster is. Near a
score of miles from here. It is a place much too good for you ever
to have much to do with, poor boy, Im a-thinking.”
“And will Mr. Phillotson always be there?”
“How can I tell?”
“Could I go to see him?”
“Lord, no! You didnt grow up hereabout, or you wouldnt ask such as
that. Weve never had anything to do with folk in Christminster, nor
folk in Christminster with we.”
Jude went out, and, feeling more than ever his existence to be an
undemanded one, he lay down upon his back on a heap of litter near
the pig-sty. The fog had by this time become more translucent, and
the position of the sun could be seen through it. He pulled his
straw hat over his face, and peered through the interstices of the
plaiting at the white brightness, vaguely reflecting. Growing up
brought responsibilities, he found. Events did not rhyme quite as
he had thought. Natures logic was too horrid for him to care for.
That mercy towards one set of creatures was cruelty towards another
sickened his sense of harmony. As you got older, and felt yourself
to be at the centre of your time, and not at a point in its
circumference, as you had felt when you were little, you were seized
with a sort of shuddering, he perceived. All around you there seemed
to be something glaring, garish, rattling, and the noises and glares
hit upon the little cell called your life, and shook it, and warped
it.
If he could only prevent himself growing up! He did not want to be a
man.
Then, like the natural boy, he forgot his despondency, and sprang up.
During the remainder of the morning he helped his aunt, and in the
afternoon, when there was nothing more to be done, he went into the
village. Here he asked a man whereabouts Christminster lay.
“Christminster? Oh, well, out by there yonder; though Ive never bin
there—not I. Ive never had any business at such a place.”
The man pointed north-eastward, in the very direction where lay that
field in which Jude had so disgraced himself. There was something
unpleasant about the coincidence for the moment, but the fearsomeness
of this fact rather increased his curiosity about the city. The
farmer had said he was never to be seen in that field again; yet
Christminster lay across it, and the path was a public one. So,
stealing out of the hamlet, he descended into the same hollow which
had witnessed his punishment in the morning, never swerving an inch
from the path, and climbing up the long and tedious ascent on the
other side till the track joined the highway by a little clump of
trees. Here the ploughed land ended, and all before him was bleak
open down.

@ -0,0 +1,4 @@
“I shant forget you, Jude,” he said, smiling, as the cart moved off.
“Be a good boy, remember; and be kind to animals and birds, and read
all you can. And if ever you come to Christminster remember you hunt
me out for old acquaintance sake.”

File diff suppressed because it is too large Load Diff

@ -0,0 +1,16 @@
#lang racket/base
;; todo next
;; segfault on command line?
;; adaptive linebreak using fu-formula
;; pagination: vertical measuring
;; use smawk penalty system for pagination too?
;; unified model of filling / positioning that works for every quad, recursively
;; how to handle constraint failure. At least a good error message.
;; imperative line break
;; why do certain MB fonts only appear in bold?
;; deal with separating / recombining footnote flow
;; deal with page number flags (for toc, index, etc)
;; disk cache of previously wrapped lines
;; fractional point sizes (asked Flatt)
;; how are opentype features handled (asked Flatt)

@ -0,0 +1,266 @@
#lang racket/base
(require sugar/list sugar/define)
(require (for-syntax racket/syntax racket/base) racket/string racket/contract racket/list sugar/debug racket/bool hyphenate racket/function math/flonum)
(require "quads.rkt" "world.rkt" "measure.rkt")
;; predicate for use below
(define (list-of-mergeable-attrs? xs)
(and (list? xs) (andmap (λ(x) (or (quad? x) (quad-attrs? x) (hashable-list? x))) xs)))
;; faster than (listof pair?)
(define (pairs? x) (and (list? x) (andmap pair? x)))
;; push together multiple attr sources into one list of pairs.
;; mostly a helper function for the two attr functions below.
(define+provide/contract (join-attrs quads-or-attrs-or-lists)
(list-of-mergeable-attrs? . -> . pairs?)
(append-map hash->list (filter-not false? (map (λ(x)
(cond
[(quad? x) (quad-attrs x)]
[(quad-attrs? x) x]
[(hashable-list? x) (apply hash x)]
[else #f])) quads-or-attrs-or-lists))))
;; merge concatenates attributes, with later ones overriding earlier.
;; most of the work is done by join-attrs.
(define+provide/contract (merge-attrs . quads-or-attrs-or-lists)
(() #:rest list-of-mergeable-attrs? . ->* . quad-attrs?)
(define all-attrs (join-attrs quads-or-attrs-or-lists))
(apply hash (flatten all-attrs)))
;; functionally update a quad attr. Similar to hash-set
(define+provide/contract (quad-attr-set q k v)
(quad? symbol? any/c . -> . quad?)
(quad (quad-name q) (merge-attrs (quad-attrs q) (list k v)) (quad-list q)))
;; functionally update multiple quad attrs. Similar to hash-set*
(define+provide/contract (quad-attr-set* q . kvs)
((quad?) #:rest hashable-list? . ->* . quad?)
(for/fold ([current-q q])([kv-list (in-list (slice-at kvs 2))])
(apply quad-attr-set current-q kv-list)))
;; functionally remove a quad attr. Similar to hash-remove
(define+provide/contract (quad-attr-remove q k)
(quad? symbol? . -> . quad?)
(if (quad-attrs q)
(quad (quad-name q) (hash-remove (quad-attrs q) k) (quad-list q))
q))
;; functionally remove multiple quad attrs. Similar to hash-remove
(define+provide/contract (quad-attr-remove* q . ks)
((quad?) #:rest (λ(ks) (and (list? ks) (andmap symbol? ks))) . ->* . quad?)
(for/fold ([current-q q])([k (in-list ks)])
(quad-attr-remove current-q k)))
(define+provide/contract (quad-map proc q)
(procedure? quad? . -> . quad?)
(quad (quad-name q) (quad-attrs q) (map proc (quad-list q))))
;; flatten merges attributes, but applies special logic suitable to flattening
;; for instance, resolving x and y coordinates.
(define+provide/contract (flatten-attrs . quads-or-attrs-or-falses)
(() #:rest (listof (or/c quad? quad-attrs?)) . ->* . quad-attrs?)
(define all-attrs (join-attrs quads-or-attrs-or-falses))
(define-values (x-attrs y-attrs other-attrs-reversed)
(for/fold ([xas null][yas null][oas null])([attr (in-list all-attrs)])
(cond
[(equal? (car attr) world:x-position-key) (values (cons attr xas) yas oas)]
[(equal? (car attr) world:y-position-key) (values xas (cons attr yas) oas)]
[else (values xas yas (cons attr oas))])))
(define (make-cartesian-attr key attrs) (if (empty? attrs) empty (cons key (apply + (map cdr attrs)))))
(define-values (x-attr y-attr) (apply values (map make-cartesian-attr (list world:x-position-key world:y-position-key) (list x-attrs y-attrs))))
(apply hash (flatten (list* x-attr y-attr (reverse other-attrs-reversed)))))
;; pushes attributes down from parent quads to children,
;; resulting in a flat list of quads.
;; input is often large, so macro allows us to avoid allocation
(provide flatten-quad)
(define-syntax-rule (flatten-quad q)
; (quad? . -> . quads?)
(flatten
(let loop ([x q][parent #f])
(cond
[(quad? x)
(let ([x-with-parent-attrs (quad (quad-name x)
(flatten-attrs parent x) ; child positioned last so it overrides parent attributes
(quad-list x))])
(if (empty? (quad-list x))
x-with-parent-attrs ; no subelements, so stop here
(map (λ(xi) (loop xi x-with-parent-attrs)) (quad-list x))))] ; replace quad with its elements
[(string? x) (quad (quad-name parent) (quad-attrs parent) (list x))]))))
;; flatten quad as above,
;; then dissolve it into individual character quads while copying attributes
;; input is often large, so macro allows us to avoid allocation
(provide split-quad)
(define-syntax-rule (split-quad q)
;(quad? . -> . quads?)
(letrec ([do-explode (λ(x [parent #f])
(cond
[(quad? x)
(if (empty? (quad-list x))
x ; no subelements, so stop here
(map (λ(xi) (do-explode xi x)) (quad-list x)))] ; replace quad with its elements, exploded
[(string? x) (map (λ(xc) (quad 'word (quad-attrs parent) (list xc))) (regexp-match* #px"." x))]))])
(flatten (map do-explode (flatten-quad q)))))
;; merge chars into words (and boxes), leave the rest
;; if two quads are mergeable types, and have the same attributes,
;; they get merged.
;; input is often large, so macro allows us to avoid allocation
(provide join-quads)
(define-syntax-rule (join-quads qs-in)
;((quads?)(quads?) . ->* . quads?)
(let ([make-matcher (λ (base-q)
(λ(q)
(and (member (quad-name q) world:mergeable-quad-types)
(not (whitespace/nbsp? q))
;; if key doesn't exist, it is compared against the default value.
;; this way, a nonexistent value will test true against a default value.
(andmap (λ(key default) (equal? (quad-attr-ref base-q key default) (quad-attr-ref q key default)))
(list world:font-name-key
world:font-size-key
world:font-weight-key
world:font-style-key)
(list (world:font-name-default)
(world:font-size-default)
(world:font-weight-default)
(world:font-style-default))))))])
(let loop ([qs qs-in][acc null])
(if (null? qs)
(reverse acc)
(let* ([base-q (first qs)]
[mergeable-and-matches-base? (make-matcher base-q)]) ; make a new predicate function for this quad
(cond
[(mergeable-and-matches-base? base-q)
;; take as many quads that match, using the predicate function
(define-values (matching-qs other-qs) (splitf-at (cdr qs) mergeable-and-matches-base?))
(define new-word (word (quad-attrs base-q) (string-append* (append-map quad-list (cons base-q matching-qs)))))
(loop other-qs (cons new-word acc))]
;; otherwise move on to the next in line
[else (loop (cdr qs) (cons base-q acc))]))))))
;; the last char of a quad
(define+provide/contract (quad-last-char q)
(quad? . -> . (or/c #f string?))
(define split-qs (split-quad q)) ; split makes it simple, but is it too expensive?
(if (or (empty? split-qs) (empty? (quad-list (last split-qs))))
#f
(car (quad-list (last split-qs)))))
;; the first char of a quad
(define+provide/contract (quad-first-char q)
(quad? . -> . (or/c #f string?))
(define split-qs (split-quad q)) ; explosion makes it simple, but is it too expensive?
(if (or (empty? split-qs) (empty? (quad-list (first split-qs))))
#f
(car (quad-list (first split-qs)))))
;; propagate x and y adjustments throughout the tree,
;; using parent x and y to adjust children, and so on.
(define+provide/contract (compute-absolute-positions i [parent-x 0][parent-y 0])
((quad?) (integer? integer?) . ->* . quad?)
(cond
[(quad? i)
(define adjusted-x (round-float (+ (quad-attr-ref i world:x-position-key 0) parent-x)))
(define adjusted-y (round-float (+ (quad-attr-ref i world:y-position-key 0) parent-y)))
(quad (quad-name i) (merge-attrs i (list world:x-position-key adjusted-x world:y-position-key adjusted-y)) (map (λ(ii) (compute-absolute-positions ii adjusted-x adjusted-y)) (quad-list i)))]
[else i]))
;; simple assert. should get moved to sugar/debug
(provide assert)
(define-syntax-rule (assert pred expr)
(let ([result expr])
(if (pred result)
result
(error 'assert-failure (format "\n~a\nevaluates to:\n~a\nwhich is not:\n~a" 'expr result 'pred)))))
;; peeks at arguments and times execution
(provide snoop)
(define-syntax (snoop stx)
(syntax-case stx ()
[(_ proc arg ... . rest)
(with-syntax ()
#'(begin
(displayln (format "Evaluating ~s" '(proc arg ... . rest)))
(let ([start (current-milliseconds)]
[result (proc arg ... . rest)]
[end (current-milliseconds)])
(displayln (format "Evaluation of ~s took ~a ms\nResult ~a" '(proc arg ... . rest) (- end start) result))
result)))]))
;; find total pages in doc by searching on page count key.
(define+provide/contract (pages-in-doc doc)
(doc? . -> . integer?)
(add1 (apply max (map (curryr quad-attr-ref world:page-key 0) (quad-list doc)))))
;; todo: how to guarantee line has leading key?
(define+provide/contract (compute-line-height line)
(line? . -> . line?)
(quad-attr-set line world:height-key (quad-attr-ref/parameter line world:leading-key)))
(define (fixed-height? q) (quad-has-attr? q world:height-key))
(define+provide/contract (quad-height q)
(quad? . -> . number?)
(quad-attr-ref q world:height-key 0))
;; use heights to compute vertical positions
(define+provide/contract (add-vert-positions starting-quad)
(quad? . -> . quad?)
(define-values (new-quads final-height)
(for/fold ([new-quads empty][height-so-far 0])([q (in-list (quad-list starting-quad))])
(values (cons (quad-attr-set q world:y-position-key height-so-far) new-quads)
(round-float (+ height-so-far (quad-height q))))))
(quad (quad-name starting-quad) (quad-attrs starting-quad) (reverse new-quads)))
;; recursively hyphenate strings in a quad
(define+provide/contract (hyphenate-quad x)
(quad? . -> . quad?)
(cond
[(quad? x) (quad-map hyphenate-quad x)]
[(string? x) (hyphenate x
#:min-length 6
#:min-left-length 3
#:min-right-length 3)]
[else x]))
;; just because it comes up a lot
(provide split-last)
(define-syntax-rule (split-last xs)
(let-values ([(first-list last-list) (split-at-right xs 1)])
(values first-list (car last-list))))
;; like cons, but joins a list to an atom
(provide snoc)
(define-syntax-rule (snoc xs x)
(append xs (list x)))
;; folded flonum operators
;; (for use with multiple args, standard flonum ops have arity = 2)
(define-syntax (define-folded-op stx)
(syntax-case stx ()
[(_ op starting-val)
(with-syntax ([fold-op (format-id stx "fold-~a" #'op)]
[ops (format-id stx "~as" #'op)])
#'(begin
(provide fold-op ops)
(define-syntax-rule (ops x (... ...))
(fold-op (list x (... ...))))
(define-syntax-rule (fold-op xs)
(foldl op starting-val xs))))]))
(define-folded-op fl+ 0.0)
(define-folded-op fl- 0.0)
(define-folded-op fl* 1.0)
(define-folded-op fl/ 1.0)

@ -0,0 +1,96 @@
#lang racket/base
(require (for-syntax racket/base racket/syntax))
(provide (prefix-out world: (all-defined-out)))
(define-syntax-rule (define-parameter name val)
(define name (make-parameter val)))
(define-syntax (define-key-and-parameter stx)
(syntax-case stx ()
[(_ name keyname val)
(with-syntax ([name-key (format-id #'name "~a-key" #'name)]
[name-default (format-id #'name "~a-default" #'name)])
#'(begin
(define name-key keyname)
(define-parameter name-default val)))]))
(define-key-and-parameter measure 'measure 300)
(define-key-and-parameter font-size 'size 13)
(define-key-and-parameter font-name 'font "Triplicate T4")
(define-key-and-parameter font-weight 'weight 'normal)
(define-key-and-parameter font-style 'style 'normal)
(define-key-and-parameter font-color 'color "black")
(define-key-and-parameter font-background 'background "none")
(define-key-and-parameter column-count 'column-count 2)
(define-key-and-parameter column-gutter 'column-gutter 30)
(define max-quality 100)
(define draft-quality 20)
(define-key-and-parameter quality 'quality max-quality)
(define-key-and-parameter horiz-alignment 'x-align 'left)
(define-key-and-parameter leading 'leading (floor (* (font-size-default) 1.4)))
(define-key-and-parameter paper-width 'paper-width (* 8.5 72))
(define-key-and-parameter paper-height 'paper-height (* 11 72))
(define line-looseness-key 'looseness)
(define width-key 'width)
(define horiz-alignment-last-line-key 'x-align-last-line)
(define word-break-key 'word-break)
(define no-break-key 'nb)
(define before-break-key 'bb)
(define ascent-key 'ascent)
(define height-key 'height)
(define unbreakable-key 'no-break)
(define line-index-key 'line-idx)
(define total-lines-key 'lines)
(define page-index-key 'page-idx)
(define column-index-key 'column-idx)
(define x-position-key 'x)
(define y-position-key 'y)
(define page-key 'page)
(define soft-hyphen #\u00AD)
(define hyphens-and-dashes (list "-" "" "" (format "~a" soft-hyphen)))
(define spaces '(" "))
(define empty-string '"")
(define mergeable-quad-types '(char run word))
(define-parameter default-word-break-list '(nb "" bb "-"))
(define-parameter optical-overhang 0.8)
(define line-looseness-tolerance 0.05) ; 0.04 seems to be the magic point that avoids a lot of hyphenation
(define hyphen-limit 1) ; does not work with first-fit wrapping
(define minimum-last-line-chars 5)
(define allow-hyphenated-last-word-in-paragraph #t)
(define allowed-overfull-ratio 1.015)
(define last-line-can-be-short #t)
(define use-optical-kerns? #t)
(define use-hyphenation? #t)
(define new-line-penalty 5000)
(define hyphen-penalty 5000)
(define hanging-chars '("." "-" "," "" "" "" "" "'" "\"" ")" "(" "[" "]" "{" "}" ":" ";"))
(define minimum-lines-per-column 4)
(define min-first-lines 2)
(define min-last-lines 2)
(define default-lines-per-column 36)
(define-parameter logging-level 'info)

@ -0,0 +1,543 @@
#lang racket/base
(require sugar/coerce sugar/define sugar/list sugar/debug racket/list racket/format racket/function racket/string (for-syntax racket/base racket/syntax) math/flonum racket/vector sugar/cache)
(require "ocm.rkt" "quads.rkt" "utils.rkt" "measure.rkt" "world.rkt" "logger.rkt" )
;; predicate for the soft hyphen
(define+provide/contract (soft-hyphen? x)
(string? . -> . boolean?)
(equal? (~a world:soft-hyphen) x))
;; visible characters that also mark possible breakpoints
(define+provide/contract (visible-breakable? x)
(string? . -> . coerce/boolean?)
(member x world:hyphens-and-dashes))
;; invisible characters that denote possible breakpoints
(define+provide/contract (invisible-breakable? x)
(string? . -> . coerce/boolean?)
(member x (cons world:empty-string world:spaces)))
;; union of visible & invisible
(define+provide/contract (breakable? x)
(any/c . -> . boolean?)
(cond
[(string? x) (or (visible-breakable? x) (invisible-breakable? x))]
[(word? x) (breakable? (word-string x))]
[else #f]))
;; used by insert-spacers to determine which characters
;; can be surrounded by stretchy spacers
(define+provide/contract (takes-justification-space? x)
(any/c . -> . boolean?)
(whitespace/nbsp? x))
;; test if a quad can be a word break:
;; either it's an explicit word break,
;; or it's breakable (and can be converted to a word break)
(define+provide/contract (possible-word-break-quad? q)
(quad? . -> . boolean?)
(or (word-break? q) (breakable? q)))
;; convert a possible word break into an actual one
(define+provide/contract (convert-to-word-break q)
(possible-word-break-quad? . -> . word-break?)
(cond
[(word-break? q) q]
[(word? q)
(define str (word-string q)) ; str will be one character long, because we've exploded our input
(apply word-break
(merge-attrs q ; take q's attributes for formatting purposes
(cond
;; a space is ordinarily visible, but disappears at the end of a line
[(equal? str " ") (list world:no-break-key " " world:before-break-key "")]
;; soft hyphen is ordinarily invisible, but appears at the end of a line
[(soft-hyphen? str) (list world:no-break-key "" world:before-break-key "-")]
;; a visible breakable character is always visible
[(visible-breakable? str) (list world:no-break-key str world:before-break-key str)]
[else (world:default-word-break-list)])) (quad-list q))]))
(define (make-unbreakable q)
(quad-attr-set q world:unbreakable-key #t))
;; take list of atomic quads and gather them into pieces
;; a piece is an indivisible chunk of a line.
;; meaning, a line can wrap at a piece boundary, but not elsewhere.
;; hyphenation produces more, smaller pieces, which means more linebreak opportunities
;; but this also makes wrapping slower.
(define+provide/contract (make-pieces qs)
(quads? . -> . pieces?)
(define-values (breakable-items items-to-make-unbreakable) (split-at-right qs (min world:minimum-last-line-chars (length qs))))
(define unbreak-qs (append breakable-items (map make-unbreakable items-to-make-unbreakable)))
(define lists-of-quads (slicef-at unbreak-qs (λ(q) (or (not (possible-word-break-quad? q))
(quad-attr-ref q world:unbreakable-key #f)))))
(define-values (first-lists-of-quads last-list-of-quads) (split-last lists-of-quads))
(define (make-first-pieces qs)
(let-values ([(first-qs last-q) (split-last qs)])
(apply piece (list world:word-break-key (convert-to-word-break last-q)) first-qs)))
(append (map make-first-pieces first-lists-of-quads)
(list (apply piece #f last-list-of-quads))))
;; extract font attributes from quad, or get default values
(provide font-attributes-with-defaults)
(define-syntax-rule (font-attributes-with-defaults q)
(list
(let ([size (quad-attr-ref/parameter q world:font-size-key)])
(if (exact-integer? size) (fl size) size))
(quad-attr-ref/parameter q world:font-name-key)
(quad-attr-ref/parameter q world:font-weight-key)
(quad-attr-ref/parameter q world:font-style-key)))
;; get the width of a quad.
;; Try the attr first, and if it's not available, compute the width.
;; comes in fast or slow versions.
;; not designed to update the source quad.
(define+provide/contract (quad-width q [fast? #f])
((quad?) (boolean?) . ->* . flonum?)
(cond
[(quad-has-attr? q world:width-key) (fl (quad-attr-ref q world:width-key))]
[(ormap (λ(pred) (pred q)) (list char? run? word? word-break?))
(apply measure-text (word-string q)
(font-attributes-with-defaults q))]
[(line? q) (fold-fl+ (map quad-width (quad-list q)))]
[else 0.0]))
;; shorthand for fast version of quad-width.
(define+provide (quad-width-fast q)
(quad-width q #t))
;; get the ascent (distance from top of text to baseline)
;; used by renderer to align text runs baseline-to-baseline.
;; consult the attrs, and if not available, compute it.
;; not designed to update the source quad.
(define+provide/contract (ascent q)
(quad? . -> . flonum?)
(or (quad-attr-ref q world:ascent-key #f)
(cond
[(ormap (λ(pred) (pred q)) (list char? run? word? word-break?))
(apply measure-ascent (word-string q) (font-attributes-with-defaults q))]
[else 0.0])))
;; convert a piece into its final form, which depends on location.
;; if a piece appears at the end of a line, it is rendered in "before break" mode.
;; if a piece appears elsewhere in a line, it is rendered in "no break" mode.
;; this allows the appearance of a piece to change depending on whether it's at the end.
;; and thus give correct behavior to trailing word spaces, soft hyphens, etc.
(define+provide/contract (render-piece p [before-break? #f])
((piece?) (boolean?) . ->* . piece?)
;; a piece doesn't necessarily have a word-break item in it.
;; only needs it if the appearance of the piece changes based on location.
;; so words are likely to have a word-break item; boxes not.
;; the word break item contains the different characters needed to finish the piece.
(define the-word-break (quad-attr-ref p world:word-break-key #f))
(let ([p (quad-attr-remove p world:word-break-key)]) ; so it doesn't propagate into subquads
(if the-word-break
(quad (quad-name p) (quad-attrs p)
(append (quad-list p) (let ([rendered-wb ((if before-break?
word-break->before-break
word-break->no-break) the-word-break)])
(if (> (string-length (word-string rendered-wb)) 0) ; if rendered-wb is "", don't append it
(list rendered-wb)
empty))))
p)))
;; shorthand
(define+provide (render-piece-before-break p)
(render-piece p #t))
;; helper macro to convert quad into word-break.
;; look up the break character and convert the quad based on what is found.
(define-syntax-rule (render-word-break wb key)
(let ([break-char (quad-attr-ref wb key)])
(quad (if (whitespace? break-char) 'word-break 'word)
(hash-remove (hash-remove (quad-attrs wb) world:no-break-key) world:before-break-key) (list (quad-attr-ref wb key)))))
;; uses macro above in no-break mode.
(define (word-break->no-break wb)
(render-word-break wb world:no-break-key))
;; uses macro above in before-break mode.
(define (word-break->before-break wb)
(render-word-break wb world:before-break-key))
;; is this the last line? compare current line-idx to total lines
(define+provide/contract (last-line? line)
(line? . -> . boolean?)
(define line-idx (quad-attr-ref line world:line-index-key #f))
(define lines (quad-attr-ref line world:total-lines-key #f))
(and line-idx lines (= (add1 line-idx) lines)))
;; optical kerns are automatically inserted at the beginning and end of a line
;; (by the pieces->line function)
;; but may also be found elsewhere, imperatively (e.g., before an indent)
;; they allow certain characters to hang over the line margin.
;; optical kerns aren't considered when the line is being composed,
;; rather they are an adjustment added to a composed line.
;; the optical kern doesn't have left- or right-handed versions.
;; it just looks at quads on both sides and kerns them if appropriate.
;; in practice, only one will likely be used.
(define+provide/contract (render-optical-kerns exploded-line-quads)
(quads? . -> . quads?)
(define (overhang-width q)
(if (and (word? q) (member (word-string q) world:hanging-chars))
(fl*s -1.0 (world:optical-overhang) (apply measure-text (word-string q) (font-attributes-with-defaults q)))
0.0))
(cond
[(not (empty? exploded-line-quads))
;; after exploding, each quad will have a string with one character.
(for/list ([(q-left q q-right) (apply in-parallel (shift exploded-line-quads '(1 0 -1)))])
(if (optical-kern? q)
(quad-attr-set q world:width-key (fl+ (overhang-width q-left) (overhang-width q-right)))
q))]
[else exploded-line-quads]))
;; ultimately every line is filled to fit the whole measure.
;; spacers are used to soak up extra space left over in a line.
;; depending on where the spacers are inserted, different formatting effects are achieved.
;; e.g., left / right / centered / justified.
(define+provide/contract (insert-spacers-in-line line [alignment-override #f])
((line?) ((or/c #f symbol?)) . ->* . line?)
;; important principle: avoid peeking into quad-list to get attributes.
;; because non-attributed quads may be added.
;; here, we know that common attributes are hoisted into the line.
;; so rely on line attributes to get horiz alignment.
(define key-to-use (if (and (last-line? line) (quad-has-attr? line world:horiz-alignment-last-line-key))
world:horiz-alignment-last-line-key
world:horiz-alignment-key))
(define horiz-alignment (or alignment-override (quad-attr-ref line key-to-use (world:horiz-alignment-default))))
(define default-spacer (spacer))
(define-values (before middle after) (case horiz-alignment
[(left) (values #f #f default-spacer)]
[(right) (values default-spacer #f #f)]
[(center) (values default-spacer #f default-spacer)]
[(justified justify) (values #f default-spacer #f)]
[else (values #f #f #f)]))
(define (copy-with-attrs q attr-source)
(define keys-to-ignore '(width)) ; width will be determined during fill routine
(define filtered-hash (and (quad-attrs attr-source)
(foldl (λ(k ht) (hash-remove ht k)) (quad-attrs attr-source) keys-to-ignore)))
(quad (quad-name q) (merge-attrs filtered-hash q) (quad-list q)))
(define result
(quad (quad-name line) (quad-attrs line) (flatten (let ([qs (quad-list line)])
`(,@(when/splice before (copy-with-attrs before (first qs)))
,@(map (λ(q) (if (and middle (takes-justification-space? q))
(let ([interleaver (copy-with-attrs middle q)])
(list interleaver q interleaver))
q)) qs)
,@(when/splice after (copy-with-attrs after (last qs))))))))
result)
;; installs the width in the quad.
;; this becomes the value reported by quad-width.
(define (embed-width q w)
(quad-attr-set q world:width-key w))
;; installs the ascent in the quad.
(define (record-ascent q)
(quad-attr-set q world:ascent-key (ascent q)))
;; helper function: doesn't need contract because it's already covered by the callers
(define (render-pieces ps)
(define-values (initial-ps last-p) (split-last ps))
(snoc (map render-piece initial-ps) (render-piece-before-break last-p)))
;; compose pieces into a finished line.
;; take the contents of the rendered pieces and merge them.
;; compute looseness for line as a whole.
;; also add ascent to each component quad, which can be different depending on font & size.
(define+provide (pieces->line ps measure-quad-proc)
(pieces? procedure? . -> . line?)
;; handle optical kerns here to avoid resplitting and rejoining later.
(define rendered-pieces (render-pieces ps))
(define split-pieces (map quad-list rendered-pieces))
(define line-quads (append* split-pieces))
(define line-quads-maybe-with-opticals
(if world:use-optical-kerns?
(render-optical-kerns
(let ([my-ok (list (optical-kern (quad-attrs (car line-quads))))]) ; take attrs from line, incl measure
(append my-ok line-quads my-ok)))
line-quads))
(define merged-quads (join-quads line-quads-maybe-with-opticals))
(define merged-quad-widths (map measure-quad-proc merged-quads)) ; 10% of function time
(log-quad-debug "making pieces into line = ~v" (string-append* (map quad->string merged-quads)))
;; if measure key isn't present, allow an error, because that's weird
(when (not (quad-has-attr? (first line-quads) world:measure-key))
(error 'pieces->line "quad has no measure key: ~a" (first line-quads)))
(define measure (fl (quad-attr-ref (first merged-quads) world:measure-key)))
(define looseness (round-float (fl/ (fl- measure (fold-fl+ merged-quad-widths)) measure)))
;; quads->line function hoists common attributes into the line
(let* ([new-line-quads (map embed-width merged-quads merged-quad-widths)] ; 15% of time
[new-line-quads (map record-ascent new-line-quads)] ; 35% of time
[new-line (apply line (quad-attrs (car new-line-quads)) new-line-quads)]
[new-line (quad-attr-set new-line world:line-looseness-key looseness)])
new-line))
;; a faster line-measuring function used by the wrapping function to test lines.
(define+provide (measure-potential-line ps)
;(pieces? . -> . flonum?)
(for*/sum ([rendered-piece (in-list (render-pieces ps))]
[piece-quad (in-list (quad-list rendered-piece))])
(quad-width-fast piece-quad)))
(define (vector-break-at vec bps)
(define-values (vecs _) ;; loop backward
(for/fold ([vecs empty][end (vector-length vec)])([start (in-list (reverse (cons 0 bps)))])
(if (= start end)
(values vecs start)
(values (cons (vector-copy vec start end) vecs) start))))
vecs)
(define-syntax-rule (report-time0 name expr)
(let ([op (open-output-string)])
(parameterize ([current-output-port op])
(define result (time expr))
(report (string-trim (get-output-string op)) name)
(values result))))
(define-syntax-rule (report-time name expr)
expr)
;; makes a wrap function by combining component functions.
(define+provide (make-wrap-proc
#:make-pieces-proc make-pieces-proc
#:measure-quad-proc measure-quad-proc
#:compose-line-proc compose-line-proc
#:find-breakpoints-proc find-breakpoints-proc)
(λ(qs [measure #f])
(let* ([measure (fl+ (fl (or measure (quad-attr-ref/parameter (car qs) world:measure-key))) 0.0)]
[qs (if (quad-has-attr? (car qs) world:measure-key)
qs
(map (curryr quad-attr-set world:measure-key measure) qs))])
(log-quad-debug "wrapping on measure = ~a" measure)
(define pieces (make-pieces-proc qs)) ; 5%
(define bps (report-time 'find-bps (find-breakpoints-proc (list->vector pieces) measure))) ; 50%
(define broken-pieces (break-at pieces bps)) ; 5%
; (report (add1 (length bps)) 'lines-in-paragraph)
(report-time 'compose-lines (map (λ(bp) (compose-line-proc bp measure-quad-proc)) broken-pieces))))) ; 50%
(define width? flonum?)
(define measure? flonum?)
(define (breakpoints? x) (and (list? x) (andmap integer? x)))
(define (install-measurement-keys p)
(define basic-width (round-float (apply + (map quad-width-fast (quad-list p)))))
(define p-word-break (quad-attr-ref p world:word-break-key #f))
(define before-break-width (fl+ basic-width (if p-word-break
(quad-width-fast (word (quad-attrs p-word-break) (quad-attr-ref p-word-break world:before-break-key)))
0.0)))
(define no-break-width (fl+ basic-width (if p-word-break
(quad-width-fast (word (quad-attrs p-word-break) (quad-attr-ref p-word-break world:no-break-key)))
0.0)))
(quad-attr-set* p 'bb-width before-break-width 'nb-width no-break-width))
(define (make-piece-vectors pieces)
(define pieces-measured
(report-time 'make-wrap-vector (for/list ([p (in-vector pieces)])
(define wb (quad-attr-ref p world:word-break-key #f))
(vector
(fold-fl+ (for/list ([q (in-list (quad-list p))])
(define str (quad->string q))
(if (equal? str "")
(fl (quad-attr-ref q world:width-key 0.0))
(apply measure-text (quad->string q) (font-attributes-with-defaults q)))))
(if wb (apply measure-text (quad-attr-ref wb world:no-break-key) (font-attributes-with-defaults wb)) 0.0)
(if wb (apply measure-text (quad-attr-ref wb world:before-break-key) (font-attributes-with-defaults wb)) 0.0)))))
(values
(for/flvector ([p (in-list pieces-measured)]) (fl+ (vector-ref p 0) (vector-ref p 1))) ; first = word length, second = nb length
(for/flvector ([p (in-list pieces-measured)]) (fl+ (vector-ref p 0) (vector-ref p 2))))) ; first = word length, third = bb length
(define (trial-line pieces-rendered-widths pieces-rendered-before-break-widths i j)
(define flvec (flvector-copy pieces-rendered-widths i j))
(flvector-set! flvec (sub1 (flvector-length flvec)) (flvector-ref pieces-rendered-before-break-widths (sub1 j)))
flvec)
(define (get-line-width line) (round-float (fold-fl+ (flvector->list line))))
;; optimal linefitting: minimize the penalty function across all linebreaks in the paragraph
;; slower but nicer. TeX algorithm + SMAWK speed.
(define+provide (best-fit-proc pieces measure)
;((pieces? . -> . width?) . -> . (pieces? measure? . -> . breakpoints?))
;; don't use struct for penalty, because of read/write overhead
(define $penalty vector)
(define ($penalty-width x) (vector-ref x 1))
(define ($penalty-hyphens x) (vector-ref x 0))
;; Reduce the vector to an integer by treating it as magnitude from origin.
;(define ($penalty->integer v) (sqrt (apply + (map (compose1 (curryr expt 2)) (list ($penalty-width v))))))
(define ($penalty->value v) ($penalty-width v))
(define initial-value ($penalty 0 0.0))
;(define initial-value 0)
(define matrix-value->number identity)
(define checked-ijs (make-hash))
;; this is the winning performance strategy: extract the numbers first, then just wrap on those.
;; todo: how to avoid re-measuring pieces later?
;; todo: how to retain information about words per line and hyphen at end?
(define-values (pieces-rendered-widths pieces-rendered-before-break-widths)
(make-piece-vectors pieces))
(define pieces-with-word-space (vector-map (λ(piece) (and (quad-has-attr? piece world:word-break-key) (equal? (quad-attr-ref (quad-attr-ref piece world:word-break-key) 'nb) " "))) pieces))
(log-quad-debug "~a pieces to wrap = ~v" (vector-length pieces) (vector-map quad->string pieces))
(define (penalty i j)
(hash-set! checked-ijs (cons i j) #t)
(define out-of-bounds-signal ($penalty 0 (fl* -1.0 (fl i)))) ; for ocm
(define last-line? (= j (vector-length pieces)))
(cond
[(or (>= i j) ; implies negative or zero length line
(> j (vector-length pieces))) ; exceeds available pieces
out-of-bounds-signal]
[else
(define penalty-up-to-i (ocm-min-value ocm i))
(define words (fl (vector-count identity (vector-copy pieces-with-word-space i (sub1 j)))))
(define last-piece-to-test (vector-ref pieces (sub1 j)))
(define new-hyphen?
(and (quad-has-attr? last-piece-to-test world:word-break-key)
(equal? (quad-attr-ref (quad-attr-ref last-piece-to-test world:word-break-key) world:before-break-key) "-")))
(define cumulative-hyphens (if (not new-hyphen?)
0
(add1 ($penalty-hyphens penalty-up-to-i))))
(define line-width (get-line-width (trial-line pieces-rendered-widths pieces-rendered-before-break-widths i j)))
($penalty
cumulative-hyphens
(round-float
(fl+s
(if (> cumulative-hyphens world:hyphen-limit)
(fl world:hyphen-penalty)
0.0)
(fl world:new-line-penalty)
($penalty->value penalty-up-to-i)
(cond
;; overfull line: huge penalty prevents break; multiplier is essential for monotonicity.
;; multiply by -1 because line-width is longer than measure, thus diff is negative
[(fl> line-width (fl* world:allowed-overfull-ratio measure))
(fl* (fl- line-width measure) (flexpt 10.0 7.0))]
;; standard penalty, optionally also applied to last line (by changing operator)
[((if world:last-line-can-be-short < <=) j (vector-length pieces)) (fl/ (flexpt (fl- measure line-width) 2.0) (flmax 1.0 words))]
;; only option left is (= j (length pieces)), meaning we're on the last line.
;; 0 penalty means any length is ok.
;[(< (length pieces-to-test) (world:minimum-last-line-pieces)) 50000]
[else 0.0]))))]))
(define ocm (make-ocm penalty initial-value $penalty->value))
;; starting from last position, ask ocm for position of row minimum (= new-pos)
;; collect this value, and use it as the input next time
;; until you reach first position.
(define first-position 0)
(define last-position (vector-length pieces))
(define result (let loop ([pos last-position][acc null])
(let ([next-pos (ocm-min-index ocm pos)]) ; first look ahead ...
(if (= next-pos first-position) ; therefore we're done
acc
(loop next-pos (cons next-pos acc))))))
(log-quad-debug "penalty pieces vs. pairs checked = ~a ~a" (vector-length pieces) (exact->inexact (/ (length (hash-keys checked-ijs)) (vector-length pieces))))
(log-quad-debug "best-fit breakpoints = ~a" result)
result)
;; greedy linefitting: find the biggest line that will fit, then set the next
;; faster but coarser. Web browsers & most word processors use this approach.
(define+provide (first-fit-proc pieces measure)
;((pieces? . -> . width?) . -> . (pieces? measure? . -> . breakpoints?))
(define-values (pieces-rendered-widths pieces-rendered-before-break-widths)
(make-piece-vectors pieces))
(define bps
(for/fold ([bps '(0)])([j-1 (in-range (vector-length pieces))])
(if (fl> (get-line-width (trial-line pieces-rendered-widths
pieces-rendered-before-break-widths
(car bps) (add1 j-1)))
(fl* world:allowed-overfull-ratio measure))
(cons j-1 bps)
bps)))
(log-quad-debug "first-fit breakpoints = ~a" (cdr (reverse bps)))
(cdr (reverse bps)))
;; wrap proc based on greedy proc
(define+provide wrap-first (make-wrap-proc
#:make-pieces-proc make-pieces
#:measure-quad-proc quad-width
#:compose-line-proc pieces->line
#:find-breakpoints-proc first-fit-proc))
;; wrap proc based on penalty function
(define+provide wrap-best (make-wrap-proc
#:make-pieces-proc make-pieces
#:measure-quad-proc quad-width
#:compose-line-proc pieces->line
#:find-breakpoints-proc best-fit-proc))
(define (fixed-width? q) (quad-has-attr? q world:width-key))
;; build quad out to a given width by distributing excess into spacers
;; todo: adjust this to work recursively, so that fill operation cascades down
(define+provide/contract (fill starting-quad [target-width? #f])
((quad?) ((or/c #f flonum?)) . ->* . quad?)
(define target-width (fl (or target-width? (quad-attr-ref starting-quad world:measure-key))))
(define subquads (quad-list starting-quad))
(define-values (flexible-subquads fixed-subquads) (partition spacer? subquads)) ; only puts fill into spacers.
(define width-used (fold-fl+ (map quad-width fixed-subquads)))
(define width-remaining (round-float (fl- target-width width-used)))
(cond
;; check for zero condition because we want to divide by this number
;; if there's no spacers, put one in
;; todo: go in two rounds, once for word spacers, and once for line spacers?
;; or separate the line alignment & word-spacing properties?
[(fl= 0.0 (fl (length flexible-subquads))) (fill (insert-spacers-in-line starting-quad (world:horiz-alignment-default)) target-width)]
[else (define width-per-flexible-quad (round-float (fl/ width-remaining (fl (length flexible-subquads)))))
(define new-quad-list (map (λ(q) (if (spacer? q)
(quad-attr-set q world:width-key width-per-flexible-quad)
q)) subquads))
(quad (quad-name starting-quad) (quad-attrs (quad-attr-set starting-quad world:width-key target-width)) new-quad-list)]))
;; add x positions to a list of fixed-width quads
;; todo: adjust this to work recursively, so that positioning operation cascades down
(define+provide/contract (add-horiz-positions starting-quad)
((and/c quad? fixed-width?) . -> . quad?)
(define-values (new-quads final-width)
(for/fold ([new-quads empty][width-so-far 0.0])([q (in-list (quad-list starting-quad))])
(values (cons (quad-attr-set q world:x-position-key width-so-far) new-quads) (round-float (fl+ (quad-width q) width-so-far)))))
(quad (quad-name starting-quad) (quad-attrs starting-quad) (reverse new-quads)))
(module+ main
(define eqs (split-quad (block '(x-align center font "Equity Text B" size 10) "Foo-d" (word '(size 13) "og ") "and " (box) " Zu" (word-break '(nb "c" bb "k-")) "kerman's. Instead of a circle, the result is a picture of the code that, if it were used as an expression, would produce a circle. In other words, code is not a function, but instead a new syntactic form for creating pictures; the bit between the opening parenthesis with code is not an expression, but instead manipulated by the code syntactic form. This helps explain what we meant in the previous section when we said that racket provides require and the function-calling syntax. Libraries are not restricted to exporting values, such as functions; they can also define new syntactic forms. In this sense, Racket isnt exactly a language at all; its more of an idea for how to structure a language so that you can extend it or create entirely " (word '(font "Courier" size 5) "lang."))))
(define megs (split-quad (block '(size 15) "Meg is an ally.")))
(define trials 1)
(time-repeat trials (let () (wrap-first megs 36) (void)))
(time-repeat trials (let ([measure 36]) (wrap-best megs measure) (void)))
(time-repeat trials (let () (wrap-first eqs 54) (void)))
(time-repeat trials (let ([measure 54]) (wrap-best eqs measure) (void)))
)
Loading…
Cancel
Save