Revisited: Tcpdf – Variable Height Table Rows With MultiCell
A few weeks ago I wrote an article about creating variable height table rows using tcpdf. It was a neat solution and I liked it, which was the reason I blogged about it in the first place. However, it turns out that ’sharing the love’ is not the only reason to blog about these things. It can also be a learning experience thanks to the commenting from you out there in the interwebs.
The particular comment in question, from mike, made me realise that there was a bit of a flaw in my neat idea – page breaks. Because I was first drawing the text, the automatic page breaks played havoc with the positioning when it came to drawing the borders. If the text was so long that it wrapped across enough lines to cause a page break, when I tried to reposition to draw the borders I was in completely the wrong place and it all went a little Pete Tong.
Still, not deterred by this little setback, I re-looked at the problem and have come up with a different approach that solves this problem and adds the ability to do a few more things.
When I relooked at the documentation, I discovered the useful little function: getNumLines(). This actually allows us to determine how many lines a string of text will take up, given a particular width. In effect, it allows us to do what I was using MultiCell to return, without actually drawing anything. This lets us to determined the maximum cell height with one line of code:
$linecount = max($pdf->getNumLines($row['cell1data'], 80),$pdf->getNumLines($row['cell2data'], 80),$pdf->getNumLines($row['cell3data'], 80));
Now that we know the cell height before we have drawn anything, we can use MultiCell() to draw the text and the borders at the same time.
This was not the end, however. My particular implementation was a grid of data that had borders between the column and not between the rows. All very pretty until that automatic page break happened and gave us a grid of data without a border at the bottom. To add insult to injury, the new page ended up with a grid of data with no border at the top!
To solve this, I needed to know if the row was going to be the first of the page, so I could draw the bottom border on the previous row and give the new row a top border. With the use of the page dimensions I could work this out:
$dimensions = $pdf->getPageDimensions(); $hasBorder = false; //flag for fringe case foreach($data as $row) { $rowcount = 0; //work out the number of lines required $rowcount = max($pdf->getNumLines($row['cell1data'], 80),$pdf->getNumLines($row['cell2data'], 80),$pdf->getNumLines($row['cell3data'], 80)); $startY = $pdf->GetY(); if (($startY + $rowcount * 6) + $dimensions['bm'] > ($dimensions['hk'])) { //this row will cause a page break, draw the bottom border on previous row and give this a top border //we could force a page break and rewrite grid headings here if ($hasborder) { $hasborder = false; } else { $pdf->Cell(240,0,'','T'); //draw bottom border on previous row $pdf->Ln(); } $borders = 'LTR'; } elseif ((ceil($startY) + $rowcount * 6) + $dimensions['bm'] == floor($dimensions['hk'])) { //fringe case where this cell will just reach the page break //draw the cell with a bottom border as we cannot draw it otherwise $borders = 'LRB'; $hasborder = true; //stops the attempt to draw the bottom border on the next row } else { //normal cell $borders = 'LR'; } //now draw it $pdf->MultiCell(80,$rowcount * 6,$row['cell1data'],$borders,'L',0,0); $pdf->MultiCell(80,$rowcount * 6,$row['cell2data'],$borders,'L',0,0); $pdf->MultiCell(80,$rowcount * 6,$row['cell3data'],$borders,'L',0,0); $pdf->Ln(); } $pdf->Cell(240,0,'','T'); //last bottom border
Note that there is a fringe case that I came across. This was when the row would not cause a page break as it was exactly the height of the remaining space. Any attempt to draw the bottom border with a separate call would cause a page break. I solved this by ensuring in that particular case, the cell was drawn with the bottom border already.
What gives this method more power (apart from actually working) is that we know when there is going to be a new page and we could choose to force a new page and if we wanted. We could then re-plot the grid headings before creating the row. That way we can have the grid headings on every page.
I am determined to get a water-tight solution to this problem and I think this gets us closer. If you do find any problems with it, let me know and I will see if we can solve them. Thanks for your feedback.
Update: Bretton Eveleigh has written a class that encapsulates this and other helper methods into a format that makes creating tables with Tcpdf much easier. See the comments below for the link. Thanks Bretton.
29 Responses to “Revisited: Tcpdf – Variable Height Table Rows With MultiCell”
Leave a Reply

mike on March 31st, 2009
Dan,
Great work on this project.
One Question: I don’t understand how you are creating the array inside the the foreach loop. On each iteration of the loop, the next array element in $data get’s assigned to the non array variable “$row”, but then you are referring to the array element “$row['cell1data']“. Am I missing something?
Anyway, your code gave me some ideas for my invoice form. I ran into a few snags, however. First, the Multicell function’s minimum height parameter will cause the cell to overwrite the footer, so I cannot use that to get all the column heights equal. Instead, I am using the getNumLines function, as you are, to get the height of my detail column (this is the always the longest column), then padding the bottom of the other columns with a number of line returns equal to the value returned from getNumLines.
As it states in the tcpdf documentation, the getNumLines function only returns the “estimated” number of lines, and this is the problem I am running into. Sometimes the detail column is shorter than the others, sometimes longer.
Since you have more experience with this class, I was wondering if you might have another brilliant idea to fix this.
Again, I appreciate your posting on this subject, and kudos to your SE optimization – Google really seems to like you
admin on April 2nd, 2009
Mike, the $data variable is assumed to be an array of rows of data, each of which is an array of columns. This would mean than $row['cell1ddata'] refers to the first column of the row. Not sure about the getNumLines, seems to work well for me.
mike on April 6th, 2009
Thanks for the clarification – wasn’t thinking multi-dimensional.
As for the getNumLines, I found that if the the last line in the details (longest) column is blank (i.e., “\n”), the function incorrectly calculates the number of lines. So the solution is to strip off any line returns from the end of the data.
zawmn83 on April 30th, 2009
Great Article
But I tried and found that the lines between rows are not appear. And also top border of table not appear. I only see the buttom border line.
What wrong with me ?
dan on April 30th, 2009
zawmn83 – Have you setup your colors, line widths etc?
$pdf->SetDrawColor(0,0,0);
$pdf->SetLineWidth(.5);
Also check the you have the correct values for the borders parameter of the MultiCell function.
zawmn83 on May 1st, 2009
Hi
I note that the following line has problem. What is $cellcount variable?
elseif ((ceil($startY) + $cellcount * 6) + $dimensions['bm'] == floor($dimensions['hk']))
dan on May 1st, 2009
Thanks for pointing that out zawmn83 – a typo – I meant $rowcount. I have updated the post accordingly.
zawmn83 on May 1st, 2009
Hi Dan
I’m not sure what the result layout of output. I want to download pdf file in table format.
Could you explain which code draw the horizontal line between table rows.
I only see
$borders = ‘LR’ // for normal cell there is no T and B borders
My current output file has left and right for normal rows and left, right and bottom borders only for the last row.
dan on May 1st, 2009
You are a correct in that this code will only draw borders on the left and right sides for the cells. That is the way I wanted it for the project I wrote it for. You can have top and bottom borders by adjusting the $borders variable.
zawmn83 on May 3rd, 2009
Oh! I see.
Now I change $borders variable and it is working great.
Thanks
zawmn83 on May 3rd, 2009
Hi Dan
When a row has long data in a cell, and the row height overflow the page height. The output is one column per page for this row.
Any suggestion on this ?
dan on May 3rd, 2009
@zawmn83 I have tested the code when a cell overflows to a new page and it appears to work fine (you can see by my comments on the code that it handles this case). Perhaps there is something particular about the way you have set up the page dimensions that is causing your issue? Maybe stepping through your code and examining the $dimensions variable will give some clue? I am not sure what else to suggest.
zawmn83 on May 4th, 2009
hi Dan
I mean the case of $rowcount * 6 is greater than $dimensions['hk'].
Have you try such case ?
dan on May 4th, 2009
@zawmn83 Yes I have tried such a case. It is the reason for the first part of the ‘if’ statement. I have made an assumption that the Multicell function will automatically cause a page break when it is too high. I guess this could depend on what your setting for SetAutoPageBreak() is. Have you checked that? Otherwise you could always force a page break with AddPage(), which I noted the comments. Good luck!
zawmn83 on May 4th, 2009
Hi Dan
Do you mean I need to off SetAutoPageBreak ?
Currently I set SetAutoPageBreak(true, 10);
I think your first ‘if’ statement check whether there is enough space for new row or not.
If not enough space, new row will be drawn on new page. In my case, my new row even not enough on the new page height. Because of my row height is greater than the whole page height, it need to show separately on two pages.
dan on May 4th, 2009
@zawmn83 Ah, now I get it. Sorry, I miss understood. I did not realise that your row height was taller than the entire page. I do not take this into account with my routine. My assumption was that a grid would not have so much content in a single cell.
You will need to check for this scenario and then chop up your cell data so that it will fit on a single page. It will make the routine somewhat more complex but is achievable. As you will not know where to cut the cell data in two, you will need to decide on an approximate number of characters that would be a row and do a rough calculation to determine where to chop your data up. You can then test to see if it will fit on the page and chop off a bit more if it is still too long. The remaining cell content will need to be drawn on the next page before continuing as per normal.
zawmn83 on May 5th, 2009
Hi Dan
Thanks for your idea.
Let me try
fm on May 10th, 2009
hi dan, zawmn83
thanx
i ve tried the code and make an example – pure code from this site, with 1-row table at the end of page.
row has jumped on other site, but top line is at both – first and second site too. could you help?
or has somebody procedure/method how to create multicell table in tcpdf?
drp on July 10th, 2009
Hey dan,
Interesting post. The really confusing thing about your code is how you actually determine the height of the cell. It appears you are hard-coding it as 6, but you can correct me if I’m wrong. The way I’m determining the height of a cell is this (if you care to know):
($tcpdf->GetFontSize() * $tcpdf->getCellHeightRatio()) + (2 * $tcpdf->cMargin)
Doing if this way makes it flexible to whatever the font you are using.
dan on July 21st, 2009
Thanks Derek,
Yes you are correct, I did choose a fixed line height for this example. Determining the height dynamically is definitely a more robust approach. Thanks for the example on how to do this. I have noted this for future use
Nathan Brown on October 13th, 2009
I am haivng an issue with a pdf that is being generated with a long narrative, and I think the getNumLines() would work, but I am unsure about how to correct this. I would glad to hire you “Admin” to look this issue and possible resolve it.
Thank you for your time.
dan on October 14th, 2009
@Nathan Thanks for your offer, unfortunately I just don’t have a lot of time at the moment – I am juggling a fulltime job and a startup. TCPDF has an active forum at http://sourceforge.net/projects/tcpdf/forums/forum/435311 that may be of some help.
mr b on January 7th, 2010
Hi Dan and others, I’ve been using ROSpdf for a few years, but decided to try another pdf library since ROSpdf is no longer being dev’d/maintained and is stuck in PHP4 land… rosPDf had a really cool method called easyTable, which did all the hard graft for formatting a PDF table… many of my apps reports rely on pdf table structures… i too noted the irritating issue with getNumLines… i’ve got an accurate robust workaround… i’ve build a class for automating tables using TCPDF and your work(thanks)… with the following features:
- include pdf images per cell
- control pdf image size, horizontal and vertical alignment
- cell width control, as well as an auto width feature, for all or selected cells
- accurate multi line cell height measuring
- define row fill style… no fill, fill all, fill alternate rows
- set text alignment per column
- set inter cell spacing, vertical and horizontal seperately
I am busy with the column headers at the moment, as soon as this is done I’ll supply the code…
In the meantime take a look at a sample here:
http://www.oceanit.co.za/SnapShotXML2PDF.pdf
I’ll am planning on running this like a mini project and adding requested features as I have time…
Bretton
dan on January 7th, 2010
@mrb – Thats great news. I look forward to seeing the finished class
mr b on January 7th, 2010
The header per page was a headache, I tried to override the setTableHeader method(tcpdf) for HTML tables, but it was a miserable failure… tried to override the header(tcpdf) method, failure… got a successful workaround(hack), just tweaking a few issues that the workaround created…
Hopefully I’ll post the code later today as a beta…
bretton
mr b on January 8th, 2010
It’s been a busy day, i’ve got a beta version ready for all of you…
First another example:
http://www.oceanit.co.za/easyTable/example.pdf
Classes(EasyTable and PDFImage) + notes:
http://www.oceanit.co.za/easyTable/EasyTable.beta.zip
If you got any questions you can contact me at:
easytable-at-oceanit-dot-co-dot-za(no dashes)
I may consider integrating the PDFImage class into EasyTable, thoughts/feedback are welcome…
Maybe it’s time for my own blog??
Bretton
mr b on January 25th, 2010
i’ve been using the easyTable class and have done some bug fixes and updates… here is the latest version, now supports multi image formats of TCPDF…
Latest Version
The notes.txt file has the changelog in it, highlighting fixes/updates…
The first beta is no longer available, heres an example, using the latest version, from one of my projects… the green image blocks are to test PNG support, it’s not perfect but I’ll keep tweaking till I’m happy!
Example
mr b
dan on January 25th, 2010
@mrb – Great to see an update. Thanks.
mr b on January 25th, 2010
Tx Dan… i’m using it in my project, and tweaking as I go…