how to put my box in every row of my looping - fpdf

I want to put multiple box in every 1st and last column per row in my array of looping. Also the result of my looping is twice as it is
$w = array(82,95); //for XY dimension
for($i=0; $i < 2; $i++) {
$pdf->Row(array(utf8_decode($pdf->Rect($w[$i], 10, 7, 3, 'D')),
utf8_decode($client_code),
utf8_decode($full_name),
utf8_decode($prod_name),
number_format($loan_amount,2),
number_format($total_outstanding_principal + $int_bal,2),
date_format(date_create($start_date),"d F Y"),
$num_paid,
$num_unpaid,
$total_installment,
number_format($repayment - $total_penalty,2),
date_format(date_create($end_date),"d F Y"),
date_format(date_create($last_paid_date),"d F Y"),
number_format($repayment,2),
number_format($overdue,2),""));
}

Related

Sorting elements in perl structure n x n

I need to find the next neighbor using the euklid-distance-algorithm.
Given are two hashes with each 100 elements in this format:
$hash{$i}{price}
$hash{$i}{height}
I need to compare each hash elements with each other so that my result is a 100 x 100 matrix.
After that my matrix shoul be sorted after the following rules:
$hash{$i,$j} {lowest_euklid_distance}
$hash{$i,$j+1}{lowest_euklid_distance + 1}
$hash{$i,$j+2}{lowest_euklid_distance + 2}
.
.
$hash{$i+1,$j}{lowest_euklid_distance}
$hash{$i+1,$j}{lowest_euklid_distance + 2}
.
.
$hash{$i+n,$j+n}{lowest_euklid_distance+n}
My problem is to sort these elements properly.
Any adivices?
Thanks in advance.
/edit: adding more information:
I create the distance with the following subroutine:
sub euklid_distance{
#w1 = price_testdata
#w2 = height_testdata
#h1 = price_origindata
#h2 = height_origindata
my $w1 = trim($_[0]);
my $w2 = trim($_[1]);
my $h1 = trim($_[2]);
my $h2 = trim($_[3]);
my $result = (((($w2-$w1)**2)+(($h2-$h1)**2))**(1/2));
return $result;
}
I fetch the test and the origindata from two seperate lists.
The resulthash with the 100x100 matrix is created by the following code:
my %distancehash;
my $countvar=0;
for (my $j=0;$j<100;$j++){
for (my $i=0;$i<100;$i++){
$distancehash{$countvar}{distance} = euklid_distance( $origindata{$i}{price}, $testdata{$j}{price}, $origindata{$i}{height}, $testdata{$j}{height} );
$distancehash{$countvar}{originPrice} = $origindata{$i}{price};
$distancehash{$countvar}{originHeight} = $origindata{$i}{height};
$distancehash{$countvar}{testPrice} = $testdata{$j}{price};
$distancehash{$countvar}{testHeight} = $testdata{$j}{height};
$countvar++;
}
}
where $j goes over the testdata and $i over the origindata.
My goal is to have a new hash which is sorted by the lowest distance from the current $j ascending to the highest.

How can I improve Perl compare performance

I have an array ref of about 50,000 users. I want to go through all those users and compare each one to all the others in order to build a weighted list of matches (if the name is an exact match it's worth x, a partial match is worth y etc).
After going through the list and doing all the checks, I then want to go get the 10 highest weighted matches. Here is sort of a example of what I'm doing to help explain:
#!/usr/bin/perl
######################################################################
# Libraries
# ---------
use strict;
use warnings;
my $users = [];
$users->[0]{'Name'} = 'xxx';
$users->[0]{'Address'} = 'yyyy';
$users->[0]{'Phone'} = 'xxx';
$users->[1]{'Name'} = 'xxx';
$users->[1]{'Address'} = 'yyyy';
$users->[1]{'Phone'} = 'xxx';
$users->[2]{'Name'} = 'xxx';
$users->[3]{'Address'} = 'yyyy';
$users->[4]{'Phone'} = 'xxx';
foreach my $user_to_check (#$users) {
my $matched_users = [];
foreach my $user (#$users) {
$user_to_check->{'Weight'} = 0;
if (lc($user_to_check->{'Name'}) eq lc($user->{'Name'})) {
$user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 10);
} elsif ((length($user_to_check->{'Name'}) > 2) && (length($user->{'Name'}) > 2) && ($user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i)) {
$user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 5);
}
if (lc($user_to_check->{'Address'}) eq lc($user->{'Address'})) {
.....
}
if ($user_to_check->{'Weight'} > 0) {
# We have matches, add to matched users
push (#$matched_users,$user);
}
}
# Now we want to get just the top 10 highest matching users
foreach my $m_user (sort { $b->{'Weight'} <=> $a->{'Weight'} } #$matched_users ) {
last if $counter == 10;
.... # Do stuff with the 10 we want
}
}
The problem is, it's sooo slow. It takes more than a day to run (and I've tried it on multiple machines). I know that the "sort" is a killer but I did also try inserting the results into a tmp mysql table and then at the end instead of doing the Perl sort, I just did an order by select, but the difference in time was very minor.
As I'm just going through a existing data structure and comparing it I'm not sure what I could do (if anything) to speed it up. I'd appreciate any advise.
O(n²)
You compare each element in #$users against every element in there. That is 5E4² = 2.5E9 comparisions. For example, you wouldn't need to compare an element against itself. You also don't need to compare an element against one you have already compared. I.e. in this comparision table
X Y Z
X - + +
Y - - +
Z - - -
there only have to be three comparision to have compared each element against all others. The nine comparisions you are doing are 66% unneccessary (asymptotically: 50% unneccessary).
You can implement this by looping over indices:
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
...;
}
}
But this means that upon match, you have to increment the weight of both matching users.
Do things once, not 100,000 times
You lowercase the name of each user 1E5 times. This is 1E5 - 1 times to much! Just do it once for each element, possibly at data input.
As a side note, you shouldn't perform lowercasing, you should do case folding. This is available since at least v16 via the fc feature. Just lowercasing will be buggy when you have non-english data.
use feature 'fc'; # needs v16
$user->[NAME] = fc $name;
or
use Unicode::CaseFold;
$user->[NAME] = fc $name;
When hashes are not fast enough
Hashes are fast, in that a lookup takes constant time. But a single hash lookup is more expensive than an array access. As you only have a small, predefined set of fields, you can use the following trick to use hash-like arrays:
Declare some constants with the names of your fields that map to indices, e.g.
use constant {
WEIGHT => 0,
NAME => 1,
ADDRESS => 2,
...;
};
And then put your data into arrays:
$users->[0][NAME] = $name; ...;
You can access the fields like
$userA->[WEIGHT] += 10;
While this looks like a hash, this is actually a safe method to access only certain fields of an array with minimal overhead.
Regexes are slow
Well, they are quite fast, but there is a better way to determine if a string is a substring of another string: use index. I.e.
$user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i
Can be written as
(-1 != index $user_to_check->{Name}, $user->{Name})
assuming both are already lowercased case folded.
Alternative implementation
Edit: this appears to be invalidated by your edit to your question. This assumed you were trying to find some global similarities, not to obtain a set of good matches for each user
Implementing these ideas would make your loops look somewhat like
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
if ($userA->[NAME] eq $userB->[NAME]) {
$userA->[WEIGHT] += 10;
$userB->[WEIGHT] += 10;
} elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
$userA->[WEIGHT] += 5 if -1 != index $userA->[NAME], $userB->[NAME];
$userB->[WEIGHT] += 5 if -1 != index $userB->[NAME], $userA->[NAME];
}
if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
..... # More checks
}
}
}
my (#top_ten) = (sort { $b->[WEIGHT] <=> $a->[WEIGHT] } #$users)[0 .. 9];
Divide and conquer
The task you show is highly parallelizable. If you have the memory, using threads is easy here:
my $top10 = Thread::Queue->new;
my $users = ...; # each thread gets a copy of this data
my #threads = map threads->create(\&worker, $_), [0, int($#$users/2)], [int($#$users/2)+1, $#users];
# process output from the threads
while (defined(my $ret = $top10->dequeue)) {
my ($user, #top10) = #$ret;
...;
}
$_->join for #threads;
sub worker {
my ($from, $to) = #_;
for my $i ($from .. $to) {
my $userA = $users->[$i];
for $userB (#$users) {
...;
}
my #top10 = ...;
$top10->enqueue([ $userA, #top10 ]); # yield data to the main thread
}
}
You should probably return your output via a queue (as shown here), but do as much processing as possible inside the threads. With more advanced partitioning of the workload, should spawn as many threads as you have processors available.
But if any kind of pipelining, filtering or caching can decrease the number of iterations needed in the nested loops, you should do such optimizations (think map-reduce-style programming).
Edit: Elegantly reducing complexity through hashes for deduplication
What we are essentially doing is calculating a matrix of how good our records match, e.g.
X Y Z
X 9 4 5
Y 3 9 2
Z 5 2 9
If we assume that X is similar to Y implies Y is similar to X, then the matrix is symmetric, and we only need half of it:
X Y Z
X \ 4 5
Y \ 2
Z \
Such a matrix is equivalent to a weighted, undirected graph:
4 X 5 | X – Y: 4
/ \ | X – Z: 5
Y---Z | Y – Z: 2
2 |
Therefore, we can represent it elegantly as a hash of hashes:
my %graph;
$graph{X}{Y} = 4;
$graph{X}{Z} = 5;
$graph{Y}{Z} = 2;
However, such a hash structure implies a direction (from node X to node Y). To make querying the data easier, we might as well include the other direction too (due to the implementation of hashes, this won't lead to a large memory increase).
$graph{$x}{$y} = $graph{$y}{$x} += 2;
Because each node is now only connected to those nodes it is similar to, we don't have to sort through 50,000 records. For the 100th record, we can get the ten most similar nodes like
my $node = 100;
my #top10 = (sort { $graph{$node}{$b} <=> $graph{$node}{$a} } keys %{ $graph{$node} })[0 .. 9];
This would change the implementation to
my %graph;
# build the graph, using the array indices as node ID
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
if ($userA->[NAME] eq $userB->[NAME]) {
$graph{$j}{$i} = $graph{$i}{$j} += 10;
} elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
$graph{$j}{$i} = $graph{$i}{$j} += 5
if -1 != index $userA->[NAME], $userB->[NAME]
or -1 != index $userB->[NAME], $userA->[NAME];
}
if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
..... # More checks
}
}
}
# the graph is now fully populated.
# do somethething with each top10
while (my ($node_id, $similar) = each %graph) {
my #most_similar_ids = (sort { $similar->{$b} <=> $similar->{$a} } keys %$similar)[0 .. 9];
my ($user, #top10) = #$users[ $node_id, #most_similar_ids ];
...;
}
Building the graph this way should take half the time of naive iteration, and if the average number of edges for each node is low enough, going through similar nodes should be considerably faster.
Parallelizing this is a bit harder, as the graph each thread produces has to be combined before the data can be queried. For this, it would be best for each thread to perform the above code with the exception that the iteration bounds are given as parameters, and that only one edge should produced. The pair of edges will be completed in the combination phase:
THREAD A [0 .. 2/3] partial
\ graph
=====> COMBINE -> full graph -> QUERY
/ partial
THREAD B [2/3 .. 1] graph
# note bounds recognizing the triangular distribution of workload
However, this is only beneficial if there are only very few similar nodes for a given node, as combination is expensive.

Randomly selecting letters by frequency of use

After feeding few Shakespeare books to my Perl script I have a hash with 26 english letters as keys and the number of their occurences in texts - as value:
%freq = (
a => 24645246,
b => 1409459,
....
z => 807451,
);
and of course the total number of all letters - let's say in the $total variable.
Is there please a nice trick to generate a string holding 16 random letters (a letter can occur several times there) - weighted by their frequency of use?
To be used in a word game similar to Ruzzle:
Something elegant - like picking a random line from a file, as suggested by a Perl Cookbook receipt:
rand($.) < 1 && ($line = $_) while <>;
The Perl Cookbook trick for picking a random line (which can also be found in perlfaq5) can be adapted for weighted sampling too:
my $chosen;
my $sum = 0;
foreach my $item (keys %freq) {
$sum += $freq{$item};
$chosen = $item if rand($sum) < $freq{$item};
}
Here, $sum corresponds to the line counter $. and $freq{$item} to the constant 1 in the Cookbook version.
If you're going to be picking a lot of weighted random samples, you can speed this up a bit with some preparation (note that this destroys %freq, so make a copy first if you want to keep it):
# first, scale all frequencies so that the average frequency is 1:
my $avg = 0;
$avg += $_ for values %freq;
$avg /= keys %freq;
$_ /= $avg for values %freq;
# now, prepare the array we'll need for fast weighted sampling:
my #lookup;
while (keys %freq) {
my ($lo, $hi) = (sort {$freq{$a} <=> $freq{$b}} keys %freq)[0, -1];
push #lookup, [$lo, $hi, $freq{$lo} + #lookup];
$freq{$hi} -= (1 - $freq{$lo});
delete $freq{$lo};
}
Now, to draw a random weighted sample from the prepared distribution, you just do this:
my $r = rand #lookup;
my ($lo, $hi, $threshold) = #{$lookup[$r]};
my $chosen = ($r < $threshold ? $lo : $hi);
(This is basically the Square Histogram method described in Marsaglia, Tsang & Wang (2004), "Fast Generation of Discrete Random Variables", J. Stat. Soft. 11(3) and originally due to A.J. Walker (1974).)
I have no clue about Perl syntax so I'll just write pseudo-code. You can do something like that
sum <= 0
foreach (letter in {a, z})
sum <= sum + freq[letter]
pick r, a random integer in [0, sum[
letter <= 'a' - 1
do
letter <= letter + 1
r <= r - freq(letter)
while r > 0
letter is the resulting value
The idea behind this code is to make a stack of boxes for each letter. The size of each box is the frequency of the letter. Then we choose a random location on this stack and see which letter's box we landed.
Example :
freq(a) = 5
freq(b) = 3
freq(c) = 3
sum = 11
| a | b | c |
- - - - - - - - - - -
When we choose a 0 <= r < 11, we have the following probabilities
Pick a 'a' = 5 / 11
Pick a 'b' = 3 / 11
Pick a 'c' = 3 / 11
Which is exactly what we want.
You can first built a table of the running sum of the frequency. So if you have the following data:
%freq = (
a => 15,
b => 25,
c => 30,
d => 20
);
the running sum would be;
%running_sums = (
a => 0,
b => 15,
c => 40, # 15 + 25
d => 70, # 15 + 25 + 30
);
$max_sum = 90; # 15 + 25 + 30 + 20
To pick a single letter with the weighted frequency, you need to select a number between [0,90), then you can do a linear search on the running_sum table for the range that includes the letter. For example, if your random number is 20 then the appropriate range is 15-40, which is for the letter 'b'. Using linear search gives a total running time of O(m*n) where m is the number of letters we need and n is the size of the alphabet (therefore m=16, n=26). This is essentially what #default locale do.
Instead of linear search, you can also do a binary search on the running_sum table to get the closest number rounded down. This gives a total running time of O(m*log(n)).
For picking m letters though, there is a faster way than O(m*log(n)), perticularly if n < m. First you generate m random numbers in sorted order (which can be done without sorting in O(n)) then you do a linear matching for the ranges between the list of sorted random numbers and the list of running sums. This gives a total runtime of O(m+n). The code in its entirety running in Ideone.
use List::Util qw(shuffle);
my %freq = (...);
# list of letters in sorted order, i.e. "a", "b", "c", ..., "x", "y", "z"
# sorting is O(n*log(n)) but it can be avoided if you already have
# a list of letters you're interested in using
my #letters = sort keys %freq;
# compute the running_sums table in O(n)
my $sum = 0;
my %running_sum;
for(#letters) {
$running_sum{$_} = $sum;
$sum += $freq{$_};
}
# generate a string with letters in $freq frequency in O(m)
my $curmax = 1;
my $curletter = $#letters;
my $i = 16; # the number of letters we want to generate
my #result;
while ($i > 0) {
# $curmax generates a uniformly distributed decreasing random number in [0,1)
# see http://repository.cmu.edu/cgi/viewcontent.cgi?article=3483&context=compsci
$curmax = $curmax * (1-rand())**(1. / $i);
# scale the random number $curmax to [0,$sum)
my $num = int ($curmax * $sum);
# find the range that includes $num
while ($num < $running_sum{$letters[$curletter]}) {
$curletter--;
}
push(#result, $letters[$curletter]);
$i--;
}
# since $result is sorted, you may want to use shuffle it first
# Fisher-Yates shuffle is O(m)
print "", join('', shuffle(#result));

Consistent random colour highlights

In a table I have columns with to and from dates, I highlight overlaps between rows taking into account the periods, this is done exhaustively in nested loops. This is not the issue.
I need the same colour for the rows that overlap.
sub highlight_overlaps {
my $date_from1;
my $date_to1;
my $date_from2;
my $date_to2;
my $i = 0;
my $j = 0;
for ($i; $i < $#DATE_HOLDER; $i++) {
$date_from1 = $DATE_HOLDER[$i][0];
$date_to1 = $DATE_HOLDER[$i][1];
my $red = int(rand(65)) + 190;
my $green = int(rand(290)) - 55;
my $blue = int(rand(290)) - 55;
for ($j=$i+1; $j<=$#DATE_HOLDER; $j++) {
$date_from2 = $DATE_HOLDER[$j][0];
$date_to2 = $DATE_HOLDER[$j][1];
if (($date_from1 le $date_to2 && $date_to1 ge $date_to2) ||
($date_from1 le $date_from2 && $date_to1 le $date_to2) ||
($date_from1 gt $date_from2 && $date_from1 lt $date_to2)) {
$tb->setCellStyle($i+2, 6, "background-color:rgb($red,$green,$blue);font-size:9pt");
$tb->setCellStyle($i+2, 7, "background-color:rgb($red,$green,$blue);font-size:9pt");
$tb->setCellStyle($j+2, 6, "background-color:rgb($red,$green,$blue);font-size:9pt");
$tb->setCellStyle($j+2, 7, "background-color:rgb($red,$green,$blue);font-size:9pt");
}
}
}
}
This works fine if it's just a pair of dates; say:
1) 25-06-2012 27-06-2012
2) 18-06-2012 29-06-2012
Will get the same colour
If though I have
0) 26-06-2012 28-06-2012
1) 25-06-2012 27-06-2012
2) 18-06-2012 29-06-2012
0 will get a different colour while 1 & 2 are paired as intended.
When and how to pick colours so that different colours are only applied to different overlaps?
Following up on the first answer; how may I represent overlaps in order to store them in a data structure, so that I can colour them after their detection?
You'll have to compare each interval against each other interval, and put them in 'buckets' when they are equal. Now when you compare an interval to a third interval, you put the third in the same bucket as the interval.
Then you print the buckets.
Perl's hash would make for fine buckets.
About your overlap detection
There is no overlap if
date1_to < date2_from OR
date2_to < date1_from
Or, in Perl:
if ($date_to1 lt $date_from2 || $date_to2 lt $date_from1) {
#overlap
}
Invert that either using Perl's unless, or using de Morgan:
if ($date_to1 ge $date_from2 && $date_to2 ge $date_from1) {
#overlap
}

fpdf multicell issue

how we display fpdf multicell in equal heights having different amount of content
Great question.
I solved it by going through each cell of data in your row that will be multicelled and determine the largest height of all these cells. This happens before you create your first cell in the row and it becomes the new height of your row when you actually go to render it.
Here's some steps to achieve this for just one cell, but you'll need to do it for every multicell:
This assumes a $row of data with a String attribute called description.
First, get cell content and set the column width for the cell.
$description = $row['desciption']; // MultiCell (multi-line) content.
$column_width = 50;
Get the width of the description String by using the GetStringWidth() function in FPDF:
$total_string_width = $pdf->GetStringWidth($description);
Determine the number of lines this cell will be:
$number_of_lines = $total_string_width / ($column_width - 1);
$number_of_lines = ceil( $number_of_lines ); // Round it up.
I subtracted 1 from the $column_width as a kind of cell padding. It produced better results.
Determine the height of the resulting multi-line cell:
$line_height = 5; // Whatever your line height is.
$height_of_cell = $number_of_lines * $line_height;
$height_of_cell = ceil( $height_of_cell ); // Round it up.
Repeat this methodology for any other MultiCell cells, setting the $row_height to the largest $height_of_cell.
Finally, render your row using the $row_height for your row's height.
Dance.
I was also getting same issue when i have to put height equal to three lines evenif i have content of half or 1 and half line, i solved this by checking if in a string no of elements are less than no of possible letters in a line it is 60. If no of letters are less or equal to one line then ln() is called for two empty lines and if no of words are equal to or less than 2 line letters then one ln() is called.
My code is here:
$line_width = 60; // Line width (approx) in mm
if($pdf->GetStringWidth($msg1) < $line_width)
{
$pdf->MultiCell(75, 8, $msg1,' ', 'L');
$pdf->ln(3);
$pdf->ln(3.1);
}
else{
$pdf->MultiCell(76, 4, $msg1,' ', 'L');
$pdf->ln(3.1);
}
Ok I've done the recursive version. Hope this helps even more to the cause!
Take in account these variables:
$columnLabels: Labels for each column, so you'll store data behind each column)
$alturasFilas: Array in which you store the max height for each row.
//Calculate max height for each column for each row and store the value in //////an array
$height_of_cell = 0;
$alturasFilas = array();
foreach ( $data as $dataRow ) {
for ( $i=0; $i<count($columnLabels); $i++ ) {
$variable = $dataRow[$i];
$total_string_width = $pdf->GetStringWidth($variable);
$number_of_lines = $total_string_width / ($columnSizeWidth[$i] - 1);
$number_of_lines = ceil( $number_of_lines ); // Redondeo.
$line_height = 8; // Altura de fuente.
$height_of_cellAux = $number_of_lines * $line_height;
$height_of_cellAux = ceil( $height_of_cellAux );
if($height_of_cellAux > $height_of_cell){
$height_of_cell = $height_of_cellAux;
}
}
array_push($alturasFilas, $height_of_cell);
$height_of_cell = 0;
}
//--END--
Enjoy!
First, it's not the question to get height of Multicell. (to #Matias, #Josh Pinter)
I modified answer of #Balram Singh to use this code for more than 2 lines.
and I added new lines (\n) to lock up height of Multicell.
like..
$cell_width = 92; // Multicell width in mm
$max_line_number = 4; // Maximum line number of Multicell as your wish
$string_width = $pdf->GetStringWidth($data);
$line_number = ceil($string_width / $cell_width);
for($i=0; $i<$max_line_number-$line_number; $i++){
$data.="\n ";
}
Of course you have to assign SetFont() before use GetStringWidth().
My approach was pretty simple. You already need to override the header() and footer() in the fpdf class.
So i just added a function MultiCellLines($w, $h, $txt, $border=0, $align='J', $fill=false).
That's a very simple copy of the original MultiCell. But it won't output anything, just return the number of lines.
Multiply the lines with your line-height and you're safe ;)
Download for my code is here.
Just rename it back to ".php" or whatever you like. Have fun. It works definitely.
Mac
My personal solution to reduce the font size and line spacing according to a preset box:
// set box size and default font size
$textWidth = 100;
$textHeight = 100;
$fontsize = 12;
// if you get text from html div (using jquery and ajax) you must replace every <br> in a new line
$desc = utf8_decode(str_replace('<br>',chr(10),strip_tags($_POST['textarea'],'<br>')));
// count newline set in $desc variable
$countnl = substr_count($desc, "\n");
// Create a loop to reduce the font according to the contents
while($pdf->GetStringWidth($desc) > ($textWidth * (($textHeight-$fontsize*0.5*$countnl) / ($fontsize*0.5)))){
$fontsize--;
$pdf->SetFont('Arial','', $fontsize);
}
// print multicell and set line spacing
$pdf->MultiCell($textWidth, ($fontsize*0.5), "$desc", 0, 'L');
That's all!
A better solution would be to use your own word wrap function, as FPDF will not cut words, so you do not have to rely on the MultiCell line break.
I have wrote a method that checks for every substring of the text if the getStringWidth of FPDF is bigger than the column width, and splits accordingly.
This is great if you care more about a good looking PDF layout, than performance.
public function wordWrapMultiCell($text, $cellWidth = 80) {
$explode = explode("\n", $text);
array_walk($explode, 'trim');
$lines = [];
foreach($explode as $split) {
$sub = $split;
$char = 1;
while($char <= strlen($sub)) {
$substr = substr($sub, 0, $char);
if($this->pdf->getStringWidth($substr) >= $cellWidth - 1) { // -1 for better getStringWidth calculating
$pos = strrpos($substr, " ");
$lines[] = substr($sub, 0, ($pos !== FALSE ? $pos : $char)).($pos === FALSE ? '-' : '');
if($pos !== FALSE) { //if $pos returns FALSE, substr has no whitespace, so split word on current position
$char = $pos + 1;
$len = $char;
}
$sub = ltrim(substr($sub, $char));
$char = 0;
}
$char++;
}
if(!empty($sub)) {
$lines[] = $sub;
}
}
return $lines;
}
This returns an array of text lines, which you could merge by using implode/join:
join("\r\n", $lines);
And what it was used for in the first place, get the line height:
$lineHeight = count($lines) * $multiCellLineHeight;
This only works for strings with a space character, no white spacing like tabs. You could replace strpos with a regexp function then.