# HG changeset patch # User Dan # Date 1220288810 14400 # Node ID bd3372a2afc141a659f7212cba6db9cae9740c40 # Parent 38dbcda3cf20049e5b34dfb25de1af9acc1ca310 Added artwork spriting support. Artwork is now displayed using a gigantic CSS sprite instead of hundreds of little images. GD required. diff -r 38dbcda3cf20 -r bd3372a2afc1 greyhound.php --- a/greyhound.php Mon Sep 01 13:05:52 2008 -0400 +++ b/greyhound.php Mon Sep 01 13:06:50 2008 -0400 @@ -126,6 +126,7 @@ $httpd->add_handler('scripts', 'dir', GREY_ROOT . '/scripts'); $httpd->add_handler('favicon.ico', 'file', GREY_ROOT . '/amarok_icon.ico'); $httpd->add_handler('apple-touch-icon.png', 'file', GREY_ROOT . '/apple-touch-icon.png'); + $httpd->add_handler('spacer.gif', 'file', GREY_ROOT . '/spacer.gif'); // load all themes if forking is enabled // Themes are loaded when the playlist is requested. This is fine for // single-threaded operation, but if the playlist handler is only loaded @@ -155,7 +156,7 @@ } catch( Exception $e ) { - if ( strstr(strval($e), "Could not bind") ) + if ( strstr(strval($e), "Could not bind") || strstr(strval($e), "Address already in use") ) { burnout("Could not bind to the port $ip:$port. Is Greyhound already running? Sometimes browsers don't close off their connections until Greyhound has been dead for about a minute, so try starting Greyhound again in roughly 60 seconds. If that doesn't work, type \"killall -9 php\" at a terminal and try starting Greyhound again in 60 seconds."); } diff -r 38dbcda3cf20 -r bd3372a2afc1 imagetools.php --- a/imagetools.php Mon Sep 01 13:05:52 2008 -0400 +++ b/imagetools.php Mon Sep 01 13:06:50 2008 -0400 @@ -38,7 +38,7 @@ /** * Scales an image to the specified width and height, and writes the output to the specified * file. Will use ImageMagick if present, but if not will attempt to scale with GD. This will - * always scale images proportionally. + * always scale images proportionally, unless $preserve_ratio is set to false. * * Ported from Enano CMS (which is also my project) * @@ -47,10 +47,11 @@ * @param int Image width, in pixels * @param int Image height, in pixels * @param bool If true, the output file will be deleted if it exists before it is written + * @param bool If true, preserves the image's aspect ratio (default) * @return bool True on success, false on failure */ -function scale_image($in_file, $out_file, $width = 225, $height = 225, $unlink = false) +function scale_image($in_file, $out_file, $width = 225, $height = 225, $unlink = false, $preserve_ratio = true) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -117,8 +118,9 @@ // ImageMagick path seems screwy return false; } - $cmdline = "\"$magick_path\" \"$in_file\" -resize \"{$width}x{$height}>\" \"$out_file\""; - system($cmdline, $return); + $op = ( $preserve_ratio ) ? '>' : '!'; + $cmdline = "\"$magick_path\" \"$in_file\" -resize '{$width}x{$height}$op' \"$out_file\""; + exec($cmdline, $output, $return); if ( !file_exists($out_file) ) return false; return true; @@ -130,29 +132,37 @@ return false; // calculate new width and height - $ratio = $width_orig / $height_orig; - if ( $ratio > 1 ) + if ( $preserve_ratio ) { - // orig. width is greater that height - $new_width = $width; - $new_height = round( $width / $ratio ); + $ratio = $width_orig / $height_orig; + if ( $ratio > 1 ) + { + // orig. width is greater that height + $new_width = $width; + $new_height = round( $width / $ratio ); + } + else if ( $ratio < 1 ) + { + // orig. height is greater than width + $new_width = round( $height / $ratio ); + $new_height = $height; + } + else if ( $ratio == 1 ) + { + $new_width = $width; + $new_height = $width; + } + if ( $new_width > $width_orig || $new_height > $height_orig ) + { + // Too big for our britches here; set it to only convert the file + $new_width = $width_orig; + $new_height = $height_orig; + } } - else if ( $ratio < 1 ) - { - // orig. height is greater than width - $new_width = round( $height / $ratio ); - $new_height = $height; - } - else if ( $ratio == 1 ) + else { $new_width = $width; - $new_height = $width; - } - if ( $new_width > $width_orig || $new_height > $height_orig ) - { - // Too big for our britches here; set it to only convert the file - $new_width = $width_orig; - $new_height = $height_orig; + $new_height = $height; } $newimage = @imagecreatetruecolor($new_width, $new_height); diff -r 38dbcda3cf20 -r bd3372a2afc1 playlist.php --- 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 = << 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 = ' Track {foreach key=tid item=track from=$playlist} - {strip} + {* strip *} @@ -75,7 +75,7 @@
-  + {sprite artist=$track.artist album=$track.album size=50}
Artist: {$track.artist|escape}
@@ -86,7 +86,7 @@
- {/strip} + {* /strip *} {/foreach} diff -r 38dbcda3cf20 -r bd3372a2afc1 webserver.php --- a/webserver.php Mon Sep 01 13:05:52 2008 -0400 +++ b/webserver.php Mon Sep 01 13:06:50 2008 -0400 @@ -93,7 +93,7 @@ */ var $response_code = 0; - + /** * Content type set by the current handler function * @var string @@ -1010,20 +1010,23 @@ return true; } - // $this->header('Transfer-encoding: chunked'); - $this->header("Content-length: " . strlen($output)); - $headers = implode("\r\n", $this->response_headers); - - // write headers - $this->send_client_headers($socket, $this->response_code, $this->content_type, $headers); - - // chunk output - // $output = dechex(strlen($output)) . "\r\n$output"; - - // write body - $socket->write($output); - - $this->headers_sent = false; + if ( !$this->headers_sent ) + { + // $this->header('Transfer-encoding: chunked'); + $this->header("Content-length: " . strlen($output)); + $headers = implode("\r\n", $this->response_headers); + + // write headers + $this->send_client_headers($socket, $this->response_code, $this->content_type, $headers); + + // chunk output + // $output = dechex(strlen($output)) . "\r\n$output"; + + // write body + $socket->write($output); + + $this->headers_sent = false; + } break; case 'sysuuid':