0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

印鑑の文字配置を自動で決める方法(丸印・角印・日付印)

0
Posted at

電子印鑑を作る 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 文字を縦に並べます。

  1. 文字数に合わせて、縦に並んだマスを作る(文字数が多いほどマスは平たくなる)
  2. 各マスに文字を合わせる
  3. 全体の「中心からいちばん遠い点」が、枠の内側の円にちょうど届くように拡大・縮小する

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% 大きくする変更を入れると、このテストがきちんと失敗することも確かめています。

まとめ

  • 文字は インクのある範囲 で扱い、マスに合わせるときは ゆがみに上限 を付ける
  • 円に収めるときは 中心からいちばん遠い点 で倍率を決める
  • 印鑑らしさは 正方形 → 円の変換 で出せる
  • 角印は 列ごとの文字数を変えて 余るマスをなくす
  • 「はみ出さない」は 全文字・全書体のテスト で守る

実際の動きは、こちらで試せます(入力した名前はサーバーに送らず、ブラウザの中だけで作っています)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?