LoginSignup
6
7

More than 1 year has passed since last update.

htmlファイルのみでPDFを生成できるようなのでドラゴン桜計算プリントを作ってみた

Last updated at Posted at 2021-07-25

はじめに

 子供が算数のテストで単純な計算ミスをよくしているようなので、
計算力をつけたいと思い、簡単な四則演算のプリントを印刷するために、
Googleスプレッドシートを使って問題を生成して毎日5分で5人家族全員で解いていました。

 これでも結構使えましたが、知り合いの子供もやりたいという声があり、共有を考えましたが、
閲覧権限だと問題のランダム生成ができないし、編集権限をつけると間違って触って式が壊れるかもしれないので、
シンプルにPDFを出力したほうがいいかと思い実現方法を調べてみました。

するとjavascriptのみでPDFを生成してフォントも埋め込めるライブラリがあったので、
それを使って作ってみた手順をまとめました。

必要なもの

パソコン:今回は Windows10 の PC を使いましたが、Mac などでも同じようにできます。
ブラウザ(Chrome最新版, IE11 は pdf-lib が対応です。):動作確認用
テキストエディタ:html 編集用
埋め込み用フォント:Rounded Mgen+ (ラウンデッド ムゲンプラス) から rounded-mgenplus-1m-regular.ttf をダウンロードしました。
Webサーバー:ローカルで html を開くとフォントを読み込めないので、 OS 問わず使いやすい Chrome拡張の Web Server for Chrome を使います。

最小限のサンプル

javascript で PDF を生成するライブラリはいくつかあるようですが、
今回は比較的新しそうな PDF-LIB を使います。

テキストエディタで以下の html ファイルを作成します。
html を保存した同じ場所(/dev/pdf)に rounded-mplus-1m-regular.ttf も配置します。

/dev/pdf/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/pdf-lib"></script>
<script src="https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.js"></script>
</head>

<body>
準備中...

<script>
(async function() {
 //PDF生成
  const pdf = await PDFLib.PDFDocument.create();

  //埋込フォント使用
  pdf.registerFontkit(fontkit);

  //日本語フォント読込
  const font = await pdf.embedFont(await (await fetch('rounded-mgenplus-1m-regular.ttf')).arrayBuffer());

  //ページ追加(単位はポイント、A4サイズにしています)
  const page = pdf.addPage([842, 595]);

  //文字描画(こちらも size, x, y の単位はポイント、原点は左下)
  page.drawText('JavaScript で PDF を生成したよ', { font: font, size: 14, x: 100, y: 300 });

  //PDF表示
  location.href = URL.createObjectURL(new Blob([await pdf.save()], { type: 'application/pdf' }));
})();
</script>

</body>

</html>

pdfフォルダは以下の内容になります。

image.png

Webサーバーの設定で、CHOOSE FOLER を押して、/dev/pdf フォルダをしています。
image.png

表示されている Web Server URL(s) をクリックしすれば以下のように PDF が表示されます。

image.png

pdf-lib ではフォントを指定しない場合、日本語が使えないようなので、これが最小のサンプルになると思います。

あとは描画する文字のサイズを計算する font.widthOfTextAtSize, font.heightAtSize, 矩形を描画する page.drawRectangle を使えば任意の位置に文字と枠線を表示できるので、四則計算100問のプリントPDFを作るプログラムを書きました。

完成版

完成版
<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/pdf-lib"></script>
<script src="https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.js"></script>
</head>

<body>
準備中...

<script>
(async function() {
/*
ランダムで計算100問のPDFを出力する
pdf-libの単位はpt: 1pt = 0.353mm, 1mm = 2.835pt
*/
  const PAGE_WIDTH = 842; //A4 210mm×297mm = 84pt×595pt
  const PAGE_HEIGHT = 595;
  const FONT_SIZE = 10.5;
  const MARGIN = FONT_SIZE * 2; //上下左右余白
  const ROWS = 27; //1ページ行数
  const COLUMNS = 4; //1ページ列数
  const COLUMN_WIDTH = (PAGE_WIDTH - MARGIN * 2) / COLUMNS;
  const THICKNESS = 0.001; //線の太さ
  const PADDING = 0.5; //セルの余白(1で1文字のサイズ)
  const FONT_URL = 'rounded-mgenplus-1m-regular.ttf'; //http://jikasei.me/font/rounded-mgenplus/
  const Y = y => PAGE_HEIGHT - y; //pdf-libのy座標は下から上方向なので上から下方向に変換
  const range = (start/*:Number*/, count/*:Number*/) => Array.from(Array(count)).map((_, i) => i + start);
  const rand_int = max/*:Number*/ => Math.floor(Math.random() * max);
  const isNumber = x => typeof(x) == 'number';
  const expand_text = text => isNumber(text) ? Array.from(Array(Number(text))).reduce((a, i) => a + ' ', '') : text;

  //問題、解答作成
  const pairs = range(1, 10).flatMap(x => range(1, 10).map(y => [x, y]));
  const questions = range(1, 100).map(n => {
    const prefix = '[' + n + '] ';
    switch(rand_int(4)) {
    case 0://加算: 0~50 + 0~50
      {
        const left = rand_int(51);
        const right = rand_int(51);
        return [prefix + left + '' + right + '', left + right];
      }
    case 1://減算: 0~99 -0~99(答えは0以上)
      {
        const pair = [rand_int(100), rand_int(100)];
        const left = pair[0] > pair[1] ? pair[0] : pair[1];
        const right = pair[0] > pair[1] ? pair[1] : pair[0];
        return [prefix + left + '' + right + '', left - right];
      }
    case 2://乗算: 1~10 × 1~10
      {
        const pair = pairs.splice(rand_int(pairs.length), 1)[0];
        const left = pair[0];
        const right = pair[1];
        return [prefix + left + '×' + right + '', left * right];
      }
    case 3://除算: 1~100 ÷ 1~10(余りなし)
      {
        const pair = pairs.splice(rand_int(pairs.length), 1)[0];
        const left = pair[0] * pair[1];
        const right = pair[1];
        return [prefix + left + '÷' + right + '', left / right];
      }
    }
  });

  //PDF作成
  const pdf = await PDFLib.PDFDocument.create();
  const title = '計算' + questions.length + ''
  pdf.setTitle(title);
  pdf.setAuthor('nakazawaken1');

  //日本語フォント読込
  pdf.registerFontkit(fontkit);
  const font = await pdf.embedFont(await (await fetch(FONT_URL)).arrayBuffer());
  const unitFontWidth = font.widthOfTextAtSize('', FONT_SIZE)
  const fontHeight = font.heightAtSize(FONT_SIZE);
  const lineHeight = (PAGE_HEIGHT - MARGIN * 2) / ROWS;

  const drawCell = (page, texts/*:String|Number|[String|Number]*/, x/*:Number|'left'|'right'|'center'*/, y/*:Number*/) => {
    if(!(texts instanceof Array)) texts = [texts];
    const widths = texts.map(t => font.widthOfTextAtSize(expand_text(t), FONT_SIZE) + unitFontWidth * PADDING * 2);
    const totalWidth = widths.reduce((total, width) => total + width);
    if(x == 'left') x = MARGIN;
    else if(x == 'right') x = PAGE_WIDTH - MARGIN - totalWidth;
    else if(x == 'center') x = (PAGE_WIDTH - MARGIN * 2 - totalWidth) / 2;
    texts.forEach((text, i) => {
      text = expand_text(text);
      page.drawText(text, { font: font, size: FONT_SIZE, x: x, y: y - fontHeight / 3 });
      page.drawRectangle({
        x: x - unitFontWidth * PADDING,
        y: y - fontHeight * 1.2 / 3 - fontHeight * PADDING,
        width: font.widthOfTextAtSize(text, FONT_SIZE) + unitFontWidth * PADDING * 2,
        height: fontHeight * (2 / 3 + PADDING * 2),
        borderColor: PDFLib.rgb(0, 0, 0),
        borderWidth: THICKNESS
      });
      x += widths[i];
    });
  };

  //ページ作成
  pages = Array.from(Array(Number(location.search.slice(1) || 1))).map(i => false);
  pages.push(true);
  pages.forEach(answer => {

    const page = pdf.addPage([PAGE_WIDTH, PAGE_HEIGHT]);

    let y = MARGIN + (lineHeight - fontHeight) / 2;

    //ヘッダ描画
    drawCell(page, title + (answer ? '解答' : ''), 'left', Y(y));
    drawCell(page, ['氏名', 6, '日時', 6, '試験時間', 6, '点数', 6], 'right', Y(y));

    y += lineHeight * 2;

    //計算式描画
    range(0, COLUMNS).forEach(column => {
      const rows = Math.ceil(questions.length / COLUMNS);
      range(0, rows).forEach(row => {
        const question = questions[column * rows + row];
        page.drawText(answer ? question[0] + question[1] : question[0], { font: font, size: FONT_SIZE, x: MARGIN + column * COLUMN_WIDTH, y: Y(y + row * lineHeight + fontHeight / 3) });
      });
    });
  });

  //表示
  location.href = URL.createObjectURL(new Blob([await pdf.save()], { type: 'application/pdf' }));
})();
</script>

</body>

</html>

image.png

以下のように作成しました。

  • 記入用ページと解答ページを出力します。記入用ページは ?5 のようにパラメータで数を指定するとページが増えるようにしています。これで例えば5人分印刷する際に印刷設定をかえなくてもそのまま印刷できます。
  • 各問題はランダムで加算、減算、乗算、除算が出力されます。
  • 加算は左辺 0~50、右辺 0~50 のランダムで式が生成されます。
  • 減算は左辺 0~99、右辺 0~99 のランダムで答えは0以上になる式が生成されます。
  • 乗算は左辺 1~10、右辺 1~10 のランダムで同じ式が出ないよう生成されます。
  • 除算は左辺 1~100、右辺 1~10 のランダムで余りなしで同じ式が出ないよう生成されます。

さいごに

デモページ

中1, 小5, 小2と父、母で最初は5分間でやって100点が取れれば1分ずつ短縮していく感じでやっています。

6
7
1

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
6
7