I am generating a PDF with fPDF.
I need to strikethrough a long text inside a MultiCell. The text is justified to left and right, which probably is the source of the problem.
Here is my code:
//get the starting x and y of the cell to strikeout
$strikeout_y_start = $pdf->GetY();
$strikeout_x = $pdf->getX();
$strikeText = "Some text with no New Lines (\n), which is wrapped automaticly, cause it is very very very very very very very very very very long long long long long long long long long long long long long long long long long long"
//draw the text
$pdf->MultiCell(180, 4, $strikeText);
//get the y end of cell
$strikeout_y_end = $pdf->GetY();
$strikeout_y = $strikeout_y_start+2;
$strikeCount = 0;
for ($strikeout_y; $strikeout_y < $strikeout_y_end - 4; $strikeout_y+=4) {
$strikeCount++;
//strike out the full width of all lines but last one - works OK
$pdf->Line($strikeout_x, $strikeout_y, $strikeout_x + 180, $strikeout_y);
}
//this works, but gives incorrect results
$width = $pdf->GetStringWidth($strikeText);
$width = $width - $strikeCount*180;
//the line below will strike out some text, but not all the letters of last line
$pdf->line($strikeout_x, $strikeout_y, $strikeout_x+$width, $strikeout_y);
The problem is that as the text in multicell is justified (and have to be), the spacec in previous lines are wider than the GetStringWidth assumes, so GetStringWidth underestimates the full width of this text.
As a result, the last line is stroked out in, say, 70%, and some letters on the end of it are not stroked out.
Any ideas how to calculate the width of last line in multicell?
I found the solution myself.
Sorry for asking unnecessary questions.
Here is what I had done:
class VeraPDF extends FPDF {
/**
* Returns width of the last line in a multicell
* useful for strike out / strike through
*
*
* #param string $s - the string measured
* #param int $lineWidth - with of the cell/line
* #return int
*/
function GetStringWidth_JustifiedLastLineWidth($s, $lineWidth)
{
//Get width of a string in the current font
$s=(string)$s;
$words = split(' ',$s);
$cw=&$this->CurrentFont['cw'];
$w=0;
$spaceWidth = $this->GetStringWidth(' ');
for($i=0, $wordsCount = count($words); $i<$wordsCount; $i++){
// sum up all the words width, and add space withs between the words
$w += $this->GetStringWidth($words[$i]) + $spaceWidth;
if ($w > $lineWidth) {
//if current width is more than the line width, then the current word
//will be moved to next line, we need to count it again
$i--;
}
if ($w >= $lineWidth) {
//if the current width is equal or grater than the line width,
//we need to reset current width, and count the width of remaining text
$w = 0;
}
}
//at last, we have only the width of the text that remain on the last line!
return $w;
}
}
Hope this helped someone :)
the spacec in previous lines are wider
than the GetStringWidth assumes, so
GetStringWidth underestimates the full
width of this text.
Have you tried to count the spaces and add the missing width yourself. Say every space is supposed to be 5px wide, but fpdf etimates it to be 4px, maybe you could add 1px per space to the total width in the end.
Related
I would like to use this configuration and make a line break every 60 characters.
The example i saw on the other pages does not explain how i can make a line break every 60 characters.
$pdf->Text(77, 94, utf8_decode("$objet"));
You can use wordwrap to break sentences into multiple lines without breaking the word and without the line exceeding the desired length.
Then simply iterate the lines, increasing the Y coordinate on each iteration, adjust the Y coordinate as necessary to suit the size of your text.
$object = "This is a very long text that I want to break into multiple lines every 60 characters.";
$object_lines = wordwrap($object, 60, "\n", true);
$object_lines = explode("\n", $object_lines);
$y = 94;
foreach ($object_lines as $line) {
$pdf->Text(77, $y, utf8_decode($line));
$y += 10;
}
I want to trim a given text at a given number of pixels.
To achieve this, I loop through the length of the text and with each loop, I add the next character and measure the string with the help of the method imagettfbbox. - This works fine for me on "normal text's" e.g. like this posting. ;)
But if I add some emoji's to my text, the length is even shorter than expected. I think this is because emojis encoded with more than 1 byte. -
That's why, I'm using mb_strwidth and mb_substr.
The method imagettfbbox expects a font-file to measure the given string. This font is able to display my emoji's.
Here's my code
$Line = "🏡 Garden, 🕓 Clock, some other things";
$MaxLength = 100; // Pixel
// Start looping through the line, start with 15 characters
for ( $Len = 15; $Len < mb_strwidth($Line); $Len++ ) {
// Grep x Chars from line
$NewLine = mb_substr($Line, 0, $Len);
// Measure string
$FontBox = imagettfbbox(12, 0, "fonts/OpenSansEmoji.ttf", $NewLine);
$TextWidth = $FontBox[2];
// Compare measured string with given max length
if ( $TextWidth > $MaxLength ) {
$Line = mb_substr($Line, 0, $Len - 2) . "...";
break;
}
}
$Line = NewLine;
Expample lines
"🏡 Garden, 🕓 Clock, some other things"
"🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡🏡 Gaaaaaaaaaaaardeeeeeeeeeeeeeeeeeen"
"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
"iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
"erufgbwuiergbvfuipwervbpiuwebvruiwbevuibwüeriuvbwieuörvböwieuvbr"
Result
As you can see, the more emoji's i use, the shorter the line.
Kind regards from a non native speaker. :)
I use the FPDF library to export some document files as PDF. One document includes a list of strings which have a different length. I print all strings as $pdf->MultiCell(). Now I would like to have the current height of that MultiCell to have the same line spacing in case that they have just one line or more.
Code Example:
//MySQL Query
while($row = mysql_fetch_array($res) {
$pdf->SetXY(18, $x);
$pdf->MultiCell(80, 5, $rowr['text']); //text has one or more lines
$x = $x + 10; // Here I would prefer a solution to say: $x = $x + 2 + height of the MultiCell()
}
I had the exact same problem; I use FPDF to generate invoices and there are four cells per row with first cell being a MultiCell with varying height (default height is 5, but if order name is too long it adds another line to a total height of 10, and so on). The problem is that the remaining 3 cells have fixed height.
After looking for solutions it seems the only way is to write a complex function or use some third party tool. Since I want my app to be as light as possible I used the following approach to solve it which in my opinion is way simpler than external plugin.
Rows with details on the Invoice start at Y=95 so I use $Y=95;
before my while loop
My first cell (the MultiCell) is as follows:
$pdf->SetXY(10,$Y);
$pdf->MultiCell(60,5,$row['Name'],1,1,'L');
I use the FPDF's GetY() function to get current height and save it as H:
$H = $pdf->GetY();
If the MultiCell's height is 5 GetY() will give back 100, if the height is 10 GetY() gives back 105 and so on.
I add new variable $height:
$height= $H-$Y;
Which as result gives me precisely the height of the MultiCell.
I use $Y and $height to set current position and column height:
$pdf->SetXY(130,$Y);
$pdf->Cell(40,$height,$row['RowName'],1,1,'L');
Before finishing the while loop set give $Y the $H's value:
$Y=$H;
Entire loop looks as follows and works perfectly:
$Y= 95;
$query = mysqli_query($con,"SELECT * FROM table");
while($row = mysqli_fetch_array($query)) {
$pdf->SetXY(10,$Y);
$pdf->MultiCell(60,5,$row['ROW1'],1,1,'L');
$H = $pdf->GetY();
$height= $H-$Y;
$pdf->SetXY(70,$Y);
$pdf->Cell(60,$height,$row['ROW2'],1,1,'L');
$pdf->SetXY(130,$Y);
$pdf->Cell(40,$height,$row['ROW3'],1,1,'L');
$pdf->SetXY(170,$Y);
$pdf->Cell(30,$height,$row['ROW4'],1,1,'L');
$Y=$H;
}
If you have 2 MultiCell columns or more in each row it gets tricky but still can be solved in a similar manner.
I found interesting solution here - https://github.com/artkonekt/pdf-invoice/blob/master/src/InvoicePrinter.php#L469
$calculateHeight = new self;
$calculateHeight->addPage();
$calculateHeight->setXY(0, 0);
$calculateHeight->SetFont($this->font, '', 7);
$calculateHeight->MultiCell($this->firstColumnWidth, 3, $item['description'], 0, 'L', 1);
$descriptionHeight = $calculateHeight->getY() + $cellHeight + 2;
So, he literally create a 'temporary' PDF, add multicell, and then simply measure height (newY - oldY)
Also, keep in mind that if text goes to new line - height of cell will be = number_of_lines * $height (height passed to MultiCell as second parameter)
So, if you passed 5 as $height, and temporary PDF measure that cell will be 15, you can be sure that text will spread to 3 lines.
I'm coding in golang so I'll show some pseudo-code. I hope the accessible methods are the same in php as in golang.
There is a method called pdf.SplitLines(text, width). You will pass your string content and the desired width and it will return an array of strings that represents the lines that'll be computed to display that content.
With that its easy. In pseudo-code it could look like:
fontSize = 10;
lineHeight = 12;
targetWidth = 50;
pdf.SetFontSize(fontSize)
nLines = length(pdf.SplitLines(content, targetWidth));
multiCellHeight = nLines * lineHeight;
pdf.Multicell(targetWidth, lineHeight, content, ...)
The rendered MultiCell will have the exact same size as stored in multiCellHeight. This way you'll get the hight before rendering.
This is working because the passed height for the MultiCell is the lineHeight of each row. If you know the rows before rendering, you'll get the total height.
I'm sorry if this fails for any reason for php. Just let me know if that's the case.
Wouldn't it be easier to simply NOT use boarders when you print out the text.
You can just use $pdf->GetY(); to get the curent y value.
Then when you have printed all the text, you can use $pdf->GetY(); to get the height after each piece of text. Compare y values to see which one is the biggest.
All you need to do then is $pdf-> SetY($y); to the original y value and paint the boarders with $pdf->Cell() now that you know height and width;
That's how I'd do it.
Edit: tested it, and seems to work. Now sing with me - "you get the beeeeest of both worlds..." no?
$cellData = array();
$cellData[0] = array();
$cellData[0][0] = array();
$cellData[0][0]['text'] = 'Audiometry';
$cellData[0][0]['width'] = '47';
$cellData[0][1] = array();
$cellData[0][1]['text'] = 'Control of Noise at Work Regulations 2005.';
$cellData[0][1]['width'] = '47';
$cellData[0][2] = array();
$cellData[0][2]['text'] = 'Fit with restrictions (as detailed below) to work in a noise controlled zone.';
$cellData[0][2]['width'] = '47';
$cellData[0][3] = array();
$cellData[0][3]['text'] = 'Recommended review date: 2021-11-27
Referral: Referred to OHP';
$cellData[0][3]['width'] = '47';
setAllCellSizes($cellData, $pdf);
function setAllCellSizes($cellData, $pdf){
$y = $pdf->GetY();
$x = $pdf->GetX();
$largestCell = 0;
for($cordI = 0; $cordI < count($cellData); $cordI++){
$curX = 10;
for($cordJ = 0; $cordJ < count($cellData[$cordI]); $cordJ++){
$pdf->SetXY($curX,$y);
$pdf-> MultiCell($cellData[$cordI][$cordJ]['width'], 5, $cellData[$cordI][$cordJ]['text'], '0', 'L');
$curX = $curX + $cellData[$cordI][$cordJ]['width'];
$cellHeight = $pdf->GetY() - $y;
if($largestCell < $cellHeight){
$largestCell = $cellHeight;
}
}
$curX = 10;
for($cordJ = 0; $cordJ < count($cellData[$cordI]); $cordJ++){
$pdf->SetXY($curX,$y);
$pdf->Cell($cellData[$cordI][$cordJ]['width'],$largestCell,'',1,1,'L');
$curX = $curX + $cellData[$cordI][$cordJ]['width'];
}
}
return $cellData;
}
I have a PHP loop that adds data into a table cell. However, I want to apply a static size to the table cell, so if more data is returned than can fit inside the cell I want the excess characters to be cut off and end with a "..."
For example, one data entry has 270 characters, but only the first 100 are displayed in the table cell. Follow by a "..."
Any ideas on how to do this?
Thanks!
if (strlen($str) > 100) $str = substr($str, 0, 100) . "...";
You can use mb_strimwidth
printf('<td>%s</td>', mb_strimwidth($cellContent, 0, 100, '…'));
If you want to truncate with respect to word boundaries, see
Truncate a multibyte String to n chars
You can also control content display with the CSS property text-overflow: ellipsis
http://www.quirksmode.org/css/textoverflow.html
Unfortunately, browser support varies.
function print_dots($message, $length = 100) {
if(strlen($message) >= $length + 3) {
$message = substr($message, 0, $length) . '...';
}
echo $message;
}
print_dots($long_text);
$table_cell_data = ""; // This would hold the data in the cell
$cell_limit = 100; // This would be the limit of characters you wanted
// Check if table cell data is greater than the limit
if(strlen($table_cell_data) > $cell_limit) {
// this is to keep the character limit to 100 instead of 103. OPTIONAL
$sub_string = $cell_limit - 3;
// Take the sub string and append the ...
$table_cell_data = substr($table_cell_data,0,$sub_string)."...";
}
// Testing output
echo $table_cell_data."<br />\n";
I try to create a PDF with multiple pages and need to calculate the height of each individual element (MultiCell) in advance to prepare for a page break. According to the documentation there are a couple of functions out there like GetCharWidth/GetStringWidth to support me in doing it on my own, but besides a potential performance lost I probably will not do it the right anyway. Suggestions to achieve my goal in a more elegant way?
Reference: TCPDF
I GOT it :D!!!!!
Create another pdf2 object
// pdf2 set x margin to pdf1's xmargin, but y margin to zero
// to make sure that pdf2 has identical settings, you can clone the object (after initializing the main pdf object)
$pdf2 = clone $pdf;
pdf2->addpage
pdf2->writeCell
$height = pdf2->getY()
pdf2->deletePage(pdf2->getPage())
pdf1->checkPageBreak($height);
pdf1->writeCell()
W00tness :D
This is an old question, but the current version (as of 7 Dec 2011) of TCPDF has a function called getStringHeight that allows you to calculate the resulting height of a string passed to MultiCell prior to actually calling MultiCell. Then this height can be used for various things, the calculation in the original question, and also for setting row height when making tables etc. Works great.
Just some info in case someone else stumbles across this question looking for a solution to this problem as I did.
While Carvell's answer is great, TCPDF mentions that getStringHeight returns the estimated height. Helpfully the documentation there provides a pretty comprehensive technique for getting the exact height which comes out as $height. As to why they don't use this themselves is a mystery...
// store current object
$pdf->startTransaction();
// store starting values
$start_y = $pdf->GetY();
$start_page = $pdf->getPage();
// call your printing functions with your parameters
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$pdf->MultiCell($w=0, $h=0, $txt, $border=1, $align='L', $fill=false, $ln=1, $x='', $y='', $reseth=true, $stretch=0, $ishtml=false, $autopadding=true, $maxh=0);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// get the new Y
$end_y = $pdf->GetY();
$end_page = $pdf->getPage();
// calculate height
$height = 0;
if ($end_page == $start_page) {
$height = $end_y - $start_y;
} else {
for ($page=$start_page; $page <= $end_page; ++$page) {
$pdf->setPage($page);
if ($page == $start_page) {
// first page
$height = $pdf->h - $start_y - $pdf->bMargin;
} elseif ($page == $end_page) {
// last page
$height = $end_y - $pdf->tMargin;
} else {
$height = $pdf->h - $pdf->tMargin - $pdf->bMargin;
}
}
}
// restore previous object
$pdf = $pdf->rollbackTransaction();
From my experience, it is nearly impossible to figure out the cell height in advance. It is much easier to use the page break handling functions of TCPDF that sort of tells you in advance if you're heading into a pagebreak. Here is example code:
$yy = $this->pdf->GetY();
$check_pagebreak = $this->pdf->checkPageBreak($height+$padding,$yy,false);
Change false to true to allow for an automatic page break, otherwise, you can handle the logic of the pagebreak, yourself, which is what I ended up doing.
Also, in case you may need this, here's another little tip: Consider using the transaction features to create your document in two passes. The first pass is used to figure out all the heights and cells, pagebreaks, etc. You can also store all your lineheights and lines per page in arrays. ON the second pass, create your document with all the correct information and no need for pagebreak logic (the second pass could be run from a seperate method, for the sake of keeping the code easier to read, and for your sanity).
Use TCPDF Example 20
Calculating MultiCell heights can be a nightmare if the cells/columns end on different pages.
Using transactions or additional pdf objects can make things very slow.
Using functions such as getNumLines() and getStringHeight() to calculate the 'estimated' (see docs) height before the cells are printed do not always work correctly. Especially if the text ends just before or just after the right border of the cell - resulting in rows being printed on top of each other.
I prefer the technique used in Example 20 where the maximum Y value of the different pages are used to calculate the position of the new row.
The example prints only two columns, but I changed its main function to be able to print an array of columns. Obviously you could add more data to the array, such as each column's font, borders, etc.
public function MultiRow($columnsArray) {
$page_start = $this->getPage();
$y_start = $this->GetY();
$pageArray = array();
$yArray = array();
// traverse through array and print one column at a time.
$columnCount = count($columnsArray);
for($i=0; $i<$columnCount; $i++)
{
if($i+1 < $columnCount)
{
// Current column is not the last column in the row.
// After printing, the pointer will be moved down to
// the right-bottom of the column - from where the
// next multiCell in the following loop will use it
// via $this->GetX().
$ln = 2;
}
else
{
// Current column is the last column in the row.
// After printing, the pointer will be moved to new line.
$ln = 1;
}
$this->MultiCell(30, 0, $columnsArray[$i], 1, 'L', 1, $ln,
$this->GetX() ,$y_start, true, 0);
$pageArray[$i] = $this->getPage();
$yArray[$i] = $this->GetY();
// Go to page where the row started - to print the
// next column (if any).
$this->setPage($page_start);
}
// Test if all columns ended on the same page
$samePage = true;
foreach ($pageArray as $val) {
if($val != $pageArray['0'])
{
$samePage = false;
break;
}
}
// Set the new page and row position by case
if($samePage == true)
{
// All columns ended on the same page.
// Get the longest column.
$newY = max($yArray);
}
else
{
// Some columns ended on different pages.
// Get the array-keys (not the values) of all columns that
// ended on the last page.
$endPageKeys = array_keys($pageArray, max($pageArray));
// Get the Y values of all columns that ended on the last page,
// i.e. get the Y values of all columns with keys in $endPageKeys.
$yValues = array();
foreach($endPageKeys as $key)
{
$yValues[] = $yArray[$key];
}
// Get the largest Y value of all columns that ended on
// the last page.
$newY = max($yValues);
}
// Go to the last page and start at its largets Y value
$this->setPage(max($pageArray));
$this->SetXY($this->GetX(),$newY);
}
The post Revisited: Tcpdf – Variable Height Table Rows With MultiCell has a lot of useful information. This is a short extract:
getNumLines() ... 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
I tried using Jay's answer and it worked for the intended purpose but for some reason caused my logo to not appear after the first page. I didn't want to do an in depth analysis, but had something to do with the cloning. I then tried the same approach, but using transactions. This produced hundreds of errors.
Then I came up with this rather simple solution using the same object.
/**
* Gets an accurate measurement of a cell's rendered height.
*
* #param float $width the width of the column to be rendered
* #param string $contents the contents to be rendered
*
* #return float
*/
private function getCellHeight(float $width, string $contents): float
{
$view = $this->view;
$currentPage = $view->getPage();
$currentX = $view->GetX();
$currentY = $view->GetY();
$view->AddPage();
$x = $view->GetX();
$start = $view->GetY();
$view->writeHTMLCell($width, 15, $x, $start, $contents, self::INSTANCE_BORDER, 1);
$height = $view->GetY() - $start;
$view->deletePage($view->getPage());
$view->setPage($currentPage);
$view->changePosition($currentX, $currentY);
return $height;
}
As the writeHTMLCell function requires a $h, I use 15, but it can be anything you want, as can the $border value.
The $ln value needs to be set to 1, otherwise the y value resets before the GetY() can get it.
changePosition is my own wrapper for SetXY().