請求書の作成 - PDF::API2で帳票作成
PDF::API2で請求書を作成してみました。見積書、申込書、レシート、サービス利用明細など、本格的な帳票の作成の基本的なひな型として利用できます。
- A4用紙サイズ
- 日本語対応
- 太字対応
- 背景色対応
- フォントサイズ対応
- 下線対応
- 表の表示
- 表の偶奇カラーリング
- 単価、数量、価格
- 価格合計
- 印鑑押印用の四角
- 会社ロゴの挿入
- 金額桁区切り
- 25項目
作成した請求書のPDFのサンプルは、こちらからダウンロードできます。
PDF::API2で書いた請求書のソースコード
PDF::API2で書いた請求書のソースコードです。
注意点
会社ロゴは、同一ディレクトリに「logo.png」という名前で、配置してください。
日本語用フォントは、テキストを描画するで紹介した、あおぞら明朝フォントを使っています。通常フォント「AozoraMinchoRegular.ttf」と太字フォント「AozoraMincho-bold.ttf」を同一ディレクトリに配置してください。
レイアウトのアルゴリズムは、表の構造になっています。1つの縦幅を指定して、行を進めています。列は、100分割されていて、その割合を示した位置に表示しています。
PDF::API2で書いた請求書のソースコード
use strict; use warnings; use utf8; use FindBin; use PDF::API2; # 商品データ(本) my $books = [ { name => '本1', unit_price => 1000, count => 3, }, { name => '本2', unit_price => 2000, count => 6, }, { name => '本3', unit_price => 1500, count => 5, } ]; # 金額小計 my $price_total_no_tax = 0; for my $book (@$books) { $price_total_no_tax += $book->{unit_price} * $book->{count}; } my $price_total_no_tax_disp = price_disp($price_total_no_tax); # 消費税 my $tax_rate = 0.1; my $tax = int($price_total_no_tax * $tax_rate); my $tax_disp = price_disp($tax); # 税込金額 my $price_total = $price_total_no_tax + $tax; my $price_total_disp = price_disp($price_total); # PDF my $pdf = PDF::API2->new; # 用紙サイズ A4設定 $pdf->mediabox('A4'); # 用紙サイズの取得 my @page_size_infos = $pdf->mediabox; my $page_width = $page_size_infos[2]; my $page_height = $page_size_infos[3]; # ページ余白 my $page_top_margin = 48; my $page_bottom_margin = 48; my $page_left_margin = 49; my $page_right_margin = 49; # 日本語に対応したTrueTypeフォントの読み込み - 通常フォントと太字フォント my $true_type_font_file = "$FindBin::Bin/AozoraMinchoRegular.ttf"; my $font = $pdf->ttfont($true_type_font_file); my $true_type_font_bold_file = "$FindBin::Bin/AozoraMincho-bold.ttf"; my $font_bold = $pdf->ttfont($true_type_font_bold_file); # フォントサイズ - デフォルト my $font_size_default = 10; # 描画開始x座標 my $render_start_x = $page_left_margin; # 描画開始y座標 my $render_start_y = $page_height - 1 - $page_top_margin; # 描画終了x座標 my $render_end_x = $page_width - 1 - $page_right_margin; # 描画終了y座標 my $render_end_y = $page_bottom_margin; # レイアウトは、テーブル構造になっていて # 縦幅と横幅の最小単位を設定、横幅は、100分割したものが最小単位 my $unit_height = 14; my $unit_width = ($render_end_x - $render_start_x) / 100; # テキスト描画のパディング my $text_bottom_padding = 3; my $text_left_padding = 3; # 受取企業欄の幅 my $receive_company_end_tds_count = 55; # 請求企業欄の開始位置 my $send_company_start_tds_count = 66.5; # 色の一覧 my $color_black = '#000'; # 線の幅 my $line_width_basic = 0.3; my $line_width_bold = 2; # ページ my $page = $pdf->page; # グラフィック描画 my $gfx = $page->gfx; # テキスト描画 my $text = $page->text; # 現在の行 my $cur_row = 0; # 一番上太線 $gfx->move($render_start_x, $render_start_y); $gfx->hline($render_end_x); $gfx->linewidth(10); $gfx->stroke; $cur_row += 1.5; # 申し込み日付 $text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->strokecolor($color_black); $text->fillcolor($color_black); $text->text_right('2019年10月14日'); $gfx->move($render_start_x + $send_company_start_tds_count * $unit_width); $gfx->hline($render_end_x); $gfx->linewidth($line_width_basic); $gfx->stroke; $cur_row += 2; # ロゴの開始位置を保存 my $cur_logo_row = $cur_row - 1.5; # 請求書タイトル $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font_bold, 20); $text->text('請求書'); $cur_row += 3; # 右列の開始位置を保存 my $cur_right_row = $cur_row + 1; # 納品先企業名 my $receive_company_name = 'XXXXX株式会社'; $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($receive_company_name); $text->translate( $render_start_x + $receive_company_end_tds_count * $unit_width, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->text_right('御中'); $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row); $gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width); $gfx->linewidth($line_width_basic); $gfx->stroke; $cur_row += 4; # 件名 my $subject_label = '件 名:'; $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($subject_label); my $subject = '2019年3月3日の商品売買契約'; $text->translate( $render_start_x + 10 * $unit_width, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($subject); $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row); $gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width); $gfx->linewidth($line_width_basic); $gfx->stroke; $cur_row += 1; # 納品日 my $delivery_date_label = '納品日:'; $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($delivery_date_label); my $delivery_date = '2019年3月30日'; $text->translate( $render_start_x + 10 * $unit_width, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($delivery_date); $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row); $gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width); $gfx->linewidth($line_width_basic); $gfx->stroke; $cur_row += 2.8; # 請求メッセージ $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text('下記の通り、ご請求申し上げます。'); $cur_row += 2; # 金額 my $price_total_top_label = '金額'; $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($price_total_top_label); my $price_total_top = "¥${price_total_disp}円"; $text->translate( $render_start_x + 48 * $unit_width, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default + 5); $text->text_right($price_total_top); my $price_total_top_zeikomi = '(税込)'; $text->translate( $render_start_x + 50 * $unit_width, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($price_total_top_zeikomi); $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row); $gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width); $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row - 2); $gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width); $gfx->linewidth($line_width_basic); $gfx->stroke; $cur_row += 0.5; # 会社ロゴ挿入 my $logo_image_file = "$FindBin::Bin/logo.png"; my $logo_image_object = $pdf->image_png($logo_image_file); my $logo_image_width = 60; $gfx->image( $logo_image_object, $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_logo_row - $logo_image_width, $logo_image_width, $logo_image_width, ); # 請求者 my $sender_company_name = 'Perlクラブ'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_company_name); $cur_right_row += 1; # 郵便番号 my $sender_zip_code = '住所:123-4567'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_zip_code); $cur_right_row += 1; # 住所 my $sender_address = '東京都港区〇〇 123-4'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_address); $cur_right_row += 1; # 電話番号 my $sender_tel = 'TEL:090-1234-5678'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_tel); $cur_right_row += 1; # FAX my $sender_fax = 'FAX:090-1234-5679'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_fax); $cur_right_row += 1; # 担当 my $sender_staff = '担当:田中太郎'; $text->translate( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($sender_staff); # 印鑑押印四角形 my $sleal_width = 55; $gfx->rectxy( $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2, $render_start_y - $unit_height * ($cur_right_row - 1) + $sleal_width, $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 3, $render_start_y - $unit_height * ($cur_right_row - 1) ); $gfx->rectxy( $render_start_x + $send_company_start_tds_count * $unit_width, $render_start_y - $unit_height * $cur_right_row , $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 3, $render_start_y - $unit_height * $cur_right_row - $sleal_width ); $gfx->poly( $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width, $render_start_y - $unit_height * $cur_right_row , $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width, $render_start_y - $unit_height * $cur_right_row - $sleal_width ); $gfx->poly( $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2, $render_start_y - $unit_height * $cur_right_row , $render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2, $render_start_y - $unit_height * $cur_right_row - $sleal_width ); $gfx->strokecolor('#bbb'); $gfx->stroke; # 見積書見出し背景 $gfx->rectxy( $render_start_x, $render_start_y - $unit_height * $cur_row, $render_end_x, $render_start_y - $unit_height * ($cur_row + 1) ); $gfx->fillcolor('#eee'); $gfx->fill; # 見積書見出し上飾り太線 $gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row); $gfx->hline($render_end_x); $gfx->linewidth($line_width_bold); $gfx->strokecolor($color_black); $gfx->stroke; $cur_row += 1; # 見積書見出し下飾り太線の位置を保存 my $header_bottom_line_row = $cur_row; my $cur_column_units_count = 0; # No my $no_units_count = 10; $text->translate($render_start_x + $cur_column_units_count * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text('No.'); $cur_column_units_count += $no_units_count; # 項目 my $name_units_count = 34; $text->translate($render_start_x + $cur_column_units_count * $unit_width + ($name_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_center('項目'); $cur_column_units_count += $name_units_count; # 数量 my $count_units_count = 14; $text->translate($render_start_x + $cur_column_units_count * $unit_width + ($count_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_center('数量'); $cur_column_units_count += $count_units_count; # 単価 my $unit_price_units_count = 14; $text->translate($render_start_x + $cur_column_units_count * $unit_width + ($unit_price_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_center('単価'); $cur_column_units_count += $unit_price_units_count; # 金額 my $price_units_count = 28; $text->translate($render_start_x + $cur_column_units_count * $unit_width + ($price_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_center('金額'); $cur_column_units_count = 0; $cur_row++; my $rows_count = 25; # 各項目の枠の描画 for (my $row = 0; $row < $rows_count; $row++) { my $book = $books->[$row]; # 行を交互に塗り分ける if ($row % 2 == 1) { $gfx->rectxy( $render_start_x, $render_start_y - $unit_height * $cur_row, $render_end_x, $render_start_y - $unit_height * ($cur_row - 1), ); $gfx->fillcolor('#eee'); $gfx->fill; } # No if ($book) { $text->translate( $render_start_x + $cur_column_units_count * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($row + 1); } $cur_column_units_count += $no_units_count; # 項目 $gfx->poly( $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * $cur_row, $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * ($cur_row - 1) ); $gfx->linewidth(1.5); $gfx->strokecolor('#ccc'); $gfx->stroke; if ($book) { $text->translate( $render_start_x + $cur_column_units_count * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text($book->{name}); } $cur_column_units_count += $name_units_count; # 数量 $gfx->poly( $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * $cur_row, $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * ($cur_row - 1) ); $gfx->linewidth(1.5); $gfx->strokecolor('#ccc'); $gfx->stroke; if ($book) { $text->translate( $render_start_x + $cur_column_units_count * $unit_width + ($count_units_count * $unit_width) - $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); $text->text_right($book->{count}); } $cur_column_units_count += $count_units_count; # 単価 $gfx->poly( $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * $cur_row, $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * ($cur_row - 1) ); $gfx->linewidth(1.5); $gfx->strokecolor('#ccc'); $gfx->stroke; if ($book) { $text->translate( $render_start_x + $cur_column_units_count * $unit_width + ($unit_price_units_count * $unit_width) - $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); my $unit_price_disp = price_disp($book->{unit_price}); $text->text_right($unit_price_disp); } $cur_column_units_count += $unit_price_units_count; # 金額 $gfx->poly( $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * $cur_row, $render_start_x + $cur_column_units_count * $unit_width, $render_start_y - $unit_height * ($cur_row - 1) ); $gfx->linewidth(1.5); $gfx->strokecolor('#ccc'); $gfx->stroke; if ($book) { $text->translate( $render_start_x + $cur_column_units_count * $unit_width + ($price_units_count * $unit_width) - $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding ); $text->font($font, $font_size_default); my $price_disp = price_disp($book->{unit_price} * $book->{count}); $text->text_right($price_disp); } $cur_column_units_count = 0; $cur_row++; } # 見積書見出し下飾り太線(灰色の縦線の上に書くためこの位置で描画) $gfx->move($render_start_x, $render_start_y - $unit_height * $header_bottom_line_row); $gfx->hline($render_end_x); $gfx->linewidth($line_width_bold); $gfx->strokecolor($color_black); $gfx->stroke; # 太線 $gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1)); $gfx->hline($render_end_x); $gfx->linewidth($line_width_bold); $gfx->strokecolor($color_black); $gfx->stroke; # 小計 my $price_total_no_tax_label = '小計'; $text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($price_total_no_tax_label); $text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_right($price_total_no_tax_disp); $cur_row += 1; # 消費税 my $tax_label = '消費税'; $text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($tax_label); $text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_right($tax_disp); $cur_row += 1; # 消費税 my $price_total_label = '税込合計'; $text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($price_total_label); $text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text_right("¥$price_total_disp"); $cur_row += 1; # 太線 $gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1)); $gfx->hline($render_end_x); $gfx->linewidth($line_width_bold); $gfx->strokecolor($color_black); $gfx->stroke; # 振込先 my $furikomi_disp = <<"EOS"; お振込先 みずほ銀行芝支店 普通口座 34521xx Perlクラブ EOS for my $line (split /\n/, $furikomi_disp) { $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($line); $cur_row += 1; } # 細い線 $gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1)); $gfx->hline($render_end_x); $gfx->linewidth($line_width_basic); $gfx->strokecolor($color_black); $gfx->stroke; # 備考 my $bikou_disp = <<"EOS"; 納品日の翌月の末日までのお支払いをお願いいたします。 EOS for my $line (split /\n/, $bikou_disp) { $text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding); $text->font($font, $font_size_default); $text->text($line); $cur_row += 1; } # 太線 $gfx->move($render_start_x, $render_end_y); $gfx->hline($render_end_x); $gfx->linewidth($line_width_bold); $gfx->strokecolor($color_black); $gfx->stroke; # 3桁区切りで金額を表示してくれるサブルーチン sub price_disp { my ($price) = @_; 1 while $price =~ s/(.*\d)(\d\d\d)/$1,$2/; return $price; } my $pdf_file = 'invoice.pdf'; $pdf->saveas($pdf_file);