]> git.donarmstrong.com Git - lilypond.git/blobdiff - scm/page-layout.scm
(Top): Add guile, python and
[lilypond.git] / scm / page-layout.scm
index db4b4485ce819f5cf2bdd8cd631b8aaccc7e0175..1a00c37bdc92dc301b86f3e8d2eb91d2b1961866 100644 (file)
-;;;; page-layout.scm -- page layout functions
-;;;;
+;;; page-layout.scm -- page breaking and page layout
+;;;
 ;;;;  source file of the GNU LilyPond music typesetter
-;;;; 
+;;;;
 ;;;; (c) 2004 Jan Nieuwenhuizen <janneke@gnu.org>
+;;;;          Han-Wen Nienhuys <hanwen@cs.uu.nl>
 
-(define (ly:modules-lookup modules sym)
-  (let ((v (module-variable (car modules) sym)))
-    (if (and v (variable-bound? v) (variable-ref v))
-       (variable-ref v)
-       (if (module? (cdr modules)) (ly:modules-lookup (cdr modules) sym)))))
-
-(define (page-properties paper)
-  (list (append `((linewidth . ,(ly:paper-get-number
-                                paper 'linewidth)))
-               (ly:paper-lookup paper 'text-font-defaults))))
+(use-modules (oop goops describe)
+            (oop goops))
 
-(define-public (book-title paper scopes)
-  "Generate book title from header strings."
-  
-  (define (get sym)
-    (let ((x (ly:modules-lookup scopes sym)))
-      (if (and x (not (unspecified? x))) x "")))
-  
-  (let ((props (page-properties paper)))
-    
-    (interpret-markup
-     paper props
-     (markup
-      #:column
-      (#:override '(baseline-skip . 4)
-      #:column
-      (#:fill-line
-       (#:normalsize (get 'dedication))
-       #:fill-line
-       (#:huge #:bigger #:bigger #:bigger #:bigger #:bold (get 'title))
-       #:override '(baseline-skip . 3)
-       #:column
-       (#:fill-line
-       (#:large #:bigger #:bigger #:bold (get 'subtitle))
-       #:fill-line (#:bigger #:bigger #:bold (get 'subsubtitle)))
-       #:override '(baseline-skip . 5)
-       #:column ("")
-       #:override '(baseline-skip . 2.5)
-       #:column
-       (#:fill-line
-       (#:bigger (get 'poet) #:large #:bigger #:caps (get 'composer))
-       #:fill-line (#:bigger (get 'texttranslator) #:bigger (get 'opus))
-       #:fill-line
-       (#:bigger (get 'meter) #:bigger (get 'arranger))
-       ""
-       #:fill-line (#:large #:bigger (get 'instrument))
-       " "
-       #:fill-line (#:large #:bigger #:caps (get 'piece) ""))))))))
-
-(define-public (user-title paper markup)
-  "Generate book title from header markup."
-  (if (markup? markup)
-      (let ((props (page-properties paper))
-           (baseline-skip (chain-assoc-get 'baseline-skip props 2)) )
-       (stack-lines DOWN 0 BASELINE-SKIP
-                    (list (interpret-markup paper props markup))))))
-
-(define-public (score-title paper scopes)
-  "Generate score title from header strings."
-  
-  (define (get sym)
-    (let ((x (ly:modules-lookup scopes sym)))
-      (if (and x (not (unspecified? x))) x "")))
-  
-  (let ((props (page-properties paper)))
-    
-    (interpret-markup
-     paper props
-     (markup
-      #:column
-      (#:override '(baseline-skip . 4)
-      #:column
-      (#:fill-line
-       ("" (get 'opus))
-       #:fill-line (#:large #:bigger #:caps (get 'piece) "")))))))
-
-(define-public (make-header paper page-number)
-  (let ((props (page-properties paper) ))
-    (interpret-markup paper props
-                     (markup #:fill-line
-                             ;; FIXME: font not found
-                             ;; ("" #:bold (number->string page-number))))))
-                             ("" (number->string page-number))))))
-
-(define-public (make-footer paper page-number)
-  (let ((props (page-properties paper)))
-
-    (interpret-markup paper props
-                   (markup #:fill-line ("" (number->string page-number))))))
 
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(define TAGLINE
-  (string-append "Engraved by LilyPond (version " (lilypond-version) ")"))
+(define-class <optimally-broken-page-node> ()
+  (prev #:init-value '() #:accessor node-prev #:init-keyword #:prev)
+  (page #:init-value 0 #:accessor node-page-number #:init-keyword #:pageno)
+  (penalty #:init-value 0 #:accessor node-penalty #:init-keyword #:penalty)
+  (lines #:init-value 0 #:accessor node-lines #:init-keyword #:lines))
 
-(define-public (make-tagline paper scopes)
-  (let* ((props (page-properties paper))
-        (tagline-var (ly:modules-lookup scopes 'tagline))
-        (tagline (if (markup? tagline-var) tagline-var TAGLINE)))
+(define-method (display (node <optimally-broken-page-node>) port)
+  (map (lambda (x) (display x port))
+       (list
+       "Page " (node-page-number node)
+       " Lines: " (node-lines node)
+       " Penalty " (node-penalty node)
+       "\n")))
 
-    (cond ((string? tagline)
-          (if (not (equal? tagline ""))
-              (interpret-markup paper props
-                                (markup #:fill-line (tagline "")))))
-         ((markup? tagline) (interpret-markup paper props tagline)))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(define-public (make-copyright paper scopes)
-  (let ((props (page-properties paper))
-       (copyright (ly:modules-lookup scopes 'copyright)))
-    
-    (cond ((string? copyright)
-          (if (not (equal? copyright ""))
-              (interpret-markup paper props
-                                (markup #:fill-line (copyright "")))))
-         ((markup? copyright) (interpret-markup paper props copyright)))))
+(define TAGLINE
+  (string-append "Engraved by LilyPond (version " (lilypond-version) ")"))
 
+;; TODO: take <optimally-broken-page-node> iso. page-number
+;; for all of these functions ?
 
-;;; optimal page breaking
+(define-public (plain-header paper scopes page-number last?)
+  "Standard header for a part: page number --outside--  and instrument--centered."
 
-;;; This is not optimal page breaking, this is optimal distribution of
-;;; lines over pages; line breaks are a given.
+  (let* ((props (page-properties paper) )
+        (pnum (markup #:bold (number->string page-number)))
+        (instr (ly:modules-lookup scopes 'instrument))
+        (line (list "" (if (markup? instr) instr "") pnum)))
 
-;;; TODO:
-;;;    - user tweaking:
-;;;       + \pagebreak, \nopagebreak
-;;;       + #pages?
-;;;    - short circut SCORE=-1 (dismiss path)
+    (if (even? page-number)
+       (set! line (reverse line)))
 
+    (if (< 1 page-number)
+       (interpret-markup
+        paper props (make-fill-line-markup line))
+       '())
+    ))
 
-(use-modules (oop goops describe))
 
-(define-class <break-node> ()
-  (prev #:init-value '() #:accessor node-prev #:init-keyword #:prev)
-  (line #:init-value 'barf #:accessor node-line #:init-keyword #:line)
-  (page #:init-value 0 #:accessor node-page #:init-keyword #:page)
-  (score #:init-value 0 #:accessor node-score #:init-keyword #:score)
-  (height #:init-value 0 #:accessor node-height #:init-keyword #:score))
+;; TODO: add publisher ID on non-first page.
+(define-public (plain-footer paper scopes page-number last?)
+  "Standard footer. Empty, save for first (copyright) and last (tagline) page."
+  
+  (let*
+      ((props (page-properties paper))
+       (copyright (ly:modules-lookup scopes 'copyright))
+       (tagline-var (ly:modules-lookup scopes 'tagline))
+       (tagline (if (markup? tagline-var) tagline-var TAGLINE))
+       (stencil #f))
+
+    (if last?
+       (set! stencil
+             (ly:stencil-combine-at-edge
+              stencil Y DOWN (interpret-markup paper props tagline)
+              0.0
+              )))
+
+    (if (and (= 1 page-number)
+            (markup? copyright))
+
+       (set! stencil
+             (ly:stencil-combine-at-edge
+              stencil Y DOWN (interpret-markup paper props copyright)
+              0.0
+              )))
+
+    stencil))
+  
+(define (page-headfoot paper scopes number sym sepsym dir last?)
+  "Create a stencil including separating space."
+  (let*
+      ((header-proc (ly:output-def-lookup paper sym))
+       (sep (ly:output-def-lookup paper sepsym))
+       (stencil (ly:make-stencil "" '(0 . 0) '(0 . 0)))
+       (head-stencil
+       (if (procedure? header-proc)
+           (header-proc paper scopes number last?)
+           #f)))
+
+    (if (and (number? sep) (ly:stencil? head-stencil))
+       (set! head-stencil
+             (ly:stencil-combine-at-edge
+              stencil Y  dir head-stencil
+              sep 0.0)))
+
+    head-stencil))
+
+(define-public (default-page-music-height paper scopes number last?)
+  "Printable area for music and titles; matches default-page-make-stencil." 
+  (let*
+      ((h (- (ly:output-def-lookup paper 'vsize)
+            (ly:output-def-lookup paper 'top-margin)
+            (ly:output-def-lookup paper 'bottom-margin)))
+       (head (page-headfoot paper scopes number 'make-header 'head-sep UP last?))
+       (foot (page-headfoot paper scopes number 'make-footer 'foot-sep DOWN last?)))
+    (- h (if (ly:stencil? head)
+            (interval-length (ly:stencil-extent head Y))
+            0)
+       (if (ly:stencil? foot)
+          (interval-length (ly:stencil-extent foot Y))
+          0))
+    ))
+
+
+(define-public (default-page-make-stencil lines paper scopes number last? )
+  "Construct a stencil representing the page from LINES.  "
+  (let*
+     ((top-margin  (ly:output-def-lookup paper 'top-margin))
+      
+      ;; TODO: naming vsize/hsize not analogous to TeX.
+      
+      (hsize (ly:output-def-lookup paper 'hsize))
+      (left-margin (/ (- hsize
+                        (ly:output-def-lookup paper 'linewidth)) 2))
+      (vsize (ly:output-def-lookup paper 'vsize))
+      (bottom-edge (- vsize
+                     (ly:output-def-lookup paper 'bottom-margin)))
+                    
+      (head (page-headfoot paper scopes number 'make-header 'head-sep UP last?))
+      (foot (page-headfoot paper scopes number 'make-footer 'foot-sep DOWN last?))
+      (line-stencils (map ly:paper-system-stencil lines))
+      (height-proc (ly:output-def-lookup paper 'page-music-height))
+      (music-height (height-proc paper scopes number last?))
+      (ragged (ly:output-def-lookup paper 'raggedbottom))
+      (ragged-last   (ly:output-def-lookup paper 'raggedlastbottom))
+      (ragged-bottom (or (eq? #t ragged)
+                        (and last? (eq? #t ragged-last))))
+
+      (spc-left (-  music-height
+                  (apply + (map (lambda (x)
+                                  (interval-length (ly:stencil-extent x Y)))
+                       line-stencils))))
+      (stretchable-lines (remove ly:paper-system-title? (cdr lines)))
+      (stretch (if (or (null? stretchable-lines)
+                      (> spc-left (/ music-height 2))
+                      ragged-bottom)
+                  0.0
+                  (/ spc-left (length stretchable-lines))))
+
+      (page-stencil (ly:make-stencil '()
+                   (cons left-margin hsize)
+                   (cons (- top-margin) 0)))
+      (was-title #t))
+
+    (set! page-stencil (ly:stencil-combine-at-edge
+         page-stencil Y DOWN head 0. 0.))
+
+    (for-each
+     (lambda (l)
+       (set! page-stencil
+            (ly:stencil-combine-at-edge
+             page-stencil Y DOWN (ly:paper-system-stencil l)
+             (if was-title
+                 0.0
+                 stretch)
+             ))
+
+       (set! was-title (ly:paper-system-title? l)))
+     lines)
+
+    (if (ly:stencil? foot)
+       (set! page-stencil
+             (ly:stencil-add
+              page-stencil
+              (ly:stencil-translate
+               foot
+               (cons 0
+                     (+ (- bottom-edge) (- (car (ly:stencil-extent foot Y)))))
+               ))))
+
+    (ly:stencil-translate page-stencil (cons left-margin 0))
+  ))
+  
 
-(define (node-line-number node)
-  (let ((line (node-line node)))
-    (if (null? line) 0
-       (ly:paper-line-number line))))
 
-(define (node-break-score node)
-  (let ((line (node-line node)))
-    (if (null? line) 0
-       (ly:paper-line-break-score line))))
 
-(define (make-node prev line page score)
-  (make <break-node> #:prev prev #:line line #:page page #:score score))
+;;; optimal page breaking
 
-;; print debuggging stuff
-(define pld? #f)
-(define MAX-CRAMP -5)
+;;; This is not optimal page breaking, this is optimal distribution of
+;;; lines over pages; line breaks are a given.
 
-(define-public (ly:optimal-page-breaks lines book-height text-height
-                                      first-diff last-diff)
+; TODO:
+;
+; - density scoring
+; - separate function for word-wrap style breaking?
+; - raggedbottom? raggedlastbottom? 
+
+(define-public (ly:optimal-page-breaks
+               lines paper-book)
+  "Return pages as a list starting with 1st page. Each page is a list
+of lines. "
+
+  (define (make-node prev lines page-num penalty)
+    (make <optimally-broken-page-node>
+      #:prev prev
+      #:lines lines
+      #:pageno page-num
+      #:penalty penalty))
+
+  (define MAXPENALTY 1e9)
+  (define bookpaper (ly:paper-book-book-paper paper-book))
+  (define scopes (ly:paper-book-scopes paper-book))
+  (define (line-height line)
+    (ly:paper-system-extent line Y))
 
   ;; FIXME: may need some tweaking: square, cubic
-  (define (height-score available used)
-    (let* ((empty (- available used))
-          (norm-empty (* empty (/ 100 available))))
-      (if (< norm-empty 0)
-         (if (< (/ empty available) MAX-CRAMP)
-             ;; cannot fill more than MAX-CRAMP
-             -1
-             ;; overfull page is still worse by a power
-             (* -1 norm-empty norm-empty norm-empty))
-          (* norm-empty norm-empty))))
-
-  (define (page-height page-number page-count)
-    (let ((h text-height))
-      (if (= page-number 1)
-         (set! h (+ h first-diff)))
-      (if (= page-number page-count)
-         (set! h (+ h last-diff)))
-      h))
+  (define (height-penalty available used)
+    ;; FIXME, simplistic
+    (let* ((left (- available used))
+          ;; scale-independent
+          (relative (abs (/ left available))))
+      (if (negative? left)
+
+         ;; too full, penalise more
+         (* 10 (1+ relative) relative)
+         
+         ;; Convexity: two half-empty pages is better than 1 completely
+         ;; empty page
+         (* (1+ relative) relative))))
+
+  (define (page-height page-number last?)
+    (let
+       ((p (ly:output-def-lookup bookpaper 'page-music-height)))
+
+      (if (procedure? p)
+         (p bookpaper scopes page-number last?)
+         10000)))
 
+  
   (define (cumulative-height lines)
-    (apply + (map ly:paper-line-height lines)))
-
-  (define (get-path node)
-    (if (null? node)
-       '()
-       (cons node (get-path (node-prev node)))))
-
-  (define (add-scores . lst)
-    (if (null? (filter (lambda (x) (> 0 x)) lst))
-       (apply + lst)
-       -1))
-
-  (define (density-variance nodes)
-    (define (sqr x) (* x x))
-    (define (density node)
-      (let ((p (page-height (node-page node) (node-page (car nodes))))
-           (h (node-height node)))
-       (if (and p h) (* (- p h) (/ h 100)) 0)))
-    (let* ((densities (map density nodes))
-          (mean (/ (apply + densities) (length densities)))
-          (diff (map (lambda (x) (- x mean)) densities))
-          (var (map sqr diff)))
-      (if pld?
-         (begin
-           (format (current-error-port) "densities: ~S\n" densities)
-           (format (current-error-port) "mean: ~S\n" mean)
-           (format (current-error-port) "diff: ~S\n" diff)
-           (format (current-error-port) "density-var: ~S\n" var)))
-      (apply + var)))
-
-  (define (walk-paths best node lines nodes paths)
-    (if pld?
+    (apply + (map line-height lines)))
+
+  (define (get-path node done)
+    "Follow NODE.PREV, and return as an ascending list of pages. DONE
+is what have collected so far, and has ascending page numbers."
+    (if (is-a? node <optimally-broken-page-node>)
+       (get-path (node-prev node) (cons node done))
+       done))
+
+  (define (combine-penalties user page prev)
+    (+ prev page user))
+
+  (define (walk-paths done-lines best-paths current-lines  last? current-best)
+    "Return the best optimal-page-break-node that contains
+CURRENT-LINES.  DONE-LINES.reversed ++ CURRENT-LINES is a consecutive
+ascending range of lines, and BEST-PATHS contains the optimal breaks
+corresponding to DONE-LINES.
+
+CURRENT-BEST is the best result sofar, or #f."
+
+    (let* ((this-page-num (if (null? best-paths)
+                             1
+                             (1+ (node-page-number (car best-paths)))))
+          (prev-penalty (if (null? best-paths)
+                            0.0
+                            (node-penalty (car best-paths))))
+          (page-height (page-height this-page-num last?))
+          (space-used (cumulative-height current-lines))
+          (this-page-penalty (height-penalty  page-height space-used))
+          (user-penalty (ly:paper-system-break-penalty (car current-lines)))
+          (total-penalty (combine-penalties
+                          user-penalty this-page-penalty prev-penalty))
+          (better? (or
+                    (not current-best)
+                    (< total-penalty (node-penalty current-best))))
+          (new-best (if better?
+                        (make-node (if (null? best-paths)
+                                       #f
+                                       (car best-paths))
+                                   current-lines
+                                   this-page-num total-penalty)
+                        current-best)))
+
+      (if #f ;; debug
+         (display
+          (list
+           "user pen " user-penalty " prev-penalty "
+           prev-penalty "\n"
+           "better? " better? " total-penalty " total-penalty "\n"
+           "height " page-height " spc used: " space-used "\n"
+           "pen " this-page-penalty " lines: " current-lines "\n")))
+
+      (if (and (pair? done-lines)
+              ;; if this page is too full, adding another line won't help
+              (< this-page-penalty MAXPENALTY))
+         (walk-paths (cdr done-lines) (cdr best-paths)
+                     (cons (car done-lines) current-lines)
+                     last? new-best)
+         new-best)))
+
+  (define (walk-lines done best-paths todo)
+    "Return the best page breaking as a single
+<optimal-page-break-node> for optimally breaking TODO ++
+DONE.reversed. BEST-PATHS is a list of break nodes corresponding to
+DONE."
+    (if (null? todo)
+       (car best-paths)
+       (let* ((this-line (car todo))
+              (last? (null? (cdr todo)))
+              (next (walk-paths done best-paths (list this-line) last? #f)))
+       
+         (walk-lines (cons this-line done)
+                     (cons next best-paths)
+                     (cdr todo)))))
+
+  (define (line-number node)
+    (ly:paper-system-number (car (node-lines node))))
+
+  (let* ((best-break-node (walk-lines '() '() lines))
+        (break-nodes (get-path best-break-node '()))
+        )
+
+    (if (ly:get-option 'verbose)
        (begin
-         (format (current-error-port) "node: ")
-         (describe node)))
-    (let* ((height (cumulative-height lines))
-          (page (page-height (node-page node) (if (= (node-score node) 0)
-                                                  (node-page node) 0))))
-      (set! (node-height node) height)
-      
-      (let* ((break-score (node-break-score node))
-            (density-score (if (null? paths) 0
-                               ;; FIXME: 5 may need some tweaking
-                               (* 5 (density-variance
-                                     (cons node (get-path (car paths)))))))
-            (page-score (height-score page height))
-            (this-score (add-scores page-score break-score density-score))
-            (path-score (if (null? paths) 0 (node-score (car paths))))
-            (score (add-scores path-score this-score))
-            (nbpn (+ (if (null? paths) 0 (node-page (car paths))) 1)))
-           
-       (if pld?
-           (begin
-             (format (current-error-port) "lines: ~S\n" lines)
-             (format (current-error-port) "page-height: ~f\n" page)
-             (format (current-error-port) "height: ~f\n" height)
-             (format (current-error-port) "break-score: ~f\n" break-score)
-             (format (current-error-port) "density-score: ~f\n" density-score)
-             (format (current-error-port) "this-score: ~f\n" this-score)
-             (format (current-error-port) "path: ~f ~S\n" path-score
-                     (if (null? paths) '()
-                         (map node-line-number (get-path (car paths)))))
-             (format (current-error-port) "score: ~f\n" score)
-             (format (current-error-port) "best: ~f ~S\n" (node-score best)
-                     (map node-line-number (get-path best)))
-             (format (current-error-port) "nbpn: ~f\n" nbpn)
-             (format (current-error-port) "breaking after: ~S scores: ~S\n"
-                     (node-line-number node)
-                     score)))
-      
-       (set! (node-score node) score)
-       (if (and (>= score 0)
-                (or (< score (node-score best))
-                    (= (node-score best) -1)
-                    ;;ugh
-                    (= (node-score best) 0)))
-           ;; FIXME: (set! best node) ?
-           (begin
-             (set! (node-score best) score)
-             (set! (node-page best) nbpn)
-             (set! (node-prev best) node)
-             (set! (node-height best) height)
-             
-             (if pld?
-                 (format (current-error-port) "NEW BEST: ~f ~S\n"
-                         (node-score best)
-                       (map node-line-number (get-path best)))
-                 (format (current-error-port) "breaking after: ~S scores: ~S\n"
-                         (node-line-number node)
-                         score)))
-           (if pld?
-               (format (current-error-port) "BEST still better\n")))
-       (if (null? (cdr nodes))
-           best
-           (walk-paths best (car paths) (cons (node-line node) lines)
-                       (cdr nodes) (cdr paths))))))
+         (format (current-error-port) "breaks: ~S\n" (map line-number break-nodes))
+         (force-output (current-error-port))))
+
     
-  (define (walk-lines lines nodes paths)
+    ; create stencils.
     
-    (if (null? (cdr lines))
-       paths
-       (let ((next (make-node (car nodes) (cadr lines) 0 0))
-             (best (car nodes)))
-
-         (if pld?
-             (begin
-               (format (current-error-port) "\n***********TOP*************")
-               (describe best))
-             (newline (current-error-port)))
-
-         (let ((break (walk-paths next best
-                                  (list (node-line best))
-                                  (cons best nodes)
-                                  paths)))
-           
-           (if pld?
-               (format (current-error-port) "break: ~f ~S\n"
-                       (node-score break)
-                       (map node-line-number (get-path break))))
-           (walk-lines (cdr lines)
-                       (cons (make-node '() (cadr lines) 0 0) nodes)
-                       (cons break paths))))))
-  
-  (let* ((dummy (make-node '() '() 0 0))
-        (result (walk-lines lines
-                            (list (make-node dummy (car lines) 0 0))
-                            (list dummy)))
-        (path (get-path (car result)))
-        ;; CDR: junk dummy node
-        (breaks (cdr (reverse (map node-line-number path)))))
-
-    (format (current-error-port) "breaks: ~S\n" breaks)
-    (force-output (current-error-port))
-    (list->vector breaks)))
+    (map (lambda (node)
+          ((ly:output-def-lookup bookpaper 'page-make-stencil)
+           (node-lines node)
+           bookpaper
+           scopes
+           (node-page-number node)
+           (eq? node best-break-node)))
+        break-nodes)))
+
+