電子印鑑を作る Web サービス「電子印鑑作るさん」で使っている、印鑑の文字配置を自動で決める方法をまとめます。名前や会社名を入れるだけで、丸印・角印・日付印の形に自動で並べています。
座標はすべて 100 × 100 の正方形(SVG の viewBox="0 0 100 100")で考え、最後に実寸(mm)で出力します。
準備:文字の「インクのある範囲」で考える
書体の文字には、送り幅(1 文字分の箱)と、実際にインクのある範囲(ink bbox)があります。印鑑では文字を枠いっぱいに広げたいので、送り幅ではなくインクのある範囲を基準にします。
まず、書体から取り出した輪郭(曲線)を細かい折れ線にして、点の最小・最大から範囲を求めます。
export function bounds(contours: Contour[]): Box {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const c of contours) {
for (const [x, y] of c) {
minX = Math.min(minX, x); maxX = Math.max(maxX, x);
minY = Math.min(minY, y); maxY = Math.max(maxY, y);
}
}
return { minX, minY, maxX, maxY };
}
文字をマスに合わせる(ゆがみは上限つき)
文字をマス(cell)に合わせるときは、縦と横を別々に伸ばして、マスいっぱいにします。ただし「一」のような横長の文字を縦に何倍も伸ばすとおかしくなるので、縦横比のゆがみに上限を付けます。
export function fitToCell(contours: Contour[], cell: Box, maxDistort: number): Contour[] {
const b = bounds(contours);
const bw = Math.max(b.maxX - b.minX, 0.05);
const bh = Math.max(b.maxY - b.minY, 0.05);
const cw = cell.maxX - cell.minX;
const ch = cell.maxY - cell.minY;
let sx = cw / bw;
let sy = ch / bh;
if (sx / sy > maxDistort) sx = sy * maxDistort; // 横に伸ばしすぎない
if (sy / sx > maxDistort) sy = sx * maxDistort; // 縦に伸ばしすぎない
const fit = Math.min(1, cw / (bw * sx), ch / (bh * sy));
sx *= fit;
sy *= fit;
// マスの中心に置く
const gx = (b.minX + b.maxX) / 2, gy = (b.minY + b.maxY) / 2;
const cx = (cell.minX + cell.maxX) / 2, cy = (cell.minY + cell.maxY) / 2;
return contours.map((c) => c.map(([x, y]) => [cx + (x - gx) * sx, cy + (y - gy) * sy]));
}
上限は、書体そのままの形を活かすときは 1.6 倍、印鑑らしく枠いっぱいに広げるときは 2.4 倍にしています。
丸印:縦に並べてから、円に収める
丸印(認印)は 1〜4 文字を縦に並べます。
- 文字数に合わせて、縦に並んだマスを作る(文字数が多いほどマスは平たくなる)
- 各マスに文字を合わせる
- 全体の「中心からいちばん遠い点」が、枠の内側の円にちょうど届くように拡大・縮小する
3 の「いちばん遠い点で決める」のがコツです。文字の形によってインクの位置は違うので、四角いマスの角で決めると、円からはみ出したり、逆に小さくなりすぎたりします。点で測れば、どんな文字でもはみ出さずに最大の大きさになります。
const maxR = maxRadius(contours, 0, 0); // 中心から最も遠い点までの距離
const s = targetRadius / maxR; // 円の内側にちょうど届く倍率
contours = contours.map((c) => c.map(([x, y]) => [50 + x * s, 50 + y * s]));
「印鑑風」:正方形を円に写す
実際の印鑑(印相体など)のように、外側の線を枠に沿って曲げたいときは、文字を正方形 [-1, 1]² いっぱいに並べてから、正方形を円に写す変換をかけます。
/** 正方形 [-1,1]² を円に写す(四隅が円周に来る)。strength=0 で何もしない、1 で完全に円 */
export function squareToDisk([x, y]: Point, strength: number): Point {
const u = x * Math.sqrt(Math.max(0, 1 - (y * y) / 2));
const v = y * Math.sqrt(Math.max(0, 1 - (x * x) / 2));
return [x + (u - x) * strength, y + (v - y) * strength];
}
完全に円に写す(strength = 1)と曲がりすぎて読みにくくなるので、0.75 くらいにしています。また、変換の前に長い直線に点を足しておかないと、直線のまま写されて曲がりません。
角印:右の列から読む・余るマスを作らない
角印は、右の列から左の列へ、各列は上から下に読みます。難しいのは「何列にするか」です。8 文字を 3 × 3 のマスに入れると、1 マス余ってしまいます。
そこで、列ごとの文字数を変えて、余りが出ないようにしています。
/** 文字数から、列ごとの文字数を決める(右の列から) */
export function squareGrid(n: number): number[] {
let cols = Math.ceil(Math.sqrt(n));
if (cols > 1 && n <= cols * (cols - 1)) cols -= 1; // 1 列減らしても入るなら減らす
const base = Math.floor(n / cols);
const extra = n % cols;
// 余った分は右の列(読み始め)から 1 文字ずつ多くする
return Array.from({ length: cols }, (_, i) => base + (i < extra ? 1 : 0));
}
| 文字数 | 列ごとの文字数(右の列から) | 例 |
|---|---|---|
| 4 | 2, 2 | 見本之印 |
| 8 | 3, 3, 2 | 株式会社見本之印 |
| 13 | 4, 3, 3, 3 | 長めの会社名+之印 |
| 16 | 4, 4, 4, 4 |
各列の中では、その列の文字数でマスの高さを割るので、文字数の少ない列の文字は少し縦長になります。実際の角印でもよく見る形です。
日付印:円と横線のあいだの「弓形」に文字を入れる
日付印は、円を 2 本の横線で 3 段に分け、上段に部署名、中段に日付、下段に名前を入れます。
上段と下段は、円と横線にはさまれた弓形の部分です。ここに長方形の枠を取るには、**その高さでの円の幅(弦の長さ)**を使います。
const halfChord = (y: number) => Math.sqrt(Math.max(0, r * r - y * y));
// 横線から少し離した位置 nearY から、さらに segH だけ外側までを文字の枠にする
const farY = nearY + segH;
const halfWidth = halfChord(farY) * 0.94; // 外側ほど円の幅が狭いので、外側の幅に合わせる
日付(例:26.10.18)は、送り幅で横に並べてから、枠に入るように縮めます。横に長すぎるときは横方向だけ縮めますが、縮めすぎると読みにくいので、縦横比 0.6 を下限にして全体を小さくしています。
テストで「はみ出さない」を確かめる
配置のルールを変えると、どこかの文字で枠からはみ出すことがあります。そこで、
- 常用漢字 2,140 字(「𠮟」「叱」のような字形違いを含む)で、丸印を 1 字ずつ 4 書体すべてで作る
- 同じ常用漢字を 16 字ずつに区切って、角印も作る
- すべての点(線を太らせる分を含む)が、枠の内側にあるかを測る
というテストを走らせています。
function clearance(layout: StampLayout): number {
const inner = layout.inner; // 枠の内側の円
let min = Infinity;
for (const t of layout.texts) {
for (const c of t.contours) {
for (const [x, y] of c) {
const room = inner.r - Math.hypot(x - inner.cx, y - inner.cy);
min = Math.min(min, room - t.strokeWidth / 2);
}
}
}
return min; // 負なら、はみ出している
}
わざと枠より 2% 大きくする変更を入れると、このテストがきちんと失敗することも確かめています。
まとめ
- 文字は インクのある範囲 で扱い、マスに合わせるときは ゆがみに上限 を付ける
- 円に収めるときは 中心からいちばん遠い点 で倍率を決める
- 印鑑らしさは 正方形 → 円の変換 で出せる
- 角印は 列ごとの文字数を変えて 余るマスをなくす
- 「はみ出さない」は 全文字・全書体のテスト で守る
実際の動きは、こちらで試せます(入力した名前はサーバーに送らず、ブラウザの中だけで作っています)。