build($invoice); } private function build($invoice) { $this->currency = strtoupper($this->clean($invoice['currency'] ?? 'INR')) ?: 'INR'; $customer = is_array($invoice['customer'] ?? null) ? $invoice['customer'] : array(); $business = is_array($invoice['business'] ?? null) ? $invoice['business'] : array(); $items = is_array($invoice['items'] ?? null) ? $invoice['items'] : array(); $tax_breakup = is_array($invoice['tax_breakup'] ?? null) ? $invoice['tax_breakup'] : array(); $this->new_page(); $this->title(__('Invoice', 'aibill-maker')); $invoice_number = $this->clean($invoice['invoice_number'] ?? 'INV'); $this->text(42, 772, $invoice_number, 18, true); $this->text(42, 752, __('Generated by AiBill Maker', 'aibill-maker'), 9, false); $this->text(402, 790, __('Total due', 'aibill-maker'), 10, true, 'right'); $this->text(553, 790, $this->money($invoice['grand_total'] ?? 0), 15, true, 'right'); $this->text(402, 765, __('Issue date', 'aibill-maker') . ': ' . $this->clean($invoice['issue_date'] ?? ''), 9, false); $this->text(402, 750, __('Due date', 'aibill-maker') . ': ' . $this->clean($invoice['due_date'] ?? ''), 9, false); $this->line(42, 732, 553, 732); $this->y = 708; $left = 42; $right = 310; $this->text($left, $this->y, __('From', 'aibill-maker'), 11, true); $this->text($right, $this->y, __('Bill to', 'aibill-maker'), 11, true); $this->y -= 18; $left_end = $this->party($business, $left, $this->y, 230); $right_end = $this->party($customer, $right, $this->y, 230); $this->y = min($left_end, $right_end) - 24; $this->items_table($items); $this->summary($invoice, $tax_breakup); $this->notes($invoice); $this->finish_page(); return $this->render_pdf(); } private function new_page() { if (!empty($this->commands)) { $this->finish_page(); } $this->commands = array(); $this->y = 800; } private function finish_page() { if (empty($this->commands)) { return; } $this->pages[] = implode("\n", $this->commands); $this->commands = array(); } private function title($title) { $this->text(42, 805, $title, 26, true); } private function items_table($items) { $this->ensure_space(80); $this->text(42, $this->y, __('Items', 'aibill-maker'), 12, true); $this->y -= 18; $this->table_header(); if (empty($items)) { $items = array(array('name' => __('Item', 'aibill-maker'), 'quantity' => 1, 'unit_price' => 0, 'tax_rate' => 0, 'line_total' => 0)); } foreach ($items as $item) { $name = $this->clean($item['name'] ?? __('Item', 'aibill-maker')); $description = $this->clean($item['description'] ?? ''); $hsn_sac = $this->clean($item['hsn_sac'] ?? ''); $lines = $this->wrap($name, 42); if ($description !== '') { $lines = array_merge($lines, $this->wrap($description, 50)); } if ($hsn_sac !== '') { $lines[] = __('HSN/SAC', 'aibill-maker') . ': ' . $hsn_sac; } $row_height = max(26, 12 + (count($lines) * 11)); $this->ensure_space($row_height + 20, true); $y_top = $this->y; $this->line(42, $y_top, 553, $y_top); $line_y = $y_top - 14; foreach ($lines as $index => $line) { $this->text(48, $line_y, $line, $index === 0 ? 9 : 8, $index === 0); $line_y -= 10; } $qty = number_format((float) ($item['quantity'] ?? 0), 2) . ' ' . $this->clean($item['unit'] ?? ''); $this->text(284, $y_top - 14, $qty, 9, false, 'right'); $this->text(377, $y_top - 14, $this->money($item['unit_price'] ?? 0), 9, false, 'right'); $this->text(440, $y_top - 14, number_format((float) ($item['tax_rate'] ?? 0), 2) . '%', 9, false, 'right'); $this->text(553, $y_top - 14, $this->money($item['line_total'] ?? 0), 9, true, 'right'); $this->y -= $row_height; } $this->line(42, $this->y, 553, $this->y); $this->y -= 24; } private function table_header() { $this->fill_rect(42, $this->y - 18, 511, 20, 0.94); $this->text(48, $this->y - 13, __('Item', 'aibill-maker'), 9, true); $this->text(284, $this->y - 13, __('Qty', 'aibill-maker'), 9, true, 'right'); $this->text(377, $this->y - 13, __('Rate', 'aibill-maker'), 9, true, 'right'); $this->text(440, $this->y - 13, __('Tax', 'aibill-maker'), 9, true, 'right'); $this->text(553, $this->y - 13, __('Total', 'aibill-maker'), 9, true, 'right'); $this->y -= 22; } private function summary($invoice, $tax_breakup) { $this->ensure_space(120); $x_label = 360; $x_value = 553; $this->summary_row(__('Subtotal', 'aibill-maker'), $this->money($invoice['subtotal'] ?? 0), $x_label, $x_value); if (!empty($tax_breakup['cgst']) || !empty($tax_breakup['sgst'])) { $this->summary_row(__('CGST', 'aibill-maker'), $this->money($tax_breakup['cgst'] ?? 0), $x_label, $x_value); $this->summary_row(__('SGST', 'aibill-maker'), $this->money($tax_breakup['sgst'] ?? 0), $x_label, $x_value); } elseif (!empty($tax_breakup['igst'])) { $this->summary_row(__('IGST', 'aibill-maker'), $this->money($tax_breakup['igst'] ?? 0), $x_label, $x_value); } else { $this->summary_row(__('Tax', 'aibill-maker'), $this->money($invoice['tax_total'] ?? 0), $x_label, $x_value); } $this->line($x_label, $this->y - 2, $x_value, $this->y - 2); $this->y -= 18; $this->summary_row(__('Grand total', 'aibill-maker'), $this->money($invoice['grand_total'] ?? 0), $x_label, $x_value, true); $this->y -= 10; } private function summary_row($label, $value, $x_label, $x_value, $bold = false) { $this->text($x_label, $this->y, $label, $bold ? 11 : 9, $bold); $this->text($x_value, $this->y, $value, $bold ? 11 : 9, true, 'right'); $this->y -= 16; } private function notes($invoice) { foreach (array( 'notes' => __('Notes', 'aibill-maker'), 'terms' => __('Terms', 'aibill-maker'), 'payment_instructions' => __('Payment instructions', 'aibill-maker'), ) as $key => $label) { $value = $this->clean($invoice[$key] ?? ''); if ($value === '') { continue; } $this->ensure_space(52); $this->text(42, $this->y, $label, 10, true); $this->y -= 14; foreach ($this->wrap($value, 95) as $line) { $this->ensure_space(18); $this->text(42, $this->y, $line, 9, false); $this->y -= 11; } $this->y -= 6; } } private function party($party, $x, $y, $width_chars) { $lines = array(); $name = $this->clean($party['name'] ?? $party['business_name'] ?? ''); if ($name === '') { $name = __('Not specified', 'aibill-maker'); } $lines[] = array($name, true); foreach (array('gstin' => __('GSTIN', 'aibill-maker'), 'email' => __('Email', 'aibill-maker'), 'phone' => __('Phone', 'aibill-maker'), 'address' => __('Address', 'aibill-maker')) as $key => $label) { if (!empty($party[$key])) { $value = $label . ': ' . $this->clean($party[$key]); foreach ($this->wrap($value, (int) ($width_chars / 5)) as $line) { $lines[] = array($line, false); } } } $current_y = $y; foreach ($lines as $line) { $this->text($x, $current_y, $line[0], $line[1] ? 10 : 8, $line[1]); $current_y -= $line[1] ? 13 : 10; } return $current_y; } private function ensure_space($needed, $repeat_table_header = false) { if (($this->y - $needed) > 60) { return; } $this->new_page(); $this->text(42, 805, __('Invoice continued', 'aibill-maker'), 14, true); $this->y = 770; if ($repeat_table_header) { $this->table_header(); } } private function text($x, $y, $text, $size = 10, $bold = false, $align = 'left') { $font = $bold ? 'F2' : 'F1'; $text = $this->clean($text); if ('right' === $align) { $x -= $this->text_width($text, $size); } $this->commands[] = sprintf('BT /%s %0.2F Tf %0.2F %0.2F Td (%s) Tj ET', $font, $size, $x, $y, $this->escape($text)); } private function line($x1, $y1, $x2, $y2) { $this->commands[] = sprintf('0.75 w %0.2F %0.2F m %0.2F %0.2F l S', $x1, $y1, $x2, $y2); } private function fill_rect($x, $y, $w, $h, $gray = 0.95) { $this->commands[] = sprintf('%0.2F g %0.2F %0.2F %0.2F %0.2F re f 0 g', $gray, $x, $y, $w, $h); } private function wrap($text, $limit) { $text = preg_replace('/\s+/', ' ', $this->clean($text)); $wrapped = wordwrap($text, max(10, (int) $limit), "\n", true); $lines = array_filter(array_map('trim', explode("\n", $wrapped)), static function ($line) { return $line !== ''; }); return empty($lines) ? array('') : array_values($lines); } private function money($amount) { return $this->currency . ' ' . number_format((float) $amount, 2, '.', ','); } private function text_width($text, $size) { return strlen($this->clean($text)) * $size * 0.48; } private function clean($text) { $text = wp_strip_all_tags((string) $text); $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); $replacements = array( '₹' => 'INR ', '€' => 'EUR ', '£' => 'GBP ', '…' => '...', '—' => '-', '–' => '-', '“' => '"', '”' => '"', '‘' => "'", '’' => "'", ); $text = strtr($text, $replacements); if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); if (false !== $converted) { $text = $converted; } } return trim(preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $text)); } private function escape($text) { return str_replace(array('\\', '(', ')'), array('\\\\', '\\(', '\\)'), $this->clean($text)); } private function render_pdf() { $objects = array(); $objects[] = '<< /Type /Catalog /Pages 2 0 R >>'; $objects[] = ''; $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'; $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>'; $kids = array(); foreach ($this->pages as $index => $content) { $page_obj = 5 + ($index * 2); $content_obj = $page_obj + 1; $kids[] = $page_obj . ' 0 R'; $objects[$page_obj - 1] = sprintf('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %0.2F %0.2F] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents %d 0 R >>', $this->page_width, $this->page_height, $content_obj); $stream = $content . "\n"; $objects[$content_obj - 1] = '<< /Length ' . strlen($stream) . " >>\nstream\n" . $stream . "endstream"; } $objects[1] = '<< /Type /Pages /Kids [' . implode(' ', $kids) . '] /Count ' . count($kids) . ' >>'; ksort($objects); $pdf = "%PDF-1.4\n%AiBill Maker\n"; $offsets = array(0); foreach ($objects as $id => $object) { $number = $id + 1; $offsets[$number] = strlen($pdf); $pdf .= $number . " 0 obj\n" . $object . "\nendobj\n"; } $xref = strlen($pdf); $pdf .= "xref\n0 " . (count($objects) + 1) . "\n"; $pdf .= "0000000000 65535 f \n"; for ($i = 1; $i <= count($objects); $i++) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]); } $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF"; return $pdf; } }