--- a/playlist.php Mon Sep 01 13:05:52 2008 -0400
+++ b/playlist.php Mon Sep 01 13:06:50 2008 -0400
@@ -61,6 +61,7 @@
'position.js'
));
$smarty->assign('allow_control', $allowcontrol);
+ $smarty->register_function('sprite', 'smarty_function_sprite');
$smarty->display('playlist.tpl');
}
@@ -68,6 +69,48 @@
{
global $amarok_home;
+ // get PATH_INFO
+ $pathinfo = @substr(@substr($_SERVER['REQUEST_URI'], 1), @strpos(@substr($_SERVER['REQUEST_URI'], 1), '/')+1);
+
+ // should we do a collage (for CSS sprites instead of sending hundreds of individual images)?
+ if ( preg_match('/^collage(?:\/([0-9]+))?$/', $pathinfo, $match) )
+ {
+ // default size is 50px per image
+ $collage_size = ( isset($match[1]) ) ? intval($match[1]) : 50;
+
+ $artwork_dir = "$amarok_home/albumcovers";
+ if ( !file_exists("$artwork_dir/collage_{$collage_size}.png") )
+ {
+ if ( !generate_artwork_collage("$artwork_dir/collage_{$collage_size}.png", $collage_size) )
+ {
+ echo 'Error: generate_artwork_collage() failed';
+ return;
+ }
+ }
+
+ $target_file = "$artwork_dir/collage_{$collage_size}.png";
+ // we have it now, send the image through
+ $fh = @fopen($target_file, 'r');
+ if ( !$fh )
+ return false;
+
+ $httpd->header('Content-type: image/png');
+ $httpd->header('Content-length: ' . filesize($target_file));
+ $httpd->header('Expires: Wed, 1 Jan 2020 01:00:00 GMT');
+
+ // kinda sorta a hack.
+ $headers = implode("\r\n", $httpd->response_headers);
+ $httpd->send_client_headers($socket, $httpd->response_code, $httpd->content_type, $headers);
+
+ while ( $d = fread($fh, 10240) )
+ {
+ $socket->write($d);
+ }
+ fclose($fh);
+
+ return;
+ }
+
if ( !isset($_GET['artist']) || !isset($_GET['album']) )
{
echo 'Please specify artist and album.';
@@ -94,6 +137,7 @@
$artwork_filetype = get_image_filetype("$artwork_dir/large/$artwork_hash");
if ( !$artwork_filetype )
{
+ // image is not supported (PNG, GIF, or JPG required)
return false;
}
// we'll need to copy the existing artwork file to our thumbnail dir to let scale_image() detect the type properly (it doesn't use magic bytes)
@@ -117,12 +161,18 @@
$fh = @fopen($target_file, 'r');
if ( !$fh )
return false;
+
$httpd->header('Content-type: image/png');
$httpd->header('Content-length: ' . filesize($target_file));
$httpd->header('Expires: Wed, 1 Jan 2020 01:00:00 GMT');
+
+ // kinda sorta a hack.
+ $headers = implode("\r\n", $httpd->response_headers);
+ $httpd->send_client_headers($socket, $httpd->response_code, $httpd->content_type, $headers);
+
while ( !feof($fh) )
{
- socket_write($socket, fread($fh, 51200));
+ $socket->write(fread($fh, 51200));
}
fclose($fh);
}
@@ -135,4 +185,300 @@
}
}
+/**
+ * Generates a collage of all album art for use as a CSS sprite. Also generates a textual .map file in the format of "hash xpos ypos\n"
+ * to allow retrieving positions of images. Requires GD.
+ * @param string Name of the collage file. Map file will be the same filename except with the extension ".map"
+ * @param int Size of each image, in pixels. Artwork images will be stretched to a 1:1 aspect ratio. Optional, defaults to 50.
+ * @return bool True on success, false on failure.
+ */
+function generate_artwork_collage($target_file, $size = 50)
+{
+ // check for required GD functionality
+ if ( !function_exists('imagecopyresampled') || !function_exists('imagepng') )
+ return false;
+
+ status("generating size $size collage");
+ $stderr = fopen('php://stderr', 'w');
+ if ( !$stderr )
+ // this should really never fail.
+ return false;
+
+ // import amarok globals
+ global $amarok_home;
+ $artwork_dir = "$amarok_home/albumcovers";
+
+ // map file path
+ $mapfile = preg_replace('/\.[a-z]+$/', '', $target_file) . '.map';
+
+ // open map file
+ $maphandle = @fopen($mapfile, 'w');
+ if ( !$maphandle )
+ return false;
+
+ $mapheader = <<<EOF
+# this artwork collage map gives the locations of various artwork images within the collage
+# format is:
+# hash x y
+# x and y are indices, not pixel values (obviously), and hash is the name of the artwork file in large/
+
+EOF;
+ fwrite($maphandle, $mapheader);
+
+ // build a list of existing artwork files
+ $artwork_list = array();
+ if ( $dh = @opendir("$artwork_dir/large") )
+ {
+ while ( $fp = @readdir($dh) )
+ {
+ if ( preg_match('/^[a-f0-9]{32}$/', $fp) )
+ {
+ $artwork_list[] = $fp;
+ }
+ }
+ closedir($dh);
+ }
+ else
+ {
+ return false;
+ }
+
+ // at least one image?
+ if ( empty($artwork_list) )
+ return false;
+
+ // asort it to make sure map is predictable
+ asort($artwork_list);
+
+ // number of columns
+ $cols = 20;
+ // number of rows
+ $rows = ceil( count($artwork_list) / $cols );
+
+ // image dimensions
+ $image_width = $cols * $size;
+ $image_height = $rows * $size;
+
+ // create image
+ $collage = imagecreatetruecolor($image_width, $image_height);
+
+ // generator loop
+ // start at row 0, column 0
+ $col = -1;
+ $row = 0;
+ $srow = $row + 1;
+ fwrite($stderr, " -> row $srow of $rows\r");
+ $time_map = microtime(true);
+ foreach ( $artwork_list as $artwork_file )
+ {
+ // calculate where we are
+ $col++;
+ if ( $col == $cols )
+ {
+ // reached column limit, reset $cols and increment row
+ $col = 0;
+ $row++;
+ $srow = $row + 1;
+ fwrite($stderr, " -> row $srow of $rows\r");
+ }
+ // x and y offset of scaled image
+ $xoff = $col * $size;
+ $yoff = $row * $size;
+ // set offset
+ fwrite($maphandle, "$artwork_file $col $row\n");
+ // load image
+ $createfunc = ( get_image_filetype("$artwork_dir/large/$artwork_file") == 'jpg' ) ? 'imagecreatefromjpeg' : 'imagecreatefrompng';
+ $aw = @$createfunc("$artwork_dir/large/$artwork_file");
+ if ( !$aw )
+ {
+ $aw = @imagecreatefromwbmp("$artwork_dir/large/$artwork_file");
+ if ( !$aw )
+ {
+ // couldn't load image, silently continue
+ continue;
+ }
+ }
+ list($aw_width, $aw_height) = array(imagesx($aw), imagesy($aw));
+ // scale and position image
+ $result = imagecopyresampled($collage, $aw, $xoff, $yoff, 0, 0, $size, $size, $aw_width, $aw_height);
+ if ( !$result )
+ {
+ // couldn't scale image, silently continue
+ continue;
+ }
+ // free the temp image
+ imagedestroy($aw);
+ }
+ $time_map = round(1000 * (microtime(true) - $time_map));
+ $time_write = microtime(true);
+ fclose($maphandle);
+ fwrite($stderr, " -> saving image\r");
+ if ( !imagepng($collage, $target_file) )
+ return false;
+ imagedestroy($collage);
+ $time_write = round(1000 * (microtime(true) - $time_write));
+
+ $avg = round($time_map / count($artwork_list));
+
+ status("collage generation complete, returning success; time (ms): map/avg/write $time_map/$avg/$time_write");
+ return true;
+}
+
+/**
+ * Returns an img tag showing artwork from the specified size collage sprite.
+ * @param string Artist
+ * @param string Album
+ * @param int Collage size
+ * @return string
+ */
+
+function get_artwork_sprite($artist, $album, $size = 50)
+{
+ // import amarok globals
+ global $amarok_home;
+ $artwork_dir = "$amarok_home/albumcovers";
+
+ if ( !is_int($size) )
+ return '';
+
+ // hash of cover
+ $coverid = md5(strtolower(trim($artist)) . strtolower(trim($album)));
+
+ $tag = '<img alt=" " src="/spacer.gif" width="' . $size . '" height="' . $size . '" ';
+ if ( file_exists("$artwork_dir/collage_{$size}.map") )
+ {
+ $mapdata = parse_collage_map("$artwork_dir/collage_{$size}.map");
+ if ( isset($mapdata[$coverid]) )
+ {
+ $css_x = -1 * $size * $mapdata[$coverid][0];
+ $css_y = -1 * $size * $mapdata[$coverid][1];
+ $tag .= "style=\"background-image: url(/artwork/collage/$size); background-repeat: no-repeat; background-position: {$css_x}px {$css_y}px;\" ";
+ }
+ }
+ $tag .= '/>';
+
+ return $tag;
+}
+
+/**
+ * Parses the specified artwork map file. Return an associative array, keys being the artwork file hashes and values being array(x, y).
+ * @param string Map file
+ * @return array
+ */
+
+function parse_collage_map($mapfile)
+{
+ if ( !file_exists($mapfile) )
+ return array();
+
+ $fp = @fopen($mapfile, 'r');
+ if ( !$fp )
+ return false;
+
+ $map = array();
+ while ( $line = fgets($fp) )
+ {
+ // parse out comments
+ $line = trim(preg_replace('/#(.+)$/', '', $line));
+ if ( empty($line) )
+ continue;
+ list($hash, $x, $y) = explode(' ', $line);
+ if ( !preg_match('/^[a-f0-9]{32}$/', $hash) || !preg_match('/^[0-9]+$/', $x) || !preg_match('/^[0-9]+$/', $y) )
+ // invalid line
+ continue;
+
+ // valid line, append map array
+ $map[$hash] = array(
+ intval($x),
+ intval($y)
+ );
+ }
+ fclose($fp);
+ return $map;
+}
+
+/**
+ * Finds out if a collage file is outdated (e.g. missing artwork images)
+ * @param int Size of collage
+ * @return bool true if outdated
+ */
+
+function collage_is_outdated($size = 50)
+{
+ global $amarok_home;
+ $artwork_dir = "$amarok_home/albumcovers";
+ $mapfile = "$artwork_dir/collage_{$size}.map";
+ if ( !file_exists($mapfile) )
+ {
+ // consider it outdated if it doesn't exist
+ return true;
+ }
+
+ // load existing image map
+ $map = parse_collage_map($mapfile);
+
+ // build a list of existing artwork files
+ $artwork_list = array();
+ if ( $dh = @opendir("$artwork_dir/large") )
+ {
+ while ( $fp = @readdir($dh) )
+ {
+ if ( preg_match('/^[a-f0-9]{32}$/', $fp) )
+ {
+ // found an artwork file
+ if ( !isset($map[$fp]) )
+ {
+ // this artwork isn't in the map file, return outdated
+ closedir($dh);
+ status("size $size collage is outdated");
+ return true;
+ }
+ }
+ }
+ closedir($dh);
+ }
+
+ // if we reach here, we haven't found anything missing.
+ return false;
+}
+
+/**
+ * Smarty function for sprite generation.
+ * @access private
+ */
+
+function smarty_function_sprite($params, &$smarty)
+{
+ // don't perform the exhaustive check more than once per execution
+ static $checks_done = array();
+
+ if ( empty($params['artist']) )
+ return 'Error: missing "artist" parameter';
+ if ( empty($params['album']) )
+ return 'Error: missing "album" parameter';
+ if ( empty($params['size']) )
+ $params['size'] = 50;
+
+ $params['size'] = intval($params['size']);
+ $size =& $params['size'];
+
+ // if the collage file doesn't exist or is missing artwork, renew it
+ // but only perform this check once per execution per size
+ if ( !isset($checks_done[$size]) )
+ {
+ global $amarok_home;
+ $artwork_dir = "$amarok_home/albumcovers";
+
+ $collage_file = "$artwork_dir/collage_{$size}";
+ $collage_is_good = file_exists("$collage_file.png") && file_exists("$collage_file.map") && !collage_is_outdated($size);
+ if ( !$collage_is_good )
+ {
+ generate_artwork_collage("$collage_file.png", $size);
+ }
+ $checks_done[$size] = true;
+ }
+
+ return get_artwork_sprite($params['artist'], $params['album'], $params['size']);
+}
+