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.

 

58 thoughts on “Revisited: Tcpdf – Variable Height Table Rows With MultiCell

  1. For an automatic cell height calculation of a multicell, i have used the following code snippet

    $stringHeight = ceil($pdf->getStringHeight(100, “YOUR TEXT FROM CELL HERE”) * $this->getCellHeightRatio());

  2. Thanks a lot for the article – it helped me a lot in switching from FPDF to TCPDF (a result of trying to move a project to Unicode/UTF-8 …).
    I just found the following, which gave me a big headache: GetNumLines sometimes seemed to get wrong values in comparison to the version printed afterwards. This was very nasty, as it only happened under certain circumstances.

    For example I had a String “ABC-Someweirdcatalogdescription (Rev 5)”
    Which with the given width of my column would be wrapped in into:
    “ABC-Someweirdcatalogdes
    cription (Rev 5)” by getNumLines
    but would turn into:
    “ABC-
    Someweirdcatalogdescription
    (Rev 5)”
    when using MultiCell (which finally calls Write).

    The reason is that GetNumLines does not check for hyphenation marks when trying to determine the splitting point, while Write cares about this opportuninity.

    Replace Line 6119 by the following (which is somewhat copied from Write()):
    if (($chars[$i] != 160)
    AND (($chars[$i] == 173)
    OR preg_match($this->re_spaces,
    TCPDF_FONTS::unichr($chars[$i],
    $this->isunicode))
    OR ($chars[$i] == 45)
    )
    ){
    $lastSeparator = $i;
    }

    This may not be a complete solution by now, I am going to investigate it a bit more if I can find the time.

  3. It really helped. Thanks fellows, specially bretton.

    One fix that i want to share,

    Prototype:
    MultiCell ($w, $h, $txt, $border=0, $align=’J’, $fill=false, $ln=1, $x=”, $y=”, $reseth=true, $stretch=0, $ishtml=false, $autopadding=true, $maxh=0, $valign=’T’, $fitcell=false)

    Code:
    $this->MultiCell($cellWidth, $cellHeight,$cellText, 1, $textAlign, $this->_FillCell, 0, ”, ”, true, 0, false, true, $cellHeight);

    Everything worked great, except that it was still having some issues with text overlapping when we reduce the width of column, and cell height was not getting auto-extended. I pushed the $cellHeight var in “maxh” and it worked.

    Hope it help someone!

  4. Very nice! getNumLines() was the missing link I had been hoping for, and turns out it was there all along… Thank you for pointing it out.

  5. None of the links to Bretton Eveleigh’s class seem to be live anymore – anyone know where they went to?

  6. Ln() doesn’t move the cursor on the next available position…It start’s drawing all multicell commands from 0,0 or something. If i want to display multiple query results using a while structure only the last result will be drawn in the PDF file. It’s like overwriting the same MultiCell over and over again. Any ideeas?

  7. Works Very Nice! But i got a problem. When a row breaks in 1st page the content overwrites in the next page. Any Solution for that?

  8. Hello all,
    Anyone can help me how we wrap the word in pdf by using this TCPDF class
    Eg.suppose my column width is 10 but my word size is 50 then i want wrap that word in to 5 lines
    anyone know this then plz rpl its argnt for me

  9. @mrb – I’m having a problem generating a table with no images in it. After the first page break, the row heights are really high and all row field text are rendered on top of each other.

    I’ll post an example with source if you like.

  10. 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

  11. 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

  12. 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

  13. 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

  14. 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.

  15. 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 🙂

  16. 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.

  17. hi dan, zawmn83
    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? 🙁 thanx

  18. @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.

  19. 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.

  20. @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!

  21. hi Dan

    I mean the case of $rowcount * 6 is greater than $dimensions[‘hk’].
    Have you try such case ?

  22. @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.

  23. 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 ?

  24. Oh! I see.
    Now I change $borders variable and it is working great.

    Thanks

  25. 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.

  26. 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.

  27. Thanks for pointing that out zawmn83 – a typo – I meant $rowcount. I have updated the post accordingly.

  28. Hi
    I note that the following line has problem. What is $cellcount variable?

    elseif ((ceil($startY) + $cellcount * 6) + $dimensions[‘bm’] == floor($dimensions[‘hk’]))

  29. 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.

  30. 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 ?

  31. 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.

  32. 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.

  33. 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 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.