Our dev server was recently upgraded to PHP v5.2.13. With that upgrade we have found that our png images are having kerning (letter spacing) problems. We've tried numerous fonts and haven't found a solution yet.
We are creating images using the GD library and writing text to the images using font files and the imagettftext() or imagefttext() functions.
Has anyone else run into this? Am I misunderstanding something or should this be submitted to PHP as a bug? Are there any cool workarounds I haven't thought of yet?
Here's an example of the new and old tahoma bold. Other fonts (bold and non-bold) have the same problem. Some letters and numbers seem like they're off-center or something like that.
Bad - new PHP
Good - old PHP v5.2.11 (the words are slightly different because this is our dev server and the other one is the live server)
"Tracking" is a similar term for how tight or loose text is set. You might have better luck googling for that, such as this result.
Kerning didn't work for us thanks to the font we used, so we had to put in manual kerning for specific letter combinations like AV, AW ...etc.
/**
* This function lets you write a string with your own letter spacing ($t)
* and kern specific letter combinations like AV
*
* #param type $im An image resource, returned by one of the image creation functions
* #param type $size The font size. Depending on your version of GD, this should be specified as the pixel size (GD1) or point size (GD2).
* #param type $angle The angle in degrees, with 0 degrees being left-to-right reading text. Higher values represent a counter-clockwise rotation. For example, a value of 90 would result in bottom-to-top reading text.
* #param type $t Letter Spacing
* #param type $k Kerning Spacing
* #param type $x The coordinates given by x and y will define the basepoint of the first character (roughly the lower-left corner of the character). This is different from the imagestring(), where x and y define the upper-left corner of the first character. For example, "top left" is 0, 0.
* #param type $y The y-ordinate. This sets the position of the fonts baseline, not the very bottom of the character.
* #param type $color The color index. Using the negative of a color index has the effect of turning off antialiasing. See imagecolorallocate().
* #param type $font The path to the TrueType font you wish to use.
* #param type $text Text to write/print to the image
*/
function ImageTTFTextWithSpacing($im, $size, $angle, $t, $k, $x, $y, $color, $font, $text) {
$numchar = strlen($text);
for($i = 0; $i < $numchar; $i++) {
# Assign character
$char[$i] = substr($text, $i, 1);
//Top is wider than bottom of character
$up = ['Y','V','W'];
//Bottom is wider than top of character
$down = ['A'];
//From the second letter on
if( $i > 0 &&
//check whether we have TOP and BOTTOM type character
//next to each other so we need to adjust spacing
((in_array($char[$i], $up) && in_array($char[$i-1], $down)) ||
(in_array($char[$i-1], $up) && in_array($char[$i], $down)) )) {
$w -= $k;
}
# Write character
imagettftext($im, $size, $angle, ($x + $w + ($i * $t)), $y, $color, $font, $char[$i]);
# Get width of character
$width = imagettfbbox($size, $angle, $font, $char[$i]);
$w = $w + $width[2];
}
}
Related
In a recent competition I was given the task to extract binary data (another PNG) from a PNG image file's alpha channel. The data was encoded in such a way that if I read the values in the alpha channel for each pixel from the top left (e.g. 80,78,71,13,10,26,10) up to a specific point then the resulting data would form another image.
Initially I tried to complete this task using PHP, but I hit a roadblock that I could not overcome. Consider the code below:
function pad($hex){
return strlen($hex) < 2 ? "0$hex" : $hex;
}
$channel = '';
$image = 'image.png';
$ir = imagecreatefrompng($image);
imagesavealpha($ir, true);
list($width, $height) = getimagesize($image);
for ($y = 0; $y < $height; $y++){
for ($x = 0; $x < $width; $x++){
$pixel = imagecolorat($ir, $x, $y);
$colors = imagecolorsforindex($ir, $pixel);
$a = pad(dechex($colors['alpha']));
$channel.= $a;
}
}
After running this, I noticed that the output did not contain the PNG magic number, and I just didn't know what went wrong. After a bit of digging I found out that $colors['alpha'] only contained values less than or equal to 127. Due to the data having been encoded with a 0-255 range in mind, I could not find a way to extract it, and ended up (successfully) solving the problem with node.js instead.
So, my question is: Is there any way I could have read the PNG file's alpha channel that would have returned the values in a 0 to 255 range as opposed to 0-127, or is this a hard-coded limitation of PHP and/or GD?
For the record, I tried to use ($colors['alpha']/127)*255 in order to try and forge the value in the incorrect range to the correct one, but to no avail.
It is a limitation of GD. According to https://bitbucket.org/libgd/gd-libgd/issues/132/history-behind-7-bit-alpha-component, in GD source it says:
gd has only 7 bits of alpha channel resolution, and 127 is
transparent, 0 opaque. A moment of convenience, a lifetime of
compatibility.
If you install ImageMagick PHP extension, you can get the alpha value between 0-255 for a pixel (let's say with x=300 and y=490) like this:
$image = new Imagick(__DIR__ . DIRECTORY_SEPARATOR . 'image.png');
$x = 300;
$y = 490;
$pixel = $image->getImagePixelColor($x, $y);
$colors = $pixel->getColor(true);
echo 'Alpha value: ' . round($colors['a'] * 255, 0);
ImageMagick: https://www.imagemagick.org
ImageMagick for PHP (called Imagick): http://php.net/manual/en/book.imagick.php
In your code:
list($width, $height) = getimagesize($image);
references a variable '$image' that was not defined in the code.
using getimagesize means $image is a filename.
so this line get's width and height from a filename.
This line broke the code when I tested it.
You already have your answer, but this is for posterity.
Considering the code, I feel:
$width=imagesx ($ir);
$height=imagesy ($ir);
Would be more logical (and actually works)
I'm trying to write a script that generates a PNG image from the text, size and the font it gets in the $_GET arguments, but I can't figure out how to make size of the image fit exactly to the text. I'm already using imagettfbox:
$widthPx = abs($ttfBox[2] - $ttfBox[0]);
$heightPx = abs($ttfBox[1] - $ttfBox[7]);
which probably gives me the correct measurements but when I draw my text, it gets a little bit out of bounds. For example if I try to draw an "a" using arial.ttf its at least 5 pixels out of bounds. Is there a way to draw a text of any font exactly fitting to the image without testing out?
$text = $_GET["text"];
$cmToPixel = 15.0;
$sizeCm = floatval($_GET["sizeCm"]);
$sizePx = $cmToPixel * $sizeCm;
$fontFile = "fonts/".pathinfo($_GET["font"])["filename"].".".pathinfo($_GET["font"])["extension"];
if(!file_exists($fontFile)){
die;
}
$ttfBox = imagettfbbox($sizePx, 0, $fontFile, $text);
$widthPx = abs($ttfBox[2] - $ttfBox[0]);
$heightPx = abs($ttfBox[1] - $ttfBox[7]);
$image = ImageCreate($widthPx, $heightPx);
$x = $ttfBox[0] + (imagesx($image)-$ttfBox[4] )/ 2 - 0;
$y = $ttfBox[1] + (imagesy($image) / 2) - ($ttfBox[5] / 2) - 5;
ImageRectangle($image,0,0,imagesx($image),imagesy($image), ImageColorAllocate($image,255,255,255));
imagettftext($image, $sizePx,0,$x,$y, ImageColorAllocate($image, 0, 0, 0), $fontFile, $text);
header("Content-Type: image/png");
ImagePng($image);
ImageDestroy($image);
Your calculations from the bounding box are off. This works:
<?php
/*-
* $MirOS: www/mk/ttf2png,v 1.8 2016/11/02 16:16:26 tg Exp $
*-
* Copyright (c) 2009, 2016
* mirabilos <m#mirbsd.org>
*
* Provided that these terms and disclaimer and all copyright notices
* are retained or reproduced in an accompanying document, permission
* is granted to deal in this work without restriction, including un-
* limited rights to use, publicly perform, distribute, sell, modify,
* merge, give away, or sublicence.
*
* This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to
* the utmost extent permitted by applicable law, neither express nor
* implied; without malicious intent or gross negligence. In no event
* may a licensor, author or contributor be held liable for indirect,
* direct, other damage, loss, or other issues arising in any way out
* of dealing in the work, even if advised of the possibility of such
* damage or existence of a defect, except proven that it results out
* of said person's immediate fault when using the work as intended.
*-
* Syntax:
* php ttf2png [text [size [/path/to/font.ttf]]] >out.png
*/
if (!function_exists('gd_info'))
die("Install php5-gd first.");
$gd = gd_info();
if ($gd["FreeType Support"] == false)
die("Compile php5-gd with FreeType 2 support.");
$font = "/usr/src/www/files/FNT/GenI102.ttf";
$fontsize = 30;
$text = "EINVAL";
if (isset($argv[1]))
$text = $argv[1];
if (isset($argv[2]))
$fontsize = $argv[2];
if (isset($argv[3]))
$font = $argv[3];
// Get bounding box
$bbox = imageftbbox($fontsize, 0, $font, $text);
// Transform coordinates into width+height and position
$ascender = abs($bbox[7]);
$descender = abs($bbox[1]);
$size_w = abs($bbox[0]) + abs($bbox[2]);
$size_h = $ascender + $descender;
$x = -$bbox[0];
$y = $ascender;
// Create image
$im = imagecreatetruecolor($size_w, $size_h);
// Allocate colours
$bgcol = imagecolorallocate($im, 0x24, 0x24, 0x24);
$fgcol = imagecolorallocate($im, 0xFF, 0xFF, 0xFF);
// Fill image with background colour
imagefilledrectangle($im, 0, 0, $size_w - 1, $size_h - 1, $bgcol);
// Render text into image
imagefttext($im, $fontsize, 0, $x, $y, $fgcol, $font, $text);
// Convert true colour image (needed for above) to palette image
imagetruecolortopalette($im, FALSE, 256);
// Output created image
imagepng($im, NULL, 9);
exit(0);
If you write multiple strings on the same line and need to calculate the total height and offset of the line, it’s the maximum of all ascenders plus the maximum of all descenders, and $y for all imagefttext calls for that line is similarily the maximum of all ascenders.
I have a waveform player but uses flash to generate a PNG waveform which isn't even accurate so I decided to use PHP and installed LAME Encoder on my CentOS 6.4. I found this script on github 'afreiday/php-waveform-png' and it works, based on the file you upload you get the waveform of the audiotrack. The only problem is that it's 'peaky'. An example:
As you can see in the example (a EDM track with a singing part and bassdrop hard clap part) it's just not very useful, you can't scroll to the break because you can't see where it is. Unless you look closely at the very red area in the middle. So I know it's out there and the script works, but as I said before, it's just to 'peaky' and random.
What I want is something like the picture below where you can obviously see the different parts in the track (taken from zippyshare with the same audiofile:
The part of the code where it draws the lines seperately looks like this:
$v = (int) ($data / 255 * $height);
if (!($v / $height == 0.5 && !$draw_flat))
// draw the line on the image using the $v value and centering it vertically on the canvas
imageline(
$img,
// x1
(int) ($data_point / DETAIL),
// y1: height of the image minus $v as a percentage of the height for the wave amplitude
$height * $wav - $v,
// x2
(int) ($data_point / DETAIL),
// y2: same as y1, but from the bottom of the image
$height * $wav - ($height - $v),
imagecolorallocate($img, $r, $g, $b)
);
Where $v is the volume.
Now I tried adding something to get rid of the peaks but that didn't work out well:
if(isset($previous)) {
$diff = abs($v-$previous);
$perc = $diff / $v * 100;
if($perc>40) {
$v = $previous;
}
}
Ofcourse that didn't do the trick but I thought I had something to work with there but it's not. Anybody an idea on how to solve this problem or had a similar experience?
If an alternative to that script is better, let me know. Can't figure this out.
Given the result of a call to imagettfbbox(), what is the correct, pixel-perfect point to provide to imagettftext() such that the text will not extend beyond its bounding box?
I am determining the width/height and x/y of the baseline from the bounding box like this:
$box = imagettfbbox($size, $angle, $font, $text);
$boxXCoords = array($box[0], $box[2], $box[4], $box[6]);
$boxYCoords = array($box[1], $box[3], $box[5], $box[7]);
$boxWidth = max($boxXCoords) - min($boxXCoords);
$boxHeight = max($boxYCoords) - min($boxYCoords);
$boxBaseX = abs(min($boxXCoords));
$boxBaseY = abs(min($boxYCoords));
I then draw a filled rectangle on my image of the dimensions of the bounding box:
imagefilledrectangle($image, 0, 0, $boxWidth - 1, $boxHeight - 1, $color);
After that, I draw the text:
imagettftext($image, $size, $angle, $boxBaseX, $boxBaseY, $color, $font, $text);
However, this causes the text to extend beyond the rectangle by a pixel or two. I have seen several attempts to fix this issue on PHP's imagettfbbox() documentation, but they all just suggest substracting a pixel or two here and there, which seems like a hack to me. What's happening here, and why should we need to fudge the numbers to get things right?
I believe there is no perfect way to place text with single-pixel precision on an image based on what imagettfbbox() returns and also using .ttf non-monospaced fonts. Over at the PHP manual many users have posted ways to accomplish this (with and without fudging the numbers); I recommend using jodybrabec's simple function over at the PHP manual, which calculates the exact bounding box. I have tested this one and only in extreme cases is the text positioned at most 1 pixel off in one direction. Nonetheless, if you add some padding (even if it is just 2 or 3 pixels) to your image your text will be within the dimensions of the image 100% of the time.
What happens when you don't subtract one from each of the dimensions in this line:
imagefilledrectangle($image, 0, 0, $boxWidth - 1, $boxHeight - 1, $color);
and instead do this:
imagefilledrectangle($image, 0, 0, $boxWidth, $boxHeight, $color);
The SlightlyMagic HQ Card Generator project renders cards for the strategy card game Magic: the Gathering. The generator is powered by PHP with an advanced text rendering engine built in. I don't know about logic behind the calculations, but the renderer is dead accurate for the purposes of this application. Here's the function that calculates proper bounding boxes (HQ Card Generator 8.x/scripts/classes/font.php):
private function convertBoundingBox ($bbox) {
// Transform the results of imagettfbbox into usable (and correct!) values.
if ($bbox[0] >= -1)
$xOffset = -abs($bbox[0] + 1);
else
$xOffset = abs($bbox[0] + 2);
$width = abs($bbox[2] - $bbox[0]);
if ($bbox[0] < -1) $width = abs($bbox[2]) + abs($bbox[0]) - 1;
$yOffset = abs($bbox[5] + 1);
if ($bbox[5] >= -1) $yOffset = -$yOffset;
$height = abs($bbox[7]) - abs($bbox[1]);
if ($bbox[3] > 0) $height = abs($bbox[7] - $bbox[1]) - 1;
return array(
'width' => $width,
'height' => $height,
'xOffset' => $xOffset, // Using xCoord + xOffset with imagettftext puts the left most pixel of the text at xCoord.
'yOffset' => $yOffset, // Using yCoord + yOffset with imagettftext puts the top most pixel of the text at yCoord.
'belowBasepoint' => max(0, $bbox[1])
);
}
I know this is a little late but imagettfbbox is in points not pixels.
pixel font size in imagettftext instead of point size
I'm looking for the possibility of write a 1 bit bitmap from a string with this content:
$str = "001011000111110000";
Zero is white and One is black.
The BMP file will be 18 x 1 px.
I don't want a 24bit BMP, but a real 1bit BMP.
Does anyone know the header and the conversion method in PHP?
That's a little bit of a strange request :)
So, what you'd want to use here is php-gd, for a start. Generally this is included when installing php on any OS with decent repo's, but just incase it isn't for you, you can get the installation instructions here;
http://www.php.net/manual/en/image.setup.php
First, we'll need to figure out exactly how big the image will need to be in width; height will obviously always be one.
So;
$str = $_GET['str'];
$img_width = strlen($str);
strlen will tell us how many characters are in the $str string, and since we're giving one pixel per character, the amount of characters will give us the required width.
For ease of access, split the string into an array - with each element for each separate pixel.
$color_array = str_split($str);
Now, let's set up a "pointer", for which pixel we're drawing to. It's php so you don't NEED to initalise this, but it's nice to be tidy.
$current_px = (int) 0;
And now you can initialise GD and start making the image;
$im = imagecreatetruecolor($img_width, 1);
// Initialise colours;
$black = imagecolorallocate($im, 0, 0, 0);
$white = imagecolorallocate($im, 255, 255, 255);
// Now, start running through the array
foreach ($color_array as $y)
{
if ($y == 1)
{
imagesetpixel ( $im, $current_px , 1 , $black );
}
$current_px++; // Don't need to "draw" a white pixel for 0. Just draw nothing and add to the counter.
}
This will draw your image, then all you need do is display it;
header('Content-type: image/png');
imagepng($im);
imagedestroy($im);
Note that the $white declaration isn't needed at all - I just left it in to give you an idea of how you declare different colours with gd.
You'll probably need to debug this a bit before using it - it's been a long time since I've used GD. Anyway, hope this helps!
That's NOT a strange request.
I completely agree with the purpose of the question, in fact I have to manage some 1bit monochrome images.
The answer is:
GD is not well documented in PHP website.
When you want to create an image from scratch you have to use imagecreate() or imagecreatetruecolor()
It seems that both of the just mentioned methods (functions) cannot create 1bit images from scratch.
I solved by creating an external image, 1bit monochrome png, loading it with imagecreatefrompng().
In addition: I've just downloaded the official library open source code from hereOfficial Bitbucket Repository
What I've found in gd.h?:
The definition of the upper mentioned functions:
/* Functions to manipulate images. */
/* Creates a palette-based image (up to 256 colors). */
BGD_DECLARE(gdImagePtr) gdImageCreate (int sx, int sy);
/* An alternate name for the above (2.0). */
\#define gdImageCreatePalette gdImageCreate
/* Creates a truecolor image (millions of colors). */
BGD_DECLARE(gdImagePtr) gdImageCreateTrueColor (int sx, int sy);
So the "official" solution is: create a 2 colour palette image with imagecreate() (that wraps the gdImageCreate() GD function).
The "alternative" solution is to create an external image, 1bit monochrome png, and it with imagecreatefrompng() as I said above.
For creating a monochromatic bitmap image without gd or imagemagick you can do so with pack for converting machine byte order to little endian byte order and some functions for handling string, for reference and more details you can check the Wikipedia page bitmap file format or this script on 3v4l.
For this example I will be using a more complex input, this just for better explain how each line should be aligned when creating a bitmap;
<?php
$pixelDataArray = array(
"11101010111",
"10101010101",
"11101110111",
"10001010100",
"10001010100",
);
First to convert the input into a pixel array or bitmap data, each line on the pixel array should be dword/32bit/4bytes aligned.
$pixelWidth = strlen($pixelDataArray[0]);
$pixelHeight = count($pixelDataArray);
$dwordAlignment = 32 - ($pixelWidth % 32);
if ($dwordAlignment == 32) {
$dwordAlignment = 0;
}
$dwordAlignedLength = $pixelWidth + $dwordAlignment;
Now we can proper align the string then convert it to a array of 1 byte integers and after to a binary string.
$pixelArray = '';
foreach (array_reverse($pixelDataArray) as $row) {
$dwordAlignedPixelRow = str_pad($row, $dwordAlignedLength, '0', STR_PAD_RIGHT);
$integerPixelRow = array_map('bindec', str_split($dwordAlignedPixelRow, 8));
$pixelArray .= implode('', array_map('chr', $integerPixelRow));
}
$pixelArraySize = \strlen($pixelArray);
Then lets build the color table
$colorTable = pack(
'CCCxCCCx',
//blue, green, red
255, 255, 255, // 0 color
0, 0, 0 // 1 color
);
$colorTableSize = \strlen($colorTable);
Now the bitmap information header, for better support BITMAPINFOHEADER (40 bytes header) will be used.
$dibHeaderSize = 40;
$colorPlanes = 1;
$bitPerPixel = 1;
$compressionMethod = 0; //BI_RGB/NONE
$horizontal_pixel_per_meter = 2835;
$vertical_pixel_per_meter = 2835;
$colorInPalette = 2;
$importantColors = 0;
$dibHeader = \pack('VVVvvVVVVVV', $dibHeaderSize, $pixelWidth, $pixelHeight, $colorPlanes, $bitPerPixel, $compressionMethod, $pixelArraySize, $horizontal_pixel_per_meter, $vertical_pixel_per_meter, $colorInPalette, $importantColors);
The last part is the file header
$bmpFileHeaderSize = 14;
$pixelArrayOffset = $bmpFileHeaderSize + $dibHeaderSize + $colorTableSize;
$fileSize = $pixelArrayOffset + $pixelArraySize;
$bmpFileHeader = pack('CCVxxxxV', \ord('B'), \ord('M'), $fileSize, $pixelArrayOffset);
Now just concatenate all into a single string and it is ready for use.
$bmpFile = $bmpFileHeader . $dibHeader . $colorTable . $pixelArray;
$bmpBase64File = base64_encode($bmpFile);
?>
<img src="data:image/bitmap;base64, <?= $bmpBase64File ?>" style="image-rendering: crisp-edges;width: 100px;"/>
<img src="data:image/bitmap;base64, Qk1SAAAAAAAAAD4AAAAoAAAACwAAAAUAAAABAAEAAAAAABQAAAATCwAAEwsAAAIAAAAAAAAA////AAAAAACKgAAAioAAAO7gAACqoAAA6uAAAA==" style="image-rendering: crisp-edges;width: 100px;height: ;"/>