見出し画像

JavaScriptで年賀状を書いてみよう

この記事はjig.jp Advent Calendar 2023の12月4日(月)の記事です。


はじめに

jig.jp Advent Calendar 2023の12/4(月)を担当する山地です。よろしくお願いします。
今回はPDF-LIBというJavaScriptのライブラリをDenoから利用して、プログラマブルなはがき作成ツールを開発してみます。
近頃は年賀状を書くことも減っているようですが、元旦に郵便受けを確認して友達や家族からの年賀状に嬉しくなる経験はそこそこ代えがたいものです。ですよね?
ですが楽できるところは楽したいのがエンジニアの性というもの。ここは宛名書きと定型文までをプログラムとプリンターにやってもらって、年賀状を書くハードルを下げることにしましょう。

QuickStartしておく

今回はDenoを使います。インストール方法はこちらの記事を御覧ください
PDF-LIBのサイトにQuickStartがあるのでやってみます。

$ deno run --allow-write https://pdf-lib.js.org/deno/quick_start.ts

この実行するファイルの内容は↓のようになっています。

import { PDFDocument } from 'https://cdn.pika.dev/pdf-lib@^1.7.0';

const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([350, 400]);
page.moveTo(110, 200);
page.drawText('Hello World!');
const pdfBytes = await pdfDoc.save();
await Deno.writeFile('./out.pdf', pdfBytes);
console.log('PDF file written to ./out.pdf');
console.log();
console.log(
  'See https://github.com/Hopding/pdf-lib#deno-usage for more Deno examples!',
);

出来上がったものが↓になります。

Hello World!

サンプルコード代わりのQuickStartを見れたおかげでたいぶやっていることがわかりました。コード中にあるリンクはGitHubの「Deno Usage」を指しているのでここも一緒に見つつやっていきます。

ハガキのPDFを作る

1. はがきサイズのページを作る

では実際にハガキのPDFを作ってみます。まずは宛名側。
https://github.com/Hopding/pdf-lib/blob/master/src/api/sizes.ts などから逆算すると、PDF-LIB内はpt(ポイント)を単位としているようでした。
ミリメートルはインチになおすのに 25.4で割って、その後72を掛ければptに単位変換できるはずです。
↓のような関数を用意しておけば便利そうですね。

const calcPt = (mm: number) => (mm / 25.4) * 72;

pdfDoc.addPage([calcPt(100), calcPt(148)]); としてページを作ってみましたが、それっぽいサイズ感です。

はがきっぽいサイズになった様子

「郵便はがき」と郵便番号を書く

はがきの書式については「内国郵便約款」の第3節に第二種郵便物として記載があり、これによると上端中央に「郵便はがき」もしくは「POSTCARD」の記述が必要だそうです。
あとの記述は一般的なはがきの作法に習います。縦書きの場合は左上に切手を貼付し、右上に郵便番号があり、その下に送り先住所、宛名、送り主住所、送り主名前を縦書きで書いてあることが多いですね。
今回は↑の形で書くことにします。

試しに郵便番号を表示してみましょう。

import { PDFDocument } from "https://cdn.skypack.dev/pdf-lib@^1.7.0";

const calcPt = (mm: number) => (mm / 25.4) * 72;

const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([calcPt(100), calcPt(148)]);
page.moveTo(calcPt(30), calcPt(132));
page.drawText('〒123-4567');
const pdfBytes = await pdfDoc.save();
await Deno.writeFile('./out.pdf', pdfBytes);
console.log('PDF file written to ./out.pdf');

としてみると、エラーが発生しました。

error: Uncaught Error: WinAnsi cannot encode "〒" (0x3012)
        throw new Error(msg);
              ^
    at Encoding2.encodeUnicodeCodePoint (https://cdn.skypack.dev/-/@pdf-lib/standard-fonts@v1.0.0-Z8Dxw2SAoP39vRoTIKF1/dist=es2019,mode=imports/optimized/@pdf-lib/standard-fonts.js:146:15)
    at StandardFontEmbedder2.encodeTextAsGlyphs (https://cdn.skypack.dev/-/pdf-lib@v1.17.1-P8YmSI9gov9vMnrV7v2f/dist=es2019,mode=imports/optimized/pdf-lib.js:3258:35)
    at StandardFontEmbedder2.encodeText (https://cdn.skypack.dev/-/pdf-lib@v1.17.1-P8YmSI9gov9vMnrV7v2f/dist=es2019,mode=imports/optimized/pdf-lib.js:3198:23)
    at PDFFont2.encodeText (https://cdn.skypack.dev/-/pdf-lib@v1.17.1-P8YmSI9gov9vMnrV7v2f/dist=es2019,mode=imports/optimized/pdf-lib.js:9852:26)
    at PDFPage2.drawText (https://cdn.skypack.dev/-/pdf-lib@v1.17.1-P8YmSI9gov9vMnrV7v2f/dist=es2019,mode=imports/optimized/pdf-lib.js:12523:35)
    at file:///Users/yamaji/Documents/GitHub/deno-hagaki/gen-hagaki.deno.ts:8:6

フォントがないことでレンダリングできずエラーになっているようなので、Embedding a Font with Deno
を参考にフォントを読み込みます。
ここではGoogle Fontsを利用させてもらいました。M PLUS Rounded 1cがいい感じなので、こちらをダウンロードしてローカルに配置し、読み込みます。この対応でうまく表示されるようになりました。他の日本語も問題ないか確認するために「郵便はがき」の表示も追加して確認しました。

import { PDFDocument } from "https://cdn.skypack.dev/pdf-lib@^1.7.0";
import fontkit from 'https://cdn.skypack.dev/@pdf-lib/fontkit@^1.0.0?dts';

const calcPt = (mm: number) => (mm / 25.4) * 72;

const fontFilePath = 'M_PLUS_Rounded_1c/MPLUSRounded1c-Regular.ttf'
const fontBytes = await Deno.readFile(fontFilePath);

const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);

const page = pdfDoc.addPage([calcPt(100), calcPt(148)]);
const { width, height } = page.getSize();
page.drawText('郵便はがき', {
  font: customFont,
  x: (width / 2) - (customFont.widthOfTextAtSize('郵便はがき', 10.5) / 2),
  y: calcPt(142),
  size: 10.5
})
page.drawText('〒123-4567', {
  font: customFont,
  x: calcPt(47),
  y: calcPt(132)
});
const pdfBytes = await pdfDoc.save();
await Deno.writeFile('./out.pdf', pdfBytes);
console.log('PDF file written to ./out.pdf');
「郵便はがき」と郵便番号

縦書きで住所等を書く

PDF-LIBには水平方向の記述方向指定はありますが、どうやら縦書きの指定はないようでした。
なので↓のような関数を用意して縦書きを簡単にできるようにします。

const drawVertical = (page: any, text: string, font: any, x: number, y: number, size: number) => {
  let textHeight = text.length * font.heightAtSize(size)
  for(let i = 0; i < text.length; i ++) {
    page.drawText(text.at(i), {
      font: customFont,
      x: x,
      y: y + textHeight - i * customFont.heightAtSize(size),
      size: size
    })
  }
}

drawVertical(page, '送り先住所', customFont, calcPt(87), calcPt(57), 24) のように書くことで↓のように縦書きで表示できるようにできました。

住所表示

宛名側完成

後は宛名、送り主住所、送り主名、送り主郵便番号を書き足して完成です。それっぽい!

それっぽい!
import { PDFDocument, degrees } from "https://cdn.skypack.dev/pdf-lib@^1.7.0";
import fontkit from 'https://cdn.skypack.dev/@pdf-lib/fontkit@^1.0.0?dts';

const calcPt = (mm: number) => (mm / 25.4) * 72;
const drawVertical = (page: any, text: string, font: any, x: number, y: number, size: number) => {
  let textHeight = text.length * font.heightAtSize(size)
  for(let i = 0; i < text.length; i ++) {
    page.drawText(text.at(i), {
      font: customFont,
      x: x,
      y: y + textHeight - i * customFont.heightAtSize(size),
      size: size
    })
  }
}

const fontFilePath = 'M_PLUS_Rounded_1c/MPLUSRounded1c-Regular.ttf'
const fontBytes = await Deno.readFile(fontFilePath);

const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);

const atena = pdfDoc.addPage([calcPt(100), calcPt(148)]);
const { width, height } = atena.getSize();
atena.drawText('郵便はがき', {
  font: customFont,
  x: (width / 2) - (customFont.widthOfTextAtSize('郵便はがき', 10.5) / 2),
  y: calcPt(142),
  size: 10.5
})
atena.drawText('〒123-4567', {
  font: customFont,
  x: calcPt(47),
  y: calcPt(132),
  size: 24
});

drawVertical(atena, '送り先住所', customFont, calcPt(77), calcPt(57), 24)
drawVertical(atena, '宛名 太郎 様', customFont, (width / 2) - (customFont.widthOfTextAtSize('宛', 30) / 2) + calcPt(5), calcPt(10), 30)
drawVertical(atena, '送り主住所', customFont, calcPt(20), calcPt(5), 18)
drawVertical(atena, '送り主名', customFont, calcPt(5), calcPt(5), 18)

atena.drawText('〒765-4321', {
  font: customFont,
  x: calcPt(4),
  y: calcPt(4),
  size: 12
})

const pdfBytes = await pdfDoc.save();
await Deno.writeFile('./out.pdf', pdfBytes);
console.log('PDF file written to ./out.pdf');

文面側を作る

文面側(本文を書く側)をつくります。
まずは文面側のページを pdfDoc.addPage で用意します。
文面には「謹賀新年」と「今年もよろしくお願いします」、あと写真なんかを入れたいですね。

写真はdrawImageで表示できます。今回は自分がよく使ってるアイコンを入れてみます。

const iconPicture = await Deno.readFile('icon.png')
const iconImage = await pdfDoc.embedPng(iconPicture)
const iconDims = iconImage.scale(0.2)
bunmen.drawImage(iconImage, {
  x: calcPt(10),
  y: calcPt(5),
  width: iconDims.width,
  height: iconDims.height
})
文面側

これでひとまず年賀状のテンプレートができましたね。あとはcsvなどから宛名等を流し込めるようにするだけです。
が、疲れたのとまだ年賀状の締め切りまでは時間があるので今回はここまでとします。

まとめ

今回は「JavaScriptで年賀状を書いてみよう」ということで、JavaScriptを使って葉書のテンプレートを作り、年賀状を書くハードルを下げる試みを行いました。かなり楽に書けそうですよね?

完成図

ちなみにですが、年賀状の受付期間は12/15(金)〜12/25(月)までだそうですので、皆様出し忘れなどないようお気をつけくださいね。

今回のソースはGitHubにあります。これをベースに色々試してみてもらえると嬉しいです!

この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?