- Perl ›
- Form creation ›
- 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.
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);