この写真、まだ場所を覚えている
写真に残っている撮影地の座標・カメラ本体のシリアル番号・編集前の埋め込みサムネイルを危険度の高い順に見せ、JPEGの区画だけを外して画質を1バイトも変えずに保存する。写真はブラウザの中だけで処理し、外部へは通信を1本も出さない
制作メモ
- AIに任せた
- JPEGのセグメント分解・分類・除去の実装。SOS(0xFFDA)から末尾までを1バイトも変えずに写し、除去後のバイト列が元と完全一致することをユニットテストで固定した/規格を読んで分かっていた落とし穴の実装。ExifとXMPはどちらもAPP1なのでマーカー番号ではなく署名文字列で見分け、64KB超で分割された同種の区画を全部消す(最初の1つで打ち切らない)/TIFF/IFDの解析。IIとMMの両方のバイトオーダー、GPSの度分秒、IFD1のサムネイル切り出し、壊れたオフセットや異常なエントリ数で例外を投げずに失敗を返す作り/4状態(空・読込中・エラー・不正入力)+結果+対象外の切り替えと、通った順を data-state-log に残す仕組み/サンプル写真の自作(表札の住所が写った絵を描き、それを切り落とした本体に、切り落とす前の絵をIFD1のサムネイルとして差し込む)/ユニット73件・E2E42件のテスト。同一オリジン以外への通信が0本であることもE2Eで固定した/保存したバイト列をアプリ自身でもう一度読み直し、区画ごとの結果(Exifなし/ICCは残しています/画素は元のまま)を画面に出す検算。免責の文章より、その場で確かめられることのほうが効くという調査結果から足した/権利表示(Copyright / Artist)を「あなたにつながるもの」から切り離して第4のカテゴリにする組み替えと、XMPの権利表示(dc:rights・xmpRights:UsageTerms など)を文字列一致だけで拾う判定/個人情報保護法・著作権法(権利管理情報)・製造物責任法・消費者契約法・電気通信事業法の一次情報にあたる調査と、同種の日本語ツール7件の実地調査。そこから「免責文言より画面設計が効く」「権利表示は利用者の利益の向きが逆なので別カテゴリにする」「権利表示だけを消す機能は作らない」を設計へ反映した
- 自分でやった
- 実装に入る前に要件(REQUIREMENTS.md)と設計(DESIGN.md:ユーザーストーリー+UX5階層)を書き切ってから実装へ渡す、という進め方の指定/免責・注意書き・警告の文言は法務レビューの結果であとで差し替えるので、1か所(lib/copy.js)にまとめておくという指示/この日の成果物をローカルに留め、gitの操作をしないという判断/法的リスクを調べたうえで作る、という進め方の指示。調査そのものはAIが行い、その結果をどう設計へ反映するかを決めた/画面・README・meta.json に、侵害を促す表現と法的評価の断定を書かないという線引き
- 失敗と修正
- 実装に入る前に公開前チェック(npm run precheck)を走らせたら、いきなり落ちた。要件定義に書いていたサンプル写真のメーカー名に実在の名前を使っていたため。このリポジトリは公開なので、そのまま作っていたらサンプルのExifにもドキュメントにも残った → メーカー名を架空のもの(SAMPLE OPTICS)に変え、要件定義側にも変更理由を書き足した。サンプルに実在の名前は使わない
meta.json を draft にすると、ビルドがそのDayを dist/ へコピーしない。E2Eは配信された dist/ を見るので、そのまま書いていたらテストが全部404で落ちるところだった → spec の中に、アプリのフォルダだけを配る小さなサーバーを立てた。公開に切り替えたら通常の baseURL に寄せられる
向き1件だけのExifを組み立てる関数が、文字列の '6' も受け付けていた。中で Number() に通していたため、数でない値が黙って通っていた → 型そのものを見るようにした。ユニットテストが先に捕まえた
壊れたJPEGのテストで、期待した「区切りが壊れている」ではなく「JPEGではない」が返った → 実装ではなくテストの題材が誤っていた(先頭3バイトがJPEGでないバイト列を使っていたので、区切りを読む前に弾かれていた)。テスト側を直した
Exifの中身が壊れたJPEGを、まとめて「読めません」のエラー画面へ送っていた。区画を落とすこと自体はExifの解析結果に関係なくできるので、いちばん消したい写真だけが消せない状態だった。しかも文言のほうには「区画そのものは取り除けます」と書いてあり、画面が文言の約束を果たしていなかった → エラーは「JPEGとして最後まで読めない」場合だけに絞り、中身が読めないだけの写真は結果の画面で扱って保存ボタンを出すようにした。見出しも「見つかりませんでした」と混ざらないよう別の文にした
著作権表示と撮影者名を「あなたにつながるもの」に入れていた。GPSやシリアル番号と同じ束に入ると、利用者には「全部消すのが正解」に見える。他人が撮った写真の権利表示を、何も考えずに消す導線になっていた → 利用者にとっての利益の向きが逆(消したい情報ではなく残したい情報)なので、第4のカテゴリとして切り離した。警告は除去ボタンの真上に出したままにし、検出した値をコピーして控えられるようにした
XMPから拾った権利表示の並び順が、ファイルの中でどのセグメントが先に来るかで変わっていた。強い警告になるもの(利用条件・権利者の連絡先)が後ろに回ることがあった → 見つけた名前を集めてから、こちらが決めた順(強いものが先)に並べ直すようにした。ユニットテストが先に捕まえた
画面に本体の写真を出していないのに、サムネイルの説明だけが「本体の写真と見比べてください」と言っていた。見比べる相手が画面に無いので、このアプリ最大の見せ場(切り落としたはずの表札がサムネイルにだけ残っている)が伝わらない。しかも同じ説明文が、サムネイルの行とカードの下で二重に出ていた → 本体の写真と埋め込みサムネイルを並べて出し、それぞれに「いま見えている写真」「埋め込まれたサムネイル」のラベルを付けた(本体も blob URL で出すので、通信は増えていない)。説明文はカード側で1回だけにした。行側の文言を固定していたユニットテストは、『同じ説明を2か所で言わない』を見る形に書き換えた
「メタデータを消して保存」が390pxで上から3,093px=全長の68%の位置にあり、主要な行動に届くまで約4.5画面ぶんスクロールが要った → 判定カードの直下へ動かした(390pxで1,125px=全長の26%)。ただし権利表示の警告は保存ボタンの直上・常時表示のまま(著作権法まわりの要件)、免責は「完全には消せない」「見つからない=無い ではない」の2点だけを上に残し、全文は保存ボタンより下へ移した。画面下に固定バーを出す案は採らなかった。保存ボタンだけを固定バーへ移すと、警告をその直上に置くという条件が満たせなくなるため
「いちばん目立たない文字でもAAを満たす」というE2Eがあったのに、共通シェアの注記が3.2:1しかない状態を素通りさせていた。テストが color の指定値だけを読んでいて、opacity で薄まった後の見た目を見ていなかった(原因は shared/share.css の .share__note { opacity: 0.7 }) → 先祖にかかっている opacity を掛け合わせ、背景と混ぜた色で測るようにテストを書き直した。わざと opacity: 0.7 に戻して落ちること(3.18)を確かめてから、正本の shared/share.css で opacity をやめ、11アプリへ同期した。共通部品なので、この1か所の直しが全アプリに効く
「開く」の行を押しやすくしたことを固定するE2Eが、最初の実行で落ちた。座標で押す前に要素を画面内へスクロールしていなかったので、390pxでは対象が画面の外にあり、クリックが別の場所に当たっていた → 測る前に scrollIntoViewIfNeeded し、開いたあとも位置を測り直してから押すようにした。行の高さは25px→45pxにしてある(.group__head に上下の余白が無く、押せる高さが足りていなかった)
権利表示の警告は、面積では今も保存ボタンより大きい(390pxで警告336px対ボタン54px)。採点では「警告のほうが面積で勝っていて階層が逆転している」と指摘された点 → 文面は法務の調査結果そのもので削れないので、余白・枠の太さ・文字の大きさを落として視覚的に静かにし、常時表示は保ったまま、保存ボタンの側を大きく(横いっぱい・高さ54px・濃い青)した。面積の逆転そのものは解消できていない
共通シェアのボタンの枠が地色に対して1.7:1しかなく、面も1.1:1で「押せるもの」に見えていなかった。前回opacityの穴を塞いだコントラストのE2Eは、文字の色しか見ておらず枠の色を一度も測っていなかった → 枠を currentColor の32%から90%へ上げ、11アプリぶんの枠と地色の比を測るE2Eを足した。濃さは勘ではなく実測で決めた:いちばん条件が厳しいDay007はシェア欄の文字色そのものが地に対して3.9〜5.1:1しかなく、75%では2.6:1で3:1に届かなかった
その新しいE2Eが、最初は嘘の合格を出していた。color-mix() の計算結果を Chromium は color(srgb 0.93 0.94 0.96 / 0.75) の形で返す(成分が0〜1)のに、rgb() と同じ0〜255として読んでいたため、明るい枠を真っ黒として測って19:1と報告していた → color( で始まる値は255倍してから測るようにした。文字側の古いコントラストのE2Eにも同じ読み方が残っていたので、そちらも直した。テスト自身が壊れていると、直したつもりで何も守れていない状態になる
同じE2Eが、Day009だけ1.12:1という桁違いの低さを出した。背景をグラデーションで塗っているアプリでは backgroundColor がどの先祖でも透明のままで、白い紙の上として測っていた(実際は暗い画面) → 先祖を遡るときに background-image のグラデーションも見て、色の停止点を全部拾い、いちばん条件の悪い停止点で判定するようにした。あわせて「枠と面の比」の判定は外した。塗りのあるボタン(--primary)は面そのものが地との境目になるので、そこまで求めると塗りを濃くできなくなる
本体写真と埋め込みサムネイルを狭い画面でも横に並べたら、ラベルの長さが左右で違うせいで(「埋め込まれたサムネイル」だけが2行)、2枚の画像の上端が55pxずれた。このカードは見比べるためにあるのに、見比べにくいまま並べ替えただけになっていた → ラベルの行と画像の行を2枚で共有する行(subgrid)にそろえ、画像の上端を一致させた。subgridが無い環境では宣言が無効になり、以前と同じ挙動に戻るだけで壊れない。E2Eも「2枚が横に並んでいること」だけでなく「画像の上端がそろっていること」を見るようにした
カードの幅を本文の折り返し幅に合わせて詰めたら、今度は「どこにも送られていません。」の最後の1文字だけが2行目に落ちた。1つ直すと別の場所に出るたぐいの崩れ → 行長を32rem→33remに広げ、この行の文字を0.92rem→0.9rem(カード内の本文と同じ大きさ)にそろえた。そのうえで text-wrap: pretty を本文に掛け、最終行に1〜2文字だけ残る折り返しを全体で減らした(残っているのは3文字以上のものだけ)
権利表示の警告を保存ボタンと同じカードに入れていたので、「メタデータを消して保存」が琥珀色の箱の内側にあるように見え、警告に対するボタンだと読めていた → 警告を独立したカードにして、保存ボタンのカードと分けた(間は30px)。警告を除去ボタンの直上に常時出すという要件は、隣り合うカードとして満たしている。ただし面積の逆転(390pxで警告359px対ボタン54px)は、文面が法務の調査結果そのもので削れないため今回も解消していない
保存ボタンを上げすぎて、検出内容を見ないまま保存できる順序になっていた。採点の「CTAが68%地点」を額面どおり受け取り、判定カードの直下へ動かした結果、検出内容(分類カード)と本体写真・サムネイルの見比べが保存ボタンより下に落ちた。このアプリは「何が入っていたかを確かめてから消す」ためのものなので、いちばん見せたい中身を飛ばして主要な行動に到達できる並びは、それ自体が不具合だった。深い位置にあったのが問題だったのではなく、上にあったのが説明文ばかりで中身が無かったのが問題だったと読み違えていた → 並びを「判定 → 検出内容 → 写真の見比べ → 付随情報 → 限界の要点 → 権利表示の警告 → 保存 → 詳細」に直した。保存ボタンは390pxで判定から2,527px下・ページ全長の68%地点に戻ったが、上にあるのは利用者が見たい中身そのものなので、この深さは意図どおり。法務由来の2条件(追記2 G-2 の限界の要点/追記3 K-3 の警告の直上表示)はどちらも保存ボタンより上のまま維持している。結果カードの直下の子要素の並びをそのままE2Eで固定し、説明の壁を保存ボタンの前に積み直せないようにした
本体写真とサムネイルの見比べで、画像の上端だけをそろえて済ませていた。実際には表札(ラベル)の行数が左右で違うせいで「8.4KB」が約25px下にずれ、2枚の画像の下端も約29pxずれていた。見比べるための機能なのに、上端以外に共通の基準線が無かった → 表札・大きさ・画像の3行を2枚で共有する行にそろえ、表札には2行ぶんの高さを最初から与えて行数を固定した(subgridが無い環境でもずれない)。画像は共通の 4:3 の枠に object-fit: contain で収め、上端と下端の両方を一致させた。E2Eは上端だけでなく、下端・表札の高さ・大きさの表示の位置も測るようにした(いずれも実測0px差)









