"""いじめ重大事態対応フロー図の印刷用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 subprocess 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 = 29 * 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_NO_KADAI = ("© 安竹洋平(小平市議会議員)/公立版・私立版とも CC BY-ND 4.0" "(切り取り可・無保証)/yasutakeyohei.com/ijime-judai-jitai/flow/") # 版の情報は、SVGの右上の「最終更新:…」から取る(stamp-flow-version.py が入れる) EDITION_RE = re.compile(r'x="1796" y="56"[^>]*>([^<]*)') def edition_of(svg_name): """SVGの右上の「最終更新:…」を、PDFのメタデータ用に取り出す。""" m = EDITION_RE.search((IMAGES / svg_name).read_text(encoding="utf-8")) return m.group(1) if m else "" # (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-simple.svg", "ijime-judai-flow-simple-a4-6p.pdf", "ijime-judai-flow-simple-a4-1p.pdf", 3509, LIC_NO_KADAI, "公立学校版・簡易版"), ("ijime-judai-flow-private.svg", "ijime-judai-flow-private-a4-6p.pdf", "ijime-judai-flow-private-a4-1p.pdf", 3624, LIC_NO_KADAI, "私立学校版"), ] 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": (f"{edition_of(svg_name)}。CC BY-ND 4.0(表示—改変禁止)。公立学校版・私立学校版とも同じライセンス。" "出典 https://yasutakeyohei.com/ijime-judai-jitai/flow/"), "keywords": f"いじめ重大事態, フロー図, {label}, {edition_of(svg_name)}, 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() # 「最終更新」の日付を先に更新してから、PDFを作る subprocess.run([sys.executable, "scripts/stamp-flow-version.py"], check=True) 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)