1. 背景
- A4サイズのPDFをプリンターで設定せずにポスター印刷できるようにしたかった。
- そこで、「A4サイズのPDFを4分割し、4枚の個別のページに分けて再生成してくれる」ウェブアプリを作ることにした。
- 使用技術はhtml, css, React, PDF-LIB
- PDF-LIBは使い方が全くわからなかったので、ChatGPTをフル活用した。実装中は仕組みを理解せず進めたので、本記事の執筆を通してそこを咀嚼していきたい。
- なお、PDF-LIBを用いたPDF加工部分のみに焦点を絞り、Reactでのstateやハンドラーの管理部分には触れない。
2. PDF-LIB
各部分の構成と役割の分析
PDFを加工する部分については、個別にjsファイルを作成し、関数として記述した。それをエクスポートし、利用した。単に監理のしやすさの問題で、別にコンポーネントの中で直接記述しても問題ない。以下、ソースコードの各部分とその役割を分けてみていく。
(1) PDF-LIBはライブラリのため、このようにインポートして使用する。
import { PDFDocument } from 'pdf-lib';
(2) 以下はPDF-LIBでファイル加工する際の定型文と思って良さそうだ。PDFを分割加工する処理をsplitPdfInfoFourという定数に入れて扱えるようにしている。以降登場する関数やオブジェクト、プロパティは基本的にPDF-LIB由来のものか、ここで定義されているものなので、定型的に使うものと思っておけば良い。
const splitPdfIntoFour = async (file) => {
const fileReader = new FileReader(); //ファイルの読み込み
fileReader.readAsArrayBuffer(file);
return new Promise(async (resolve, reject) => {
fileReader.onload = async () => {
try {
const pdfDoc = await PDFDocument.load(fileReader.result); //上で読み込んだファイルをPDFとしてロード
(3) ここからケースに応じて必要な処理が変わるようだ。ここでは、PDfの加工にあたって、1ページ目(といっても、基本1ページしか基本)をコピーしている。
//上の続き
const [sourcePage] = await pdfDoc.copyPages(pdfDoc, [0]); // 1ページ目をコピー
(4) 続いて、A4サイズのページを4分割にするために、サイズと位置を設定している。
//上の続き
const { width, height } = sourcePage.getSize(); //分割するために、元のページの縦横を取得
const splitWidth = width / 2; //4分割にあたって、横を半分に
const splitHeight = height / 2; //同じく、縦を半分に
(5) 新しいPDFデータを作成する
// 上の続き
const newPdf = await PDFDocument.create();
(6) PDFを4分割して、4ページに分けたいということで、4枚のページを生成している。
for文のネストを用いており、直感的には変なやり方に見えるが、これは分割したそれぞれの箇所をコピーするための仕組みの一部になっている(後述)。
// 上の続き
for (let y = 0; y < 2; y++) {
for (let x = 0; x < 2; x++) {
const newPage = newPdf.addPage([splitWidth, splitHeight]);
(7) 3でコピーした元ページsourcePageから、特定の領域を切り取って、6で作った新しいページに描画していく。ここのキモは、変数xとyをfor文でうまくコントロールし、それを座標として使うことで、切り取りの範囲指定に生かしていることだ。また、for文で処理することで、1ページづつ順番にコピー→貼り付けができている。
// コピーしたページから特定の領域を切り取って新しいページに描画
const embeddedPage = await newPdf.embedPage(sourcePage);
newPage.drawPage(embeddedPage, {
x: -x * splitWidth,
y: -y * splitHeight,
width: width,
height: height,
});
}
}
最後に、加工したPDFをバイナリ形式でエクスポートして終了。ここも定型文と思って良いのでは。
// 上の続き
const pdfBytes = await newPdf.save();
resolve(pdfBytes);
} catch (error) {
reject(error);
}
};
fileReader.onerror = reject;
});
};
最後に、一連の関数をエクスポートし、他のjsで使用できるようにしておく。
export default splitPdfIntoFour;
スタックしたポイント
embedPageに関するエラーで完全に詰まった。
これはdrawPageメソッドの引数を正しく指定できていなかったことが問題。
動かなかったコードと問題の箇所
Const newPdf = await PDFDocument.create();
for (let y =0; y < 2; y++) {
for (let x = 0; x < 2; x++) {
const newPage = newPdf.addPage([splitwidth, splitHeight]); //ここで本来は'embedPage'メソッドでもとのページのデータを取り込まないといけない
newPage.drawPage(page, { //そして、ここでそれを描画しないといけない
x: -x * splitWidth,
y: -y * splitHeight,
width: width,
height: height,
});
}
}
コメントに付してある通り、まずembedPageメソッドで大元のデータを新しいPDFのデータに取り込み、それをdrawPageメソッドで描画するという手順を踏まなくてはならなかった。上記コードを実際に実行すると、embeddedPagesがないというエラーが表示され、PDFファイルは生成されない。
最後に
今後PDF-LIBのメソッドと用法を整理して行きたい