"""いじめ重大事態対応フロー図の印刷用PDFを、SVGからベクターで生成する。
以前は 2.5 倍のラスタPNGを埋め込んだPDF(1ページ版 約2MB / 6ページ版 約4MB)を
使っていたが、同じレイアウトのままベクターで組み直し、大幅に軽量化する
(各ファイル約200KB。文字は拡大しても綺麗)。
- A4縦・タイル分割版(2列×3行=6ページ、のりしろ重複あり)
- A4縦・全体像1ページ版(縮小・見取り図)
レイアウトは旧ラスタ版と同一(A4 3100x4382px 相当、余白100px、紙面2900x4182px、
タイル 1160x1672.8、列 x=[0,660]、行 y=[0, base/2, base])。
base は SVG の元の高さ(リンク行を追記する前の値)。SVG の末尾に追記した
解説ページへのリンク行は、PDFではフッターのリンク注釈として付ける。
使い方: python scripts/make-flow-pdfs.py
"""
import pathlib
import re
import sys
import pymupdf
sys.stdout.reconfigure(encoding="utf-8")
IMAGES = pathlib.Path(__file__).resolve().parent.parent / "public" / "ijime-judai-jitai" / "images"
URL = "https://yasutakeyohei.com/ijime-judai-jitai/flow/"
TITLE = "いじめの重大事態の対応フロー(ロール別)"
# 旧ラスタ版と同じジオメトリ(A4 3100x4382px 相当)
A4W = pymupdf.paper_rect("a4").width
A4H = pymupdf.paper_rect("a4").height
PX2PT = A4W / 3100.0
MARGIN = 100 * PX2PT
DEST = pymupdf.Rect(MARGIN, MARGIN, MARGIN + 2900 * PX2PT, MARGIN + 4182 * PX2PT)
TILE_W1 = 1160.0 # タイル幅(SVG座標=1x基準)
TILE_H1 = 1672.8 # タイル高さ
FOOT_SIZE = 50 * PX2PT # 旧: int(20*2.5)=50px
FOOT_NOTE_SIZE = 31 * PX2PT
FOOT_TOP = (4382 - 100 + 3) * PX2PT
FOOT_COLOR = (0x45 / 255, 0x5A / 255, 0x64 / 255)
FOOT_6P = TITLE + " {n}/6 (全体の一部・のりしろ重複あり)"
FOOT_1P = TITLE + " 全体像(縮小のため文字は小さめ。読みやすい印刷は6ページ版を推奨)"
# 図だけがコピーされても出所と利用条件が分かるように、フッターにもライセンスを入れる。
# 「公立版・私立版とも」で、2種類が同じライセンスであることを示す。
# 「無保証」は、内容の正確性を保証せず、誤りがあっても責任を負わない旨。
# 課題欄があるのは公立学校版だけなので、その旨も公立版にだけ入れる
LIC_PUBLIC = ("© 安竹洋平(小平市議会議員)/公立版・私立版とも CC BY-ND 4.0"
"(課題欄の省略可・無保証)/yasutakeyohei.com/ijime-judai-jitai/flow/")
LIC_PRIVATE = ("© 安竹洋平(小平市議会議員)/公立版・私立版とも CC BY-ND 4.0"
"(無保証)/yasutakeyohei.com/ijime-judai-jitai/flow/")
# (SVGファイル名, 出力の6ページ版, 出力の1ページ版, SVGの元の高さ, フッターのライセンス, 版の名前)
JOBS = [
("ijime-judai-flow.svg", "ijime-judai-flow-a4-6p.pdf",
"ijime-judai-flow-a4-1p.pdf", 4034, LIC_PUBLIC, "公立学校版"),
("ijime-judai-flow-private.svg", "ijime-judai-flow-private-a4-6p.pdf",
"ijime-judai-flow-private-a4-1p.pdf", 3624, LIC_PRIVATE, "私立学校版"),
]
ANCHOR_RE = re.compile(r"]*>|")
def strip_anchors(svg_text):
"""要素を取り除く(中身は残す)。
PyMuPDF の SVG→PDF 変換は の中身を描画しないため、PDF化するときだけ外す。
リンクとしては機能しなくなるが、元のラスタ版PDFもリンクは持っていなかった。
"""
return ANCHOR_RE.sub("", svg_text)
def one_page_rect(base_h):
"""1ページ版の配置(旧スクリプトの縮小計算を再現)。"""
sc = min((2900 / 2.5) / 1820, ((4182 / 2.5) - 30) / base_h)
w = 1820 * sc * 2.5 * PX2PT
h = base_h * sc * 2.5 * PX2PT
x = (3100 - 1820 * sc * 2.5) * PX2PT / 2
return pymupdf.Rect(x, MARGIN, x + w, MARGIN + h)
def add_footer(page, text, license_text):
y = FOOT_TOP + FOOT_SIZE * 0.8
page.insert_text(
pymupdf.Point(MARGIN, y), text, fontsize=FOOT_SIZE,
fontname="japan", color=FOOT_COLOR,
)
page.insert_text(
pymupdf.Point(MARGIN, y + FOOT_NOTE_SIZE * 1.15), license_text,
fontsize=FOOT_NOTE_SIZE, fontname="japan", color=FOOT_COLOR,
)
page.insert_link({
"kind": pymupdf.LINK_URI,
"from": pymupdf.Rect(0, FOOT_TOP - 5, A4W, A4H),
"uri": URL,
})
def build(svg_name, out_6p, out_1p, base_h, license_text, label):
path = IMAGES / svg_name
svg = pymupdf.open("svg", strip_anchors(path.read_text(encoding="utf-8")).encode("utf-8"))
# show_pdf_page は PDF しか受け付けないため、いったんベクターPDFに変換する
src = pymupdf.open("pdf", svg.tobytes(garbage=4, deflate=True))
svg.close()
xs = [0.0, 660.0]
span = base_h - TILE_H1
ys = [0.0, float(round(span / 2)), float(span)]
tiles = []
n = 0
for y in ys:
for x in xs:
n += 1
tiles.append((pymupdf.Rect(x, y, x + TILE_W1, y + TILE_H1), FOOT_6P.format(n=n)))
tiles.append((pymupdf.Rect(0, 0, 1820, base_h), FOOT_1P))
def make(pages, path):
out = pymupdf.open()
for i, (clip, footer) in enumerate(pages):
page = out.new_page(width=A4W, height=A4H)
dest = DEST if i < 6 else one_page_rect(base_h)
page.show_pdf_page(dest, src, 0, clip=clip)
add_footer(page, footer, license_text)
# PDFのメタデータにも、出典とライセンスを入れる(AIや検索が読むため)
out.set_metadata({
"title": f"{TITLE}({label})",
"author": "安竹洋平(小平市議会議員)",
"subject": ("CC BY-ND 4.0(表示—改変禁止)。公立学校版・私立学校版とも同じライセンス。"
"出典 https://yasutakeyohei.com/ijime-judai-jitai/flow/"),
"keywords": f"いじめ重大事態, フロー図, {label}, CC BY-ND 4.0, 安竹洋平, 小平市",
"creator": "https://yasutakeyohei.com/ijime-judai-jitai/flow/",
})
out.subset_fonts()
tmp = path.with_suffix(".tmp.pdf")
out.save(tmp, garbage=4, deflate=True, clean=True)
out.close()
tmp.replace(path)
print(f"{path.name}: {path.stat().st_size:,} bytes")
make(tiles[:6], IMAGES / out_6p)
make(tiles[6:], IMAGES / out_1p)
src.close()
for svg_name, out_6p, out_1p, base_h, license_text, label in JOBS:
build(svg_name, out_6p, out_1p, base_h, license_text, label)