電子印鑑(印影の画像)を作る Web サービスを作りました。名前や会社名を入れると、丸印・角印・日付印の透過 PNG ができます。
電子印鑑は、名前そのものを入力するサービスです。「入れた名前がどこかに送られていないか」を気にする人は多いはずです。そこで、入力した名前を一度もサーバーに送らないことを、いちばんの決まりにしました。この記事では、そのための工夫を5つ紹介します。
1. 状態は URL の「#」以降に入れる
作り直しやブックマークのために、入力内容を URL に残したい場面があります。ただし ? のクエリに入れると、ページを開くたびにサーバーへ送られ、アクセスログにも残ります。
そこで、状態は # 以降(フラグメント) に入れています。フラグメントは、ブラウザの仕組み上サーバーへ送られません。
// 入力のたびに、履歴を増やさずに # 以降だけを書き換える
history.replaceState(null, '', `#${toHash(state)}`);
姉妹サービスの「書類作るさん」(請求書・見積書の作成)へ印を渡すときも、# に入れて渡します。受け取った側は、読んだらすぐ消します。履歴やブックマークに名前が残らないようにするためです。
function readSealFromHash() {
if (location.hash.length < 2) return null;
const p = new URLSearchParams(location.hash.slice(1));
// 読んだらすぐ消す
history.replaceState(null, '', location.pathname + location.search);
if (p.get('from') !== 'inkan') return null;
// ……印の種類・文字・書体などを、決まった値かどうか確かめてから使う
}
2. 書体のファイルから、入力した文字を推測させない
日本語の書体は数 MB あるので、そのままでは表示が遅くなります。よくあるのは、入力された文字だけを含む小さなファイルをサーバーで作る方法です。ただしこれだと、どのファイルを取りに来たかから入力した文字が分かってしまいます。
そこで、書体を文字コードの順に 500文字ずつの決まった区切り(15ファイル) に分けて、ビルド時に静的ファイルとして置いています。ブラウザは、入力された文字を含む区切りのファイルだけを読みます。1ファイルに数百文字が入っているので、どれを読んだかからは、入力した文字までは分かりません。
3. 文字は SVG のアウトライン(path)にする
印影は SVG で作ります。このとき、文字を <text> で置くのではなく、書体から字形を取り出して path(アウトライン) にしています。
- SVG に書体ファイルを埋め込まなくてよい
- どの環境で開いても、同じ見た目になる
- PNG にするときも、そのまま Canvas に描ける
書体の読み込みと字形の取り出しは、ブラウザの中で行います。
4. 「かすれ」は種つきの疑似乱数で作る
本物のはんこのようなかすれを出すため、細かい点の集まりでマスクを作っています。乱数には、入力から計算した値を種にした疑似乱数(mulberry32) を使います。
export function mulberry32(seed: number): () => number {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
同じ入力なら、何度作ってもまったく同じ画像になります(テストで確かめています)。SVG フィルター(feTurbulence など)を使わなかったのは、描く環境によって見た目がずれるためです。
5. PNG に解像度(pHYs)を入れて、実際の大きさで貼れるようにする
Canvas から書き出した PNG には、解像度の情報が入りません。すると、Word や Excel に貼ったときの大きさがばらばらになります。
そこで、PNG のデータに pHYs チャンク(1メートルあたりの画素数) を差し込んでいます。これで、印の大きさ(たとえば 12mm)のとおりに貼れます。
export function setPngDpi(png: Uint8Array, dpi: number): Uint8Array {
const ppm = Math.round(dpi / 0.0254); // dpi → 1メートルあたりの画素数
const chunk = new Uint8Array(4 + 4 + 9 + 4);
const view = new DataView(chunk.buffer);
view.setUint32(0, 9);
chunk.set([0x70, 0x48, 0x59, 0x73], 4); // "pHYs"
view.setUint32(8, ppm);
view.setUint32(12, ppm);
chunk[16] = 1; // 単位: メートル
view.setUint32(17, crc32(chunk.subarray(4, 17)));
// 署名(8バイト) と IHDR(25バイト) のすぐ後ろに入れる(既存の pHYs は取り除く)
// ……
}
おまけ: 送信先を Content-Security-Policy でしばる
「送らない」と書くだけでは、信じてもらうしかありません。そこで、ブラウザに Content-Security-Policy の connect-src を伝えて、通信してよい先を自分のサイトと Cookie なしのアクセス解析だけに限っています。仮に、送信のプログラムがあとから紛れ込んでも、ここにない送り先へはブラウザが送りません。
確かめ方(開発者ツールのネットワーク欄の見方)も、サイトの「安全への取り組み」のページで公開しています。
サービスの改善のために数えているのは、「保存」「コピー」が押された回数と印の種類だけです。入力した文字は送りません。
まとめ
- 状態は
#に入れる。サイト間の受け渡しも#で行い、読んだら消す - 書体は決まった区切りの静的ファイルにして、読んだファイルから文字を推測させない
- 文字はアウトラインにして、書体を埋め込まない
- 乱数は種つきにして、同じ入力なら同じ画像にする
- PNG に解像度を入れて、実際の大きさで貼れるようにする
同じ考え方で、見積書・請求書を作る「書類作るさん」と、PDF をアップロードせずに結合・押印する「PDF作るさん」も作っています。
- 電子印鑑作るさん: https://inkan.dokugakulink.com/
- 書類作るさん: https://shorui.dokugakulink.com/
- PDF作るさん: https://pdf.dokugakulink.com/