1. Perl
  2. Form creation
  3. here

Invoice creation - PDF::API2 form creation

I tried to create an invoice with PDF::API2. It can be used as a basic template for creating full-scale forms such as quotations, application forms, receipts, and service usage details.

  • A4 paper size
  • Japanese support
  • Bold support
  • Background color support
  • Font size support
  • Underline support
  • Display table
  • Table even coloring
  • Unit price, quantity, price
  • Total price
  • Square for stamping stamps
  • Insert company logo
  • Amount digit separator
  • 25 items

You can download a PDF example of the invoice you created here.

Invoice PDF example

PDF::Invoice source code written in API2

This is the source code of the invoice written in PDF::API2.

Caution

Place the company logo in the same directory with the name "logo.png".

The Japanese font uses the Aozora Mincho font introduced in Drawing text. Place the normal font "AozoraMinchoRegular.ttf" and the bold font "AozoraMincho-bold.ttf" in the same directory.

The layout algorithm has a table structure. You are advancing the line by specifying one vertical width. The column is divided into 100 and is displayed at the position showing the ratio.

PDF::API2 invoice source code

use strict;
use warnings;
use utf8;
use FindBin;
use PDF::API2;

# Product data (book)
my $books = [
  {
    name =>'Book 1',
    unit_price => 1000,
    count => 3,
  },
  {
    name =>'Book 2',
    unit_price => 2000,
    count => 6,
  },
  {
    name =>'Book 3',
    unit_price => 1500,
    count => 5,
  }
];;

# Amount subtotal
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);

# consumption tax
my $tax_rate = 0.1;
my $tax = int($price_total_no_tax * $tax_rate);
my $tax_disp = price_disp ($tax);

# Tax included
my $price_total = $price_total_no_tax + $tax;
my $price_total_disp = price_disp ($price_total);

# PDF
my $pdf = PDF::API2->new;

# Paper size A4 setting
$pdf->mediabox('A4');

# Get paper size
my @page_size_infos = $pdf->mediabox;
my $page_width = $page_size_infos[2];
my $page_height = $page_size_infos[3];

# Page margin
my $page_top_margin = 48;
my $page_bottom_margin = 48;
my $page_left_margin = 49;
my $page_right_margin = 49;

# Loading TrueType fonts that support Japanese-normal fonts and bold fonts
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);

# Font size-default
my $font_size_default = 10;

# Start drawing x coordinate
my $render_start_x = $page_left_margin;

# Start drawing y coordinate
my $render_start_y = $page_height - 1- $page_top_margin;

# End of drawing x coordinate
my $render_end_x = $page_width - 1- $page_right_margin;

# Drawing end y coordinate
my $render_end_y = $page_bottom_margin;

# The layout has a table structure
# Set the minimum unit of height and width, the minimum unit of width is divided into 100
my $unit_height = 14;
my $unit_width = ($render_end_x-$render_start_x)/100;

# Text drawing padding
my $text_bottom_padding = 3;
my $text_left_padding = 3;

# Width of recipient company column
my $receive_company_end_tds_count = 55;

# Start position of billing company column
my $send_company_start_tds_count = 66.5;

# Color list
my $color_black = '# 000';

# Line width
my $line_width_basic = 0.3;
my $line_width_bold = 2;

# Page
my $page = $pdf->page;

# Graphic drawing
my $gfx = $page->gfx;

# Text drawing
my $text = $page->text;

# Current line
my $cur_row = 0;

# Top thick line
$gfx->move($render_start_x, $render_start_y);
$gfx->hline($render_end_x);
$gfx->linewidth(10);
$gfx->stroke;
$cur_row += 1.5;

# Application date
$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('October 14, 2019');
$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;

# Save logo start position
my $cur_logo_row = $cur_row - 1.5;

# Invoice title
$text->translate($render_start_x, $render_start_y-$unit_height * $cur_row + $text_bottom_padding);
$text->font($font_bold, 20);
$text->text('invoice');
$cur_row += 3;

# Save the start position in the right column
my $cur_right_row = $cur_row + 1;

# Delivery company name
my $receive_company_name = 'XXXXX Co., Ltd.';
$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('middle');
$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;

# subject
my $subject_label = 'Subject:';
$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 = 'March 3, 2019 Merchandise Sales Contract';
$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;

# Delivery date
my $delivery_date_label = 'Delivery date:';
$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 = 'March 30, 2019';
$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;

# Billing message
$text->translate($render_start_x, $render_start_y-$unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text('We will charge you as below.');
$cur_row += 2;

# Amount of money
my $price_total_top_label = 'amount';
$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} Yen";
$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 = '(tax included)';
$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;

# Insert company logo
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,
);

# Pretender
my $sender_company_name = 'Perl Club';
$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;

# Postal code
my $sender_zip_code = 'Address: 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;

# address
my $sender_address = 'Minato-ku, Tokyo 〇〇 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;

# telephone number
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;

# responsible person
my $sender_staff = 'Responsible: Taro Tanaka';
$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);

# Seal stamp square
my $slal_width = 55;
$gfx->rectxy(
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_width * 2,
  $render_start_y - $unit_height * ($cur_right_row - 1) + $slal_width,
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_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 + $slal_width * 3,
  $render_start_y-$unit_height * $cur_right_row-$slal_width
);
$gfx->poly(
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_width,
  $render_start_y-$unit_height * $cur_right_row,
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_width,
  $render_start_y-$unit_height * $cur_right_row-$slal_width
);
$gfx->poly(
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_width * 2,
  $render_start_y-$unit_height * $cur_right_row,
  $render_start_x + $send_company_start_tds_count * $unit_width + $slal_width * 2,
  $render_start_y-$unit_height * $cur_right_row-$slal_width
);
$gfx->strokecolor('# bbb');
$gfx->stroke;

# Quotation heading background
$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;

# Quotation heading top decoration thick line
$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;

# Save the position of the thick line under the quotation heading
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;

# Item
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('item');
$cur_column_units_count += $name_units_count;

# quantity
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('quantity');
$cur_column_units_count += $count_units_count;

# unit price
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('unit price');
$cur_column_units_count += $unit_price_units_count;

# Amount of money
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('amount');
$cur_column_units_count = 0;

$cur_row ++;

my $rows_count = 25;

# Drawing the frame of each item
for (my $row = 0; $row < $rows_count; $row ++) {
  my $book = $books->[$row];
  
  # Lines are painted alternately
  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;

  # Item
  $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;

  # quantity
  $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_defa)ult);
    $text->text_right($book->{count});
  }
  $cur_column_units_count += $count_units_count;

  # unit price
  $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;

  # Amount of money
  $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 ++;
}

# Quotation heading under decoration thick line (draw at this position to write on the gray vertical line)
$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;

# Thick line
$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;

# Subtotal
my $price_total_no_tax_label = 'subtotal';
$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;

# consumption tax
my $tax_label = 'consumption tax';
$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;

# consumption tax
my $price_total_label = 'total including tax';
$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;

# Thick line
$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;

# Payee
my $furikomi_disp = << "EOS";
Transfer destination
Mizuho Bank Shiba Branch
Ordinary account 34521xx
Perl club
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;
}

# Thin line
$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;

# remarks
my $bikou_disp = << "EOS";
Please pay by the last day of the month following the delivery date.
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;
}

# Thick line
$gfx->move($render_start_x, $render_end_y);
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;

# Subroutine that displays the amount separated by 3 digits
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);

Related Informatrion