Source of file canvas.php
Size: 19,023 Bytes - Last Modified: 2021-12-23T10:20:55+00:00
/var/www/docs.ssmods.com/process/src/thirdparty/css-crush/plugins/canvas.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 | <?php /** * Bitmap image generator * * @see docs/plugins/canvas.md */ namespace CssCrush; use stdClass; Plugin::register('canvas', array( 'enable' => function ($process) { $process->hooks->add('capture_phase2', 'CssCrush\canvas_capture'); $process->functions->add('canvas', 'CssCrush\canvas_generator'); $process->functions->add('canvas-data', 'CssCrush\canvas_generator'); } )); function canvas_capture($process) { $process->string->pregReplaceCallback( Regex::make('~@canvas\s+(?<name>{{ ident }})\s*{{ block }}~iS'), function ($m) { Crush::$process->misc->canvas_defs[strtolower($m['name'])] = new Template($m['block_content']); return ''; }); } function canvas_generator($input, $context) { $process = Crush::$process; // Check GD requirements are met. static $requirements; if (! isset($requirements)) { $requirements = canvas_requirements(); } if ($requirements === false) { return ''; } // Check process cache. $cache_key = $context->function . $input; if (isset($process->misc->canvas_cache[$cache_key])) { return $process->misc->canvas_cache[$cache_key]; } // Parse args, bail if none. $args = Functions::parseArgs($input); if (! isset($args[0])) { return ''; } $name = strtolower(array_shift($args)); // Bail if name not registered. $canvas_defs =& $process->misc->canvas_defs; if (! isset($canvas_defs[$name])) { return ''; } // Apply args to template. $block = $canvas_defs[$name]($args); $raw = DeclarationList::parse($block, array( 'keyed' => true, 'lowercase_keys' => true, 'flatten' => true, 'apply_hooks' => true, )); // Create canvas object. $canvas = new Canvas(); // Parseable canvas attributes with default values. static $schema = array( 'fill' => null, 'background-fill' => null, 'src' => null, 'canvas-filter' => null, 'width' => null, 'height' => null, 'margin' => 0, ); // Resolve properties, set defaults if not present. $canvas->raw = array_intersect_key($raw, $schema) + $schema; // Pre-populate. canvas_preprocess($canvas); // Apply functions. canvas_apply_css_funcs($canvas); // debug($canvas); // Create fingerprint for this canvas based on canvas object. $fingerprint = substr(md5(serialize($canvas)), 0, 7); $generated_filename = "cnv-$name-$fingerprint.png"; if (! empty($process->options->asset_dir)) { $generated_filepath = $process->options->asset_dir . '/' . $generated_filename; $generated_url = Util::getLinkBetweenPaths( $process->output->dir, $process->options->asset_dir) . $generated_filename; } else { $generated_filepath = $process->output->dir . '/' . $generated_filename; $generated_url = $generated_filename; } $cached_file = file_exists($generated_filepath); // $cached_file = false; if (! $cached_file) { // Source arguments take priority. if ($src = canvas_fetch_src($canvas->raw['src'])) { // Resolve the src image dimensions and positioning. $dst_w = $src->width; $dst_h = $src->height; if (isset($canvas->width) && isset($canvas->height)) { $dst_w = $canvas->width; $dst_h = $canvas->height; } elseif (isset($canvas->width)) { $dst_w = $canvas->width; $dst_h = ($src->height/$src->width) * $canvas->width; } elseif (isset($canvas->height)) { $dst_w = ($src->width/$src->height) * $canvas->height; $dst_h = $canvas->height; } // Update the canvas height and width based on the src. $canvas->width = $dst_w; $canvas->height = $dst_h; // Create base. canvas_create($canvas); // Apply background layer. canvas_fill($canvas, 'background-fill'); // Filters. canvas_apply_filters($canvas, $src); // Place the src image on the base canvas image. imagecopyresized( $canvas->image, // dest_img $src->image, // src_img $canvas->margin->left, // dst_x $canvas->margin->top, // dst_y 0, // src_x 0, // src_y $dst_w, // dst_w $dst_h, // dst_h $src->width, // src_w $src->height // src_h ); imagedestroy($src->image); } else { // Set defaults. $canvas->width = isset($canvas->width) ? intval($canvas->width) : 100; $canvas->height = isset($canvas->height) ? intval($canvas->height) : 100; $canvas->fills += array('fill' => 'black'); // Create base. canvas_create($canvas); // Apply background layer. canvas_fill($canvas, 'background-fill'); canvas_fill($canvas, 'fill'); } } else { // debug('file cached'); } // Either write to a file. if ($context->function === 'canvas' && $process->ioContext === 'file') { if (! $cached_file) { imagepng($canvas->image, $generated_filepath); } $url = new Url($generated_url); $url->noRewrite = true; } // Or create data uri. else { if (! $cached_file) { ob_start(); imagepng($canvas->image); $data = ob_get_clean(); } else { $data = file_get_contents($generated_filepath); } $url = new Url('data:image/png;base64,' . base64_encode($data)); } $label = $process->tokens->add($url); // Cache the output URL. $process->misc->canvas_cache[$cache_key] = $label; return $label; } function canvas_fn_linear_gradient($input, $context) { $args = Functions::parseArgs($input) + array( 'white', 'black', ); $first_arg = strtolower($args[0]); static $directions = array( 'to top' => array('vertical', true), 'to right' => array('horizontal', false), 'to bottom' => array('vertical', false), 'to left' => array('horizontal', true), ); if (isset($directions[$first_arg])) { list($direction, $flip) = $directions[$first_arg]; array_shift($args); } else { list($direction, $flip) = $directions['to bottom']; } // Create fill object. $fill = new stdClass(); $fill->stops = array(); $fill->direction = $direction; canvas_set_fill_dims($fill, $context->canvas); // Start color. $color = Color::parse($args[0]); $fill->stops[] = $color ? $color : array(0,0,0,1); // End color. $color = Color::parse($args[1]); $fill->stops[] = $color ? $color : array(255,255,255,1); if ($flip) { $fill->stops = array_reverse($fill->stops); } $context->canvas->fills[$context->currentProperty] = $fill; } function canvas_fn_filter($input, $context) { $args = Functions::parseArgs($input); array_unshift($context->canvas->filters, array($context->function, $args)); } function canvas_apply_filters($canvas, $src) { foreach ($canvas->filters as $filter) { list($name, $args) = $filter; switch ($name) { case 'greyscale': case 'grayscale': imagefilter($src->image, IMG_FILTER_GRAYSCALE); break; case 'invert': imagefilter($src->image, IMG_FILTER_NEGATE); break; case 'opacity': canvas_fade($src, floatval($args[0])); break; case 'colorize': $rgb = $args + array('black'); if (count($rgb) === 1) { // If only one argument parse it as a CSS color value. $rgb = Color::parse($rgb[0]); if (! $rgb) { $rgb = array(0,0,0); } } imagefilter($src->image, IMG_FILTER_COLORIZE, $rgb[0], $rgb[1], $rgb[2]); break; case 'blur': $level = 1; if (isset($args[0])) { // Allow multiple blurs for a stronger effect. // Set hard limit. $level = min(max(intval($args[0]), 1), 20); } while ($level--) { imagefilter($src->image, IMG_FILTER_GAUSSIAN_BLUR); } break; case 'contrast': if (isset($args[0])) { // By default it works like this: // (max) -100 <- 0 -> +100 (min) // But we're flipping the polarity to be more predictable: // (min) -100 <- 0 -> +100 (max) $level = intval($args[0]) * -1; } imagefilter($src->image, IMG_FILTER_CONTRAST, $level); break; case 'brightness': if (isset($args[0])) { // -255 <- 0 -> +255 $level = intval($args[0]); } imagefilter($src->image, IMG_FILTER_BRIGHTNESS, $level); break; } } } function canvas_apply_css_funcs($canvas) { static $functions; if (! $functions) { $functions = new stdClass(); $functions->fill = new Functions(array('canvas-linear-gradient' => 'CssCrush\canvas_fn_linear_gradient')); $functions->generic = new Functions(array_diff_key(Crush::$process->functions->register, $functions->fill->register)); $functions->filter = new Functions(array( 'contrast' => 'CssCrush\canvas_fn_filter', 'opacity' => 'CssCrush\canvas_fn_filter', 'colorize' => 'CssCrush\canvas_fn_filter', 'grayscale' => 'CssCrush\canvas_fn_filter', 'greyscale' => 'CssCrush\canvas_fn_filter', 'brightness' => 'CssCrush\canvas_fn_filter', 'invert' => 'CssCrush\canvas_fn_filter', 'blur' => 'CssCrush\canvas_fn_filter', )); } $context = new stdClass(); foreach ($canvas->raw as $property => &$value) { if (! is_string($value)) { continue; } $value = $functions->generic->apply($value); $context->canvas = $canvas; if (in_array($property, array('fill', 'background-fill'))) { $context->currentProperty = $property; $value = $functions->fill->apply($value, $context); } elseif ($property === 'canvas-filter') { $value = $functions->filter->apply($value, $context); } } } function canvas_preprocess($canvas) { if (isset($canvas->raw['margin'])) { $parts = canvas_parselist($canvas->raw['margin']); $count = count($parts); if ($count === 1) { $margin = array($parts[0], $parts[0], $parts[0], $parts[0]); } elseif ($count === 2) { $margin = array($parts[0], $parts[1], $parts[0], $parts[1]); } elseif ($count === 3) { $margin = array($parts[0], $parts[1], $parts[2], $parts[1]); } else { $margin = $parts; } } else { $margin = array(0,0,0,0); } foreach (array('fill', 'background-fill') as $fill_name) { if (isset($canvas->raw[$fill_name])) { $canvas->fills[$fill_name] = $canvas->raw[$fill_name]; } } $canvas->margin = (object) array( 'top' => $margin[0], 'right' => $margin[1], 'bottom' => $margin[2], 'left' => $margin[3], ); $canvas->width = $canvas->raw['width']; $canvas->height = $canvas->raw['height']; } function canvas_fetch_src($url_token) { if ($url_token && $url = Crush::$process->tokens->get($url_token)) { $file = $url->getAbsolutePath(); // Testing the image availability and getting info. if ($info = @getimagesize($file)) { $image = null; // If image is available copy it. switch ($info['mime']) { case 'image/png': $image = imagecreatefrompng($file); break; case 'image/jpg': case 'image/jpeg': $image = imagecreatefromjpeg($file); break; case 'image/gif': $image = imagecreatefromgif($file); break; case 'image/webp': $image = imagecreatefromwebp($file); break; } if ($image) { return (object) array( 'file' => $file, 'info' => $info, 'width' => $info[0], 'height' => $info[1], 'image' => $image, ); } } } return false; } /* Adapted from GD Gradient Fill by Ozh (http://planetozh.com): http://planetozh.com/blog/my-projects/images-php-gd-gradient-fill */ function canvas_gradient($canvas, $fill) { $image = $canvas->image; // Resolve drawing direction. if ($fill->direction === 'horizontal') { $line_numbers = $fill->x2 - $fill->x1; } else { $line_numbers = $fill->y2 - $fill->y1; } list($r1, $g1, $b1, $a1) = $fill->stops[0]; list($r2, $g2, $b2, $a2) = $fill->stops[1]; $r = $g = $b = $a = -1; for ($line = 0; $line < $line_numbers; $line++) { $last = "$r,$g,$b,$a"; $r = $r2 - $r1 ? intval($r1 + ($r2 - $r1) * ($line / $line_numbers)): $r1; $g = $g2 - $g1 ? intval($g1 + ($g2 - $g1) * ($line / $line_numbers)): $g1; $b = $b2 - $b1 ? intval($b1 + ($b2 - $b1) * ($line / $line_numbers)): $b1; $a = $a2 - $a1 ? ($a1 + ($a2 - $a1) * ($line / $line_numbers)) : $a1; $a = canvas_opacity($a); if ($last != "$r,$g,$b,$a") { $color = imagecolorallocatealpha($image, $r, $g, $b, $a); } switch($fill->direction) { case 'horizontal': imagefilledrectangle($image, $fill->x1 + $line, $fill->y1, $fill->x1 + $line, $fill->y2, $color); break; case 'vertical': default: imagefilledrectangle($image, $fill->x1, $fill->y1 + $line, $fill->x2, $fill->y1 + $line, $color); break; } imagealphablending($image, true); } } function canvas_create($canvas) { $margin = $canvas->margin; $width = $canvas->width + $margin->right + $margin->left; $height = $canvas->height + $margin->top + $margin->bottom; // Create image object. $canvas->image = canvas_create_transparent($width, $height); } function canvas_create_transparent($width, $height) { $image = imagecreatetruecolor($width, $height); // Set transparent canvas background. imagealphablending($image, false); imagesavealpha($image, true); imagefill($image, 0, 0, imagecolorallocatealpha($image, 0, 0, 0, 127)); return $image; } function canvas_fade($src, $opacity) { $width = imagesx($src->image); $height = imagesy($src->image); $new_image = canvas_create_transparent($width, $height); $opacity = canvas_opacity($opacity); // Perform pixel-based alpha map application for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $colors = imagecolorsforindex($src->image, imagecolorat($src->image, $x, $y)); imagesetpixel($new_image, $x, $y, imagecolorallocatealpha( $new_image, $colors['red'], $colors['green'], $colors['blue'], $opacity)); } } imagedestroy($src->image); $src->image = $new_image; } function canvas_fill($canvas, $property) { if (! isset($canvas->fills[$property])) { return false; } $fill = $canvas->fills[$property]; // Gradient fill. if (is_object($fill)) { canvas_gradient($canvas, $fill); } // Solid color fill. elseif ($solid = Color::parse($fill)) { list($r, $g, $b, $a) = $solid; $color = imagecolorallocatealpha($canvas->image, $r, $g, $b, canvas_opacity($a)); $fill = new stdClass(); $canvas->currentProperty = $property; canvas_set_fill_dims($fill, $canvas); imagefilledrectangle($canvas->image, $fill->x1, $fill->y1, $fill->x2, $fill->y2, $color); imagealphablending($canvas->image, true); } // Can't parse. else { return false; } } function canvas_set_fill_dims($fill, $canvas) { // Resolve fill dimensions and coordinates. $margin = $canvas->margin; $fill->x1 = 0; $fill->y1 = 0; $fill->x2 = $canvas->width + $margin->right + $margin->left; $fill->y2 = $canvas->height + $margin->top + $margin->bottom; if (isset($canvas->currentProperty) && $canvas->currentProperty === 'fill') { $fill->x1 = $margin->left; $fill->y1 = $margin->top; $fill->x2 = $canvas->width + $fill->x1 - 1; $fill->y2 = $canvas->height + $fill->y1 - 1; } } function canvas_requirements() { $requirements_met = true; if (! extension_loaded('gd')) { $requirements_met = false; warning('GD extension not available.'); } else { $gd_info = implode('|', array_keys(array_filter(gd_info()))); foreach (array('jpe?g' => 'JPG', 'png' => 'PNG') as $file_ext_patt => $file_ext) { if (! preg_match("~\b(?<ext>$file_ext_patt) support\b~i", $gd_info)) { $requirements_met = false; warning("GD extension has no $file_ext support."); } } } return $requirements_met; } /* Canvas object. */ class Canvas { public $image, $fills = array(), $filters = array(); public function __destruct() { if (isset($this->image)) { imagedestroy($this->image); } } } /* Helpers. */ function canvas_opacity($float) { return 127 - max(min(round($float * 127), 127), 0); } function canvas_parselist($str, $numbers = true) { $list = preg_split('~ +~', trim($str)); return $numbers ? array_map('floatval', $list) : $list; } |