Skip to content

Instantly share code, notes, and snippets.

@trueroad
Last active September 18, 2024 10:19
Show Gist options
  • Save trueroad/cc8bc24b703e7024b80ea517bc23f9dd to your computer and use it in GitHub Desktop.
Save trueroad/cc8bc24b703e7024b80ea517bc23f9dd to your computer and use it in GitHub Desktop.
Test cairo overwrite notehead.
\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 {}
}
#
# 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 "長すぎ" $^ $@
#!/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()
<!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>
<!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>
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment