Last active
September 18, 2024 10:19
-
-
Save trueroad/cc8bc24b703e7024b80ea517bc23f9dd to your computer and use it in GitHub Desktop.
Revisions
-
trueroad revised this gist
Sep 18, 2024 . 7 changed files with 654 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,186 @@ # # LilyPond で生成した楽譜の符頭にバツ印やテキストを上書きしてみるテスト # # # 動作環境 # LilyPond 2.24.3 # Python 3.9 # python-cairo, pypdf # Poppler # GNU make, sed, etc. # # 使い方 # `make` で SVG が生成される。 # .html をブラウザで開くと楽譜の符頭に上書きされたものが見える。 # # # ターゲットの拡張子なしファイル名 # TARGET_STEM = invention1 # # 同梱の .ly をそのまま使用して生成するファイル # # PDF 経由で生成する SVG CROPPED_PDF_SVG = $(TARGET_STEM).cropped.pdf.svg # PDF 経由せず直接生成する SVG CROPPED_SVG = $(TARGET_STEM).cropped.svg # # ポイント&クリックを有効にした(アノテーションを含んだ) .ly と生成ファイル # # .ly ANNOT_LY = $(TARGET_STEM).annot.ly # リンクなどアノテーションを含んだ PDF ANNOT_CROPPED_PDF = $(TARGET_STEM).annot.cropped.pdf # PDF の CropBox とリンク情報のテキストファイル LINK_TXT = $(TARGET_STEM).annot.cropped.link.txt # # SMF (.mid) を出力する .ly と生成ファイル # # .ly MIDI_LY = $(TARGET_STEM).midi.ly # SMF (.mid) MIDI_MID = $(TARGET_STEM).midi.mid # # 音楽イベントを出力する .ly と生成ファイル # # .ly EVENT_LY = $(TARGET_STEM).event.ly # 音楽イベント一覧のテキストファイル NOTES_TEXT = $(TARGET_STEM).event-unnamed-staff.notes # # 符頭に上書きする SVG # # バツ印 CROSS_SVG = $(TARGET_STEM).overwrite-notehead.cross.svg # テキスト(英文) CROSS_TEXT_ENGLISH = $(TARGET_STEM).overwrite-notehead.text.english.svg # テキスト(和文) CROSS_TEXT_JAPANESE = $(TARGET_STEM).overwrite-notehead.text.japanese.svg TARGET = $(CROPPED_PDF_SVG) $(CROPPED_SVG) \ $(ANNOT_LY) $(ANNOT_CROPPED_PDF) $(LINK_TXT) \ $(MIDI_LY) $(MIDI_MID) \ $(EVENT_LY) $(NOTES_TEXT) \ $(CROSS_SVG) $(CROSS_TEXT_ENGLISH) $(CROSS_TEXT_JAPANESE) all: $(TARGET) .PHONY: all clean SED = sed LILYPOND = lilypond PDFTOCAIRO = pdftocairo SHOW_PDF_LINK = ./show_pdf_link.py TEST_CAIRO_OVERWRITE_NOTEHEAD = ./test-cairo-overwrite-notehead.py target: $(TARGET) clean: $(RM) *~ $(TARGET) # PDF から SVG を生成する # # Poppler 付属の pdftocairo を使用する。 # 文字はすべてアウトライン化される。 # 寸法や位置関係など PDF と完全一致した SVG が出力される。 # リンク等は消滅する。 %.cropped.pdf.svg: %.cropped.pdf $(PDFTOCAIRO) -svg $< $@ # LilyPond でクロップされた SVG を直接出力する # # 非クロップ版 SVG も出力されるので削除する。 # # LilyPond 2.24.3 では PDF 出力と SVG 出力ではバックエンドの違いからか # クロップ範囲が微妙に異なるようで微妙にズレてしまう。 # `Allegro` や `( = 96)` 等の文字は文字として出力され # フォントの違いなどからかそもそもの配置が異なる。 # 音楽記号は音符休符などだけでなく拍子記号の `C` や強弱記号の `mf` も含め、 # アウトライン化されたパスとして出力され、相対的な位置関係は一致する。 # ただし、文字中の四分音符などは形状こそ一致するものの、 # 文字の配置がことなるためか他の音楽記号とは位置がズレる。 # cairo バックエンドを持つ LilyPond ならば PDF, SVG 双方とも # 同じ cairo バックエンドで出力すれば完全一致するのかもしれないが # LilyPond 2.24.3 ではデフォルト無効なので通常は使用できない。 %.cropped.svg: %.ly $(LILYPOND) -dcrop --svg $< $(RM) $*.svg # LilyPond でクロップされた PDF を出力する # # PNG と非クロップ版 PDF も出力されてしまうので削除する。 %.cropped.pdf: %.ly $(LILYPOND) -dcrop --pdf $< $(RM) $*.cropped.png $*.pdf # LilyPond で SMF (.mid) を出力する # # Linux や Cygwin ではデフォルト拡張子が .midi なので .mid を指定する。 %.mid: %.ly $(LILYPOND) -dmidi-extension=mid $< # LilyPond で音楽イベントを出力する # # 譜の名前を付けていないのでファイル名に `unnamed` が付く。 # 出力が上書きではなく追記になってしまうので一旦出力ファイルを消す。 # `\layout {}` が無くて補完されてしまい PDF が出力されるので削除する。 %-unnamed-staff.notes: %.ly $(RM) $@ $(LILYPOND) $< $(RM) $*.pdf # ポイント&クリックを有効にした .ly を生成する %.annot.ly: %.ly $(SED) -r \ -e 's/\\pointAndClickOff/\\pointAndClickOn/' \ $< > $@ # PDF を出力せずに SMF (.mid) を出力する .ly を生成する %.midi.ly: %.ly $(SED) -r \ -e 's/\\layout \{\}/% \\layout \{\}/' \ -e 's/% \\midi \{\}/\\midi \{\}/' \ $< > $@ # 音楽イベントを出力し(event-listener.ly をインクルードする) # ポイント&クリックを有効にし、 # PDF を出力しない .ly を生成する。 %.event.ly: %.ly $(SED) -r \ -e 's/%%% INCLUDE %%%/\\include "event-listener.ly"/' \ -e 's/\\pointAndClickOff/\\pointAndClickOn/' \ -e 's/\\layout \{\}/% \\layout \{\}/' \ $< > $@ # PDF の CropBox とリンク情報を出力する %.link.txt: %.pdf $(SHOW_PDF_LINK) $< > $@ # バツ印の SVG を生成する %.overwrite-notehead.cross.svg: %.annot.cropped.link.txt \ %.event-unnamed-staff.notes $(TEST_CAIRO_OVERWRITE_NOTEHEAD) --cross $^ $@ # テキスト(英文)の SVG を生成する %.overwrite-notehead.text.english.svg: %.annot.cropped.link.txt \ %.event-unnamed-staff.notes $(TEST_CAIRO_OVERWRITE_NOTEHEAD) --text "Too long" $^ $@ # テキスト(和文)の SVG を生成する %.overwrite-notehead.text.japanese.svg: %.annot.cropped.link.txt \ %.event-unnamed-staff.notes $(TEST_CAIRO_OVERWRITE_NOTEHEAD) --text "長すぎ" $^ $@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ \version "2.24.3" %%% INCLUDE %%% \pointAndClickOff upper = \relative { \clef treble \key c \major \time 4/4 \tempo "Allegro" 4 = 96 r16_\mf c'( d e f d e c g'8) c b^\prall c | d16 g,( a b c a b g d'8) g f^\prall g | e16 a g f e g f a g f e d c e d f | } lower = \relative { \clef bass \key c \major \time 4/4 r2 r16 c( d e f d e c | g'8) g, r4 r16 g'( a b c a b g | c8) b( c d e) g,( a b) | } \score { \new PianoStaff << \new Staff = "upper" \upper \new Staff = "lower" \lower >> \layout {} % \midi {} } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,92 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Show PDF link annots. https://gist.github.com/trueroad/4810ab0845d86fb97510e4e3d3bbed13 Copyright (C) 2024 Masamichi Hosoda. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import sys from typing import cast, Optional, Union from pypdf import PageObject, PdfReader from pypdf.generic import (ArrayObject, DictionaryObject, IndirectObject, PdfObject, RectangleObject) def main() -> None: """Do main.""" if len(sys.argv) != 2: print('Usage: ./show_pdf_link.py [INPUT.PDF]', file=sys.stderr) sys.exit(1) reader: PdfReader = PdfReader(sys.argv[1]) page: PageObject for page in reader.pages: cropbox: RectangleObject = page.cropbox crop_left: float = cropbox.left.as_numeric() crop_bottom: float = cropbox.bottom.as_numeric() crop_right: float = cropbox.right.as_numeric() crop_top: float = cropbox.top.as_numeric() print(f'\nCropBox\t{crop_left}\t{crop_bottom}' f'\t{crop_right}\t{crop_top}') if '/Annots' in page: annot: Union[IndirectObject, PdfObject] for annot in cast(ArrayObject, page['/Annots']): if type(annot) is not IndirectObject: raise RuntimeError('It is not IndirectObject.') obj: Optional[Union[DictionaryObject, PdfObject]] = \ annot.get_object() if obj is None: raise RuntimeError('get_object() returns None.') if type(obj) is not DictionaryObject: raise RuntimeError('It is not DictionaryObject.') if str(obj['/Subtype']) == '/Link': a: Union[DictionaryObject, PdfObject] = obj['/A'] if type(a) is not DictionaryObject: raise RuntimeError('It is not DictionaryObject.') if str(a['/S']) == '/URI': rect: Union[ArrayObject, PdfObject] = obj['/Rect'] if type(rect) is not ArrayObject: raise RuntimeError('It is not ArrayObject.') link_left: float = rect[0].as_numeric() link_bottom: float = rect[1].as_numeric() link_right: float = rect[2].as_numeric() link_top: float = rect[3].as_numeric() uri: str = str(a['/URI']) print(f'Link\t{link_left}\t{link_bottom}' f'\t{link_right}\t{link_top}' f'\t{uri}') if __name__ == '__main__': main() This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,267 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Test cairo overwrite notehead. https://gist.github.com/trueroad/cc8bc24b703e7024b80ea517bc23f9dd Copyright (C) 2024 Masamichi Hosoda. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from dataclasses import dataclass import os import sys from typing import Any, Dict, Optional, TextIO, Union import cairo @dataclass(frozen=True) class rect_container: """Rectangle container class.""" left: float top: float right: float bottom: float @dataclass(frozen=True) class point_and_click_container: """Point and click container class.""" row: int column: int @dataclass(frozen=True) class note_container: """Note container class.""" time: float noteno: int point_and_click: point_and_click_container class link_text: """Link text class.""" def __init__(self) -> None: """__init__.""" # PDF CropBox self.cropbox: rect_container # PDF links self.links: dict[point_and_click_container, rect_container] = {} # notes self.notes: list[note_container] = [] def load_link(self, filename: Union[str, bytes, os.PathLike[Any]] ) -> None: """Load link text.""" f: TextIO with open(filename, 'r') as f: line: str for line in f: items: list[str] = line.split() if len(items) == 5 and items[0] == 'CropBox': self.cropbox = rect_container(left=float(items[1]), bottom=float(items[2]), right=float(items[3]), top=float(items[4])) elif len(items) == 6 and items[0] == 'Link': if not items[5].startswith('textedit://'): continue pc_items: list[str] = items[5].split(':') self.links[ point_and_click_container( row=int(pc_items[-3]), column=int(pc_items[-2]))] = \ rect_container( left=float(items[1]), bottom=float(items[2]), right=float(items[3]), top=float(items[4])) def load_notes(self, filename: Union[str, bytes, os.PathLike[Any]] ) -> None: """Load notes.""" f: TextIO with open(filename, 'r') as f: line: str for line in f: items: list[str] = line.split('\t') if len(items) == 6 and items[1] == 'note': pc_items: list[str] = items[5].split() if len(pc_items) != 3 or \ pc_items[0] != 'point-and-click': raise RuntimeError('Notes file format error.') self.notes.append(note_container( time=float(items[0]), noteno=int(items[2]), point_and_click=point_and_click_container( row=int(pc_items[2]), column=int(pc_items[1])))) def calc_size(self) -> tuple[float, float]: """ Calc SVG size. Returns: tuple[float, float]: SVG width (pt), height (pt) """ width: float = self.cropbox.right - self.cropbox.left height: float = self.cropbox.top - self.cropbox.bottom return (width, height) def conv_axis(self, x: float, y: float) -> tuple[float, float]: """ Convert axis PDF to SVG. Args: x (float): PDF x axis (pt) y (float): PDF y axis (pt) Returns: tuple[float, float]: SVG x axis (pt) ,y axis (pt) """ svg_x: float = x - self.cropbox.left svg_y: float = self.cropbox.top - self.cropbox.bottom - y return (svg_x, svg_y) def conv_rect(self, rect: rect_container) -> rect_container: """ Convert rect PDF to SVG. Args: rect (rect_container): PDF rect Returns: rect_container: SVG rect """ svg_left: float svg_top: float svg_right: float svg_bottom: float svg_left, svg_top = self.conv_axis(rect.left, rect.top) svg_right, svg_bottom = self.conv_axis(rect.right, rect.bottom) return rect_container(left=svg_left, top=svg_top, right=svg_right, bottom=svg_bottom) def draw_crosses(self, context: cairo.Context) -> None: """Draw crosses.""" nc: note_container for nc in self.notes: rect: rect_container = self.links[nc.point_and_click] draw_cross(context, self.conv_rect(rect)) def draw_texts(self, context: cairo.Context, text: str) -> None: """Draw texts.""" nc: note_container for nc in self.notes: rect: rect_container = self.links[nc.point_and_click] draw_text(context, self.conv_rect(rect), text) def draw_cross(context: cairo.Context, rect: rect_container) -> None: """Draw cross.""" context.move_to(rect.left, rect.top) context.line_to(rect.right, rect.bottom) context.move_to(rect.right, rect.top) context.line_to(rect.left, rect.bottom) def draw_text(context: cairo.Context, rect: rect_container, text: str) -> None: """Draw text.""" context.select_font_face('sans-serif') context.set_font_size(rect.bottom - rect.top) # 指定した座標がベースラインの左端になる模様。 # よくあるフォントはベースラインの上が 0.88 下が 0.12 あるので、 # 符頭の上下ピッタリに合わせるには以下のようにする。 # (和文フォントはこれでだいたい合うが欧文はフォントや環境次第) context.move_to(rect.left, rect.top + (rect.bottom - rect.top) * 0.88) context.show_text(text) def main() -> None: """Do main.""" print('Test cairo overwrite notehead\n' 'https://gist.github.com/trueroad/' 'cc8bc24b703e7024b80ea517bc23f9dd\n\n' 'Copyright (C) 2024 Masamichi Hosoda.\n' 'All rights reserved.\n') import argparse parser: argparse.ArgumentParser = argparse.ArgumentParser() parser.add_argument('LINK.TXT', help='(in) Link file.') parser.add_argument('STAFF.NOTES', help='(in) Notes file.') parser.add_argument('OUTPUT.SVG', help='(out) Output SVG.') parser.add_argument('--cross', help='Overwrite cross.', action='store_true') parser.add_argument('--text', help='Overwrite text.', type=str, nargs=1) args: argparse.Namespace = parser.parse_args() vargs: Dict[str, Any] = vars(args) link_filename: str = vargs['LINK.TXT'] notes_filename: str = vargs['STAFF.NOTES'] svg_filename: str = vargs['OUTPUT.SVG'] b_cross: bool = vargs['cross'] text_str: Optional[str] = None if vargs['text'] is not None: text_str = vargs['text'][0] print(f'Link filename : {link_filename}\n' f'Notes filename: {notes_filename}\n' f'Output SVG : {svg_filename}\n' f'Cross : {b_cross}\n' f'Text : {text_str}\n') lt: link_text = link_text() lt.load_link(link_filename) lt.load_notes(notes_filename) width: float height: float width, height = lt.calc_size() surface: cairo.SVGSurface # ファイル名、横 pt 、縦 pt with cairo.SVGSurface(svg_filename, width, height) as surface: context: cairo.Context = cairo.Context(surface) if b_cross: context.set_line_width(2) context.set_source_rgba(1, 0, 0, 0.7) lt.draw_crosses(context) context.stroke() if text_str is not None: context.set_source_rgba(1, 0, 0, 0.9) lt.draw_texts(context, text_str) if __name__ == '__main__': main() This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,23 @@ <!DOCTYPE html> <html> <head> <title>テスト(バツ印)</title> </head> <style> .relative { position: relative; } .absolute { position: absolute; } </style> <body> <h1>テスト(バツ印)</h1> <div class="relative"> <img src="invention1.cropped.pdf.svg" class="absolute" /> <img src="invention1.overwrite-notehead.cross.svg" class="absolute" /> </div> </body> </html> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,24 @@ <!DOCTYPE html> <html> <head> <title>Test (text english)</title> </head> <style> .relative { position: relative; } .absolute { position: absolute; } </style> <body> <h1>Test (text english)</h1> <div class="relative"> <img src="invention1.cropped.pdf.svg" class="absolute" /> <img src="invention1.overwrite-notehead.text.english.svg" class="absolute" /> </div> </body> </html> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,24 @@ <!DOCTYPE html> <html> <head> <title>テスト(テキスト和文)</title> </head> <style> .relative { position: relative; } .absolute { position: absolute; } </style> <body> <h1>テスト(テキスト和文)</h1> <div class="relative"> <img src="invention1.cropped.pdf.svg" class="absolute" /> <img src="invention1.overwrite-notehead.text.japanese.svg" class="absolute" /> </div> </body> </html> -
trueroad created this gist
Sep 18, 2024 .There are no files selected for viewing