← 기사 목록
日本語https://zenn.dev/topics/ai/feed

CSV更新→PDF自動生成→印刷会社へ。社員名刺の発注ワークフローをコード化した

추출된 키워드

35
PDF·5名刺·5自動生成·5CSV·5CMYK·4フォントアウトライン化·4Ghostscript·4GitHub Actions·4ReportLab·4ワークフロー·4Python·4株式会社マインディア·3DTP入稿·3SVG·3TOML·3塗り足し·3トンボ·3svglib·3PyMuPDF·3Pydantic v2·3Claude Code·3Claude Opus 4.7·3Gemini 3.1 Pro·3Illustrator·3tomllib·2Typer·2fitz·2pyproject.toml·2オンボーディング·2情シス·2SRE·2EmailStr·2linearGradient·2MHTデザイン·2Apple Silicon·1

원문

31,679
CSV更新→PDF自動生成→印刷会社へ。社員名刺の発注ワークフローをコード化した

CSV更新→PDF自動生成→印刷会社へ。社員名刺の発注ワークフローをコード化した

3秒まとめ

  • Illustratorを立ち上げて1人ずつ名刺データを作る運用をやめた。社員名簿からコピー&ペースト→保存→アウトライン化→書き出しで1人あたり10分が固定で溶けていたので、CSV+Pythonで自動生成するパイプラインに置き換えた
  • 印刷業者のテンプレ(トンボ・塗り足し3mm・CMYK・フォントアウトライン化)に完全一致するPDFを ReportLab+Ghostscriptで出力
  • employees.csv
    を更新したらGitHub Actions が走って全員分のPDFをZIPで成果物公開。新入社員オンボーディングのワークフローに組み込めば、入社日にあわせて自動発注まで一気通貫にできる
  • コード生成は Gemini 3.1 Pro / Claude Opus 4.7 / Claude Code のの3レビュアー体制でデザイン指摘を吸い上げた
    frontend-design
    Skill

CSV→GitHub Actions→印刷業者入稿の自動化で完成した名刺PDF(電話番号・メールアドレスはマスク済)

どんな人向けの記事?

  • Illustratorで名刺やDM、はがきを 1人ずつ作っていて消耗してる総務・コーポレートの人
  • 入社のたびにIllustratorを開いて名簿からコピー&ペーストして書き出して、を繰り返している総務・コーポレートの人
  • DTP入稿向けのPDF(トンボ・塗り足し・CMYK・フォントアウトライン化)を コードから生成したい人
  • 入社オンボーディングの「貸与品・名刺・備品」発注を自動化したいSREや情シスの人
  • AIエージェントにレビューさせるソフトウェア開発の流れを実例で見たい人

なぜ作ったのか

弊社(株式会社マインディア)の名刺はこんな感じで、デザインはかなりシンプルです。表面は日本語、裏面は英語、コーポレートカラーのオレンジ系グラデーションにロゴが入るだけ。

それでも今までは、新入社員が入ってくるたびに 管理部のメンバーが以下の手順を毎回繰り返す 運用でした。

  • 超重いIllustratorを起動する
  • テンプレートとなる名刺データ(.ai)を開く
  • 社員名簿のスプレッドシートを横に並べて、 氏名・肩書き・メールアドレスをコピー&ペーストする
  • ファイルを名前を付けて保存する
  • テキストを全選択してアウトライン化する
  • PDFに書き出す
  • 印刷業者の入稿サイトにアップロードして発注

これで 1人あたりおよそ10分 が固定で溶けます。1人だけなら「まあ10分か」で済むんですが、入社が立て込むタイミングだと10人分まとめてやることになるので 半日が消えます

しかも単純作業に見えて、 どの工程もミスれない タチの悪さがあります。

  • Illustratorの起動でまず数十秒待たされる(Apple Siliconでも)
  • コピー&ペースト元の名簿セルを1つズラすと、別人のメールアドレスが入った名刺ができあがる
  • アウトライン化を忘れると、業者環境でフォントが化けて全部やり直し
  • 書き出すPDFのプリセット(CMYK / 塗り足し)を間違えると入稿リジェクト

しかも、弊社の名刺デザインは シンプル です。テキストとロゴとグラデーションだけ。「人間がIllustratorでファイルを1つずつ書き換える」運用にすべき理由は、客観的にはひとつも無いんですよね。

管理部の 本来のコア業務にもっと時間を使ってもらう ためにも、ここはコードで殴るのが正解だろうと判断しました。CTO(私)が「自動化基盤を作って渡す」役割、管理部は「CSVを更新する」役割、という分業に再設計したのが今回の取り組みです。

完成したもの

最終的にこういう体験になりました。

# 全社員分を生成 (未入力行はスキップ)
$ make build

# サンプル CSV で 1 名分を確認
$ make sample
$ open output/taro_yamada.pdf

# 1名分だけ生成 (employee_id 指定)
$ python -m minedia_namecard --csv employees.csv --out output --filter-id XX

出力は印刷業者(MHTデザイン を想定)の入稿テンプレに 完全一致 したPDF。トンボ・塗り足し・CMYK・フォントアウトライン化済み。そのまま入稿サイトにアップロードすれば発注完了です。

make build
を打つと、
employees.csv
の全社員分が
output/
配下にPDFで吐かれます。
output/
├── taro_yamada.pdf
├── hanako_sato.pdf
├── ichiro_tanaka.pdf
└── ...

そして後述しますが、

employees.csv
main
ブランチにpushすると GitHub Actions が全員分を生成してリリースとして公開 します。管理部側からは「CSVに行を1つ足してPRをマージするだけ」で名刺発注用のファイルが手に入る、という体験です。

全体アーキテクチャ

ざっくり書くとこういう流れです。

データ・テンプレ・設定・コードをきっちり分離しているのがポイントです。氏名やメールアドレスのような 可変データ はCSV、会社住所や色のような 半固定値 はTOML、ロジックは Python、デザインの正解は 業者テンプレ に置いてあります。

そしてCI/CD側はこう。

技術スタック

用途採用したもの理由
言語Python 3.11+
tomllib
標準搭載、Pydantic v2、データ加工とPDF生成の両方が得意
PDF生成

svglibPydantic v2
EmailStr
型を指定するだけで、メールアドレス形式のチェックを自前で書かなくて済むTyper
--csv
--out
--filter-id
のような引数定義が型注釈だけで済むGhostscript(
gs -dNoOutputFonts
)パス化。業者のフォント環境に依存しなくなるPyMuPDF(
fitz
)実測して再現
pyproject.toml
+
pip install -e .

入稿仕様の落とし穴と、それをどう超えたか

ここからが本題の技術パートです。

「名刺PDFを作って印刷業者に送るだけ」と聞くと簡単そうに見えますが、商用印刷向けのPDFには 守らないとリジェクトされる仕様 がいくつもあります。

1. ページサイズ・トンボ位置を業者テンプレと完全一致させる

印刷業者のサイトには「入稿テンプレ」が配布されていて、これと 寸分違わぬ位置にトンボ・塗り足し・仕上がり線を置く ことが求められます。1mmでもずれると、断裁線が狂って名刺がズレた状態で仕上がります。

弊社が使っている業者の

template_91x55.pdf
をPyMuPDFで開いて、トンボ(コーナーのL字線)の座標を 実測 して持ってきました。
@lru_cache(maxsize=1)
def _extract_tombo_lines() -> tuple[tuple[float, float, float, float], ...]:
    """template_91x55.pdf からコーナー近傍の黒い直線のみ抽出する."""
    doc = fitz.open(str(TEMPLATE_PATH))
    page = doc[0]
    page_h = page.rect.height

    lines: list[tuple[float, float, float, float]] = []
    for d in page.get_drawings():
        if d.get("type") != "s":
            continue
        color = d.get("color")
        if color is None or not all(abs(c) < 0.01 for c in color):
            continue  # 黒以外は除外
        for it in d.get("items", []):
            if it[0] != "l":
                continue
            p1, p2 = it[1], it[2]
            mx, my = (p1.x + p2.x) / 2, (p1.y + p2.y) / 2
            if not _is_near_corner(mx, my):
                continue  # bleed コーナー近傍のみ
            # 軸並行のみ採用 (テキスト罫線等のノイズを除外)
            if abs(p1.x - p2.x) > 0.05 and abs(p1.y - p2.y) > 0.05:
                continue
            x1, y1 = p1.x, page_h - p1.y
            x2, y2 = p2.x, page_h - p2.y
            lines.append((x1, y1, x2, y2))

    doc.close()
    return tuple(sorted(set(lines)))

PyMuPDFのY軸は上原点ですが、ReportLabは下原点。

page_h - p1.y
座標系を変換 しています。地味だけど、ここを間違えるとトンボが上下逆さまになって入稿不可になります。

工夫したポイントは2つ。

  • 業者テンプレに混ざってる注意書きテキスト(「一般名刺 91×55mm」「裁断位置はここ」みたいな線)を全部除外したい。コーナー近傍(30pt以内)かつ軸並行(横線か縦線)の黒線のみをフィルタしています
  • @lru_cache
    で結果を1回だけ計算。社員数分のPDFを生成するとき、毎回PDFを開いて抽出しなおすのは無駄なのでキャッシュ

2. CMYK + フォントアウトライン化

商用印刷の業者は基本RGBを受け付けません。CMYK(シアン・マゼンタ・イエロー・ブラック)でデータを作る必要があります。さらに、業者の環境にフォントが入っていなくても文字化けしないよう、フォントを「文字情報」ではなく「パスの集合」に変換 する必要があります(アウトライン化)。

ReportLabは一応CMYK出力もできるのですが、フォントのアウトライン化はできません。そこでGhostscriptで後処理しています。

def build_gs_command(input_pdf: Path, output_pdf: Path) -> list[str]:
    gs = shutil.which("gs") or "gs"
    return [
        gs,
        "-o", str(output_pdf),
        "-sDEVICE=pdfwrite",
        "-dNoOutputFonts",                   # ← これがアウトライン化
        "-dCompatibilityLevel=1.6",
        "-sColorConversionStrategy=CMYK",    # ← CMYK変換
        "-dProcessColorModel=/DeviceCMYK",
        "-dPDFSETTINGS=/prepress",           # ← 商用印刷品質プリセット
        "-dNOPAUSE",
        "-dBATCH",
        "-dQUIET",
        str(input_pdf),
    ]

-dNoOutputFonts
がGhostscriptの「フォント情報を出力せず、すべてパスに変換する」フラグです。これでフォント未埋め込みでも文字が崩れなくなります。
-dPDFSETTINGS=/prepress
で商用印刷向けの解像度・圧縮設定にしてくれます。

CLIの呼び出し側ではこの後処理を オプションでスキップ できるようにもしています(デバッグ時はテキスト選択可能なPDFがほしい)。

with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
    intermediate = Path(tmp.name)

try:
    render_card(emp, info, intermediate)
    if no_outline:
        intermediate.replace(final_path)
    else:
        outline_pdf(intermediate, final_path)
        intermediate.unlink(missing_ok=True)
finally:
    if intermediate.exists():
        intermediate.unlink(missing_ok=True)

tempfile.NamedTemporaryFile(delete=False)
で一旦中間PDFを書いて、Ghostscriptに食わせて最終PDFを生成。例外が起きても
finally
で必ず中間ファイルを掃除しています。

3. ロゴのSVGは自前パーサで描画する(svglibの限界)

会社ロゴは Adobe Illustrator からSVGエクスポートしたものを

assets/logo/text_logo.svg
に置いてます。アイコン(メール・電話・URL)も同様。

ところが、

svglib
には linearGradientがうまく描画されない という地雷があります。弊社のロゴは「左下サーモン#F54040→右上ピーチ#F5AA77」のCIグラデーションが命なので、これがフラットな単色で描画されるのは許容できない。

そこで、SVGの <path d="..."/> だけを正規表現で取り出して、自前パーサでReportLabのPath APIに流し込む という実装にしました。

def _emit_path(p, d: str, sx: float, sy: float, tx: float, ty: float) -> None:
    """SVG d を ReportLab Path に流し込む.

    座標変換: ReportLab_x = svg_x * sx + tx, ReportLab_y = svg_y * sy + ty.
    SVG は Y軸下向き、ReportLab は上向きなので sy は負を渡す。
    対応コマンド: M / L / H / V / C / Z (絶対座標のみ)
    """
    cmds = _parse_path(d)
    cx, cy = 0.0, 0.0
    start_x, start_y = 0.0, 0.0

    for cmd, args in cmds:
        if cmd == "M":
            ...
        elif cmd == "C":
            i = 0
            while i + 5 < len(args):
                x1 = args[i] * sx + tx
                y1 = args[i + 1] * sy + ty
                x2 = args[i + 2] * sx + tx
                y2 = args[i + 3] * sy + ty
                x = args[i + 4] * sx + tx
                y = args[i + 5] * sy + ty
                p.curveTo(x1, y1, x2, y2, x, y)
                cx, cy = x, y
                i += 6
        ...

汎用SVGパーサを作るのは大変ですが、Illustratorから書き出された自社ロゴのSVGは絶対座標 M / L / H / V / C / Z しか使わない と分かっているので、対応コマンドはこれだけで十分です。「ライブラリの欠点を、自前実装で限定的にバイパスする」という割り切り。

これでReportLab native の

clipPath
+
linearGradient
を組み合わせて、左下→右上のCIグラデーションを完全に再現できました。
def draw_logo_gradient(c, x_right, y_bottom, target_w_pt):
    sx, sy, tx, ty, target_h = _transform(x_right, y_bottom, target_w_pt)

    bbox_left = tx
    bbox_right = tx + target_w_pt
    bbox_bottom = y_bottom
    bbox_top = y_bottom + target_h

    for d in _get_paths():
        p = c.beginPath()
        _emit_path(p, d, sx, sy, tx, ty)
        c.saveState()
        c.clipPath(p, fill=0, stroke=0)
        c.linearGradient(
            bbox_left, bbox_bottom,
            bbox_right, bbox_top,
            (colors.CI_GRAD_START, colors.CI_GRAD_END),
        )
        c.restoreState()

文字のパスでクリップしておいてから、グラデーションで塗りつぶす。SVGのfill=url(#linearGradient...)と等価な処理を、ReportLabの語彙に翻訳した感じです。

4. フォントサイズ下限ガード

これは安価オフセット印刷の 物理的な制約 から生まれたガードです。

# MHTデザインのような低価格オフセット業者では、用紙が安価で
# インクの滲み (dot gain) が大きい。一般的な業界下限 (本文 6pt) では
# かすれ・潰れリスクがあるため、本プロジェクトでは **7pt を絶対下限** と定める。

MIN_FONT_SIZE_PT = 7.0
FONT_SIZE_NAME_PT = 14.0
FONT_SIZE_BODY_PT = 8.0
FONT_SIZE_SUPPLEMENTARY_PT = 7.0


def assert_min_font(size_pt: float) -> float:
    """フォントサイズ下限を強制するガード."""
    if size_pt < MIN_FONT_SIZE_PT:
        raise ValueError(
            f"font size {size_pt}pt < project minimum {MIN_FONT_SIZE_PT}pt "
            f"(安い印刷業者では潰れる可能性があります)"
        )
    return size_pt

将来、誰かが「もうちょっと文字小さくしてもいいんじゃない?」と言い出して 6pt にしたとき、刷り上がってきた名刺を見て「読めねえ……」となるのを防ぐためのガード。ドメイン知識をコードに埋め込む タイプのバリデーションです。

「コードレベルでデザイナーの代わりをする」という発想。実物の印刷物が出来上がるまでフィードバックが2週間以上かかる領域なので、こういう静的な制約は手厚めに置いておきました。

5. データはPydanticで型安全に

CSVもTOMLもPydanticでスキーマ化しています。

email: EmailStr
と型を1行書くだけで、
hoge@@example
のような壊れたメールアドレスをパース段階で弾けます。正規表現を自前で書く必要なし。
class Employee(BaseModel):
    model_config = {"extra": "ignore"}  # name_kana など未定義カラムは無視

    employee_id: Optional[str] = None
    name_ja: str
    name_en: str
    title_ja: Optional[str] = None
    title_en: Optional[str] = None
    email: EmailStr
    direct_phone: Optional[str] = None
    filename: Optional[str] = None

    @field_validator("employee_id")
    @classmethod
    def _validate_id_chars(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and not _EMPLOYEE_ID_RE.match(v):
            raise ValueError(
                f"employee_id must match [A-Za-z0-9]+, got: {v!r}"
            )
        return v

extra="ignore"
で、CSVに
name_kana
のような「PDFには使わないけど社内管理で持っておきたい」カラムが混ざっていても、シカトしてくれます。社内のスプレッドシートをそのままexportしてもパースできる柔軟性をデフォルトに。

load_csv
全行を1度パースしてからまとめてエラーを返す 実装にしました。「1行目のエラーで止まる → 直して再実行 → 2行目のエラーで止まる」という地獄を避けるためです。
for line_no, row in enumerate(reader, start=2):
    row = {k: (v or "").strip() for k, v in row.items()}

    if skip_incomplete and not _is_row_complete(row):
        continue

    try:
        emp = Employee(**row)
    except ValidationError as e:
        errors.append(f"line {line_no}: {e}")
        continue

    if emp.employee_id is not None:
        if emp.employee_id in seen_ids:
            errors.append(
                f"line {line_no}: duplicate employee_id={emp.employee_id}"
            )
            continue
        seen_ids.add(emp.employee_id)
    employees.append(emp)

if errors:
    raise CsvParseError("\n".join(errors))

エラーを蓄積しておいて最後にまとめて投げる。CSVを直すときの認知負荷がぐっと下がります。

設定の分離: config.toml が会社の「定義」

可変データはCSV、半固定値はTOML、ロジックはコード、と書きました。

config.toml
はこんな感じです。
[colors]
# CMYK [C, M, Y, K] (0.0-1.0)。MHT入稿はCMYKのみ。
black              = [0.0, 0.0, 0.0, 1.0]
ci_grad_start      = [0.0, 0.83, 0.74, 0.0]   # #F54040 サーモン
ci_grad_end        = [0.0, 0.40, 0.56, 0.0]   # #F5AA77 ピーチ
back_accent_orange = [0.0, 0.50, 0.65, 0.0]
back_subtle_dark   = [0.0, 0.10, 0.20, 0.78]

[front]
company = "株式会社マインディア"
address = "〒107-0052 東京都港区赤坂8-5-8 1F"
main_phone = "0X0-XXXX-XXXX"
main_phone_label = "(代表)"
direct_phone_label = "(直通)"
url = "https://corporate.minedia.com/"

[back]
company = "Minedia, Inc."
address_line1 = "1F, Akasaka 8-5-8, Minato, Tokyo"
address_line2 = "107-0052"
main_phone = "0X0-XXXX-XXXX"
direct_phone_label = "(direct)"
url = "https://corporate.minedia.com/"

オフィス移転したとき、CI色を変えたいとき、代表電話を変えたいとき、コードを触らずにTOMLだけ書き換えれば 全社員分のPDFが新しい情報で再生成されます。

色は最初からCMYK値で書いておくのもポイントです。RGB→CMYKは可逆変換じゃないので、デザイナー(と相談したAI)が意図したCMYKを そのまま入稿 できるようにしています。

employees.csv は人事システムのインターフェイス

employee_id,name_ja,name_kana,name_en,title_ja,title_en,email,direct_phone,filename
,山田 太郎,やまだ たろう,Taro Yamada,代表取締役 CEO,Chief Executive Officer,taro.yamada@example.com,,
,佐藤 花子,さとう はなこ,Hanako Sato,取締役 CTO,Chief Technology Officer,hanako.sato@example.com,,
,田中 一郎,たなか いちろう,Ichiro Tanaka,取締役 CFO,Chief Financial Officer,ichiro.tanaka@example.com,0X0-XXXX-XXXX,

employee_id
列は本記事では伏せています(実運用ではここに社員番号が入ります)。
employee_id
を入れると出力ファイル名が
<id>_<name_en>.pdf
の形式になり、社員番号順にソートされた状態でZIPに収まります。

このCSVを 人事システム側からexport すれば、そのまま名刺生成のインプットになります。今は手動コミットですが、将来的にはfreee人事労務や SmartHR からAPI経由で取得して自動コミット、までやりたいところ。

AIによるデザインレビュー: 3レビュアー体制

ここからは「シンプルな名刺をAIにデザインさせたら、デザイナーいなくても回せるのか?」という実験パートです。

結論から言うと、1発できれいには作ってくれませんでした。 ただし、複数のLLMにレビューさせると、最終的にプロ並みの指摘が出てきます。

レビュアーは3名を起用しました。

レビュアー役割
Google Gemini 3.1 Pro 大局的な視点。レイアウトの違和感を素早く検出
Claude Code
frontend-design
Skill
プロのフロントエンドエンジニア視点。色のコントラスト、タイポグラフィの基本ルール
Claude Opus 4.7 コードリーディングしながら「実装上の根拠」を踏まえた指摘

AIから出てきた具体的な指摘

採用した指摘の一部を紹介します。

「アイコンと文字のベースラインが揃ってない」

最初の実装ではメールアイコンと文字の縦位置が微妙にズレていました。アイコンは画像の中央基準で配置していたのに対し、文字はベースライン基準なので、視覚的に揃わない。

# 連絡先 (アイコンは本文 x-height に合わせて 3.0mm)
line_h = mm(4.5)
icon_size = mm(3.5)
icons_dir = ASSET_ROOT / "icons"
contact_x = text_x
text_offset = icon_size + mm(2.0)
contact_y = top_y - mm(20.0)

c.setFillColor(colors.BACK_ACCENT_ORANGE)
c.setFont("NotoSans-Regular", constants.FONT_SIZE_BODY_PT)
_draw_icon(c, icons_dir / "mail_back.svg", contact_x, contact_y, icon_size)
c.drawString(contact_x + text_offset, contact_y + mm(0.8), email)

+ mm(0.8)
という微小なオフセットは、アイコンと文字の 見た目のベースライン を合わせるための調整です。x-heightを基準にしているので、CapitalLetterとアイコンの中心が揃って見えるようになっています。

「カラム内でベースラインが揃っていない」

表面の連絡先と裏面の連絡先で、行高(line-height)がバラついているという指摘。

# 行高 4.0mm で裏面と縦リズムを統一
line_h = mm(4.5)

line_h
を両面で同じ値にすることで、表裏の縦のリズムを揃えています。両面印刷のあとに重ね合わせたとき、透けて見える連絡先の位置が一致する という地味な配慮。

「文字のコントラストがないから認識しづらい」

裏面の住所が黒文字だと「氏名(オレンジ)→連絡先(オレンジ)→会社名・住所(黒)」と情報優先度の階段が崩れる、という指摘。

# 5) 会社名・住所 (ウォームチャコール: 連絡先より情報優先度を下げる)
bottom_y = oy + mm(constants.CARD_MARGIN_BOTTOM_MM)
c.setFillColor(colors.BACK_SUBTLE_DARK)
c.setFont("NotoSans-Regular", constants.FONT_SIZE_SUPPLEMENTARY_PT)
c.drawString(text_x, bottom_y + mm(6.0), info.company)
c.drawString(text_x, bottom_y + mm(3.0), info.address_line1)
c.drawString(text_x, bottom_y, info.address_line2)

back_subtle_dark = [0.0, 0.10, 0.20, 0.78]
のCMYK値、つまり K単色じゃなくてウォームチャコール(CMYに少しずつ色を載せた濃いグレー) を採用しました。視認性は保ちつつ、氏名のオレンジを引き立てる役割。

色名を

back_subtle_dark
と意図ベースで命名してるところもポイントです。「黒」じゃなくて「補足情報用の控えめな暗色」。コードを読み返したとき、この色が何のために存在するか一目で分かります。

「細字ウェイトは安価オフセットで消える」

Light/Thinウェイトは線が細いので、安い紙+オフセット印刷で 掠れて消える という指摘。これを定数のコメントに恒久的に書き残しました。

# 細字 (Light/Thin ウェイト) は同サイズでも線が消えやすいので
# Regular/Bold のみ使用すること。

ドキュメントに書くと風化するので、 使う直前のコードに警告として置く のがポイント。

assert_min_font
のような実行時バリデーションと併用しています。

1発でキレイにならなかった話

ぶっちゃけ、最初は 悲惨でした

  • ロゴが歪んでた(svglibのlinearGradient問題)
  • トンボが業者テンプレと1.5mmズレてた(座標系変換ミス)
  • フォントサイズがバラバラ
  • 表裏で位置がズレてた

これを「Gemini 3.1 Proで指摘を出してもらう→Claude Codeで実装する→Opus 4.7で構造レビュー→frontend-design Skillでビジュアルレビュー」のラウンドを 2周 回したら、入稿可能なレベルに到達しました。

正直、今回くらいシンプルなデザインなら 1発で満足のいくアウトプットが出てくるかも と期待していました。実際にはそうではなく、ドメイン知識を持った人間がレビュー観点を回せるかどうか で品質が決まる、という当たり前の結論に着地しました。今回でいうと「印刷物としての名刺の制約(CMYK・トンボ・ドットゲイン)」を私が知っていて、それをAIへの問いかけに翻訳できたのが肝でした。

GitHub Actionsでビルド自動化

CSVを更新したら全員分のPDFが自動生成されるようにしました。

name: Build name cards

on:
  push:
    branches: [main]
    paths:
      - 'employees.csv'
      - 'config.toml'
      - 'src/**'
      - 'assets/**'
      - 'Makefile'
      - 'pyproject.toml'
      - '.github/workflows/build-namecards.yml'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends \
            libcairo2-dev pkg-config python3-dev ghostscript

      - name: Install package
        run: pip install -e ".[dev]"

      - name: Build PDFs
        run: make build

      - name: Zip output
        run: |
          cd output
          zip -r "../${{ steps.meta.outputs.package_name }}.zip" .

      - name: Upload artifact
        uses: actions/upload-artifact@v7
        with:
          name: ${{ steps.meta.outputs.package_name }}
          path: ${{ steps.meta.outputs.package_name }}.zip
          if-no-files-found: error
          retention-days: 90

      - name: Create release
        uses: softprops/action-gh-release@v3
        with:
          tag_name: build-${{ steps.meta.outputs.stamp }}-${{ steps.meta.outputs.short_sha }}
          name: Name cards build ${{ steps.meta.outputs.stamp }}
          files: ${{ steps.meta.outputs.package_name }}.zip
          fail_on_unmatched_files: true

工夫しているのは2点。

  • paths:
    CSV・config・コードのいずれかが変わったときだけワークフローを走らせる。READMEのtypo修正でPDFを焼き直さない
  • Workflow Artifacts(90日保持)GitHub Releases(恒久保存)の両方に出力。新人が入社して名刺発注したいとき、いつでもRelease一覧から最新のZIPを取りに来られる

GitHub Releasesに 恒久保存 しておくのが地味に重要で、過去の名刺バージョン(住所変更前・CI色変更前など)にいつでもアクセスできる デザインの歴史記録 にもなっています。

入社オンボーディングへの応用

ここまで作ると、次は 入社ワークフロー全体の自動化 につなげたくなります。

理想形はこれ。人事システムが「入社1ヶ月前」のタイミングで

employees.csv
にPRを自動で出して、マージされたら印刷業者APIを叩いて発注、Slackに通知が流れる。これで 人手は完全に消える

弊社が使ってる業者にAPIはまだ無いので「ZIPをDLしてアップロード」の最後の1ステップは人間が必要ですが、ここを切り出してエスカレーションする だけで運用としては成り立ちます。

ベンダー直の発注APIが整備されれば、これは完全な無人化に到達します。 Vista Print みたいなグローバル業者が日本に来てくれると話が早いんですが、まあ気長に待ちます。

規模感: コード行数と「人間が書いたら何日?」の概算

「これってどのくらいの規模感のプロジェクトなの?」というのが気になる方もいると思うので、行数と工数の概算を出しておきます。

行数

区分行数中身
ソースコード(Python)1,122行
src/minedia_namecard/
配下16ファイル(CLI / CSV/TOMLローダ / フロント・バックレンダラ / トンボ抽出 / ロゴSVGパーサ / Ghostscriptラッパ 等)
テストコード494行
tests/
配下9ファイル(CSVバリデーション / レイアウト定数 / レンダリングsmoke / 統合テスト 等)
設定・CI・Makefile174行
config.toml
/
pyproject.toml
/
Makefile
/ GitHub Actions workflow
合計 約1,790行

ソースコードの内訳を見ると、ロゴSVGパーサ(176行)と表面レンダラ(141行)が重め。ロゴのlinearGradient再現と、デザインの微調整が積み重なった結果です。

「人間が書いたら?」の概算

私の体感ベースで「AIアシスト無しで、Python+ReportLabに慣れた中堅エンジニアが1人で書く」と仮定した場合の工数概算は以下です。

フェーズ想定工数内容
ReportLab / CMYK / DTP入稿仕様の調査・spike0.5〜1人日ReportLabのCMYK出力、塗り足し3mm、トンボ仕様、Ghostscriptの
-dNoOutputFonts
あたりを軽く試行錯誤
CSVローダ・config loader・CLI骨格0.5人日Pydantic v2 + Typer の組み合わせなら一気に書ける
カードレイアウト(表面・裏面)1〜1.5人日フォントサイズ・行高・アイコンとの相対位置を実装&微調整
印刷業者テンプレからのトンボ抽出(PyMuPDF)0.5人日PyMuPDFのAPIに慣れていれば実測→転記でサクッと
ロゴSVGの自前パーサ(linearGradient対応)1人日svglibの限界に気づいてから、必要最小限のパーサを書き切る
Ghostscriptラッパ(アウトライン化+CMYK化)0.3人日フラグはググればすぐ
テスト整備0.5人日統合テストとレンダリングsmokeを中心に
GitHub Actions(CI/CD・Release公開)0.3人日apt-getでghostscript入れて
make build
を回すだけ
合計 約5人日 フォーカスタイム換算で 約40時間、実カレンダーで 1〜2週間

実印刷でのフィードバックは別途、 入稿→納品の物理リードタイムで1〜2週間 が乗りますが、これは人間でもAIでも変わらないので工数には入れていません。

実際にかかった時間

これに対して、今回の取り組みは AIエージェント(Claude Code / Gemini)と並走しながら、実時間 約2時間 で1.0版に到達しました。レビューラウンドを2周回した時間込みです。

人間1人でやった場合の 約5人日(フォーカスタイム換算で約40時間) が、わずか2時間 にまで圧縮された計算になります。 ざっくり20倍の加速

正直、自分でもこの数字を打ち込むときに「いやそんなはずないだろ……」と二度見しましたが、振り返ってもこのオーダーで合っています。やったことは「Claude Codeに作業ディレクトリで実装させる」「途中で別のLLM(Gemini 3.1 Pro /

frontend-design
Skill)にレビューさせる」「指摘を当該LLMに戻す」のループだけで、私自身がエディタを開いて手で書いたコードはほぼゼロ。

何が加速したのかを分解するとこんな感じです。

  • 既知のAPIに対する初動の速さ: ReportLabやPyMuPDFのAPIを「とりあえずこう書く」の初版がほぼ即時に出る
  • 試行錯誤の総量: 1日に10〜20回くらいレンダリング結果を見てフィードバックを返すサイクルが回せる
  • デザインレビューの代替: ベースラインやコントラストといったタイポグラフィの基礎を、フロントエンドデザイン系のSkillが指摘してくれる
  • ドキュメント代わりの実装: 「アイコンと文字のベースラインを揃えるべき」のようなデザイナーの暗黙知が、コメント付きコードとして書き残される

逆に、AIが弱かったのは以下です。

  • 物理出力のフィードバック: 紙で刷ったときの掠れ・滲みはAIには見えない。
    MIN_FONT_SIZE_PT = 7.0
    のような物理制約ベースのドメイン知識は人間がコードに埋め込む必要がある
  • 入稿仕様の正確な解釈: 業者のテンプレを「実測して再現する」発想は、人間が「これが正解」と教えないと辿り着けなかった

設計判断のおさらい

最後に、この実装をする過程で繰り返し意識したことをまとめておきます。

1. データ・設定・コードを完全に分離する

CSV更新だけで運用が回るように設計するなら、コードの中に氏名・メールアドレスがハードコードされていてはいけません。逆に、業者ごとの紙サイズ・トンボ位置はめったに変わらないので、コード(または業者テンプレ)に置きました。「変わる頻度」で配置レイヤーを決める。

2. ドメイン知識をコードに埋め込む

「7pt未満はNG」「Light/Thinはオフセットで消える」「アイコンは本文x-heightに揃える」のような知識は、ドキュメントじゃなくて コードのコメントとバリデーション にしました。半年後の自分が暴走しないために。

3. AIには「複数の役割」をやらせる

ひとつのLLMに「いい名刺を作って」と言っても、いい名刺は出てきません。「全体感を見る役」「タイポグラフィを見る役」「実装根拠を見る役」と 役割を分けてレビュー させると、人間のシニアデザイナー+エンジニア+PMがチームで見たような指摘が返ってきます。

4. 印刷物は実物が返ってくるまで2週間

ソフトウェアと違って 本物のフィードバックが遅い ドメインです。だからこそ、静的に潰せるバグ(フォントサイズ下限、CMYK値、トンボ位置)はコードのレイヤーで全部潰しておく。Pydantic、

assert_min_font
、テンプレからの実測再現、これらは全部「印刷してから後悔しないため」の保険です。

まとめ

  • Illustrator+手作業の名刺運用を、CSV+Python+GitHub Actionsで完全に置き換えました
  • ReportLab + Ghostscriptで、CMYK・トンボ・塗り足し・フォントアウトライン化を満たした 入稿可能PDFをコードから出力できます
  • ロゴSVGのlinearGradient問題は、自前のSVG path パーサで限定的にバイパス
  • 印刷業者テンプレからPyMuPDFでトンボ座標を 実測することで、業者の入稿仕様に完全一致
  • デザインがシンプルな会社なら、AIエージェントを 3レビュアー体制で運用することでタイポグラフィ品質をプロ並みに近づけられます
  • 入社オンボーディングと連携すれば、名刺発注は完全自動化が射程に入ります

入社のたびにIllustratorを開いてコピー&ペーストで1人ずつ作っている会社のすべて に同じ手法が刺さると思います。社内便箋、メール署名画像、入社証、社員ID裏、ぜんぶ同じ構造で自動生成できるはず。

おまけ: ソースコード公開について

実装の詳細を読んでいただいた方には申し訳ないのですが、 このリポジトリは現時点では非公開 にしてあります。会社の住所・電話・色定義・社員データなど、機微な情報を分離する作業が残っているからです。

ただ、ニーズがあれば公開準備したいので、この記事に「いいね」が30個以上ついたらソースコードを公開する用に整備します。住所・社員情報・ロゴを差し替え可能なテンプレ形式にして、フォーク1発で別の会社でも使えるようにする予定です。

「うちでも使いたい」「もっと詳しい実装を読みたい」と思った方は、ぜひ❤️ボタンをポチっとお願いします。

おまけ2: 個人的な気づき

  • Illustratorを開かなくていい人生は控えめに言って最高です。集中の流れが切れない
  • AIエージェントに 役割分担させる発想は、コードレビューだけじゃなくデザインレビューでも超強力。異なるLLMで並列レビューさせるのが特に効きます
  • 名刺やDMのような「データ × デザイン × 物理出力」の領域は、まだまだ手動運用が多くて自動化の余地が大きい。コーポレートエンジニアリングのフロンティアです
  • 印刷業界も APIを公開してくれる業者が出てくれば、コーポレートの大量の手作業がほぼ無人化できます。誰か始めませんか

それでは、また!

おまけ3: 昔つくった類似プロジェクト

実は 画像をプログラムで生成するタイプのツール は昔も作ったことがあって、コロナ初期にTwitterアイコンに「STAY HOME」みたいなフレームを自動で被せるWebサービスをやってました。

これは入力画像にPNG/SVGの装飾をオーバーレイして、Twitter用の 正方形ピクセル画像 を吐くだけのシンプルなもので、サクッと作れました。

今回の名刺PDF生成と比べると、ピクセル画像と印刷物の世界はぜんぜん別物 だなと改めて感じます。ピクセル画像は「ブラウザで見られればOK」だけど、印刷物は

  • CMYK色空間(RGB入稿は基本NG)
  • 塗り足し3mm(断裁ズレ対策)
  • トンボ(断裁位置・センター位置の指示マーク)
  • フォントアウトライン化(業者環境のフォント有無に依存しないため)
  • 解像度・線幅の物理的下限(インクの滲み込み)
  • 入稿テンプレへの正確な位置一致

と、画面で完結する画像生成より 守るべきルールが一段増えた印象 です。1mmずれると断裁線がズレ、6ptで刷ると掠れて読めない、というフィードバックが 実物が刷り上がるまで分からない のがやっかい。

ピクセル画像なら「あれ違うな」→直す→F5、で30秒で次のイテレーション。印刷物だと「あれ違うな」→直す→入稿→印刷→納品で 早くて1週間 。だからこそ、静的に潰せる制約はコード側でガチガチに固めておいたほうがいい、という発想に落ち着きました。