#(define song-list '()) #(define song-number 0) #(define (files-in-directory dirname) ;;; Generate list containing filenames (let ((dir (opendir dirname))) (let next ((f (readdir dir)) (files '())) (cond ((eof-object? f) (closedir dir) files) (else (next (readdir dir) (if (ly:regex-match? (ly:regex-exec (ly:make-regex "^(0|\\.)") f)) files (cons f files)))))))) #(define (file-to-stats filename) (set! song-list (cons filename song-list))) #(define additional-page-switch-label-list '()) #(define additional-page-numbers #f) normalPageNumbers = #(define-void-function (parser location) () (set! additional-page-numbers #f) ) additionalPageNumbers = #(define-void-function (parser location) () (set! additional-page-numbers #t) ) #(define (real-page-number layout label) (let ((table (ly:output-def-lookup layout 'label-page-table))) (if (list? table) (assoc-get label table) #f)) ) #(define display-pages-list '()) #(define (build-display-pages-list layout) (if (null? display-pages-list) (let calculate-display-page ((switch-label-list additional-page-switch-label-list)) (let* ((label (caar switch-label-list)) (is-additional (cdar switch-label-list)) (real-page (real-page-number layout label)) (rest-switch-label-list (cdr switch-label-list)) (display-page (if (null? rest-switch-label-list) (if is-additional (- real-page 1) real-page) (let* ((previous-label (caar rest-switch-label-list)) (previous-is-additional (cdar rest-switch-label-list)) (previous-display-page (calculate-display-page rest-switch-label-list))) (+ previous-display-page (if previous-is-additional (if is-additional 0 1) (if is-additional (- (- real-page (real-page-number layout previous-label)) 1) (- real-page (real-page-number layout previous-label))) ) ) ) )) ) (set! display-pages-list (acons label display-page display-pages-list)) display-page ))) display-pages-list ) % Fuer Zusatzseiten: Label der ersten Seite des zusammenhaengenden % Zusatzseiten-Blocks, zu dem label gehoert (bestimmt den Buchstaben-Suffix a, b, ...). #(define (earliest-additional-label label) (let find-earliest-additional-label ((rest-additional-page-switch-label-list (member (cons label #t) additional-page-switch-label-list))) (if (cdadr rest-additional-page-switch-label-list) (find-earliest-additional-label (cdr rest-additional-page-switch-label-list)) (caar rest-additional-page-switch-label-list)))) % Sichtbare Seitenzahl der Seite mit diesem Label: normalerweise eine Zahl, % auf Zusatzseiten ein String mit Buchstaben-Suffix (z.B. "12a"); gleiche % Logik wie custom-page-number unten. #(define (visible-page-value layout label) (let ((display-page (assq-ref (build-display-pages-list layout) label))) (if (assq-ref additional-page-switch-label-list label) (ly:format "~a~a" display-page (string (integer->char (+ 97 (- (real-page-number layout label) (real-page-number layout (earliest-additional-label label))))))) display-page))) % TODO: % Eigentlich können wir das direkt in oddFooderMarkup und evenFooterMarkup aufrufen % vermutlich sogar ohne den delay kram. Wir sollten außerdem einfach nur die property % page:page-number-string setzen dann klappts auch mit PDF Seiten #(define-markup-command (custom-page-number layout props label real-current-page-number) (symbol? number?) #:category other " @cindex referencing page number, in text Reference to a page number. @var{label} is the label set on the referenced page (using the @code{\\label} command), @var{gauge} a markup used to estimate the maximum width of the page number, and @var{default} the value to display when @var{label} is not found. (If the current book or bookpart is set to use roman numerals for page numbers, the reference will be formatted accordingly -- in which case the @var{gauge}'s width may require additional tweaking.)" (let* ((gauge-stencil (interpret-markup layout props "XXX")) (x-ext (ly:stencil-extent gauge-stencil X)) (y-ext (ly:stencil-extent gauge-stencil Y))) (ly:stencil-outline (ly:make-stencil `(delay-stencil-evaluation ,(delay (ly:stencil-expr (let* ((display-page (assq-ref (build-display-pages-list layout) label)) (real-current-page (if (negative? real-current-page-number) (real-page-number layout label) real-current-page-number)) (number-type (ly:output-def-lookup layout 'page-number-type)) (page-markup (if (assq-ref additional-page-switch-label-list label) (make-concat-markup (list (number-format number-type display-page) (make-char-markup (+ 97 (- real-current-page (real-page-number layout (earliest-additional-label label))))))) (number-format number-type (+ display-page (- real-current-page (real-page-number layout label)))) )) (page-stencil (interpret-markup layout props page-markup)) (gap (- (interval-length x-ext) (interval-length (ly:stencil-extent page-stencil X))))) (interpret-markup layout props (make-line-markup (list (make-hspace-markup gap) page-markup))))))) x-ext y-ext) (make-filled-box-stencil x-ext y-ext)))) %% Schreibt die Daten aller Lieder des Buchs (extract-song-data, siehe %% data_extractor.ily) als YAML-Datei: oberster Key contents, darunter eine %% Liste mit den Lied-Daten, jeweils angereichert um den Key page mit der %% sichtbaren Seitenzahl des Liedanfangs. Einbinden wie \write-toc-csv in %% einem Markup des Buchs; wie dort passiert das Schreiben ueber ein %% delayed stencil, weil die Label->Seiten-Tabelle erst beim Ausgeben des %% Buchs feststeht. Der Dateiname ist der Ausgabename des Buchs %% (\bookOutputName, sonst der .ly-Basisname) mit angehaengtem .yml -- %% landet also wie das PDF relativ zum Arbeitsverzeichnis. #(define-markup-command (write-book-yaml layout props) () (let ((filename (string-append (ly:parser-output-name) ".yml"))) (ly:make-stencil `(delay-stencil-evaluation ,(delay (begin (scm->yml-file filename (list (cons 'contents (map (lambda (song) (let ((songvars (cdr song))) (cons (cons 'page (visible-page-value layout (assq-ref songvars 'label))) (extract-song-data (assq-ref songvars 'header) (assq-ref songvars 'music) ;; Textseiten-Strophen aus dem Render- ;; Collector-Store (Key = Liedname); beim ;; Ausgeben des Buchs sind alle Textseiten ;; bereits interpretiert. (hash-ref chordpro-song-store (symbol->string (car song))) (symbol->string (car song)))))) (reverse (alist-delete 'markupPage (alist-delete 'imagePage (alist-delete 'emptyPage song-list)))))))) empty-stencil)))))) %% Schreibt den ChordPro-Export aller Lieder des Buchs (in Dokument- %% reihenfolge) als EINE Datei statt als Einzeldateien pro Lied. Lieder %% werden mit der ChordPro-Direktive {new_song} getrennt; vor dem ersten %% Lied ist sie nicht noetig, sie gilt am Dateianfang implizit (siehe %% https://www.chordpro.org/chordpro/directives-new_song/). Einbinden wie %% \write-book-yaml an beliebiger Stelle im Buch -- unabhaengig von %% chordpro-export-enabled (das steuert nur den Pro-Song-Export), beide %% Befehle koennen auch gleichzeitig genutzt werden. Wie bei %% \write-book-yaml passiert das Schreiben ueber ein delayed stencil, %% weil der Render-Collector-Store erst beim Ausgeben des Buchs %% vollstaendig gefuellt ist. Der Dateiname ist wie bei \write-book-yaml %% der Ausgabename des Buchs mit angehaengtem .cho. #(define-markup-command (write-book-chordpro layout props) () (let ((filename (string-append (ly:parser-output-name) ".cho"))) (ly:make-stencil `(delay-stencil-evaluation ,(delay (begin (with-output-to-file filename (lambda () (let loop ((songs (filter ;; \chordlyrics-Strophen ODER (bei ;; chordpro-include-music-lyrics) aus der ;; Musik nachgeholte Kandidaten. (lambda (song) (or (> (chordpro-song-verse-index song) 0) (pair? (chordpro-song-music-candidates song)))) (filter-map (lambda (song) (hash-ref chordpro-song-store (symbol->string (car song)))) (reverse (alist-delete 'markupPage (alist-delete 'imagePage (alist-delete 'emptyPage song-list))))))) (first? #t)) (when (pair? songs) (unless first? (display "{new_song}\n")) (display (chordpro-format-song (car songs))) (loop (cdr songs) #f))))) empty-stencil)))))) includeSong = #(define-void-function (filename) (string?) #{ \bookOutputName #filename #} (ly:parser-parse-string (ly:parser-clone) (ly:format "\\include \"~a/~a/~a.ly\"" songPath filename filename)) ;; ChordPro: Titel/Autoren dieses Lieds im Render-Collector-Store ;; registrieren -- basicSongInfo haelt direkt nach dem Include genau ;; dieses Lied. Der Key ist der Liedname; die Collector-Daten (Verse, ;; Akkorde) kommen spaeter beim Rendern ueber \setsongfilename unter ;; demselben Key dazu. Unabhaengig von chordpro-export-enabled, damit ;; sowohl der Pro-Song-Export (der die Flag braucht) als auch ;; \write-book-chordpro (das nur explizit aufgerufen werden muss) auf ;; vollstaendige Metadaten zugreifen koennen. (chordpro-register-song-metadata filename HEADER) (let ((label (gensym "index"))) (set! additional-page-switch-label-list (acons label additional-page-numbers additional-page-switch-label-list)) (set! song-list (acons (string->symbol filename) `( (label . ,label) (header . ,HEADER) (music . ,MUSIC) (layout . ,LAYOUT) (text-pages . ,(map (lambda (text) #{ \markuplist \setsongfilename $filename $text #}) TEXT_PAGES)) ) song-list)) )) blankpage = #(define-void-function () () (set! song-list (acons 'emptyPage '() song-list))) imagepage = #(define-void-function (xsize filename) (number? string?) (set! song-list (acons 'imagePage `((xsize . ,xsize) (filename . ,filename)) song-list))) markuppage = #(define-void-function (mkup) (markup?) (set! song-list (acons 'markupPage `((markup . ,mkup)) song-list))) #(define chapters '()) songsChapter = #(define-void-function (title) (string?) (set! chapters (cons (cons title (length song-list)) chapters))) #(define-markup-command (pagecenter layout props stuff)(markup?) (interpret-markup layout props (let ((halfpaperheight (/ (ly:output-def-lookup layout 'paper-height) 2)) (halfstuffheight (/ (interval-length (ly:stencil-extent (interpret-markup layout props stuff) Y)) 2))) (make-fill-line-markup (list (make-pad-to-box-markup '(0 . 0) (cons (- (- halfpaperheight halfstuffheight)) (+ halfpaperheight halfstuffheight)) stuff))) ))) songs = #(define-void-function () () (let* ((ordered (reverse song-list)) (total (length ordered)) ;; Ein Lied erzeugt nur dann einen PDF-Outline-Eintrag (/OUT), wenn ;; title oder starttext gesetzt sind (siehe build-full-title). Nur solche ;; Lieder duerfen als Kapitel-Kinder gezaehlt werden -- sonst wird die ;; /Count zu hoch und der erste Folgeeintrag (z.B. ein spaeteres Kapitel) ;; rutscht faelschlich als Kind mit hinein. (emits-bookmark? (lambda (e) (and (not (eq? (car e) 'emptyPage)) (not (eq? (car e) 'imagePage)) (not (eq? (car e) 'markupPage)) (let ((vars (extract-and-check-vars-from-header (assq-ref (cdr e) 'header) '(title starttext)))) (if (or (assq-ref vars 'title) (assq-ref vars 'starttext)) #t #f))))) (count-bookmark-songs (lambda (start end) (let loop ((i 0) (l ordered) (c 0)) (cond ((or (null? l) (>= i end)) c) ((< i start) (loop (+ i 1) (cdr l) c)) (else (loop (+ i 1) (cdr l) (if (emits-bookmark? (car l)) (+ c 1) c))))))) ;; chapters: (title . start-index). start-index = Anzahl der song-list- ;; Eintraege beim Aufruf von songsChapter = Index in `ordered`, ab dem ;; das Kapitel beginnt. (chapters-asc (sort chapters (lambda (a b) (< (cdr a) (cdr b))))) ;; pending: aufsteigende Liste von (start title count); count = Anzahl ;; der Lesezeichen-Lieder von diesem Kapitel bis zum Beginn des naechsten. (pending (let loop ((chs chapters-asc) (acc '())) (if (null? chs) (reverse acc) (let* ((title (caar chs)) (start (cdar chs)) (end (if (null? (cdr chs)) total (cdadr chs))) (cnt (count-bookmark-songs start end))) (loop (cdr chs) (cons (list start title cnt) acc)))))) ;; Aktuell aktives Kapitel (Titel-String) waehrend des Durchlaufs. Wird ;; gesetzt, sobald ein Kapitel aktiviert (aus `pending` entfernt) wird -- ;; egal ob auf einer Seite (chapter-prefixed-markup) oder auf dem ersten ;; Song. Jeder danach folgende Song erhaelt dieses Kapitel als ;; chapterref-Header-Feld, woraus toc_include.ily den Kapitel-Index baut. (current-chapter #f) ;; Stellt einer beliebigen Seite (markup-/image-/emptyPage) den Kapitel- ;; Eintrag (chapter-to-pdf-toc) bei, falls an Position `this-index` ein ;; Kapitel faellig ist, und entfernt es aus `pending`. So fuehrt die ;; Kapitel-Referenz zur ersten Seite nach dem \songsChapter -- egal ;; welcher Seitentyp das ist. Der Eintrag wird als unsichtbares Overlay ;; (\combine) beigelegt, damit er das Seitenlayout nicht veraendert; ;; \with-dimensions gibt dem sonst extent-losen pdfmark einen minimalen ;; Extent, damit er auch auf leeren Seiten nicht verworfen wird. (chapter-prefixed-markup (lambda (this-index base-markup) (if (and (pair? pending) (<= (car (car pending)) this-index)) (let ((cnt (caddr (car pending))) (title (cadr (car pending)))) (set! pending (cdr pending)) (set! current-chapter title) (make-combine-markup base-markup (make-with-dimensions-markup '(0 . 0) '(0 . 0.1) (make-chapter-to-pdf-toc-markup cnt title)))) base-markup))) (index 0)) (for-each (lambda (songitems) (let ((this-index index)) (set! index (+ index 1)) (ly:book-add-bookpart! (ly:parser-lookup '$current-book) (let ((filename (car songitems)) (songvars (cdr songitems))) (if (eq? filename 'emptyPage) #{ \bookpart { \markup { $(chapter-prefixed-markup this-index (make-null-markup)) } } #} (if (eq? filename 'markupPage) ;; Kapitelanfang auf einer markuppage: Kapitel-Eintrag wird dieser ;; Seite beigelegt (siehe chapter-prefixed-markup). #{ \bookpart { \markup { $(chapter-prefixed-markup this-index (assq-ref songvars 'markup)) } } #} (if (eq? filename 'imagePage) (let* ((xsize (assq-ref songvars 'xsize)) (filename (ly:format "~a/~a" imagePagePath (assq-ref songvars 'filename))) (img-markup #{ \markup { \pagecenter { \epsfile #X #xsize #filename } } #})) #{ \bookpart { \paper { %{ inner-margin = 0 outer-margin = 0 binding-offset = 0 top-margin = 0 bottom-margin = 0 %} print-page-number = ##f last-bottom-spacing = #'((basic-distance . 0) (minimum-distance . 0) (padding . 0)) page-count = 1 } \markup { $(chapter-prefixed-markup this-index img-markup) } } #} ) ;; Ist ein Kapitel faellig (Start <= aktuelle Position) und dies ein ;; echtes Lied, wird die Kapitelinfo an dieses Lied gehaengt und aus ;; `pending` entfernt. (let* ((newnumber (+ 1 song-number)) (chaptertitle (if (and (pair? pending) (<= (car (car pending)) this-index)) (cadr (car pending)) #f)) (chaptercount (if chaptertitle (caddr (car pending)) 0)) ;; Startet das Kapitel erst auf diesem Song (nicht schon auf ;; einer vorangehenden Seite), wird current-chapter jetzt ;; gesetzt und pending entfernt -- vor dem Bau des Headers, ;; damit chapterref bereits das neue Kapitel enthaelt. (_ (if chaptertitle (begin (set! current-chapter chaptertitle) (set! pending (cdr pending))))) (header #{ \bookpart { $(assq-ref songvars 'header) \header { songfilename = $(symbol->string filename) myindexlabel = #(assq-ref songvars 'label) songnumber = #(number->string newnumber) chaptertitle = #chaptertitle chaptercount = #chaptercount chapterref = #current-chapter } } #}) (music (assq-ref songvars 'music)) (layout (assq-ref songvars 'layout)) (text-pages (assq-ref songvars 'text-pages)) (label (assq-ref songvars 'label))) (set! song-number newnumber) ;; ChordPro_music_collector (Strophen unter den Noten, siehe ;; chordpro.ily) braucht den Song-Key beim tatsaechlichen ;; Interpretieren dieses Songs' Hauptscore. Im Buch ist DAS ;; NICHT synchron zu dieser for-each-Schleife -- LilyPond ;; interpretiert die \score-Bloecke aller Lieder gesammelt fuer ;; das Seitenlayout, oft NACHDEM die Schleife schon beim ;; naechsten (oder letzten) Lied angelangt ist. Ein globaler ;; chordpro-active-song-key waere zu diesem Zeitpunkt also auf ;; ein ANDERES Lied gesetzt -- die gesammelten Strophen landen ;; dann fälschlich im Store-Eintrag des zuletzt verarbeiteten ;; Lieds (beobachtet: mehrere Lieder ohne eigene \chordlyrics ;; wurden alle in den letzten Song der Test-Bibliothek ;; geschrieben). Deshalb setzen wir den Key per \applyContext ;; DIREKT IN DER MUSIK in chordpro-context-song-keys (siehe ;; chordpro.ily) -- das laeuft synchron zur tatsaechlichen ;; Interpretation dieses Scores und ist deshalb robust gegen die ;; verzoegerte/gesammelte Verarbeitung (eine LilyPond-Context- ;; Property ueber den \layout-Block waere hier keine Alternative: ;; LilyPond verwirft \applyContext-\ly:context-set-property!- ;; Aufrufe fuer nicht deklarierte Properties stillschweigend). ;; Der Music_collector liest bevorzugt diese Tabelle, ;; chordpro-active-song-key bleibt nur als Fallback ;; (Einzellied-Standalone-Pfad, dort synchron genug). ;; \consists nur bei chordpro-include-music-lyrics -- sonst ;; keine Kosten (Grob-Acknowledger fuer JEDEN Akkord/jede ;; Silbe/Strophenmarker des Hauptscores) fuer Buecher, die das ;; Feature nicht nutzen. Bewusst NICHT zusaetzlich an ;; chordpro-export-enabled gekoppelt -- die steuert nur, ob ;; \write-book-chordpro/\chordpro-delayed-write am Ende etwas ;; schreiben, nicht ob hier gesammelt wird. (set! chordpro-active-song-key (symbol->string filename)) (let* ((collect-music-lyrics? (and (defined? 'chordpro-include-music-lyrics) chordpro-include-music-lyrics)) (song-layout (if collect-music-lyrics? #{ \layout { $layout songfilename = $(symbol->string filename) \context { \Score \consists \ChordPro_music_collector } } #} #{ \layout { $layout songfilename = $(symbol->string filename) } #})) (score-music (if collect-music-lyrics? #{ << \applyContext #(let ((key (symbol->string filename))) (lambda (ctx) (hashq-set! chordpro-context-song-keys ctx key))) $music >> #} music))) #{ \bookpart { $header \headerToTOC #header #label \score { $score-music $song-layout } $(add-text-pages text-pages) } #}))))))))) ordered ))) #(define (boernel-stats) (let ( (songs (map (lambda (song) (symbol->string (car song))) (alist-delete 'emptyPage song-list))) (opticalline "---------------------------------------------------------")) (ly:warning (string-join (list opticalline (string-concatenate (list "Inkludiert: " (number->string (length songs)) " Lieder\n")) ;(string-join songs "\n") "Nicht inkludiert:" opticalline (string-join (sort-list (lset-difference string=? (files-in-directory songPath) songs) stringnumber (ly:regex-match-substring match x))) (cdr (iota 5))) #f))) #(define eps-references '()) #(define-public (eps-file-ref->stencil axis size just-embed file-name) (let* ((already-embedded (assq (string->symbol file-name) eps-references)) (counter (if already-embedded (cadr already-embedded) (if (null-list? eps-references) 1 (+ 1 (cadar eps-references))))) (form-name (ly:format "IDForm~a" counter)) (data-name (ly:format "ImageData~a" counter)) (eps-content (ly:gulp-file file-name)) (contents (ly:format "~a execform" form-name)) (bbox (if already-embedded (cddr already-embedded) (get-postscript-bbox (car (string-split eps-content #\nul))))) (bbox-size (if (= axis X) (- (list-ref bbox 2) (list-ref bbox 0)) (- (list-ref bbox 3) (list-ref bbox 1)) )) (factor (if (< 0 bbox-size) (exact->inexact (/ size bbox-size)) 0)) (scaled-bbox (map (lambda (x) (* factor x)) bbox)) ;; We need to shift the whole eps to (0,0), otherwise it will appear ;; displaced in lilypond (displacement will depend on the scaling!) (translate-string (ly:format "~a ~a translate" (- (list-ref bbox 0)) (- (list-ref bbox 1)))) (clip-rect-string (ly:format "~a ~a ~a ~a rectclip" (list-ref bbox 0) (list-ref bbox 1) (- (list-ref bbox 2) (list-ref bbox 0)) (- (list-ref bbox 3) (list-ref bbox 1))))) (if (not already-embedded) (set! eps-references (acons (string->symbol file-name) (cons counter bbox) eps-references))) (if bbox (ly:make-stencil (list 'embedded-ps (string-append (if (and already-embedded (not just-embed)) "" (ly:format " /~a currentfile << /Filter /SubFileDecode /DecodeParms << /EODCount 0 /EODString (*EOD*) >> >> /ReusableStreamDecode filter ~a *EOD* def /~a << /FormType 1 /BBox [~a ~a ~a ~a] /Matrix [ 1 0 0 1 0 0] /PaintProc { pop /ostate save def /showpage {} def /setpagedevice /pop load def ~a 0 setfileposition ~a cvx exec ostate restore } bind >> def " data-name eps-content form-name (list-ref bbox 0) (list-ref bbox 1) (list-ref bbox 2) (list-ref bbox 3) data-name data-name)) (if just-embed "" (ly:format " gsave currentpoint translate BeginEPSF ~a dup scale ~a ~a %%BeginDocument: ~a ~a %%EndDocument EndEPSF grestore " factor translate-string clip-rect-string file-name contents )))) ;; Stencil starts at (0,0), since we have shifted the eps, and its ;; size is exactly the size of the scaled bounding box (if just-embed '(0 . 0) (cons 0 (- (list-ref scaled-bbox 2) (list-ref scaled-bbox 0)))) (if just-embed '(0 . 0) (cons 0 (- (list-ref scaled-bbox 3) (list-ref scaled-bbox 1))))) (ly:make-stencil "" '(0 . 0) '(0 . 0))) )) #(define-markup-command (epsfileref layout props axis size file-name) (number? number? string?) #:category graphic " @cindex inlining an Encapsulated PostScript image Inline an EPS image. The image is scaled along @var{axis} to @var{size}. @lilypond[verbatim,quote] \\markup { \\general-align #Y #DOWN { \\epsfile #X #20 #\"context-example.eps\" \\epsfile #Y #20 #\"context-example.eps\" } } @end lilypond" (if (ly:get-option 'safe) (interpret-markup layout props "not allowed in safe") (eps-file-ref->stencil axis size #f file-name) )) #(define-markup-command (epsfileembed layout props file-name) (string?) #:category graphic " @cindex inlining an Encapsulated PostScript image Inline an EPS image. The image is scaled along @var{axis} to @var{size}. @lilypond[verbatim,quote] \\markup { \\general-align #Y #DOWN { \\epsfile #X #20 #\"context-example.eps\" \\epsfile #Y #20 #\"context-example.eps\" } } @end lilypond" (if (ly:get-option 'safe) (interpret-markup layout props "not allowed in safe") (eps-file-ref->stencil X 30 #t file-name) ))