全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / 帳票AI読取制作記 / Vol.35 帳票AI読取制作記 Vol.1

記事 05

複数画像対応——サムネイルストリップとモーダルナビ

1枚目にカレンダー表、2枚目に備考、3枚目に追記。こういう帳票を「まとめて1つの注文として処理したい」という要件は当然だ。1枚ずつ別の注文として登録して後からまとめる、という操作は担当者には頼めない。

2026-06-17 公開

月次の発注書が3ページに渡ることがある。

1枚目にカレンダー表、2枚目に備考、3枚目に追記。こういう帳票を「まとめて1つの注文として処理したい」という要件は当然だ。1枚ずつ別の注文として登録して後からまとめる、という操作は担当者には頼めない。

複数枚の画像を1つの注文として扱う機能が必要だった。


ファイルの受け取り方

ブラウザのinputにmultiple属性を加える。それだけで複数ファイルの選択はできる。

<input type="file" id="fax-upload" accept="image/*,.pdf" multiple>

FastAPIはList[UploadFile]で受け取る。

@app.post('/api/orders/upload')
async def upload_order(
    files: List[UploadFile] = File(...),
    supplier: str = Form(...)
):
    order_id = create_order_record(supplier)
    for i, file in enumerate(files):
        save_image(order_id, file, page_no=i+1)
    background_tasks.add_task(process_all_pages, order_id)
    return {'order_id': order_id}

page_noで順番を管理する。ページ1・2・3という順序は、担当者がファイルを選択した順番に対応する。


サムネイルストリップ

確認画面の画像エリアに、全ページのサムネイルを横に並べるストリップUIを実装した。

function renderThumbnails(images) {
  const strip = document.getElementById('thumb-strip');
  strip.innerHTML = '';
  images.forEach((img, i) => {
    const thumb = document.createElement('img');
    thumb.src = `/api/orders/${orderId}/images/${i+1}/thumbnail`;
    thumb.className = 'thumbnail';
    thumb.addEventListener('click', () => showPage(i+1));
    strip.appendChild(thumb);
  });
}

サムネイル画像はFastAPIがPillowで200px幅にリサイズして返す。FAXのJPEGは数MBになることがある。元サイズをサムネイルとして毎回送っていたら、確認画面を開くたびに重くなる。リサイズしてキャッシュする。


モーダル内のページナビ

画像をクリックすると拡大モーダルが開く。FAX画像の文字は小さいことが多い。拡大して確認したい場面がある。

モーダル内に「◀ 前のページ」「次のページ ▶」のボタンと現在のページ数(3/5など)を置いた。

let currentPage = 1;
const totalPages = images.length;

document.getElementById('modal-prev').addEventListener('click', () => {
  if (currentPage > 1) {
    currentPage--;
    updateModal();
  }
});

document.getElementById('modal-next').addEventListener('click', () => {
  if (currentPage < totalPages) {
    currentPage++;
    updateModal();
  }
});

確認画面を閉じずにページを切り替えられる。担当者が「1枚目と3枚目を見比べたい」という場面でも対応できる。


画像の回転

FAXは事務所の機械でスキャンされる。スキャンの向きが90度や180度回転していることがある。

確認画面に「90度回転」ボタンを置いた。CSSのtransformで回転させるだけだ。サーバーのファイルは変えない。

let rotation = 0;
document.getElementById('rotate-btn').addEventListener('click', () => {
  rotation = (rotation + 90) % 360;
  document.getElementById('main-image').style.transform = `rotate(${rotation}deg)`;
});

「画像が横向きだったため読取結果がおかしい」という場合、回転して確認できる。AI読取はアップロード時に既に実行済みのため、回転後の再読取は別のオペレーションとして実装した。


PDFへの対応

取引先によってはFAXがPDF形式で届く。PDFはそのままimg要素では表示できない。

pdf2imageライブラリでページごとにJPEGに変換してから保存する。

from pdf2image import convert_from_bytes

def save_pdf_as_images(order_id: int, pdf_bytes: bytes):
    images = convert_from_bytes(pdf_bytes, dpi=200)
    for i, image in enumerate(images):
        path = f'/var/orderocr/images/{order_id}/page_{i+1}.jpg'
        image.save(path, 'JPEG')
        save_image_record(order_id, path, page_no=i+1)

DPIは200に設定した。低すぎると文字がつぶれてAI読取の精度が下がる。高すぎるとファイルが重くなる。200は精度とサイズの折衷点として、実験で確認した値だ。


*次回は「HTTPS + Basic認証 + EC2相乗り——本番環境の構築」*

*シンプルシステム株式会社 代表 伊藤勝彦*