Archive for the Category 'php'

Outer-glow effect with PHP and the GD library

I was recently working on a photo gallery project for a friend who is an avid Flickr user. She had an established workflow when it came to making her work available to her friends and clients on Flickr and didn't want to change that workflow if she didn't have to.

Ok, now it's time to skip ahead. I built the side using the Flickr API, and everything is working swimmingly. I decided to hire my friend Matthew Armendariz to handle the interface design. Since the client was a friend of mine, having someone else be responsible for the creative would keep me free from having to worry about that process, and Matt is a pretty good designer.

It took no time for Matt to whip up a few comps, the one we ended up going with had a slight outer-glow around each photograph, and since we were working with seemingly fixed output from Flickr, I figured, no worries, I'll just create two "glowing" background images, and display one or the other depending on whether the current photo was portrait or landscape.

It worked like a charm... until I came across a few photographs that the client had uploaded with non-standard aspect ratios. Then I had the realization that these outer glows needed to be dynamically generated. Since as the developer there wasn't any way that I could trust that she upload files of any specific aspect ratio, and since she was using Flickr to manage her photos I couldn't (and wouldn't want to anyway) enforce any specific sizes uploaded, I needed to come up with a an easy, browser agnostic, and friendly solution.

CSS wasn't going to help me. SVG and Canvas isn't broadly enough supported. And since this site was pretty much Javascript free, I had to look to the server to help me execute the agreed upon creative work

Oh my GD!

The GD library is an often overlooked alternative to ImageMagick, but since it's light weight, comes with most PHP installs, and was already available on the production server, it seemed like the best choice.

First I had to start with making sure that the file I was generating was going to identify itself properly

PHP:
  1. header ("Content-type: image/png");

Next was to pull in the parameters i was going to need to generate the correct size image... I decided to go with a 22px "border", which is really just the size of the glow.

PHP:
  1. $border = ($_REQUEST['b'])? $_REQUEST['b'] : 22;
  2. $width = ($_REQUEST['w']) ? $_REQUEST['w']+($border*2): 500;
  3. $height = (isset($_REQUEST['h'])) ? $_REQUEST['h']+($border*2): 500;

Next we create the image, using the specified or default dimensions. And then define the colors (in RGB) that will be used to generate the image... white and light gray.

PHP:
  1. $im = @imagecreatetruecolor($width, $height)
  2.       or die("Cannot Initialize new GD image stream");
  3. $white = imagecolorallocate($im, 255, 255, 255);
  4. $gray= imagecolorallocate($im,200,200,200);

Now it's time to create our initial shape... First the image is painted entirely white, and then a box is drawn inside of the white image with the gray color.

PHP:
  1. imagefill($im, 0, 0, $white);
  2. imagefilledrectangle($im, $border, $border, $width-$border, $height-$border,$gray);

Now there's just a grey box with a white border... not only is it boring, but it's not glowing at all. It's time to fix this. GD has a few decent filters built in, just as any image library should, and the ones that I chose to use for this were IMG_FILTER_SMOOTH and IMG_FILTER_GAUSSIAN_BLUR. The gaussian blur filter, unlike the one you find in PhotoShop, does not unfortunately accept parameters, so instead I had to put calls to GD's imagefilter() method in a for loop. The loop allowed me to also easily play around with the number of times and in what order each filter was applied to the image without having to do too much cutting and pasting.

PHP:
  1. for($i = 0; $i <10; $i++){
  2.     imagefilter($im,IMG_FILTER_SMOOTH, 0);
  3.     imagefilter($im,IMG_FILTER_GAUSSIAN_BLUR);
  4.     imagefilter($im,IMG_FILTER_GAUSSIAN_BLUR);
  5. }

Now we just generate the image as a png, and then clear the memory

PHP:
  1. imagepng($im);
  2. imagedestroy($im);

And that's all there is too it. Since the default size specified is 500px square, the default output looks like this. If I need to generate an outer–glow for an image that's 250px tall by 400px wide, I add the following query to the URI of the file blur.php?h=250&w=400 and voila! Of course caching the output of this file on the server will speed up any requests after the initial one, but that is beyond the scope of this post.

I've included the source of the file below.

PHP:
  1. <?php
  2. header ("Content-type: image/png");
  3. $border = ($_REQUEST['b'])? $_REQUEST['b'] : 22;
  4. $width = ($_REQUEST['w']) ? $_REQUEST['w']+($border*2): 500;
  5. $height = (isset($_REQUEST['h'])) ? $_REQUEST['h']+($border*2): 500;
  6. $im = @imagecreatetruecolor($width, $height)
  7.       or die("Cannot Initialize new GD image stream");
  8. $white = imagecolorallocate($im, 255, 255, 255);
  9. $gray= imagecolorallocate($im,200,200,200);
  10. imagefill($im, 0, 0, $white);
  11. imagefilledrectangle($im, $border, $border, $width-$border, $height-$border,$gray);
  12. for($i = 0; $i <10; $i++){
  13.     imagefilter($im,IMG_FILTER_SMOOTH, 0);
  14.     imagefilter($im,IMG_FILTER_GAUSSIAN_BLUR);
  15.     imagefilter($im,IMG_FILTER_GAUSSIAN_BLUR);
  16. }
  17. imagepng($im);
  18. imagedestroy($im);
  19. ?>

working with a googleAPIKey and subdomain of localhost

Though all googleAPIKeys work when you run your code at http://localhost or http://localhost/any/sub/directory/, I ran into a bit of a problem with my development server's key once I tried running the site we're developing at http://projectname.localhost/.

It turns out, that Google wanted me to register for an API key for my localhost subdomain.

Now by adding this else if everything is working swimmingly.

PHP:
  1. if ($_SERVER["HTTP_HOST"] == "projectname.dev.naterkane.com"){ //for development server
  2. $googleAPIKey = "ABQIAAAAXDA3g3QE28o3DpCEOJaK7RRbiuQmYjiTrMm7iJcFkjKKtES62BTJwMA8r_9--PPf1lirxSLFF7AjoA";
  3. } else if ($_SERVER["HTTP_HOST"] = "projectname.localhost"){ //for local development subdomain
  4. $googleAPIKey = "ABQIAAAAXDA3g3QE28o3DpCEOJaK7RQd8-PxSvU34sqHMt5i1yPetZP44hRjKyK727DILgg9wBKEDitvz4a0tg";
  5. } else { //catchall for "localhost" and default production domain
  6. $googleAPIKey = "ABQIAAAAzIcwHNZqp-D2TJm3qjeWyxQjCl-yVJ0Kz7eVNz89X4I7MZtKDxSog71ziTBuu1ICptxfQLN7rq9Xag";
  7. }

Still adding support for Collections in Flickr

I updated my post about pulling Collections info from a Flickr account. I started to originally post about it a month ago, but had completely forgot as the idea got abandoned from one of my projects. For those who are curious you can find the update here: http://www.naterkane.com/blog/2007/12/01/adding-support-for-collections-in-the-flickr-api/

Adding support for Collections in the Flickr API

Over the next day or two I will be extending one or two Flickr API libraries with the goal of adding support for Collections in a user's photostream.

I can't find any information as to when Flickr is going to support collections in their API themselves. If anyone wants to save me a few hours of work, let me know. I'd be happy to not have to do the work myself. Sofar I'm expecting to have to pull out my regex reference book to give you a clue as to how it'll happen.

CURLing for Collections

I have a client who requested I build a custom front end to their Flickr account. She relies heavily on the collections feature of Flickr to organize her many many photosets. As we were trying to figure out how she can organize her account so I can quicklly parse the data returned by the Flickr API, I came up with a few proposed solutions. This is one of them, and though now what we went with, I decided to share.

CODE:
  1. <?php
  2. function getLinks($url){
  3.         $ch = curl_init();
  4.         curl_setopt($ch, CURLOPT_URL, $url);
  5.         curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
  6.         $data = curl_exec ($ch);
  7.         curl_close ($ch);      
  8.         $doc = new DOMDocument;
  9.         // We don't want to bother with white spaces
  10.         $doc->preserveWhiteSpace = false;
  11.         $doc->loadHTML($data);
  12.         $items = $doc->getElementsByTagName('a');
  13.         return $items;
  14.         }
  15.  
  16.  
  17. function listLinks($recursive,$id = ""){
  18.         $url = "http://www.flickr.com/photos/".$_SESSION['username']."/collections/".$id;
  19.  
  20.         $items = getLinks($url);
  21.         echo "<ul>";
  22.         for ($i = 0; $i <$items->length; $i++) {
  23.             if ($items->item($i)->getAttribute('class') == "Colla"){
  24.                 $thisCollectionID = split("/",$items->item($i)->getAttribute("href"));
  25.                 echo "<li>" . $items->item($i)->nodeValue . ': ' . $thisCollectionID[4];
  26.                 if ($recursive == true){
  27.                     listLinks($recursive,$thisCollectionID[4]);
  28.                 }
  29.                 echo "</li>";
  30.             } elseif ($items->item($i)->getAttribute('class') == "Seta"){
  31.                 $thisSetID = split("/",$items->item($i)->getAttribute("href"));
  32.                 echo "<li>" . $items->item($i)->nodeValue . ': ' . $thisSetID[4];
  33.                 echo "</li>";
  34.             }
  35.            
  36.         }
  37.         echo "</ul>";
  38. }
  39. ?>

Listing all of the photosets recursively

To loop through all collections and collections of collections recursively we simply call listLinks(false);. The execution time on the server was about ~3 seconds, which is unacceptably slow for realtime requests.

In order to just list the photosets that are inside of the top level collections, which takes about half the time (about 1-1.5 seconds), but is still unacceptable, you just set the parameter to false like so listLinks(false);. Simple as that.

You may have noticed that these are global functions. The Flickr API php library that I used for this project was actually not written in PHP5, and since this was just a simple proof of concept to talk the client out of using Collections, and instead a photoset naming convention, to organize the photos that were going to be made available on her site.

Using collections in the real world

Though 3 seconds of processing time would be horrible if your code was making requests all the time, using CURL to pull this data in to your server once in awhile and storing the results in a database doesn't seem too bad. An acceptable approach to working around the lack of support in the API is better than nothing at all. Right?

true asynchronous requests and ‘anti-patterns’

I caught a post on ajaxian today talking about ajax 'anti-patterns', and the term caught my eye. Apparently Ed Burnette has coined the term referring to anti-patterns as things to watch out for while developing your ajax app. Chris Cornutt from ajaxian summed it up with this short list.

  • Chatty communication - lots of “chatterâ€? back and forth between the server
  • Too much XML parsing - libraries to parse XML in the browser are still relatively slow
  • Loading everything before displaying something - don’t make the user wait if things are slow. give them something.
  • Rendering on the server - make good decisions as to where to render various page content
  • Over thinking the design - don’t use the old mentality of just a few releases, update often and in bits

This got me thinking, as am currently in the redevelopment of my site, and most importantly my portfolio, I have deliberately avoided having my serverside pass XML back to the client, and have instead opted to use formatted XHTML. I had been thinking that this was a copout and that I should in my next build deploy a much more extensible Javascript based XML parser for my list of thumbnails and subsequent portfolio item data. I mean, using XML means that I can free my server up and pass less complex data back to the client right? Well yes and no.

Currently I'm hosted with mediatemple. The renound hosting company for design geeks everywhere. And if I remember correctly, they have pretty decent servers, with decent processing power that I actually give them money every month to be able to use. Additionally, my 'lowest common denominator' testing machine here is a 6-7 year old 700mhz P3. Not so much of a powerhouse, and since it's been beaten to hell over the years, it's not the processing powerhouse that it once was back at the end of the last decade. So if somthing is going to perform badly, my trusty P3 will catch it and let me know. This is where we get to the issue of clientside XML parsing, forthought, and extensibility.

If you're anywhere on my site other than in the portfolio itself, the sidebar nav's casestudies links are generated with a simple mySQL query, and a loop in PHP that builds a link that looks like this.

HTML:
  1. http://www.naterkane.com/portfolio.php?casestudy=1&id=231

It basically tells the page to say, hey, we want to show a particular project off the bat, so instead of just calling the function to load up the project thumbnails, lets also specify a particular project, and load it up too, with the related case study data instead of just the basic project description text.

In order to do that I had to make sure that my xmlHttpRequests didn't step on each other's toes. So I instead of specifying a global request/response object, I kept it inside of each method, and used a constructor that created a local object instead.

JavaScript:
  1. function xhrCreate(type){
  2.     var xhr;
  3.     if (!type) {
  4.         type = 'text';
  5.     }
  6.     if (window.ActiveXObject) {
  7.         try {
  8.             xhr = new ActiveXObject("Msxml2.XMLHTTP");
  9.         } catch (e) {
  10.             try {
  11.                 xhr = new ActiveXObject("Microsoft.XMLHTTP");
  12.             } catch (e) {}
  13.         }
  14.     } else if (window.XMLHttpRequest) {
  15.         xhr = new XMLHttpRequest();
  16.         if (xhr.overrideMimeType) {
  17.             xhr.overrideMimeType('text/' + type);
  18.         }
  19.     }
  20. return (xhr);
  21. }
  22.  
  23. function getPortfolio(role) {
  24.     var xhrPortfolio = xhrCreate('html');
  25.     var portfolio = document.getElementById("portfolio");
  26.     var url = "includes/getPortfolio.php?role=" + role;
  27.     url +='&t=' + new Date().getTime();
  28.     portfolio.innerHTML = '<img src="http://www.naterkane.com/blog/images/loading.gif" />';
  29.     xhrPortfolio.open("GET", url , true);
  30.     xhrPortfolio.onreadystatechange = function() {
  31.         if (xhrPortfolio.readyState == 4 && xhrPortfolio.status == 200) {
  32.             portfolio.innerHTML = xhrPortfolio.responseText;
  33.             xhrPortfolio = null;
  34.         } else if (xhrPortfolio.readyState == 4 && xhrPortfolio.status == 404) {
  35.             portfolio.innerHTML = xhrPortfolio.statusText;
  36.             xhrPortfolio = null;
  37.         }
  38.     }
  39.     xhrPortfolio.send(null);
  40. }
  41.  
  42. function getPortfolioDetail(id,casestudy) {
  43.     var xhrPortfolioDetail = xhrCreate('html');
  44.     var portfolioDetail = document.getElementById("portfolioDetail");
  45.     var url = "includes/getPortfolioDetail.php?id=" + id;
  46.     url +='&t=' + new Date().getTime();
  47.     if (casestudy){
  48.         url += "&casestudy=1";
  49.     }
  50.     portfolioDetail.innerHTML = '<img src="http://www.naterkane.com/blog/images/loading.gif" />';
  51.     xhrPortfolioDetail.open("GET", url , true);
  52.     xhrPortfolioDetail.onreadystatechange = function() {
  53.         if (xhrPortfolioDetail.readyState == 4 && xhrPortfolioDetail.status == 200) {
  54.             portfolioDetail.innerHTML = xhrPortfolioDetail.responseText;
  55.             xhrPortfolioDetail = null;
  56.         } else if (xhrPortfolioDetail.readyState == 4 && xhrPortfolioDetail.status == 404) {
  57.             portfolioDetail.innerHTML = xhrPortfolioDetail.statusText;
  58.             xhrPortfolioDetail = null;
  59.         }
  60.     }
  61.     xhrPortfolioDetail.send(null);
  62. }

Ok so now I'm sending two requests at the same time, they're not colliding and everything is peachy.

The

Obj.responseText

in both methods employ AHAH (preformatted XHTML), with calls to other javascript function, and other complex data that I don't care to distribute to anyone else. So why am I not parsing it on the client side? I'd rather have the PHP do the work for me once, instead of having my PHP first have to parse the data into XML, and then use Javascript to reparse it again into somthing that the browser can display as I intend. Which would further slow down the response time of the call.

So am I right or wrong in my approach to returning data this way? I'm still not sure, but at least I know that I'm not burdening my client with additional duties.

reworking the way you read this

Since writing my own blog software is somthing that I can file under "been there/done that" I've decided that I want to do things a new way, just for fun. Today I started to write a little app that pulls my full blog off of myspace, parses it and delivers it through my personal site instead of relying on the bullshit truncated rss feed that myspace wants me to use. Why am I am doing this you ask? Because it's stupid, and I like making work for myself. Besides, what's a little regex amongst friends? (more...)

Portfolio Update

that's right, i finally got around to updating my portfolio. everything is semi organized, at least alot more so than it was before and a bit ajaxy as well. organized by year, and sortable by what role i played in the project. a few projects have case studies, and a few others have really crappy screenshots that are either 6 years old, or were pulled from the nice folks over at archive.org. as i'm looking for work right now, you should be able to see some new features popping up over the next few days/weeks.


Nater Kane naterkane personal http://www.naterkane.com LinkedIn Profile Web Technologist personal nater@naterkane.com 1978-09-12 voice 845.234.6698 | fax 707.922.0593
964 Flushing Ave. Brooklyn, NY 11206