スクリーンショットを黒塗りして複数枚まとめて渡すとき、目視確認とファイルの取り違え確認は別の作業になります。黒塗りが適切かは画像を見て確認し、その後に渡したPNGが書き出し時のものと一致するかをSHA-256で照合します。
ブラウザ内でPNGを書き出す無料ツールを作る過程で、この二つを分けた納品ZIPを試しました。この記事では、そのZIPをPythonの標準ライブラリだけで検品する手順を紹介します。顧客データは使わず、架空のサンプル画像2枚で確認しています。
ZIPに入れるもの
出力の構成は次のようにします。画像名は連番にし、元のファイル名と加工前の画像は含めません。
localbrief-export.zip
├── image-001.png
├── image-002.png
└── manifest.json
manifest.jsonには、加工してPNGへ書き出した後のバイト列から計算したハッシュを記録します。元画像のハッシュや、黒い矩形の位置だけを記録しても、納品したPNGそのものの照合には使えません。
以下は2枚のうち1枚分を抜き出した例です。
{
"tool": "LocalBrief",
"version": "0.1-validation",
"files": [
{
"file": "image-001.png",
"width": 900,
"height": 520,
"masks": 1,
"reviewed": true,
"sha256": "ad2f24bde642ea0a05187de571e53bcb81d08253be20239f12d5719a0cda1dbf"
}
]
}
reviewedは画面での確認操作の記録です。第三者による審査結果や、隠し漏れがないという認証を意味しません。
Pythonで全PNGを照合する
次をverify_export.pyとして保存します。Python 3で動作し、追加パッケージは使いません。対象は上記の構成で自分が生成した小さな検証ZIPです。
import hashlib
import json
import re
import sys
import zipfile
with zipfile.ZipFile(sys.argv[1]) as archive:
names = archive.namelist()
if len(names) != len(set(names)):
raise SystemExit("同じ名前のエントリーがあります")
manifest = json.loads(archive.read("manifest.json"))
files = manifest["files"]
expected = [item["file"] for item in files] + ["manifest.json"]
if len(expected) != len(set(expected)) or set(names) != set(expected):
raise SystemExit("ZIPとmanifestのファイル一覧が一致しません")
for item in files:
name = item["file"]
if not re.fullmatch(r"image-[0-9]{3}\.png", name):
raise SystemExit("想定外のファイル名です")
digest = hashlib.sha256()
with archive.open(name) as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
if digest.hexdigest() != item["sha256"]:
raise SystemExit(f"NG {name}: ハッシュ不一致")
print(f"OK {name}")
print(f"照合完了: {len(files)}ファイル")
実行例です。
python verify_export.py localbrief-export.zip
OK image-001.png
OK image-002.png
照合完了: 2ファイル
ZIPを展開して保存する処理は行いません。ファイル一覧が一致することを確かめ、各PNGを順に読み込んでハッシュを照合します。画像の中身や黒塗り位置の判定は、このスクリプトの対象外です。
ブラウザ側はPNG生成後にハッシュを計算する
実装の順序は「原画像をCanvasへ描画 → 不透明な黒で対象領域を塗る → PNGのBlobを生成 → そのBlobのSHA-256を計算 → PNGとmanifestをZIPに格納」です。
CSSで黒い要素を画像に重ねるだけだと、別途配る元ファイル自体には加工が反映されません。書き出したPNGを開き直して、実際の画素が黒くなっていることを確認します。
Canvasの画像をBlobへ変換するAPIはHTMLCanvasElement.toBlob()です。変換できないとコールバックにnullが渡される場合もあるため、そこで書き出しを中断します。入力ファイルのメタデータを別途コピーする処理は入れていませんが、出力PNGに情報が一切ないという保証とは分けます。
ブラウザでのハッシュ計算にはcrypto.subtle.digest()を使えます。HTTPS等の安全なコンテキストが必要で、ストリーミング入力には対応していません。画像の枚数・画素数・合計サイズに上限を設けてから使います。
ハッシュが一致しても隠し漏れは分からない
ハッシュの一致で確認できるのは、記録した値とPNGのバイト列が一致することです。元から隠し漏れのあるPNGでも、ハッシュは正しく一致します。また、PNGとmanifestの両方を書き換えられる相手に対する発行者の証明にもなりません。
そのため、バッチ処理では全画像に同じ比率の範囲を適用した後も、1枚ずつ見直す手順を残しました。幅や配置の違う画像では、同じ比率の黒塗りが同じ情報を隠すとは限りません。範囲を変えた画像は未確認に戻し、全画像の確認後にZIPを生成します。
架空サンプルで試す
開発中の無料検証版LocalBriefで、サンプル2枚の加工・個別確認・ZIP出力を試せます。運営はこの記事の投稿者です。ログインやカード登録は不要で、画像は端末内で処理します。まずは架空サンプルで操作を確認できます。
現版はPNG/JPEGが対象で、6枚・合計12MB、1枚5MB・400万画素までです。PDF・OCR・自動検出には対応していません。現在は無料の技術・需要検証で、販売やクラウドファンディングの支援受付は行っていません。
この記事の執筆・実装には生成AIを使用し、公開版の架空サンプルから出力したZIPを上記スクリプトで照合しました。実際の顧客の利用実績としては扱っていません。