【JavaScript】免許証ジェネレータを作ってみた【SVG】

# はじめに
仕事で少しSVGを扱うことがあったので、免許証ジェネレータを作ってみました。
制作工数: 2日間(20時間程度)

# 作ったもの
[免許証ジェネレータ](https://chibipalette.com/license) 🥳
パラメータを設定して↓のような免許証を生成できます。

![license(3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122701/89066b1e-35bf-4906-e2c2-01a06778b923.png)

# 使い方

下部のタブから画像やテキスト等を設定して「変更を反映」を押下します。
「画像を保存」からローカルに画像を保存できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122701/c7428b2d-a004-e427-5504-b50b044fb637.png)

# ソースコード

雑ですが↓に置いています。

https://github.com/tsuengineer/license-generator

# 免許証ジェネレータの作り方

定数を設定しようと考えましたが、めんどくさくなってマジックナンバーだらけになりました😿
SVGコードのベストプラクティス的なものがよくわかっていないので今後勉強しようと思います。

## 技術
JavaScriptとHTMLだけです。
JavaScriptでSVGを生成して、HTML要素にappendします。

## 描画メソッドを作る
線、枠、文字、画像を描画するメソッドを作成します。
座標、大きさ、色などを指定できるようにしておきます。

```svg.js
// 線を引く
function line(x1, y1, x2, y2, color) {
  let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  line.setAttribute('x1', x1);
  line.setAttribute('y1', y1);
  line.setAttribute('x2', x2);
  line.setAttribute('y2', y2);
  line.setAttribute('stroke', color);
  line.setAttribute('stroke-linejoin', 'round');
  line.setAttribute('stroke-width', '2');
  document.querySelector('svg.license-svg').appendChild(line);
}

// 枠を作る
function rect(startX, startY, width, height, radius, background, color, strokeWidth = 2) {
  let rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  rect.setAttribute('x', startX);
  rect.setAttribute('y', startY);
  rect.setAttribute('width', width);
  rect.setAttribute('height', height);
  rect.setAttribute('rx', radius);
  rect.setAttribute('ry', radius);
  rect.setAttribute('fill', background);
  rect.setAttribute('stroke', color);
  rect.setAttribute('stroke-width', strokeWidth);
  document.querySelector('svg.license-svg').appendChild(rect);
}

// 文字を設置する
function text(x, y, word, size, color, verticalWriting = false, letterSpacing = 1) {
  let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  text.setAttribute('x', x);
  text.setAttribute('y', y);
  text.setAttribute('fill', color);
  text.setAttribute('stroke', 'none');
  text.setAttribute('text-anchor', 'start');
  text.setAttribute('dominant-baseline', 'central');
  text.setAttribute('font-size', size);
  text.setAttribute('font-weight', 'bold');
  text.setAttribute('letter-spacing', letterSpacing)
  if (verticalWriting) {
    text.setAttribute('writing-mode', 'tb');
  }
  text.innerHTML = word;
  document.querySelector('svg.license-svg').appendChild(text);
}

// 画像を設置する
function image(src, x, y, width, height) {
  let image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
  image.setAttribute('x', x);
  image.setAttribute('y', y);
  image.setAttribute('width', width);
  image.setAttribute('height', height);
  image.setAttribute('href', src);
  document.querySelector('svg.license-svg').appendChild(image);
}
```

## 描画エリアの初期化
```svg.js
// SVG描画エリアの初期化
function createSvg(x1, y1, x2, y2) {
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  svg.setAttribute("viewBox", `${x1} ${y1} ${x2} ${y2} `);
  svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  svg.setAttribute("version", "1.1");
  svg.setAttribute( "class", "license-svg");
  svg.setAttribute("width", wrapper_width);
  svg.setAttribute("height", wrapper_height);
  return svg;
}

// 土台を作成する
function createBase() {
  // 外枠を背景白色で作成する
  rect(0, 0, wrapper_width, wrapper_height, wrapper_radius, "#fff", "#000");
}
```


## 枠と線を描画するメソッド

```svg.js
// 背景を描く
function drawBackground(color) {
  // 有効期限エリア(金、青、緑)
  rect(30, 190, 500, 45, 0, color);

  // 免許番号のピンク色部分
  rect(240, 377, 68, 25, 0, "#fcc");

  // 種類欄のピンク色部分
  rect(337.5, 410, 192.5, 90, 0, "#fcc");
  line(337.5, 455, 530, 410, "#fff");
  line(337.5, 500, 530, 455, "#fff");
}

// 線を引く
function drawLines() {
  // 氏名枠
  rect(30, 30, 790, 40, 20, "none", "#000");

  // メイン枠
  rect(30, 110, 790, 400, 20, "none", "#000");

  // 縦線
  line(100, 30, 100, 70, "#000"); // 氏名
  line(540, 30, 540, 70, "#000"); // 生年月日
  line(100, 110, 100, 190, "#000"); // 住所
  line(100, 370, 100, 510, "#000"); // 免許番号

  // 横線
  line(30, 150, 820, 150, "#000"); // 住所
  line(30, 190, 575, 190, "#000"); // 交付
  line(30, 410, 100, 410, "#000"); // 二・小・原
  line(30, 440, 100, 440, "#000"); // 他
  line(30, 470, 100, 470, "#000"); // 二種

  // 種類枠
  rect(310, 410, 220, 90, 0, "none", "#000"); // 枠
  line(337.5, 410, 337.5, 500, "#000");
  line(365, 410, 365, 500, "#000");
  line(392.5, 410, 392.5, 500, "#000");
  line(420, 410, 420, 500, "#000");
  line(447.5, 410, 447.5, 500, "#000");
  line(475, 410, 475, 500, "#000");
  line(502.5, 410, 502.5, 500, "#000");
  line(337.5, 455, 530, 455, "#000"); // 横線
}
```

## 文字を設置するメソッド

```svg.js
function drawFixedText() {
  text(50, 50, '氏名', '16px', '#000', false, 1.5);
  text(635, 50, '年', '16px', '#000');
  text(705, 50, '月', '16px', '#000');
  text(770, 50, '日生', '16px', '#000', false, 1.5);

  text(50, 130, '住所', '16px', '#000');
  text(50, 170, '交付', '16px', '#000');
  text(220, 170, '年', '16px', '#000');
  text(290, 170, '月', '16px', '#000');
  text(360, 170, '日', '16px', '#000');

  text(50, 255, '免許の', '14px', '#000');
  text(50, 280, '条件等', '14px', '#000');

  // (以下、省略)
```

## 動的な変数(パラメータ)を反映する

```svg.js
function drawVariableText(params) {
  // 氏名
  text(120, 50, params.name, '20px', '#000');

  // 生年月日
  text(565, 50, params.birthYear, '18px', '#000');
  text(675, 50, params.birthMonth, '18px', '#000');
  text(740, 50, params.birthDay, '18px', '#000');

  // 住所
  text(120, 130, params.address, '22px', '#000');

  // 期限
  text(50, 213, params.expirationDate, '24px', '#000');

  // (以下、省略)
```

## 画像を縦横比6:5に加工する
免許証の写真は縦3.0cm、横2.4cmなので、縦横比6:5に整形します。
```svg.js
const previewImage = (input) => {
  const img = document.getElementById('preview');
  const reader = new FileReader();
  reader.onload = (e) => {
    img.src = e.target.result;
    img.onload = () => {
      const aspectRatio = img.width / img.height;
      if (aspectRatio > 5 / 6) {
        const newWidth = img.height * 5 / 6;
        const trimLeft = (img.width - newWidth) / 2;
        const trimmedCanvas = document.createElement('canvas');
        trimmedCanvas.width = newWidth;
        trimmedCanvas.height = img.height;
        const trimmedCtx = trimmedCanvas.getContext('2d');
        trimmedCtx.drawImage(img, trimLeft, 0, newWidth, img.height, 0, 0, newWidth, img.height);
        img.src = trimmedCanvas.toDataURL();
      } else {
        const newHeight = img.width * 6 / 5;
        const trimTop = (img.height - newHeight) / 2;
        const trimmedCanvas = document.createElement('canvas');
        trimmedCanvas.width = img.width;
        trimmedCanvas.height = newHeight;
        const trimmedCtx = trimmedCanvas.getContext('2d');
        trimmedCtx.drawImage(img, 0, trimTop, img.width, newHeight, 0, 0, img.width, newHeight);
        img.src = trimmedCanvas.toDataURL();
      }
      img.style.display = 'block';
    };
  };
  reader.readAsDataURL(input.files[0]);
};
```


## 画像を保存する
SVGデータをPNG形式に変換して保存します。
```svg.js
const downloadSvg = () => {
    const svg = (document.getElementsByClassName('license-svg'))[0];

    let canvas = document.createElement('canvas')
    canvas.width = svg.width.baseVal.value
    canvas.height = svg.height.baseVal.value

    const ctx = canvas.getContext('2d')
    let image = new Image()

    image.onload = () => {
      // SVGデータをPNG形式に変換する
      ctx.drawImage(image, 0, 0, image.width, image.height)

      // license.pngという名前でダウンロードする
      let link = document.createElement("a")
      link.href = canvas.toDataURL()
      link.download = "license.png"
      link.click()
    }
    image.onerror = (error) => {
      console.log(error)
    }

    // SVGデータをXMLで取り出す
    const svgData = new XMLSerializer().serializeToString(svg)
    image.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(unescape(encodeURIComponent(svgData)))
}
```

# 使ってくれた方の紹介

ご利用いただきありがとうございます😸✨

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ちびキャラパレットで免許証を作成しました。 <a href="https://t.co/3TO3LJsXYV">https://t.co/3TO3LJsXYV</a> <br><br>雑につくってしまった <a href="https://t.co/DYeIfg1YWa">pic.twitter.com/DYeIfg1YWa</a></p>&mdash; うほうほめもたろう (@MCagnJP4OjrSXjK) <a href="https://twitter.com/MCagnJP4OjrSXjK/status/1654056238181912578?ref_src=twsrc%5Etfw">May 4, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ちびキャラパレットで免許証を作成しました。 <a href="https://t.co/2JSHLpTmiH">https://t.co/2JSHLpTmiH</a> <br>ほぼネタにまみれてます<br>あと流石にリアル情報は晒せませんので…😌 <a href="https://t.co/6iHCyN518p">pic.twitter.com/6iHCyN518p</a></p>&mdash; 藦🅰️ℹ️🎨 (@AIMas19990523) <a href="https://twitter.com/AIMas19990523/status/1654060391939596288?ref_src=twsrc%5Etfw">May 4, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/AI%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#AIイラスト</a><br>今日からAIイラスト免許交付らしい<br>無免許運転になるところだった、あぶない<br><br>ちびキャラパレットさんの免許証作成ページ <a href="https://t.co/zA2LQoV8cD">https://t.co/zA2LQoV8cD</a> <a href="https://t.co/jqJ7ULo0C6">pic.twitter.com/jqJ7ULo0C6</a></p>&mdash; おかゆーい_🔯 (@atrandomnumber1) <a href="https://twitter.com/atrandomnumber1/status/1654106304653443080?ref_src=twsrc%5Etfw">May 4, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>