blog

10週の見積もりが午後のうちに動いた、PDF入稿チェッカーの作り方と失敗

入稿前チェッカーは、印刷所に入稿する PDF を読み込むと、塗り足しの不足や解像度の足りない画像のような、よくある不備を探すツールです。同人誌の原稿のように人に見せたくない PDF も扱うので、解析はブラウザの中だけで行い、ファイルはどこにも送りません。

このツールは、仕様書が 1 本あるだけのリポジトリから始まりました。実装は Claude Code に任せ、私は方針を決めて、触って確かめる係です。この記事では、仕様書から公開までの経緯と、PDF のどこを見れば不備がわかるのか、作りながらどこでつまずいたのかを順に書きます。

仕様書には「10 週」と書いてあった

2026 年 7 月 7 日の午後、リポジトリにあったのは docs/ の仕様書だけでした。Claude Code への最初の依頼は「読み込んで実現可能かチェックしてください。問題なければ実装のプランを立ててください」です。

仕様書の初版は、いまのツールとかなり違う形をしていました。

  • お金の取り方:アカウント登録で月 3 ファイルまで無料、それ以上は 7 日 300 円か 30 日 980 円の買い切りパス
  • サーバー:Cloudflare Workers と D1 で回数を管理し、Google のログインと Stripe の決済をつなぐ
  • PDF の解析:pdf.js と pdf-lib の組み合わせ
  • デプロイ:GitHub Actions で main への push から自動で
  • 見積もり:基盤 1 週、解析 3 週、画面 2 週、認証と課金 2 週、仕上げ 2 週で合計 10 週

Claude Code は計画を立てる前に、方針を 4 つ質問してきました。どこまで作るか(認証と課金を外した「動くβ」まで)、デプロイの方法(GitHub Actions を使わず wrangler から直接)1、テスト用 PDF の作り方(暗号化 PDF も作れる Python で)、リポジトリの構成です。4 つとも推奨案を選んで、そのまま実装に入りました。

基盤のコミットが 15 時 56 分、解析エンジンが 17 時 59 分、Worker と画面がつながった「無認証βとして完動」のコミットが 18 時 09 分です。途中でセッションの利用上限に当たり、1 時間 44 分ほど止まっていたので、手を動かしていた時間は 1 時間に届きません。仕様書の見積もりで 6 週ぶん(基盤、解析、画面)にあたる部分が、その日の午後のうちに動きました。公開はその 3 日後の 7 月 10 日です。

判定に pdf.js を使わなかった理由

ブラウザで PDF を扱うなら、まず思い浮かぶのは Mozilla の pdf.js です。仕様書の初版も、色や画像の情報を pdf.js の getOperatorList()(ページの描画命令の一覧を返す API)から取る予定でした。ただし「RGB の色の命令を確実に拾えるか」は未決事項として残され、だめなら描画命令を自分で字句解析する、という逃げ道まで書かれていました。

計画の段階で、Claude Code はこの逃げ道のほうを最初から本命にしました。pdf.js は画面に描くためのライブラリなので、CMYK やグレーで指定された色も RGB に直してから命令の一覧に載せます。画像も RGB にデコードしたものを返します。これでは「元の PDF に RGB の色が混ざっているか」を区別できません。

正直に書くと、これは pdf.js を実際に動かして確かめた結論ではありません。計画を立てていた 5 分ほどのあいだに、pdf.js の作りについての知識から決めたものです。結果として、pdf.js の役目はページのサムネイルと拡大表示を描くことだけになり、判定は次の二つで組み立てました。

  • pdf-lib:PDF の構造(ページの大きさ、フォントや画像の辞書、色空間)を読む。圧縮されたデータの展開にも使う
  • 自前の解析器:ページの描画命令(コンテンツストリーム)を 1 つずつ読み、画像がどの大きさで置かれたか、どの色の命令が使われたかを調べる

解析は Web Worker の中で動かし、解析のたびに作り直して、終わったら terminate() で捨てています。大きな PDF を読んだあとのメモリを、確実に手放すためです。

// Worker呼び出しクライアント。解析ごとにWorkerを生成・破棄して
// 大容量PDFのメモリを確実に解放する。

import { proxy, transfer, wrap } from 'comlink';
// ...

export async function analyzeFile(
  file: File,
  presetId: string,
  locale: Locale,
  onProgress: (p: ProgressEvent) => void,
): Promise<CheckReport> {
  const worker = new Worker(new URL('./analysis.worker.ts', import.meta.url), {
    type: 'module',
  });
  const api = wrap<AnalysisApi>(worker);
  try {
    const buf = await file.arrayBuffer();
    const bytes = new Uint8Array(buf);
    // transfer でゼロコピー移譲(メインスレッド側の複製を持たない)
    return await api.analyze(transfer(bytes, [buf]), presetId, file.name, locale, proxy(onProgress));
  } finally {
    worker.terminate();
  }
}

Worker とのやりとりには comlink を使っていて、Worker の関数を普通の非同期関数のように呼べます。PDF のバイト列は transfer() で Worker に「移す」ので、Worker に渡すときにコピーは作られません。ただし、サムネイルを描く pdf.js のほうでも同じファイルをもう一度 arrayBuffer() で読んでいるので、PDF 全体としてはメモリに 2 回載っています。進み具合は proxy() で包んだコールバックを通じて Worker から呼び返してもらい、プログレスバーに反映しています。

塗り足しは、ページの「箱」の差で測る

PDF のページには、大きさを表す箱がいくつか定義されています2。

  • MediaBox:用紙全体の大きさ
  • TrimBox:断裁したあとの仕上がりの大きさ
  • BleedBox:塗り足しを含めた、絵柄を置いてよい範囲

TrimBox がある PDF なら、話は簡単です。TrimBox と BleedBox(無ければ MediaBox)の四辺の差を測り、どれか一辺でも 3mm に届いていなければ不足と判定します。PDF の長さの単位はポイント(1/72 インチ)で、3mm は約 8.5pt です。仕様書では、境目ぎりぎりの原稿で誤判定しないよう、合格の線を 8.4pt に置いています。

const MIN_BLEED_PT = 8.4; // 3mm = 8.5pt。8.4pt以上で許容(仕様書)

function judgePage(page: PageModel, preset: Preset, t: EngineCopy): PageOutcome {
  const uu = page.userUnit;
  if (page.trimBox) {
    // TrimBox がある場合: BleedBox(なければMediaBox)との余白を判定
    const ref = page.bleedBox ?? page.mediaBox;
    const trim = page.trimBox;
    const margins: Array<['left' | 'bottom' | 'right' | 'top', number]> = [
      ['left', (trim.x - ref.x) * uu],
      ['bottom', (trim.y - ref.y) * uu],
      ['right', (ref.x + ref.w - (trim.x + trim.w)) * uu],
      ['top', (ref.y + ref.h - (trim.y + trim.h)) * uu],
    ];
    const short = margins.filter(([, pt]) => pt < MIN_BLEED_PT);
    if (short.length === 0) {
// ...

PDF の座標は左下が原点なので、「下」の余白は y の差になります。userUnit は、1 単位を 1pt 以外の長さにする PDF のための倍率で、ほとんどの PDF では 1 です。

困るのは、TrimBox が書かれていない PDF です。この場合は用紙の大きさだけが手がかりなので、「A5 に上下左右 3mm を足した大きさ」のような規格の寸法と ±0.5mm の範囲で照らし合わせます。A5 ぴったりなら塗り足しが無い、A5 より 6mm 大きければ塗り足しがある、と判定し、どちらにも当てはまらない大きさは「規格外」として注意を出します。

横向きの原稿もあるので、幅と高さを入れ替えた組み合わせも許しています。

画像の解像度は、置かれた大きさから計算する

画像の解像度は、画像ファイルのピクセル数だけでは決まりません。同じ 1000 ピクセルの画像でも、紙の上で 2 インチの幅に置けば 500dpi、5 インチに引き伸ばせば 200dpi です。つまり「ページのどこに、どの大きさで描かれたか」を知る必要があり、その情報はページの描画命令の中にしかありません。

PDF の描画命令は、座標の変換行列(CTM)を積み重ねながら絵を描いていきます。q で今の状態を保存し、cm で変換を掛け、Q で元に戻す、という具合です。画像を描く Do 命令が来た時点の CTM を見れば、その画像が紙の上でどの大きさになるかがわかります。

function makePlacement(info: ImageInfo, ctm: Matrix, userUnit: number): ImagePlacement {
  const drawnWidthPt = Math.hypot(ctm[0], ctm[1]) * userUnit;
  const drawnHeightPt = Math.hypot(ctm[2], ctm[3]) * userUnit;
  const dpiX = drawnWidthPt > 0 ? info.width / (drawnWidthPt / 72) : Infinity;
  const dpiY = drawnHeightPt > 0 ? info.height / (drawnHeightPt / 72) : Infinity;
  return {
    image: info,
    rect: unitSquareBBox(ctm),
    drawnWidthPt,
    drawnHeightPt,
    effectiveDpi: Math.round(Math.min(dpiX, dpiY)),
  };
}

幅を ctm[0] だけで測らず Math.hypot() を使っているのは、回転して置かれた画像でも正しい長さを出すためです。縦と横で解像度が違う(縦横比を変えて置かれた)画像もあるので、低いほうを採っています。

Do で描かれるのは画像とは限りません。Form XObject(描画命令のまとまりを部品として使い回すしくみ)の場合は、その中身の命令を同じ解析器で再帰的に読みます。イラストを別の PDF から貼り込んだ原稿では、画像がこの部品の奥に入ることがあるからです。壊れた PDF で無限に潜らないよう、再帰の深さには上限を設けています。

描画命令は、バイト列から自分で切り出す

描画命令を読む解析器に命令を渡しているのは、圧縮を展開したあとのバイト列を、数値や名前や命令に切り分ける字句解析器です。PDF の描画命令は 1 0 0 1 72 720 cm のように、引数を先に並べて最後に命令を書く形をしています。

文字列として扱わず、Uint8Array のバイトを直接見ているのは、描画命令の中にバイナリのデータが混ざるからです。その代表が、描画命令の途中に画像を直接埋め込む インライン画像 です。BI から ID までが画像の設定、ID から EI までが画像のバイナリで、終わりの印を探しながら読み飛ばします。

終わりの印は、空白のあとに EI が続き、さらに空白か区切り文字かデータの末尾が来ることまで確かめています。バイナリの中にたまたま EI という 2 バイトが現れても、読み終わったと誤解しないためです。

インライン画像の設定部分では、実装中に一度取り違えがありました。BI /CS /RGB ... のように値が名前(/ で始まる語)のとき、最初の版は /RGB を次のキーとして読んでいました。いまは「キーを待っている状態か」を見て、名前を値として扱う分岐を足しています。

// 修正前
        if (c === 0x2f) {
          i++;
          pendingKey = readName();
// 修正後
        if (c === 0x2f) {
          i++;
          const name = readName();
          if (pendingKey === null) {
            pendingKey = name;
          } else {
            dict[pendingKey] = name; // 名前型の値(/CS /RGB 等)
            pendingKey = null;
          }

字句解析器は function* のジェネレーターにしていて、解析器は for...of で命令を 1 つずつ受け取ります。命令の一覧を配列に溜めないので、命令の数が多いページでも一覧の分のメモリは増えません。

RGB の混入は、色空間と色の命令の両方を見る

RGB の色が混ざる経路は一つではないので、場所ごとに調べ方を変えています。

画像は、画像の辞書に書かれた色空間で判定します。DeviceRGB のように名前で書かれていれば簡単ですが、ICC プロファイル付きの色空間(ICCBased)は名前だけではわかりません。この場合はプロファイルに書かれた色の成分数 /N を見ます。

成分が 3 つなら RGB、4 つなら CMYK、1 つならグレーです。透明効果のグループに RGB の色空間が指定されている場合も、辞書から拾っています。

図形と文字は、描画命令のほうで判定します。RGB で塗りの色を決める命令が rg、線の色を決める命令が RG なので、これが出てきたら RGB が使われています。

ただし、逆は言えません。PDF には、cs で色空間を選んでから sc や scn で色を指定する書き方もあり、こちらで RGB を使われると、いまの解析器は見逃します。CMYK で塗った図形を「有色かどうか」で見分ける判定も、まだ入っていません。どちらも、判定できる範囲を広げる余地として残っています。

フォントの埋め込みも、辞書を読むだけでわかります。フォントの説明(FontDescriptor)にフォントのデータ(FontFile FontFile2 FontFile3 のどれか)が付いていれば埋め込み済みです。日本語のフォントは構造が一段深く、説明が子のフォント(DescendantFonts)の下にあるので、そこまで降りて確かめています。

テスト用の PDF を作る段階でも、フォントで一つ引っかかりました。PDF の生成に使った Python の reportlab は、文字を一つも描いていないページにも Helvetica を登録します。そのままでは「フォントを使っていない PDF」のテストが作れないので、生成したあとに pikepdf でフォントの登録を消しています。

def strip_unused_fonts(path: Path):
    """reportlabはテキスト未使用でも/FontにHelveticaを登録するため除去する。

    フォントを使わないフィクスチャは C-04 で pass(アウトライン化済み扱い)に
    なる必要がある。
    """
    with pikepdf.open(path, allow_overwriting_input=True) as pdf:
        for page in pdf.pages:
            res = page.get("/Resources")
            if res is not None and "/Font" in res:
                del res["/Font"]
        pdf.save(path)

これに気づけたのは、生成スクリプトの最後に「作った PDF を読み直して、意図どおりの中身か確かめる」処理を付けていたからです。

1 ページ壊れていても、残りのページは調べる

実際の入稿データには、どこか一部だけ壊れた PDF が混ざります。1 ページの解析で例外が出たときに全体を止めてしまうと、残りのページの不備まで見えなくなるので、ページごとに例外を受け止めています。

読めなかったページには「描画あり」の印を付けています。付けないと、何も描かれていないページとして「白紙ページがあります」という指摘まで出てしまうからです。

白紙の判定は、塗りや線やテキストを描く命令が 1 つでもあるかどうかで決めています。

/** 塗り・線・シェーディング・テキスト表示のオペレータ(白紙判定用) */
const PAINTING_OPS = new Set([
  'S', 's', 'f', 'F', 'f*', 'B', 'B*', 'b', 'b*', 'sh', 'Tj', 'TJ', "'", '"',
]);

パスを作るだけの命令(m や l)は数えません。パスは塗るか線を引くかしない限り、紙には何も出ないからです。意図して入れる白紙(遊び紙)もあるので、白紙ページはエラーではなく「お知らせ」として出しています。

30 件の打ち切りが、11 ページ目から先を「問題なし」にしていた

公開前の 7 月 8 日、使ってみていた私が、10 ページ目以降の画像に指摘が出ていない気がする、と Claude Code に伝えました。

Claude Code はまず、15 ページに 1 枚ずつ低解像度の画像を置いた PDF を作りましたが、こちらは全ページで正しく指摘が出ました。次に目を付けたのが、低解像度の指摘を 30 件までに絞る定数です。1 ページに 3 枚なら、30 件でちょうど 10 ページ目までになります。15 ページに 3 枚ずつ、計 45 枚の PDF を作ると、11 ページ目以降は指摘も、画像を囲む枠も、まったく出ませんでした。画面上部のエラー件数も 45 ではなく 30 でした。

打ち切った分は「他に N 件の低解像度画像があります」というお知らせとして出してはいました。ただ、お知らせはエラーの件数にも、ページ上の枠にも数えられません。利用者からは「後半のページは問題なし」に見えるので、そのまま入稿して差し戻される、という一番まずい誤認につながります。

-const MAX_FINDINGS = 30;
+// 件数の打ち切りはしない。低解像度画像を1件でも捨てると、
+// そのページのサムネイルにオーバーレイが出ず「後半ページは問題なし」と
+// 誤認させてしまう(= 差し戻しを招く最悪の失敗)。
+// 一覧の表示量を絞るのは UI 側の責務(ReportScreen)。
 ...
       issueCount++;
-      if (issueCount > MAX_FINDINGS) continue;

いまは、解析は見つけた指摘をすべて返し、一覧を短く見せるのは画面側の仕事にしています(同じ種類の指摘は 8 件まで出し、残りは「あとN件」に畳む)。45 枚の PDF はテスト用の PDF に加え、15 ページ目まで 45 件すべてに枠の位置が付いていることを確かめるテストも足しました。

この件は、その日の夜のデザイン変更でも効いてきました。受け取ったデザイン案では、サムネイルを先頭の数枚だけ見せて残りを畳む作りでしたが、そのままでは同じ誤認が起きます。指摘のあるページは常に見せ、指摘の無いページだけを畳む形に変えています。

枠のずれには、原因が三つあった

同じ 7 月 8 日、「枠が微妙にずれてたり高さが足りないような気がします」とも伝えていました。問題の画像を囲む枠が、サムネイルの上でわずかにずれていたのです。調べると、原因は一つではありませんでした。

  1. Canvas の大きさを Math.ceil で整数に切り上げ、その幅から出した 1 つの倍率で x と y の両方を換算していた。縦横比が最大 0.4% ずれる
  2. サムネイルの <img> を h-full で枠いっぱいに引き伸ばしていて、そのずれを広げていた
  3. 枠線を border で描き、外側を overflow-hidden で切っていたので、ページの端いっぱいの枠は線が欠けて「高さが足りない」ように見えた

修正前は、枠の位置をピクセルで持っていました。

          canvas.width = Math.ceil(viewport.width);
          canvas.height = Math.ceil(viewport.height);
// ...
          const cssScale = THUMB_CSS_WIDTH / canvas.width;
// ...
            overlays.push({
              left: Math.min(x1, x2) * cssScale,
              top: Math.min(y1, y2) * cssScale,
              width: Math.abs(x2 - x1) * cssScale,
              height: Math.abs(y2 - y1) * cssScale,

いまは、PDF の座標をページに対する 0〜1 の割合に直して持っています。

  const base = page.getViewport({ scale: 1 });
  const overlays: Overlay[] = [];
  const push = (rect: { x: number; y: number; w: number; h: number }, sev: 'error' | 'warning', message: string) => {
    const [x1, y1] = base.convertToViewportPoint(rect.x, rect.y) as [number, number];
    const [x2, y2] = base.convertToViewportPoint(rect.x + rect.w, rect.y + rect.h) as [number, number];
    overlays.push({
      left: Math.min(x1, x2) / base.width,
      top: Math.min(y1, y2) / base.height,
      width: Math.abs(x2 - x1) / base.width,
      height: Math.abs(y2 - y1) / base.height,
      severity: sev,
      message,
    });
  };

割合で持っておけば、小さなサムネイルでも拡大表示でも、CSS の % でそのまま枠を置けます。画像は引き伸ばさず元の縦横比で出し、枠線は border の代わりに内側の box-shadow で描くようにしました。修正後、PDF の実際の座標との差は 0.003% でした。

convertToViewportPoint() を 2 回呼んで矩形を作っているのにも、経緯があります。最初は矩形をまとめて変換する convertToViewportRectangle() を使っていましたが、入れた pdfjs-dist が v6 で、この関数が無くなっていてビルドが通りませんでした。PDFDocumentProxy の destroy() も同じく無くなっていて、読み込みのタスク側の destroy() で破棄する形に変えています。

動かないと思ったら、タブが裏に回っていた

サムネイルが「生成中 (0/8)」のまま止まる、という症状には何度か出会いました。原因は毎回違っていました。

最初は、画面を作った直後の React の StrictMode です。StrictMode は開発中、effect を「実行、後片付け、もう一度実行」と二度走らせます。当時のコードは、同じファイルで二度描かないよう useRef で 2 回目を弾いていました。

  const startedFor = useRef<File | null>(null);

  useEffect(() => {
    if (startedFor.current === file) return;
    startedFor.current = file;
    let cancelled = false;

1 回目は後片付けで cancelled になって途中でやめ、2 回目は useRef に弾かれてすぐ戻るので、誰もサムネイルを描きません。ガードを外し、effect が走るたびに状態を初期化して描き直す形にして直りました。

残りは、アプリではなく確認のしかたの問題でした。編集中のホットリロードで、破棄済みの PDF を古いモジュールが握っていたことが 1 回。もう 1 回は、Claude Code が動作確認に使っていたブラウザのタブが裏に回っていたことでした。document.visibilityState が hidden のタブでは requestAnimationFrame が止まり、それに頼っている pdf.js の描画も進みません。スクリーンショットを撮った瞬間だけフレームが進み、そのときは最後まで描けていました。自動のブラウザで確認するときは、表示されていないタブで rAF が止まることを前提にしておく必要があります。

「RGB は常に問題」ではなかった

最初の設定は、汎用の設定と、印刷所を想定した設定の 2 つだけでした。7 月 8 日の朝、人気の印刷所の入稿案内を Claude Code に調べてもらいました。

わかったのは、数値の芯(塗り足し 3mm、カラー 350dpi、モノクロ 600dpi、仕上がりの寸法)はほぼ共通している一方で、前提の割れる項目があることでした。RGB での入稿をむしろ勧めている印刷所があり、そこへ出す人に「RGB が混ざっています」と警告するのは誤りです。塗り足しを 4mm 求める印刷所もあり、3mm で固定していると「足りていないのに合格」を出してしまいます。

そこで、RGB の扱いに「問題にしない」という選択肢を足し、RGB 入稿向けと塗り足し 4mm 向けの設定を加えました。設定の名前には印刷所の名前を使わず、「オンデマンド(RGB 入稿対応)」のような汎用の名前にしています。翌日には、同人作家の方の入稿前チェックリストと照らし合わせ、消し忘れたレイヤーの検出と、モノクロ本文用の設定を足しました。逆に、安全マージンやノンブル、奥付のように PDF から確実に判定できない項目は「対応しない」と明記しています。誤った安心を与えるより、見られないものは見られないと書くほうがよい、という判断です。

課金をやめ、広告もやめ、英語へ

最初に大きく変わったのは、お金の取り方でした。7 月 8 日の午後、私は課金をやめて広告で収益を得る形に切り替えました。書き直した仕様書には、入稿は年に数回しかなく、そのたびに払ってもらうのは障壁が高い、という理由が書かれています。これでログイン、決済、回数管理のサーバーがまるごと不要になり、純粋な静的サイトになりました。

7 月 10 日の公開の直後には、広告の表示も一旦すべて外しています。反応を見るために SNS で告知する予定で、反応が出るまでは広告を出さないことにしたからです。ところが告知への反応はほとんどなく、7 月 13 日には、英語に対応して英語圏の利用者を狙うことにしました。

英語化でいちばん手間がかかったのは、画面ではなく解析エンジンの指摘文でした。エンジンが日本語の文を直接組み立てていたので、文言を日英の辞書にまとめ直し、解析の関数に言語を渡すようにしています。英語圏向けには、Amazon KDP や IngramSpark のような自費出版サービスを想定した設定を 5 つ加えました。英語圏の塗り足しは 0.125 インチ、解像度は 300dpi が基準です。

公開した PDF のエンジンには、コピー対策もしています。「これ全部jsで動いてるってことだよね?コピーされたら簡単に複製サービスができちゃう」と相談し、本番のビルドでだけ、自分で書いた判定の部分を難読化しています。pdf-lib のような公開されたライブラリまで難読化しても意味がないので、ビルドの段階で判定のコードを別のチャンクに分けて、そこだけを対象にしました。そのぶん解析は遅くなり、120 ページの PDF で、難読化なしの約 45ms が難読化ありで約 150ms(試行によって 120〜216ms)になりました。それでも、待ち時間のほとんどは pdf.js がサムネイルを描く時間です。ブラウザに配った JavaScript は原理的に隠しきれないので、これは防止ではなく時間稼ぎだと割り切っています。課金をやめたことで、エンジンを認証つきでサーバーから配る構想も成り立たなくなりました。

7 月 22 日には、nyuko-checker.pages.dev から nyuko-checker.tsuyatt.com に移りました。Cloudflare Pages では、旧ホストからの 301 を _redirects でもゾーンの Redirect Rules でも書けず、Pages Functions で書くしかありません。このとき、SPA のために書いていた _redirects の /* /index.html 200 が「無限ループ」と判定されて、実はずっと無視されていたこともわかりました。このあたりは、ほかのサービスでも同じ壁に当たったので、9 つのサイトを Cloudflare Pages に載せて踏んだことにまとめています。

原稿が送られていないことは、ネットワークのタブで確かめられる

原稿を送らないことは、このツールのいちばんの約束です。その意味で反省したのが画面の文言でした。デザイン案から入った見出しは「原稿、見せてもらいますね。」で、読み込み中は「読ませてもらってます…」と出ていました。人に見られるような印象を与えるので、「原稿は、誰も見ません。」「自動でチェック中…」に書き換えています。同じ理由で、いきなり自分の原稿を選ぶのに抵抗がある人のために、見本の PDF でワンクリックで試せるようにしました。

ソースの中で fetch を使っているのは、その見本の PDF を取りに行く 1 か所だけで、利用者のファイルを送る経路はありません。気になる場合は、ブラウザの開発者ツールでネットワークのタブを開いたまま PDF を読み込んでみてください。フォントや Worker のスクリプトを取りに行く通信は出ますが、PDF を送る通信は一つも出ないことを確かめられます。

10 週の見積もりのうち、半日で動いたのは「PDF を読んで判定する」部分です。その後の 2 週間で変わったのは、お金の取り方、件数の見せ方、文言、対象の国でした。判定のしくみより、判定をどう見せて、誰に届けるかのほうに時間を使ったことになります。

  1. GitHub Actions はよいサービスですが、処理時間が長くなったり容量が重くなったりすると落ちることがあり、push のたびに走って通知が来るのも AI と開発する速さでは邪魔になったので、使わなくなりました。仕様書は GitHub Actions を指定していましたが、Claude Code は質問の中で「以前『GitHub Actionsは使わない』方針と伺っています」と確認してきました。 ↩
  2. ほかに、表示や印刷で切り取る範囲を表す CropBox と、内容のある範囲を表す ArtBox もあります。入稿の判定に使うのは本文の 3 つです。 ↩

← 記事一覧へ