I am looking for a way to add a drop shadow to an image using PHP. Before you answer or vote to close: I am not looking to do this using CSS or HTML. I want to generate an image file. This is not a duplicate of this question nor this one.
I am looking for a very specific effect. For example, given this input image:
I want to produce the following image:
TL;DR: This image above was generated using Photoshop's Drop Shadow effect. I want a look very similar to that. For reference, here's the settings our design team used. Ideally, I'd have similar control in my code for angle, distance, opacity, etc:
I have full access to our debian-linus-based servers, so any GD or ImageMagick solution will work. As will any FOSS linux software solution, although I'd prefer a way to do it with IM or GD as those are already installed and don't require new software to be installed.
The shadow must be able to be placed on a transparent, non-rectangular PNG!
I'm asking the question mainly because the scripts and solutions I have found on the web either only produce rectangular shadows, look like total poo, or just plain don't work at all.
Just for the hell of it (I know it was answered and accepted): a few months ago, in response to a question on graphic design stackexchange about recovering a mask from a PNG where the source file was lost I slapped together something which uses PHP GD functions to extract the alpha channel from a transparent PNG. As Joe in a comment mentioned above, you can use the alpha channel as the drop shadow, merely offset it by x and y pixels, and then apply an image convolution blur filter to it, then copymerge the original on top. Following code is probably SLOW and proof of concept, but it is a start and it is in PHP as you originally requested.
<?php
$im = imagecreatefrompng('./images/alphatest_nolayer.png');
$w = imagesx($im);
$h = imagesy($im);
$om = imagecreatetruecolor($w,$h);
for ($x = 0; $x < $w; $x++) {
for ($y = 0; $y < $h; $y++) {
$rgb = imagecolorat($im, $x, $y);
$colors = imagecolorsforindex($im, $rgb);
$orgb = imagecolorallocate($om,$colors['alpha'],$colors['alpha'],$colors['alpha']);
imagesetpixel($om,$x,$y,$orgb);
}
}
header('Content-Type: image/png');
imagepng($om);
imagedestroy($om);
imagedestroy($im);
?>
Actually this can be done with image magick's convert, no need for GD library:
<?php
$cmd = 'convert /path/to/source.png \( +clone -background black -shadow 80x3+4+4 \) \+swap -background none -layers merge +repage /path/to/destination.png';
exec($cmd);
?>
you can play a little with the shadow parameter.
-shadow percent-opacity{xsigma}{+-}x{+-}y{%}
You're not going to be able to do this in PHP without building in a full edge-detector algorithm and significant processing overhead. Look into using GIMP with some script-fu, and let it do the hard work for you.
I used Marc B's advice, and called upon The GIMP to do this for me. If anyone else cares, here's the code I used:
/**
* Call upon The GIMP to apply a dropshadow to a given image.
*
* NOTE: This will overwrite the image file at $filename! Be sure to make a copy
* of this file first if you need one.
*
* #param string $filename
* #param int $offset_x
* #param int $offset_y
* #param float $radius
* #param array $color
* #param int $opacity
* #return type
* #todo Resize the canvas so there's room to apply dropshadows to images which have no whitespace around them.
*/
function apply_gimp_dropshadow($filename,$offset_x=8,$offset_y=8,$radius=15,$color=false,$opacity=40)
{
if(!is_array($color))
$color = array(0,0,0);
$color = join(' ',$color);
$gimpScript = <<<END_OF_SCHEME_CODE_OH_HOW_I_HATE_YOU_SCHEME
(define (dropshadow filename)
(let* (
(image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
(drawable (car (gimp-image-get-active-layer image)))
)
(script-fu-drop-shadow image drawable 8 8 15 '($color) 40 FALSE)
(set! drawable (car (gimp-image-merge-visible-layers image 0)))
(gimp-file-save RUN-NONINTERACTIVE image drawable filename filename)
(gimp-image-delete image)
)
)
(dropshadow "$filename")
(gimp-quit 0)
END_OF_SCHEME_CODE_OH_HOW_I_HATE_YOU_SCHEME;
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
);
$cwd = '/tmp';
$gimp = proc_open('/usr/bin/gimp -i -b -', $descriptorspec, $pipes, $cwd);
if (!is_resource($gimp))
throw new Exception('Could not open a pipe to GIMP');
fwrite($pipes[0], $gimpScript);
fclose($pipes[0]);
$gimpOutput = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$gimpResult = proc_close($gimp);
return $gimpResult;
}
You can use PHPs GD Image Processing Libraries
Here is a tutorial on how to add the shadow effect. However if this doesn't fit your needs i'm sure googling "PHP GD Drop Shadow" will do the trick.
This answer doesn't provide a Python solution, but it does give the generic algorithm required.
Taking images from an outstanding question about drop-shadows that i have, we start with an image:
convert the image to black and white:
apply a gaussian blur, and offset the image as desired:
finally overlay the original image:
i don't know Python, or anything about it, but presumably this algorithm can help you, or someone else stumbling across this question.
Note: Strictly speaking this should be a comment, but i really wanted to be able to show the images.
Related
I am using Imagick::resizeImage to create a thumbnail PNG image of each page of a pdf file. However, the image I am getting back is really blurry. How can I make it a little sharper? Any pointers would be really appreciated.
I have tried adjusting the 'blur' paramter of Imagick::resizeImage between 0.1 - 1, without success.
$pdfPage = '1Mpublic.pdf[0]';
$im = new imagick($pdfPage);
$im->setImageFormat('png');
// Resize thumbnail image
$imgHeight = $im -> getImageHeight();
$imgWidth = $im -> getImageWidth();
$desiredWidth = 200;
$desiredHeight = resizedImageHeight($imgWidth, $imgHeight, $desiredWidth);
$im -> setResolution(1500, 1500);
$im -> resizeImage($desiredWidth, $desiredHeight, imagick::STYLE_NORMAL, 0.1);
/* Resize image */
function resizedImageHeight($imgWidth, $imgHeight, $desiredImgWidth){
$quoient = $imgWidth/$imgHeight;
$height = $desiredImgWidth/$quoient;
return $height;
}
original pdf link:
https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4905263/pdf/ksgt-06-04-1091539.pdf
Rather than rendering and then resizing the raster, it might be better to render the PDF to the right number of pixels in the first place. It'll be faster, and you can be sure that the amount of sharpness is correct for the content.
For example:
$ time convert -density 50 ksgt-06-04-1091539.pdf[0] x2.png
real 0m0.325s
user 0m0.299s
sys 0m0.024s
Makes:
The -density 50 makes a page about the same number of pixels across as your sample, 425.
In imagick you could do it like this (as #fmw42's excellent answer already says):
#!/usr/bin/env php
<?php
$target_width = 400;
# get natural size, calculate density we need for target width
$im = new imagick();
$im->pingImage($argv[1]);
$geo = $im->getImageGeometry();
$natural_width = $geo['width'];
$density = 72.0 * $target_width / $natural_width;
# load at correct resolution for target_width
$im = new imagick();
$im->setResolution($density, $density);
$im->readImage($argv[1]);
# and write back
$im->writeImage($argv[2]);
Doing both the ping and the read is a little slow in imagick, unfortunately:
$ time ./pdfthumb.php ksgt-06-04-1091539.pdf x.png
real 0m2.773s
user 0m2.737s
sys 0m0.036s
It's not imagick, but vipsthumbnail can do the ping and read in one operation:
$ time vipsthumbnail ksgt-06-04-1091539.pdf -s 400x -o x2.png
real 0m0.064s
user 0m0.064s
sys 0m0.011s
It might be worth considering if speed is important. libvips has a php binding so you can call it directly, but if you do that you'll run into awful licensing problems because it uses the GPL library poppler for PDF rendering, sigh. ImageMagick uses GhostScript and shells out to that for the same reason.
Unfortunately, I do not know Imagick that well. But in Imagemagick command line, I would do what is sometimes called supersampling. That is use a large density to read the PDF, then resize down by the inverse scale factor.
For example, nominal density is 72 dpi. I would read the input PDF at 4*72=288 dpi. Then after rasterizing, I would resize by 1/4=25% or for a larger result by something larger than 25%, say 50%. Here is your first page done both ways:
convert -density 288 ksgt-06-04-1091539.pdf[0] -resize 25% result1.png
convert -density 288 ksgt-06-04-1091539.pdf[0] -resize 50% result2.png
In Imagick, the key is something like:
$imagick = new Imagick();
$imagick->setImageResolution(288, 288);
$imagick->readImage('myfile.pdf');
I'm attempting to try and assign a value to an image based on its 'saturation level', to see if the image is black and white or color. I'm using Imagick, and have found what seems to be the perfect code for the command line and am trying to replicate it using the PHP library.
I think I understand the concept:
Convert image to HSL.
Extract the 'g' channel (which is the S channel in HSL).
Calculate the mean of this channel.
Command line code
convert '$image_path' -colorspace HSL -channel g -separate +channel -format '%[fx:mean]' info:
My PHP code
$imagick = new Imagick($image_path);
$imagick->setColorspace(imagick::COLORSPACE_HSL);
print_r($imagick->getImageChannelMean(imagick::CHANNEL_GREEN));
Output
My PHP code isn't outputting the same sort of values as the command line code, though. For example, a grayscale image gives 0 for the command line code, but the PHP code gives [mean] => 10845.392051182 [standardDeviation] => 7367.5888849872.
Similarly, another grayscale image gives 0 vs. [mean] => 31380.528443457 [standardDeviation] => 19703.501101904.
A colorful image gives 0.565309 vs. [mean] => 33991.552881892 [standardDeviation] => 16254.018540044.
There just doesn't seem to be any kind of pattern between the different values. Am I doing something obviously wrong?
Thanks.
Just to add, I've also tried this PHP code
$imagick = new Imagick($image_path);
$imagick->setColorspace(imagick::COLORSPACE_HSL);
$imagick->separateImageChannel(imagick::CHANNEL_GREEN);
$imagick->setFormat('%[fx:mean]');
But I get an Unable to set format error when I try and set the format. I've also tried setFormat('%[fx:mean] info:'), setFormat('%[mean]'), setFormat('%mean'), etc.
Update — FIXED!
Thanks to #danack for figuring out I needed to use transformImageColorspace() and not setColorspace(). The working code is below.
$imagick = new Imagick($image_path);
$imagick->transformImageColorspace(imagick::COLORSPACE_HSL);
$saturation_channel = $imagick->getImageChannelMean(imagick::CHANNEL_GREEN);
$saturation_level = $saturation_channel['mean']/65535;
setFormat doesn't replicate the command line option -format - the one in Imagick tries to set the image format, which should be png, jpg etc. The one in the command line is setting the format for info - the closest match for which in Imagick is calling $imagick->identifyImage(true) and parsing the results of that.
Also you're just calling the wrong function - it should be transformImageColorspace not setColorSpace. If you use that you can use the statistics from getImageChannelMean.
There are other ways to test for greyness which may be more appropriate under certain circumstances. The first is to convert the a clone of the image to grayscale, and then compare it to the original image:
$imagick = new Imagick($image_path);
$imagickGrey = clone $imagick;
$imagickGrey->setimagetype(\Imagick::IMGTYPE_GRAYSCALE);
$differenceInfo = $imagick->compareimages($imagickGrey, \Imagick::METRIC_MEANABSOLUTEERROR);
if ($differenceInfo['mean'] <= 0.0000001) {
echo "Grey enough";
}
That would probably be appropriate if you image had areas of color that were also transparent - so they theoretically have color, but not visibly.
Or if you only care about whether the image is of a grayscale format:
$imageType = $imagick->getImageType();
if ($imageType === \Imagick::IMGTYPE_GRAYSCALE ||
$imageType === Imagick::IMGTYPE_GRAYSCALEMATTE) {
//This is grayscale
}
I found a command line here:
convert image.png -colorspace HSL -channel g -separate +channel -format "%[fx:mean]" info:
It prints a number between 0 and 1, where zero means grayscale.
If your images have tint. (from scanner as example) you should do auto color for them before detecting gray scale.
You should normalizeImage(imagick::CHANNEL_ALL) for all separate channels of image. separateImageChannel()
But
I'm coding a little CMS, where you can upload several images.
These images are converted into 3 versions (big, mid und thumbnail size) with imagemagick.
The problem is that imagemagick needs 5 minutes for creating these 3 versions of 4 pictures (which were uploaded).
here is the part with the imagemagick commands:
foreach($upIMGS as $key => $filename){
list($width, $height) = getimagesize($path.$filename);
if ($width > $height) $size = "x96";
else $size = "96x";
exec(P_IMAGEMAGICK." ".$path.$filename." -resize $size -gravity center -crop 96x96+0+0 +repage ".$path."th-".$filename);
exec(P_IMAGEMAGICK." ".$path.$filename." -resize 320x320 ".$path."hl-".$filename);
exec(P_IMAGEMAGICK." ".$path.$filename." -resize 514x ".$path."fl-".$filename);
unlink($path.$filename);
}
[the $upIMGS is an array with all the filenames of the recently uploaded images]
I mean.. it does work, but toooo slow and after 5min the server gives me an error. some of the files are generated and some are not...
Would be very nice if you can give me a tip.
I recently ran into the same issue, but I was only passing through the images once to resize them from their original 2592x1944 to 300xbestFit or bestFitx300
I am using the PHP class imagick instead of command lines but I cut my time in half by changing to -scale or scaleImage in my case. Here is a snippet of my test code.
while ($images = readdir($handle)) {
// check to see if the first or second character is a '.' or '..',
// if so then remove from list
if (substr($images,0,1) != '.') {
//Check files to see if there extensions match any of the following image extensions.
// GLOB_BRACE looks for all glob criteria within the {braces}
$images = glob($dir."{*.gif,*.jpg,*.png,*.jpeg}", GLOB_BRACE);
// the glob function gives us an array of images
$i = 0;
foreach ($images as $image) {
// parse the data given and remove the images/ $dir,
// imagemagick will not take paths, only image names.
$i++;
list ($dir, $image) = split('[/]', $image);
echo $i, " ", $image, "<br />";
$magick = new Imagick($dir."/".$image);
$imageprops = $magick->getImageGeometry();
if ($imageprops['width'] <= 300 && $imageprops['height'] <= 300) {
// don't upscale
} else {
// 29 Images at 2592x1944 takes 11.555036068 seconds ->
// output size = 300 x 255
$magick->scaleImage(300,300, true);
// 29 Images at 2592x1944 takes 23.3927891254 seconds ->
// output size = 300 x 255
//$magick->resizeImage(300,300, imagick::FILTER_LANCZOS, 0.9, true);
$magick->writeImage("thumb_".$image);
}
}
}
}
I am processing 29 images at 2592x1944 and went from 23.3927891254 seconds to 11.555036068 seconds. I hope this helps.
Edit:
In addition to what I said above I just ran into to the following, which might be helpful, on ImageMagick v6 Examples -- API & Scripting:
"Shell scripts are inherently slow. It is interpreted, require
multiple steps and extra file handling to disk. This is of course
better thanks to the new IM v6 option handling, allowing you to do a
large number of image processing operations in a single command. Even
so you can rarely can do everything in a single convert command, so
you often have to use multiple commands to achieve what you want."
"When reading large or even a large number of images, it is better to
use a Read
Modifier to
resize, or crop them, as IM does not them read in the full image,
thus reducing its memory requirement."
"If you call ImageMagick as an Apache module it will also reduce
startup time, as parts will be loaded once and kept available for
multiple use, rather than needing re-loading over and over. This may
become more practical in the future with a permanently running
'daemon' IM process."
This example may help as it loads the main image into the memory and works on that to make the other images:
$cmd = " input.jpg \( -clone 0 -thumbnail x480 -write 480_wide.jpg +delete \)".
" \( -clone 0 -thumbnail x250 -write 250_wide.jpg +delete \) ".
" \( -clone 0 -thumbnail x100 -write 100_wide.jpg +delete \) -thumbnail 64x64! null: ";
exec("convert $cmd 64_square.jpg ");
This is creating 4 different size images.
Wait what? It should not take five minutes to generate 12 pictures from three uploaded pictures.
I don't see the rest of your code, but why is your getimagesize path $path.$filename but your unlink P_UPLOADS.$value? Is there a reason they are different? Where does $value come from anyway, it is not defined in your foreach() loop. Maybe you simply have a bug that causes the script to hang. I've used ImageMagick (though not with exec(), but with actual class) and it's been very fast.
Have you run diagnostics on your foreach loop()? Like print out milliseconds it takes to do each of those exec() commands.
A little trick would be to create the largest picture first and then create subsequent pictures from this one, not original.
However, 5 min seems too much for just 12 images anyway.
You have to do some investigations.
does the resize take considerable time if the source is relatively small (say 1024x768)?
does it take the same time called directly from the shell?
If the delay is inevitable, you may wish to move resize facility into dedicated server, put them in the queue and check the process with simple AJAX snippet.
I use Imagick to convert pdf to JPG. The problem is that pdf is in CMYK format and the colors from resulting jpg is slightly different from those from pdf. I use the following code to achieve the result:
$filelist = array("D3807797-8425-5-1_40.pdf[2]","D3807797-8425-5-1_40.pdf[3]");
$all = new Imagick();
foreach($filelist as $file){
$im = new Imagick($file);
$all->addImage($im);
}
$all->resetIterator();
$combined = $all->appendImages(true);
$combined->setImageFormat("jpg");
$combined->writeImage("test.jpg");
I also tried a linux command for this
$cmd = "gm convert -density 150x150 {$pdf}[2] {$pdf}[3] -append -quality 100 {$image}";
exec($cmd)
And i get the same result.
Could somebody help me with this problem?
Thanks in advance.
I cannot say definitive what is different (I for one do not see much difference between the image and pdf). But a good possibility is the conversion routine used to convert the CMYK channels to RGB. PDF uses the following formula:
red = 1 - min( 1, cyan + black )
green = 1 - min( 1, magenta + black )
blue = 1 - min( 1, yellow + black )
And there are other formulas around yielding different results, perhaps the conversion used by your tooling isn't the prescribed one in the PDF standard.
Note that in the PDF file specified I do see that both DeviceRGB and DeviceCMYK are used; everything is vector based, no images are present.
I'm trying to trim a variable amount of whitespace in an image only the left and right side using ImageMagick and PHP. Does anyone know how to do this (perhaps using something other than imagemagick?)?
Here's an example.
I have these two images:
Each has a variable amount of text that is dynamically created in a fixed width image.
What I need to do is trim the background off the right and left side so the images come out like this:
If ImageMagick can't do it, I am willing to use something else, but I will need help on how exactly because I am not much of a programmer. Thanks!
Here's my current code that trims all sides of an image:
<?php
/* Create the object and read the image in */
$i = '3';
$im = new Imagick("test".$i.".png");
/* Trim the image. */
$im->trimImage(0);
/* Ouput the image */
//header("Content-Type: image/" . $im->getImageFormat());
//echo $im;
/*** Write the trimmed image to disk ***/
$im->writeImage(dirname(__FILE__) . '/test'.$i.'.png');
/*Display Image*/
echo $img = "<img src=\"test".$i.".png\">";
?>
I think you are on the right track with ImageMagick's -trim operator 1), but the trick would be to get it tell you what it would do without actually doing it, and then modify that to do what you really want...
So, to get the trim-box ImageMagick calculates for your first image, you do this:
convert -fuzz 10% image.jpg -format "%#" info:
60x29+21+31
That is a 60x29 pixel rectangle, offset 21 across and 31 down from the top left corner. Now, we want to get these values into bash variables, so I set the IFS (Input Field Separator) to split fields on spaces, x and also + signs:
#!/bin/bash
IFS=" x+" read a b c d < <(convert -fuzz 10% image.jpg -format "%#" info:)
echo $a $b $c $d
60 29 21 31
Now I can ignore the 29 and the 31 because we are only interested in cropping the width, and crop like this:
convert image.jpg -crop "${a}x+${c}+0" out.jpg
So, for your 2 images, I get these:
and the full procedure is this:
#!/bin/bash
IFS=" x+" read a b c d < <(convert -fuzz 10% image.jpg -format "%#" info:)
convert image.jpg -crop "${a}x+${c}+0" out.jpg
Notes
1) The -format %# is just a shorthand for the -trim operator, which would be this in full
convert image.jpg -trim info:
image.jpg JPEG 72x40 200x100+16+24 8-bit sRGB 0.000u 0:00.000
From what I can see in the ImageMagick docs on cropping and borders, it doesn't seem to be possible.
you can't specify an edge for "intelligent" cropping (known as-trim on the command line), and all the cropping methods that accept a geometry argument need a fixed number for cropping.
The only idea that comes to mind is to get the colour of the shaved area in a separate call, run trimImage, and add the lost areas back using -border.
Edit: The IM manual is suggesting something similar. Check out Trimming Just One Side of an Image. I'm not familiar with IM's PHP extension to translate the code into PHP calls but it should be half-way straightforward.
The GD based library WideImage has something similar. It's called autoCrop, by default it works on all four sides.
However, you could just add another parameter and based on it only crop top/bottom or left/right.
autoCrop code
It's pretty well documented. $img is a WideImage_Image type. There is also an interactive online demo of it.
Related question: Removing black bars off video thumbnail.
Using GD:
function imageautocrop( &$img) {
$emptycol = function ( $img, $x, $min, $max) {
for( $y=$min; $y<$max; $y++) {
$col = imagecolorsforindex( $img, imagecolorat( $img, $x, $y));
if( $col['alpha'] != 127) return false;
}
return true;
}
$trim = Array('top'=>0,'bot'=>0,'lft'=>0,'rgt'=>0);
$size = Array('x'=>imagesx($img)-1,'y'=>imagesy($img)-1);
// code for affecting rows removed due to question asked
while( $emptycol( $img, $trim['lft'], $trim['top'], $size['y']-$trim['bot'])) $trim['lft']++;
while( $emptycol( $img, $size['x']-$trim['rgt'], $trim['top'], $size['y']-$trim['bot'])) $trim['rgt']++;
$newimg = imagecreate( $size['x']-$trim['lft']-$trim['rgt']+1, $size['y']-$trim['top']-$trim['bot']+1);
imagecopy( $newimg, $img, 0, 0, $trim['lft'], $trim['top'], imagesx($newimg)+1, imagesy($newimg)+1);
imagedestroy($img);
$img = $newimg;
}
It's very old code of mine, so probably not optimal, but it does the job.
It is a two step process as text is dynamically generated
Generate the text image, determine width(image)
Overlay text image into background, determine width(background)
Use one tool mentioned above, crop (width(background)-width(image)/2 on either side
The trick is figuring out the width(image). See: How can I auto adjust the width of a GD-generated image to fit the text?
Then again, if you know width(image), you can crop the width(background) first before overlay
Use cropImage() instead. Something like this, perhaps:
$img_x_size = 800; // Set these to relevant values
$img_y_size = 600;
$crop_pixels = 20; // How many pixels to crop
// cropImage(XsizeOfCrop, YsizeOfCrop, CropXPos, CropYPos)
$im->cropImage($img_x_size - $crop_pixels, $img_y_size, 0, $crop_pixels / 2);