Auto Font Size For Text (GD via PHP) - php

There is a space of x*y for text to go on $im (GD Image Resource) how can I choose a font size (or write text such that) it does not overflow over that area?

I think you look for the imagettfbbox function.
I used that some years ago for a script generating localized buttons for a Web interface. I actually resized buttons if the text didn't fit in the template, to keep text size consistent, but you can try to reduce the text size until the text fits.
If you are interested, I can paste some snippets of my code (or give it right away).
[EDIT] OK, here is some extracts of my code, someday I will clean it up (make it independent of target app, give samples) and make it public as a whole.
I hope the snippets make sense.
// Bounding boxes: ImageTTFBBox, ImageTTFText:
// Bottom-Left: $bb[0], $bb[1]
// Bottom-Right: $bb[2], $bb[3]
// Top-Right: $bb[4]; $bb[5]
// Top-Left: $bb[6], $bb[7]
define('GDBB_TOP', 5);
define('GDBB_LEFT', 0);
define('GDBB_BOTTOM', 1);
define('GDBB_RIGHT', 2);
#[ In class constructor ]#
// Get size in pixels, must convert to points for GD2.
// Because GD2 assumes 96 pixels per inch and we use more "standard" 72.
$this->textSize *= 72/96;
$this->ComputeTextDimensions($this->textSize, FONT, $this->text);
#[ Remainder of the class (extract) ]
/**
* Compute the dimensions of the text.
*/
function ComputeTextDimensions($textSize, $fontFile, $text)
{
$this->textAreaWidth = $this->imageHSize - $this->marginL - $this->marginR;
$this->textAreaHeight = $this->imageVSize - $this->marginT - $this->marginB;
// Handle text on several lines
$this->lines = explode(NEWLINE_CHAR, $text);
$this->lineNb = count($this->lines);
if ($this->lineNb == 1)
{
$bb = ImageTTFBBox($textSize, 0, $fontFile, $text);
$this->textWidth[0] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = $this->textWidth[0];
$this->textHeight[0] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
}
else
{
for ($i = 0; $i < $this->lineNb; $i++)
{
$bb = ImageTTFBBox($textSize, 0, $fontFile, $this->lines[$i]);
$this->textWidth[$i] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = max($this->maxTextWidth, $this->textWidth[$i]);
$this->textHeight[$i] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
}
}
// Is the given text area width too small for asked text?
if ($this->maxTextWidth > $this->textAreaWidth)
{
// Yes! Increase button size
$this->textAreaWidth = $this->maxTextWidth;
$this->imageHSize = $this->textAreaWidth + $this->marginL + $this->marginR;
}
// Now compute the text positions given the new (?) text area width
if ($this->lineNb == 1)
{
$this->ComputeTextPosition(0, $textSize, $fontFile, $text, false);
}
else
{
for ($i = 0; $i < $this->lineNb; $i++)
{
$this->ComputeTextPosition($i, $textSize, $fontFile, $this->lines[$i], false);
}
}
}
/**
* Compute xText and yText (text position) for the given text.
*/
function ComputeTextPosition($index, $textSize, $fontFile, $text, $centerAscDesc)
{
switch ($this->textAlign)
{
case 'L':
$this->xText[$index] = $this->marginL;
break;
case 'R':
$this->xText[$index] = $this->marginL +
$this->textAreaWidth - $this->textWidth[$index];
break;
case 'C':
default:
$this->xText[$index] = $this->marginL +
($this->textAreaWidth - $this->textWidth[$index]) / 2;
break;
}
if ($centerAscDesc)
{
// Must compute the difference between baseline and bottom of BB.
// I have to use a temporary image, as ImageTTFBBox doesn't use coordinates
// providing offset from the baseline.
$tmpBaseline = 5;
// Image size isn't important here, GD2 still computes correct BB
$tmpImage = ImageCreate(5, 5);
$bbt = ImageTTFText($tmpImage, $this->textSize, 0, 0, $tmpBaseline,
$this->color, $fontFile, $text);
// Bottom to Baseline
$baselinePos = $bbt[GDBB_BOTTOM] - $tmpBaseline;
ImageDestroy($tmpImage);
$this->yText[$index] = $this->marginT + $this->textAreaHeight -
($this->textAreaHeight - $this->textHeight) / 2 - $baselinePos + 0.5;
}
else
{
// Actually, we want to center the x-height, ie. to keep the baseline at same pos.
// whatever the text is really, ie. independantly of ascenders and descenders.
// This provide better looking buttons, as they are more consistent.
$bbt = ImageTTFBBox($textSize, 0, $fontFile, "moxun");
$tmpHeight = $bbt[GDBB_BOTTOM] - $bbt[GDBB_TOP];
$this->yText[$index] = $this->marginT + $this->textAreaHeight -
($this->textAreaHeight - $tmpHeight) / 2 + 0.5;
}
}
/**
* Add the text to the button.
*/
function DrawText()
{
for ($i = 0; $i < $this->lineNb; $i++)
{
// Increase slightly line height
$yText = $this->yText[$i] + $this->textHeight[$i] * 1.1 *
($i - ($this->lineNb - 1) / 2);
ImageTTFText($this->image, $this->textSize, 0,
$this->xText[$i], $yText, $this->color, FONT, $this->lines[$i]);
}
}

I've taken the class by PhiLho above and expanded it to dynamically scale individual lines of text to fit within the bounding box. Also I wrapped it with a call so you can see how it functions. This is pretty much copy-pasta, just change your variables and it should work out of the box.
<?
header("Content-type: image/png");
define('GDBB_TOP', 5);
define('GDBB_LEFT', 0);
define('GDBB_BOTTOM', 1);
define('GDBB_RIGHT', 2);
class DrawFont {
function DrawFont($details) {
// Get size in pixels, must convert to points for GD2.
// Because GD2 assumes 96 pixels per inch and we use more "standard" 72.
$this->textSizeMax = $details['size'];
$this->font = $details['font'];
$this->text = $details['text'];
$this->image = $details['image'];
$this->color = $details['color'];
$this->shadowColor = $details['shadowColor'];
$this->textAlign = "C";
$this->imageHSize = $details['imageHSize'];
$this->imageVSize = $details['imageVSize'];
$this->marginL = $details['marginL'];
$this->marginR = $details['marginR'];
$this->marginT = $details['marginT'];
$this->marginB = $details['marginB'];
$this->ComputeTextDimensions($this->font, $this->text);
}
/**
* Compute the dimensions of the text.
*/
function ComputeTextDimensions($fontFile, $text)
{
$this->textAreaWidth = $this->imageHSize - $this->marginL - $this->marginR;
$this->textAreaHeight = $this->imageVSize - $this->marginT - $this->marginB;
// Handle text on several lines
$this->lines = explode(' ', $text);
$this->lineNb = count($this->lines);
if ($this->lineNb == 1)
{
$this->textSize[0] = $this->textSizeMax;
$bb = ImageTTFBBox($this->textSize[0], 0, $fontFile, $text);
$this->textWidth[0] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = $this->textWidth[0];
$this->textHeight[0] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
$this->textSize[0] = $this->textSizeMax;
while ($this->textWidth[0] > $this->textAreaWidth && $this->textSize[0] > 1) {
$this->textSize[0]--;
$bb = ImageTTFBBox($this->textWidth[$i], 0, $fontFile, $text);
$this->textWidth[0] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = $this->textWidth[0];
$this->textHeight[0] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
}
}
else
{
for ($i = 0; $i < $this->lineNb; $i++)
{
$this->textSize[$i] = $this->textSizeMax;
$bb = ImageTTFBBox($this->textSize[$i], 0, $fontFile, $this->lines[$i]);
$this->textWidth[$i] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = max($this->maxTextWidth, $this->textWidth[$i]);
$this->textHeight[$i] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
while ($this->textWidth[$i] > $this->textAreaWidth && $this->textSize[$i] > 1) {
$this->textSize[$i]--;
$bb = ImageTTFBBox($this->textSize[$i], 0, $fontFile, $this->lines[$i]);
$this->textWidth[$i] = $bb[GDBB_RIGHT] - $bb[GDBB_LEFT];
$this->maxTextWidth = max($this->maxTextWidth, $this->textWidth[$i]);
$this->textHeight[$i] = $bb[GDBB_BOTTOM] - $bb[GDBB_TOP];
}
}
}
/*
// Is the given text area width too small for asked text?
if ($this->maxTextWidth > $this->textAreaWidth)
{
// Yes! Increase button size
$this->textAreaWidth = $this->maxTextWidth;
$this->imageHSize = $this->textAreaWidth + $this->marginL + $this->marginR;
}
*/
// Now compute the text positions given the new (?) text area width
if ($this->lineNb == 1)
{
$this->ComputeTextPosition(0, $this->textSize[0], $fontFile, $text, false);
}
else
{
for ($i = 0; $i < $this->lineNb; $i++)
{
$this->ComputeTextPosition($i, $this->textSize[$i], $fontFile, $this->lines[$i], false);
}
}
}
/**
* Compute xText and yText (text position) for the given text.
*/
function ComputeTextPosition($index, $textSize, $fontFile, $text, $centerAscDesc)
{
switch ($this->textAlign)
{
case 'L':
$this->xText[$index] = $this->marginL;
break;
case 'R':
$this->xText[$index] = $this->marginL +
$this->textAreaWidth - $this->textWidth[$index];
break;
case 'C':
default:
$this->xText[$index] = $this->marginL +
($this->textAreaWidth - $this->textWidth[$index]) / 2;
break;
}
if ($centerAscDesc)
{
// Must compute the difference between baseline and bottom of BB.
// I have to use a temporary image, as ImageTTFBBox doesn't use coordinates
// providing offset from the baseline.
$tmpBaseline = 5;
// Image size isn't important here, GD2 still computes correct BB
$tmpImage = ImageCreate(5, 5);
$bbt = ImageTTFText($tmpImage, $this->textSizeMax, 0, 0, $tmpBaseline,
$this->color, $fontFile, $text);
// Bottom to Baseline
$baselinePos = $bbt[GDBB_BOTTOM] - $tmpBaseline;
ImageDestroy($tmpImage);
$this->yText[$index] = $this->marginT + $this->textAreaHeight -
($this->textAreaHeight - $this->textHeight) / 2 - $baselinePos + 0.5;
}
else
{
// Actually, we want to center the x-height, ie. to keep the baseline at same pos.
// whatever the text is really, ie. independantly of ascenders and descenders.
// This provide better looking buttons, as they are more consistent.
$bbt = ImageTTFBBox($textSize, 0, $fontFile, "moxun");
$tmpHeight = $bbt[GDBB_BOTTOM] - $bbt[GDBB_TOP];
$this->yText[$index] = $this->marginT + $this->textAreaHeight -
($this->textAreaHeight - $tmpHeight) / 2 + 0.5;
}
}
/**
* Add the text to the button.
*/
function DrawText()
{
$this->maxTextHeight = 0;
// find maxTextHeight
for ($i = 0; $i < $this->lineNb; $i++)
{
if ($this->textHeight[$i] > $this->maxTextHeight) {
$this->maxTextHeight = $this->textHeight[$i];
}
}
for ($i = 0; $i < $this->lineNb; $i++)
{
// Increase slightly line height
$yText = $this->yText[$i] + $this->maxTextHeight * 1.1 *
($i - ($this->lineNb - 1) / 2);
ImageTTFText($this->image, $this->textSize[$i], 0,
$this->xText[$i]+2, $yText+2, $this->shadowColor, $this->font, $this->lines[$i]);
ImageTTFText($this->image, $this->textSize[$i], 0,
$this->xText[$i], $yText, $this->color, $this->font, $this->lines[$i]);
}
}
}
// Script starts here
$im = imagecreatefromjpeg("/home/cvgcfjpq/public_html/fb/img/page_template.jpg");
$color = imagecolorallocate($im, 235, 235, 235);
$shadowColor = imagecolorallocate($im, 90, 90, 90);
$details = array("image" => $im,
"font" => "OldSansBlack.ttf",
"text" => $_GET['name'],
"color" => $color,
"shadowColor" => $shadowColor,
"size" => 40,
"imageHSize" => 200,
"imageVSize" => 250,
"marginL" => 5,
"marginR" => 5,
"marginT" => 5,
"marginB" => 5);
$dofontobj =& new DrawFont($details);
$dofontobj->DrawText();
imagepng($im);
imagedestroy($im);
unset($px);
?>

Related

Text out of the box for converting into image in php

I am trying to convert text into image. I already did it, but the are some cases when text is out of the image box
The "e" of the word "The" is cut. I have tried decreasing the font size or increasing the width of the image, but in some cases this happen again with another text. This is the code:
$new_line_position = 61;
$angle = 0;
$left = 20;
$top = 45;
$image_width = 1210;
$image_line_height = 45;
$content_input = wordwrap($content_input, $new_line_position, "\n", true);
$lineas = preg_split('/\\n/', $content_input);
$lines_breaks = count($lineas);
$image_height = $image_line_height * $lines_breaks;
$im = imagecreatetruecolor($image_width, $image_height);
// Create some colors
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, $image_width, $image_height, $white);
$font_ttf = public_path().'/fonts/'.$ttf_font;
foreach($lineas as $linea){
imagettftext($im, $font_size, $angle, $left, $top, $black, $font_ttf, $linea);
$top = $top + $image_line_height;
}
// Add the text
imagepng($im);
imagedestroy($im);
Thank you.
The problem is that every single character may have a slightly different width, for example W and i. Becuase of that you cannot split a string by letter count per line you need a more accurate method.
The main trick is to use
imagettfbbox
which gives the bounding box of a text using TrueType fonts and from this, you can get the real width that text will use.
Here is a function for pixel perfect split found at http://php.net/manual/en/function.wordwrap.php
use it instead of wordwrap and pass extra values like image width, font-size and font path
<?php
/**
* Wraps a string to a given number of pixels.
*
* This function operates in a similar fashion as PHP's native wordwrap function; however,
* it calculates wrapping based on font and point-size, rather than character count. This
* can generate more even wrapping for sentences with a consider number of thin characters.
*
* #static $mult;
* #param string $text - Input string.
* #param float $width - Width, in pixels, of the text's wrapping area.
* #param float $size - Size of the font, expressed in pixels.
* #param string $font - Path to the typeface to measure the text with.
* #return string The original string with line-breaks manually inserted at detected wrapping points.
*/
function pixel_word_wrap($text, $width, $size, $font)
{
# Passed a blank value? Bail early.
if (!$text)
return $text;
# Check if imagettfbbox is expecting font-size to be declared in points or pixels.
static $mult;
$mult = $mult ?: version_compare(GD_VERSION, '2.0', '>=') ? .75 : 1;
# Text already fits the designated space without wrapping.
$box = imagettfbbox($size * $mult, 0, $font, $text);
if ($box[2] - $box[0] / $mult < $width)
return $text;
# Start measuring each line of our input and inject line-breaks when overflow's detected.
$output = '';
$length = 0;
$words = preg_split('/\b(?=\S)|(?=\s)/', $text);
$word_count = count($words);
for ($i = 0; $i < $word_count; ++$i) {
# Newline
if (PHP_EOL === $words[$i])
$length = 0;
# Strip any leading tabs.
if (!$length)
$words[$i] = preg_replace('/^\t+/', '', $words[$i]);
$box = imagettfbbox($size * $mult, 0, $font, $words[$i]);
$m = $box[2] - $box[0] / $mult;
# This is one honkin' long word, so try to hyphenate it.
if (($diff = $width - $m) <= 0) {
$diff = abs($diff);
# Figure out which end of the word to start measuring from. Saves a few extra cycles in an already heavy-duty function.
if ($diff - $width <= 0)
for ($s = strlen($words[$i]); $s; --$s) {
$box = imagettfbbox($size * $mult, 0, $font, substr($words[$i], 0, $s) . '-');
if ($width > ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
else {
$word_length = strlen($words[$i]);
for ($s = 0; $s < $word_length; ++$s) {
$box = imagettfbbox($size * $mult, 0, $font, substr($words[$i], 0, $s + 1) . '-');
if ($width < ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
if ($breakpoint) {
$w_l = substr($words[$i], 0, $s + 1) . '-';
$w_r = substr($words[$i], $s + 1);
$words[$i] = $w_l;
array_splice($words, $i + 1, 0, $w_r);
++$word_count;
$box = imagettfbbox($size * $mult, 0, $font, $w_l);
$m = $box[2] - $box[0] / $mult;
}
}
# If there's no more room on the current line to fit the next word, start a new line.
if ($length > 0 && $length + $m >= $width) {
$output .= PHP_EOL;
$length = 0;
# If the current word is just a space, don't bother. Skip (saves a weird-looking gap in the text).
if (' ' === $words[$i])
continue;
}
# Write another word and increase the total length of the current line.
$output .= $words[$i];
$length += $m;
}
return $output;
}
;
?>
Below is working code example:
I modified this function pixel_word_wrap a little bit. Also, modified some calculation in your code. Right now is giving me the perfect image with correctly calculated margins. I am not super happy with the code noticed that there is a $adjustment variable, that should be bigger when you use bigger font-size. I think It's down to imperfection in imagettfbbox function. But It's a practical approach that works pretty well with most font-sizes.
<?php
$angle = 0;
$left_margin = 20;
$top_margin = 20;
$image_width = 1210;
$image_line_height = 42;
$font_size = 32;
$top = $font_size + $top_margin;
$font_ttf = './OpenSans-Regular.ttf';
$text = 'After reading Mr. Gatti`s interview I finally know what bothers me so much about his #elenaFerrante`s unamsking. The whole thing is about him, not the author, not the books, just himself and his delusion of dealing with some sort of unnamed corruption';$adjustment= $font_size *2; //
$adjustment= $font_size *2; // I think because imagettfbbox is buggy adding extra adjustment value for text width calculations,
function pixel_word_wrap($text, $width, $size, $font) {
# Passed a blank value? Bail early.
if (!$text) {
return $text;
}
$mult = 1;
# Text already fits the designated space without wrapping.
$box = imagettfbbox($size * $mult, 0, $font, $text);
$g = $box[2] - $box[0] / $mult < $width;
if ($g) {
return $text;
}
# Start measuring each line of our input and inject line-breaks when overflow's detected.
$output = '';
$length = 0;
$words = preg_split('/\b(?=\S)|(?=\s)/', $text);
$word_count = count($words);
for ($i = 0; $i < $word_count; ++$i) {
# Newline
if (PHP_EOL === $words[$i]) {
$length = 0;
}
# Strip any leading tabs.
if (!$length) {
$words[$i] = preg_replace('/^\t+/', '', $words[$i]);
}
$box = imagettfbbox($size * $mult, 0, $font, $words[$i]);
$m = $box[2] - $box[0] / $mult;
# This is one honkin' long word, so try to hyphenate it.
if (($diff = $width - $m) <= 0) {
$diff = abs($diff);
# Figure out which end of the word to start measuring from. Saves a few extra cycles in an already heavy-duty function.
if ($diff - $width <= 0) {
for ($s = strlen($words[$i]); $s; --$s) {
$box = imagettfbbox($size * $mult, 0, $font,
substr($words[$i], 0, $s) . '-');
if ($width > ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
else {
$word_length = strlen($words[$i]);
for ($s = 0; $s < $word_length; ++$s) {
$box = imagettfbbox($size * $mult, 0, $font,
substr($words[$i], 0, $s + 1) . '-');
if ($width < ($box[2] - $box[0] / $mult) + $size) {
$breakpoint = $s;
break;
}
}
}
if ($breakpoint) {
$w_l = substr($words[$i], 0, $s + 1) . '-';
$w_r = substr($words[$i], $s + 1);
$words[$i] = $w_l;
array_splice($words, $i + 1, 0, $w_r);
++$word_count;
$box = imagettfbbox($size * $mult, 0, $font, $w_l);
$m = $box[2] - $box[0] / $mult;
}
}
# If there's no more room on the current line to fit the next word, start a new line.
if ($length > 0 && $length + $m >= $width) {
$output .= PHP_EOL;
$length = 0;
# If the current word is just a space, don't bother. Skip (saves a weird-looking gap in the text).
if (' ' === $words[$i]) {
continue;
}
}
# Write another word and increase the total length of the current line.
$output .= $words[$i];
$length += $m;
}
return $output;
}
$out = pixel_word_wrap($text, $image_width -$left_margin-$adjustment,
$font_size, $font_ttf);
$lineas = preg_split('/\\n/', $out);
$lines_breaks = count($lineas);
$image_height = $image_line_height * $lines_breaks;
$im = imagecreatetruecolor($image_width, $image_height + $top);
// Create some colors
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, $image_width, $image_height + $top, $white);
foreach ($lineas as $linea) {
imagettftext($im, $font_size, $angle, $left_margin, $top, $black, $font_ttf,
$linea);
$top = $top + $image_line_height;
}
header('Content-Type: image/png');
imagepng($im);
Here is an example
You can also use a monospace font. A monospace is a font whose letters and characters each occupy the same amount of horizontal space.
The problem is that your font is a variable width per letter, but you are truncating based on the number of letters and not the width of the font.
Take the following example, ten "I"s vs ten "W", the second will be over twice as long.
iiiiiiiiii
WWWWWWWWWW
The "simple" option is to use a monospaced font, such as Courier, which is used in the block below:
iiiiiiiiii
WWWWWWWWWW
But that's a boring font!. So what you need is to use the ìmagettfbbox (Image True Type Font Bounding Box" function http://php.net/manual/en/function.imagettfbbox.php) on each line to get the width. You need to run this function one line at a time, in decreasing sizes until you get the size you need.
A pseduo bit of code (please note: written off-hand and not tested, you will need to juggle it to make it perfect):
$targetPixelWidth = 300;
$maximumChactersPerLine = 200; // Make this larger then you expect, but too large will slow it down!
$textToDisplay = "Your long bit of text goes here"
$aLinesToDisplay = array();
while (strlen(textToDisplay) > 0) {
$hasTextToShow = false;
$charactersToTest = $maximumChactersPerLine;
while (!$hasTextToShow && $maximumChactersPerLine>0) {
$wrappedText = wordwrap($textToDisplay, $maximumChactersPerLine);
$aSplitWrappedText = explode("\n", $wrappedText);
$firstLine = trim($aSplitWrappedText[0]);
if (strlen($firstLine) == 0) {
// Fallback to "default"
$charactersToTest = 0;
} else {
$aBoundingBox = imagettfbbox($fontSize, 0, $firstLine, $yourTTFFontFile);
$width = abs($aBoundingBox[2] - $aBoundingBox[0]);
if ($width <= $targetPixelWidth) {
$hasTextToShow = true;
$aLinesToDisplay[] = $firstLine;
$textToDisplay = trim(substr($textToDisplay, strlen($firstLine));
} else {
--$charactersToTest;
}
}
}
if (!$hasTextToShow) {
// You need to handle this by getting SOME text (e.g. first word) and decreasing the length of $textToDisplay, otherwise you'll stay in the loop forever!
$firstLine = ???; // Suggest split at first "space" character (Use preg_split on \s?)
$aLinesToDisplay[] = $firstLine;
$textToDisplay = trim(substr($textToDisplay, strlen($firstLine));
}
}
// Now run the "For Each" to print the lines.
Caveat: The TTF Bounding box function is not perfect either - so allow a bit of "lee way", but you'll still end up with far, far better results that you are doing above (i.e. +-10 pixels). It also depends on the font-file kerning (gaps between letters) information. A bit of Goggling and reading the comments in the manual will help you get more accurate results if you need it.
You should also optimize the function above (start with 10 characters and increase, taking the last string that fits may get you an faster answer over decreasing until something fits, and reduce the number of strlen calls for example).
Addendum in response to comment "Can you expand on "the TTF Bounding box function is not perfect either"?" (reply is too long for a comment)
The function relies on the "kerning" information in the font. For example, you want V to sit closer to A (VA - see how they "overlap" slightly) than you would V and W (VW - see how the W starts after the V). There are lots of rules embedded in the fonts regarding that spacing. Some of those rules also say "I know the 'box' starts at 0, but for this letter you need to start drawing at -3 pixels".
PHP does it's best to read the rules, but gets some wrong sometimes and therefore gives you the wrong dimensions. It's the reason as why you might tell PHP to write from "0,0" but it actually starts at "-3,0" and appears to cut off the font. The easiest solution is to allow a few pixels grace.
Yes, it's a well noted "issue" (https://www.google.com/webhp?q=php%20bounding%20box%20incorrect)

How do I to make watermark image with PHP and GD like envato photodune preview image?

I would like to create watermark with PHP and GD library.
I would like to do repeating the watermark logo with auto margin(space) between each repeated logo, also zigzag position.
Is it possible to create dash line watermark that connect each watermark logo?
The result would be like this:
I have finished the zigzag by using loop function and odd even clause.
/*
* utils
$widthWatermark = imagesx($logo);
$heightWatermark = imagesy($logo);
$widthPhoto = imagesx($output);
$heightPhoto = imagesy($output);
*/
// $xLogoPosition = 0;
// $yLogoPosition = 0;
$__xRepeat = ceil($widthPhoto / $widthWatermark);
$__yRepeat = ceil($heightPhoto / $heightWatermark);
$margin = (int)self::$option['margin'];
for ($i = 0; $i <= $__xRepeat; $i++) {
if ($i % 2 === 0) {
$pre_ii = 1;
} else {
$pre_ii = 0;
}
for ($ii = 0; $ii <= $__yRepeat; $ii++) {
$ii_zero = $ii - $pre_ii;
if ($ii_zero % 2 === 0) {
$y_xindent = $widthWatermark;
}else{
$y_xindent = 0;
}
$this->imagecopymerge_alpha($output, $logo, ($xLogoPosition + $widthWatermark * $i + $y_xindent), ($yLogoPosition + $widthWatermark * $ii), 0, 0, ImageSX($logo), ImageSY($logo), self::$option['opacity']);
}
}
now I stuck at how to create dashed line that have diagonal position that connect to each other logo.
I have a hint from http://php.net/manual/en/function.imagedashedline.php
but I don't how to use and combine it with my previous code that generate zigzag logo
Edit
It turns out that PHP/GD actually has a function - imagesettile() - specifically to handle this situation.
I've modified my original answer to account for this:
<?php
// create php image of a 'dashed cross'.
$crossW = $crossH = 200;
$cross = imagecreatetruecolor($crossW, $crossH);
imagefill($cross, 0, 0, 0x7fff00ff); // transparent magenta.
imagesetthickness($cross, 1);
imagesetstyle(
$cross,
array_merge(
array_fill(0, 3, 0x7fff00ff), // transparent magenta.
array_fill(0, 8, 0x60ffffff) // partially-transparent white.
)
);
imageline($cross, 0, 0, $crossW, $crossH, IMG_COLOR_STYLED);
imageline($cross, $crossW, 0, 0, $crossH, IMG_COLOR_STYLED);
$imageFile = 'wm2.jpg';
// open the image file to be watermarked and store its height and width.
$image = imagecreatefromjpeg($imageFile);
$imWidth = imagesx($image);
$imHeight = imagesy($image);
// apply the cross pattern as a tile to the image file.
imagesettile($image, $cross);
imagefilledrectangle($image, 0, 0, $imWidth, $imHeight, IMG_COLOR_TILED);
header('Content-type: image/png');
imagepng($image);
imagedestroy($cross);
imagedestroy($image);
exit;
Input:
Result:

Size and position of one image in another via PHP

I have two images(small and big). Big one contains a small one. Like if the small one is a photo and a big one is a page from the photo album.
How do I get coordinates of that small image in the big one using PHP? And also I need to know the size of that image in big one...so just a(x,y) coordinate of any angle and sizes of sides of that presentation of the small image...
(x,y, width, height)
I've already asked the question like that and got a brilliant answer (here) but I've forgot to mention over there that the size of a small image could be different from the the size of that image in the big image...
And also if it is possible to deal with a presentation of that small image in the big image can have something covering one of its angles... Like in this example:
Small image:
Big image:
Small image always has just a rectangular shape.
Alright, this answer does not perfectly answer the question, but it should give you a good start! I know I repeat myself in the code, but my goal was simply to get something working so you can build on it, this isn't production code!
Preconditions
Starting with the large picture:
We need to find as best as possible the position of this other picture:
I decided to break the process into many substeps, which you could improve or remove depending on what you want the code to do.
For testing purposes, I did test my algorithm on different input images so you'll see a variable defining what file to load...
We start with:
function microtime_float()
{
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
$time_start = microtime_float();
$largeFilename = "large.jpg";
$small = imagecreatefromjpeg("small.jpg");
$large = imagecreatefromjpeg($largeFilename);
and
imagedestroy($small);
imagedestroy($large);
$time_end = microtime_float();
echo "in " . ($time_end - $time_start) . " seconds\n";
To have a good idea on our performances. Luckily, most of the algorithm was pretty fast so I didn't have to optimize more.
Background Detection
I started by detecting the background color. I assumed that the background color would be the color most present in the picture. To do this, I only counted how many references of each color I could find in the large picture, sort it with decending values and took the first one as the background color (should allow the code to be adaptable if you changed the source pictures)
function FindBackgroundColor($image)
{
// assume that the color that's present the most is the background color
$colorRefcount = array();
$width = imagesx($image);
$height = imagesy($image);
for($x = 0; $x < $width; ++$x)
{
for($y = 0; $y < $height; ++$y)
{
$color = imagecolorat($image, $x, $y);
if(isset($colorRefcount[$color]))
$colorRefcount[$color] = $colorRefcount[$color] + 1;
else
$colorRefcount[$color] = 1;
}
}
arsort($colorRefcount);
reset($colorRefcount);
return key($colorRefcount);
}
$background = FindBackgroundColor($large); // Should be white
Partitionning
My first step was to try to find all the regions where non background pixels were. With a little padding, I was able to group regions into bigger regions (so that a paragraph would be a single region instead of multiple individual letters). I started with a padding of 5 and got good enough results so I stuck with it.
This is broken into multiple function calls, so here we go:
function FindRegions($image, $backgroundColor, $padding)
{
// Find all regions within image where colors are != backgroundColor, including a padding so that adjacent regions are merged together
$width = imagesx($image);
$height = imagesy($image);
$regions = array();
for($x = 0; $x < $width; ++$x)
{
for($y = 0; $y < $height; ++$y)
{
$color = imagecolorat($image, $x, $y);
if($color == $backgroundColor)
{
continue;
}
if(IsInsideRegions($regions, $x, $y))
{
continue;
}
$region = ExpandRegionFrom($image, $x, $y, $backgroundColor, $padding);
array_push($regions, $region);
}
}
return $regions;
}
$regions = FindRegions($large, $background, 5);
Here, we iterate on every pixel of the picture, if its background color, we discard it, otherwise, we check if its position is already present in a region we found, if that's the case, we skip it too. Now, if we didn't skip the pixel, it means that it's a colored pixel that should be part of a region, so we start ExpandRegionFrom this pixel.
The code to check if we're inside a region is pretty simple:
function IsInsideRegions($regions, $x, $y)
{
foreach($regions as $region)
{
if(($region["left"] <= $x && $region["right"] >= $x) &&
($region["bottom"] <= $y && $region["top"] >= $y))
{
return true;
}
}
return false;
}
Now, the expanding code will try to grow the region in each direction and will do so as long as it found new pixels to add to the region:
function ExpandRegionFrom($image, $x, $y, $backgroundColor, $padding)
{
$width = imagesx($image);
$height = imagesy($image);
$left = $x;
$bottom = $y;
$right = $x + 1;
$top = $y + 1;
$expanded = false;
do
{
$expanded = false;
$newLeft = ShouldExpandLeft($image, $backgroundColor, $left, $bottom, $top, $padding);
if($newLeft != $left)
{
$left = $newLeft;
$expanded = true;
}
$newRight = ShouldExpandRight($image, $backgroundColor, $right, $bottom, $top, $width, $padding);
if($newRight != $right)
{
$right = $newRight;
$expanded = true;
}
$newTop = ShouldExpandTop($image, $backgroundColor, $top, $left, $right, $height, $padding);
if($newTop != $top)
{
$top = $newTop;
$expanded = true;
}
$newBottom = ShouldExpandBottom($image, $backgroundColor, $bottom, $left, $right, $padding);
if($newBottom != $bottom)
{
$bottom = $newBottom;
$expanded = true;
}
}
while($expanded == true);
$region = array();
$region["left"] = $left;
$region["bottom"] = $bottom;
$region["right"] = $right;
$region["top"] = $top;
return $region;
}
The ShouldExpand methods could have been written in a cleaner fashion, but I went for something fast to prototype with:
function ShouldExpandLeft($image, $background, $left, $bottom, $top, $padding)
{
// Find the farthest pixel that is not $background starting at $left - $padding closing in to $left
for($x = max(0, $left - $padding); $x < $left; ++$x)
{
for($y = $bottom; $y <= $top; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $x;
}
}
}
return $left;
}
function ShouldExpandRight($image, $background, $right, $bottom, $top, $width, $padding)
{
// Find the farthest pixel that is not $background starting at $right + $padding closing in to $right
$from = min($width - 1, $right + $padding);
$to = $right;
for($x = $from; $x > $to; --$x)
{
for($y = $bottom; $y <= $top; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $x;
}
}
}
return $right;
}
function ShouldExpandTop($image, $background, $top, $left, $right, $height, $padding)
{
// Find the farthest pixel that is not $background starting at $top + $padding closing in to $top
for($x = $left; $x <= $right; ++$x)
{
for($y = min($height - 1, $top + $padding); $y > $top; --$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $y;
}
}
}
return $top;
}
function ShouldExpandBottom($image, $background, $bottom, $left, $right, $padding)
{
// Find the farthest pixel that is not $background starting at $bottom - $padding closing in to $bottom
for($x = $left; $x <= $right; ++$x)
{
for($y = max(0, $bottom - $padding); $y < $bottom; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $y;
}
}
}
return $bottom;
}
Now, to see if the algorithm was succesful, I added some debug code.
Debug Rendering
I created a second image to store debug info and store it on disk so I could later see my progress.
Using the following code:
$large2 = imagecreatefromjpeg($largeFilename);
$red = imagecolorallocate($large2, 255, 0, 0);
$green = imagecolorallocate($large2, 0, 255, 0);
$blue = imagecolorallocate($large2, 0, 0, 255);
function DrawRegions($image, $regions, $color)
{
foreach($regions as $region)
{
imagerectangle($image, $region["left"], $region["bottom"], $region["right"], $region["top"], $color);
}
}
DrawRegions($large2, $regions, $red);
imagejpeg($large2, "regions.jpg");
I could validate that my partitioning code was doing a decent job:
Aspect Ratio
I decided to filter out some regions based on aspect ratio (the ratio between the width and the height). Other filtering could be applied such as average pixel color or something, but the aspect ratio check was very fast so I used it.
I simply defined a "window" where regions would be kept, if their aspect ration was between a minimum and maximum value;
$smallAspectRatio = imagesx($small) / imagesy($small);
function PruneOutWrongAspectRatio($regions, $minAspectRatio, $maxAspectRatio)
{
$result = array();
foreach($regions as $region)
{
$aspectRatio = ($region["right"] - $region["left"]) / ($region["top"] - $region["bottom"]);
if($aspectRatio >= $minAspectRatio && $aspectRatio <= $maxAspectRatio)
{
array_push($result, $region);
}
}
return $result;
}
$filterOnAspectRatio = true;
if($filterOnAspectRatio == true)
{
$regions = PruneOutWrongAspectRatio($regions, $smallAspectRatio - 0.1 * $smallAspectRatio, $smallAspectRatio + 0.1 * $smallAspectRatio);
DrawRegions($large2, $regions, $blue);
}
imagejpeg($large2, "aspectratio.jpg");
By adding the DrawRegions call, I now paint in blue the regions that are still in the list as potential positions:
As you can see, only 4 position remains!
Finding the Corners
We're almost done! Now, what I'm doing is looking at the colors in the four corners from the small picture, and try to find the best matching pixel in the corners of the remaining regions. This code has the most potential to fail so if you have to invest time in improving the solution, this code would be a good candidate.
function FindCorners($large, $small, $regions)
{
$result = array();
$bottomLeftColor = imagecolorat($small, 0, 0);
$blColors = GetColorComponents($bottomLeftColor);
$bottomRightColor = imagecolorat($small, imagesx($small) - 1, 0);
$brColors = GetColorComponents($bottomRightColor);
$topLeftColor = imagecolorat($small, 0, imagesy($small) - 1);
$tlColors = GetColorComponents($topLeftColor);
$topRightColor = imagecolorat($small, imagesx($small) - 1, imagesy($small) - 1);
$trColors = GetColorComponents($topRightColor);
foreach($regions as $region)
{
$bottomLeft = null;
$bottomRight = null;
$topLeft = null;
$topRight = null;
$regionWidth = $region["right"] - $region["left"];
$regionHeight = $region["top"] - $region["bottom"];
$maxRadius = min($regionWidth, $regionHeight);
$topLeft = RadialFindColor($large, $tlColors, $region["left"], $region["top"], 1, -1, $maxRadius);
$topRight = RadialFindColor($large, $trColors, $region["right"], $region["top"], -1, -1, $maxRadius);
$bottomLeft = RadialFindColor($large, $blColors, $region["left"], $region["bottom"], 1, 1, $maxRadius);
$bottomRight = RadialFindColor($large, $brColors, $region["right"], $region["bottom"], -1, 1, $maxRadius);
if($bottomLeft["found"] && $topRight["found"] && $topLeft["found"] && $bottomRight["found"])
{
$left = min($bottomLeft["x"], $topLeft["x"]);
$right = max($bottomRight["x"], $topRight["x"]);
$bottom = min($bottomLeft["y"], $bottomRight["y"]);
$top = max($topLeft["y"], $topRight["y"]);
array_push($result, array("left" => $left, "right" => $right, "bottom" => $bottom, "top" => $top));
}
}
return $result;
}
$closeOnCorners = true;
if($closeOnCorners == true)
{
$regions = FindCorners($large, $small, $regions);
DrawRegions($large2, $regions, $green);
}
I tried to find the matching color by increasing "radially" (its basically squares) from the corners until I find a matching pixel (within a tolerance):
function GetColorComponents($color)
{
return array("red" => $color & 0xFF, "green" => ($color >> 8) & 0xFF, "blue" => ($color >> 16) & 0xFF);
}
function GetDistance($color, $r, $g, $b)
{
$colors = GetColorComponents($color);
return (abs($r - $colors["red"]) + abs($g - $colors["green"]) + abs($b - $colors["blue"]));
}
function RadialFindColor($large, $color, $startx, $starty, $xIncrement, $yIncrement, $maxRadius)
{
$result = array("x" => -1, "y" => -1, "found" => false);
$treshold = 40;
for($r = 1; $r <= $maxRadius; ++$r)
{
$closest = array("x" => -1, "y" => -1, "distance" => 1000);
for($i = 0; $i <= $r; ++$i)
{
$x = $startx + $i * $xIncrement;
$y = $starty + $r * $yIncrement;
$pixelColor = imagecolorat($large, $x, $y);
$distance = GetDistance($pixelColor, $color["red"], $color["green"], $color["blue"]);
if($distance < $treshold && $distance < $closest["distance"])
{
$closest["x"] = $x;
$closest["y"] = $y;
$closest["distance"] = $distance;
break;
}
}
for($i = 0; $i < $r; ++$i)
{
$x = $startx + $r * $xIncrement;
$y = $starty + $i * $yIncrement;
$pixelColor = imagecolorat($large, $x, $y);
$distance = GetDistance($pixelColor, $color["red"], $color["green"], $color["blue"]);
if($distance < $treshold && $distance < $closest["distance"])
{
$closest["x"] = $x;
$closest["y"] = $y;
$closest["distance"] = $distance;
break;
}
}
if($closest["distance"] != 1000)
{
$result["x"] = $closest["x"];
$result["y"] = $closest["y"];
$result["found"] = true;
return $result;
}
}
return $result;
}
As you can see, I'm no PHP expert, I didn't know there was a built in function to get the rgb channels, oops!
Final Call
So now that the algorithm ran, let's see what it found using the following code:
foreach($regions as $region)
{
echo "Potentially between " . $region["left"] . "," . $region["bottom"] . " and " . $region["right"] . "," . $region["top"] . "\n";
}
imagejpeg($large2, "final.jpg");
imagedestroy($large2);
The output (which is pretty close to the real solution):
Potentially between 108,380 and 867,827
in 7.9796848297119 seconds
Giving this picture (the rectangle between 108,380 and 867,827 is drawn in green)
Hope this helps!
My solution work if there is no color (except white and black around the image, but you can modify the script to get it work differently)
$width = imagesx($this->img_src);
$height = imagesy($this->img_src);
// navigate through pixels of image
for ($y = 0; $y < $height; $y++) {
for ($x=0; $x < $width; $x++) {
list($r, $g, $b) = imagergbat($this->img_src, $x, $y);
$black = 0.1;
$white = 0.9;
// calculate if the color is next to white or black, if not register it as a good pixel
$gs = (($r / 3) + ($g / 3) + ($b / 3);
$first_pixel = array();
if ($gs > $white && $gs < $black) {
// get coordinate of first pixel (left top)
if (empty($first_pixel))
$first_pixel = array($x, $y);
// And save last_pixel each time till the last one
$last_pixel = array($x, $y);
}
}
}
And you get the coordinates of your image. You have just to crop it after this.

How to create Double bar diagram using fpdf php?

I'm using FPDF in my php project. I would like to have PDF version Double bar diagram like above image in my project. There's a way that FPDF can create Pie chart and Bar diagram in http://www.fpdf.org/en/script/script28.php. But it's not double bar diagram like what I want to get. Anyone have an idea how to create Double bar diagram using FPDF in PHP?
Many Thanks !!!
Probably you mean "COLUMN CHARTS"
It seems that there is no method to create column charts, so I tried to adapt the existing bar chart. Unfortunately I have no time to develop it further.
try this (making the necessary changes):
<?php
require('diag/sector.php');
class PDF_Diag extends PDF_Sector {
var $legends;
var $wLegend;
var $sum;
var $NbVal;
function ColumnChart($w, $h, $data, $format, $color=null, $maxVal=0, $nbDiv=4)
{
// RGB for color 0
$colors[0][0] = 155;
$colors[0][1] = 75;
$colors[0][2] = 155;
// RGB for color 1
$colors[1][0] = 0;
$colors[1][1] = 155;
$colors[1][2] = 0;
// RGB for color 2
$colors[2][0] = 75;
$colors[2][1] = 155;
$colors[2][2] = 255;
// RGB for color 3
$colors[3][0] = 75;
$colors[3][1] = 0;
$colors[3][2] = 155;
$this->SetFont('Courier', '', 10);
$this->SetLegends($data,$format);
// Starting corner (current page position where the chart has been inserted)
$XPage = $this->GetX();
$YPage = $this->GetY();
$margin = 2;
// Y position of the chart
$YDiag = $YPage + $margin;
// chart HEIGHT
$hDiag = floor($h - $margin * 2);
// X position of the chart
$XDiag = $XPage + $margin;
// chart LENGHT
$lDiag = floor($w - $margin * 3 - $this->wLegend);
if($color == null)
$color=array(155,155,155);
if ($maxVal == 0)
{
foreach($data as $val)
{
if(max($val) > $maxVal)
{
$maxVal = max($val);
}
}
}
// define the distance between the visual reference lines (the lines which cross the chart's internal area and serve as visual reference for the column's heights)
$valIndRepere = ceil($maxVal / $nbDiv);
// adjust the maximum value to be plotted (recalculate through the newly calculated distance between the visual reference lines)
$maxVal = $valIndRepere * $nbDiv;
// define the distance between the visual reference lines (in milimeters)
$hRepere = floor($hDiag / $nbDiv);
// adjust the chart HEIGHT
$hDiag = $hRepere * $nbDiv;
// determine the height unit (milimiters/data unit)
$unit = $hDiag / $maxVal;
// determine the bar's thickness
$lBar = floor($lDiag / ($this->NbVal + 1));
$lDiag = $lBar * ($this->NbVal + 1);
$eColumn = floor($lBar * 80 / 100);
// draw the chart border
$this->SetLineWidth(0.2);
$this->Rect($XDiag, $YDiag, $lDiag, $hDiag);
$this->SetFont('Courier', '', 10);
$this->SetFillColor($color[0],$color[1],$color[2]);
$i=0;
foreach($data as $val)
{
//Column
$yval = $YDiag + $hDiag;
$xval = $XDiag + ($i + 1) * $lBar - $eColumn/2;
$lval = floor($eColumn/(count($val)));
$j=0;
foreach($val as $v)
{
$hval = (int)($v * $unit);
$this->SetFillColor($colors[$j][0], $colors[$j][1], $colors[$j][2]);
$this->Rect($xval+($lval*$j), $yval, $lval, -$hval, 'DF');
$j++;
}
//Legend
$this->SetXY($xval, $yval + $margin);
$this->Cell($lval, 5, $this->legends[$i],0,0,'C');
$i++;
}
//Scales
for ($i = 0; $i <= $nbDiv; $i++)
{
$ypos = $YDiag + $hRepere * $i;
$this->Line($XDiag, $ypos, $XDiag + $lDiag, $ypos);
$val = ($nbDiv - $i) * $valIndRepere;
$ypos = $YDiag + $hRepere * $i;
$xpos = $XDiag - $margin - $this->GetStringWidth($val);
$this->Text($xpos, $ypos, $val);
}
}
function SetLegends($data, $format)
{
$this->legends=array();
$this->wLegend=0;
$this->NbVal=count($data);
}
}
$pdf = new PDF_Diag();
$pdf->AddPage();
$data[0] = array(470, 490, 90);
$data[1] = array(450, 530, 110);
$data[2] = array(420, 580, 100);
// Column chart
$pdf->SetFont('Arial', 'BIU', 12);
$pdf->Cell(210, 5, 'Chart Title', 0, 1, 'C');
$pdf->Ln(8);
$valX = $pdf->GetX();
$valY = $pdf->GetY();
$pdf->ColumnChart(110, 100, $data, null, array(255,175,100));
//$pdf->SetXY($valX, $valY);
$pdf->Output();
?>

line break problem with MultiCell in FPDF

I am using java port of fpdf. I am encountering fowwlowing errors.
1).When i call multicell 2 times every time the text is printed on a new line.
MultiCell(0, 1, "abcd", currentBorders, Alignment.LEFT, false); //prints on one line
MultiCell(0, 1, "efg", currentBorders, Alignment.LEFT, false); //prints on next line
I want that there is no line break after the call to multicell. How can i do it?
2)If i do the following thing then some part of my string gets printed on one line and some on next.
MultiCell(getStringWidth(myString), 1, myStringcurrentBorders, Alignment.LEFT, false);
3)If i do the following thing then there are many blank lines after the line on which myString is printed. It works correctly if i use one 1 ans second parameter
MultiCell(0, myFontSize, "123456", currentBorders, Alignment.LEFT, false);
What is the problem?
I would get the current Y position before writing the MultiCell and then move the "cursor" back to that Y position after the MultiCell generation. Like this:
$current_y = $pdf->GetY();
$current_x = $pdf->GetX();
$cell_width = 50;
MultiCell($cell_width, 1, "abcd", currentBorders, Alignment.LEFT, false);
$pdf->SetXY($current_x + $cell_width, $current_y);
$current_x = $pdf->GetX();
MultiCell($cell_width, 1, "abcd", currentBorders, Alignment.LEFT, false);
Something like that.
I created a new method called MultiAlignCell. It takes the same parameters as MultiCell but with the added ln field from Cell. You can add it to your extended FPDF class.
/**
* MultiCell with alignment as in Cell.
* #param float $w
* #param float $h
* #param string $text
* #param mixed $border
* #param int $ln
* #param string $align
* #param boolean $fill
*/
private function MultiAlignCell($w,$h,$text,$border=0,$ln=0,$align='L',$fill=false)
{
// Store reset values for (x,y) positions
$x = $this->GetX() + $w;
$y = $this->GetY();
// Make a call to FPDF's MultiCell
$this->MultiCell($w,$h,$text,$border,$align,$fill);
// Reset the line position to the right, like in Cell
if( $ln==0 )
{
$this->SetXY($x,$y);
}
}
I have modified the MultiCell method, it works as the above answer, and you can use the method in the same way as the Cell method.
function MultiCell($w, $h, $txt, $border=0, $ln=0, $align='J', $fill=false)
{
// Custom Tomaz Ahlin
if($ln == 0) {
$current_y = $this->GetY();
$current_x = $this->GetX();
}
// Output text with automatic or explicit line breaks
$cw = &$this->CurrentFont['cw'];
if($w==0)
$w = $this->w-$this->rMargin-$this->x;
$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;
$s = str_replace("\r",'',$txt);
$nb = strlen($s);
if($nb>0 && $s[$nb-1]=="\n")
$nb--;
$b = 0;
if($border)
{
if($border==1)
{
$border = 'LTRB';
$b = 'LRT';
$b2 = 'LR';
}
else
{
$b2 = '';
if(strpos($border,'L')!==false)
$b2 .= 'L';
if(strpos($border,'R')!==false)
$b2 .= 'R';
$b = (strpos($border,'T')!==false) ? $b2.'T' : $b2;
}
}
$sep = -1;
$i = 0;
$j = 0;
$l = 0;
$ns = 0;
$nl = 1;
while($i<$nb)
{
// Get next character
$c = $s[$i];
if($c=="\n")
{
// Explicit line break
if($this->ws>0)
{
$this->ws = 0;
$this->_out('0 Tw');
}
$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
$i++;
$sep = -1;
$j = $i;
$l = 0;
$ns = 0;
$nl++;
if($border && $nl==2)
$b = $b2;
continue;
}
if($c==' ')
{
$sep = $i;
$ls = $l;
$ns++;
}
$l += $cw[$c];
if($l>$wmax)
{
// Automatic line break
if($sep==-1)
{
if($i==$j)
$i++;
if($this->ws>0)
{
$this->ws = 0;
$this->_out('0 Tw');
}
$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
}
else
{
if($align=='J')
{
$this->ws = ($ns>1) ? ($wmax-$ls)/1000*$this->FontSize/($ns-1) : 0;
$this->_out(sprintf('%.3F Tw',$this->ws*$this->k));
}
$this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill);
$i = $sep+1;
}
$sep = -1;
$j = $i;
$l = 0;
$ns = 0;
$nl++;
if($border && $nl==2)
$b = $b2;
}
else
$i++;
}
// Last chunk
if($this->ws>0)
{
$this->ws = 0;
$this->_out('0 Tw');
}
if($border && strpos($border,'B')!==false)
$b .= 'B';
$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
$this->x = $this->lMargin;
// Custom Tomaz Ahlin
if($ln == 0) {
$this->SetXY($current_x + $w, $current_y);
}
}
in my case I didn't create any method, I just set X and Y and then at the end of the line I reset. It works perfectly too.
$pdf->SetFont('times', 'B', 10);
$x = $pdf->GetX();
$y = $pdf->GetY();
$pdf->MultiCell($etiquetas_largura, $etiquetas_altura, $campos[$linha]['B1COD'], 0, 'L', 0, 0, $x, $y, true, 0, false, true, 0);
$y = $y + 5;
$pdf->SetFont('times', '', 10);
$pdf->MultiCell($etiquetas_largura, $etiquetas_altura, $campos[$linha]['B1DESC'], 0, 'L', 0, 0, $x, $y, true, 0, false, true, 0);
// resete x y
$pdf->SetXY($x + $etiquetas_largura, $y - 5);
#Muhammad Abdul Rahim and #tomazahlin have have provided good methods. The problem they only sort out the line break problem in a single cell. They don't match the height of the subject cell with other cells in the same row. Using GetY() gets complicated if you are dealing with dynamic tables. The simplest solution I have found is identifying the column likely to have overflowing text and using it as a reference.
$l=strlen($string_of_reference_cell);
$h = ceil($l/$cell_width*1.5)*preferred_normal_height;//1.5 is a loading for allowance`depending on font size.
$pdf->cell(20,$h,$string,1,0);
$pdf->MultiAlignCell(50,5,$string_of_reference_cell,1,0);// 5 is the preferred normal height
$pdf->Cell(23,$h,$string,1,1);
When the pdf is generated, if the string of MultiAlignCell is longer than the cell width, a line break is generated. The resulting height is twice(5 x 2 = 10). The height of ten is asigned to $h. Hence the other cells take the height of $h as well and the entire row gets a uniform height.

Categories