public function __construct(modX &$modx, $graphicsLib = null) { if ($graphicsLib === null) { $graphicsLib = $modx->getOption('resizer.graphics_library', null, 2); } parent::__construct($graphicsLib); $this->debugmessages = str_replace('Reductionist', 'Resizer', $this->debugmessages); // Add some common MODX search paths for watermark images and fonts self::$assetpaths[] = $modx->getOption('assets_path'); self::$assetpaths[] = MODX_BASE_PATH; self::$assetpaths[] = MODX_CORE_PATH; self::$assetpaths[] = MODX_CORE_PATH . 'model/phpthumb/fonts/'; self::$assetpaths[] = MODX_CORE_PATH . 'model/phpthumb/images/'; }
public function processImage($input, $output, $options = array()) { if ($this->debug) { $startTime = microtime(true); } if (!is_readable($input)) { $this->debugmessages[] = 'File not ' . (file_exists($input) ? 'readable' : 'found') . ": {$input} *** Skipping ***"; return false; } $this->width = $this->height = null; if (is_string($options)) { $options = parse_str($options); } // convert an options string to an array if needed $inputParams = array('options' => $options); $outputType = pathinfo($output, PATHINFO_EXTENSION); $outputIsJpg = strncasecmp('jp', $outputType, 2) === 0; try { /* initial dimensions */ $image = $this->imagine->open($input); $size = $image->getSize(); $origWidth = $inputParams['width'] = $size->getWidth(); $origHeight = $inputParams['height'] = $size->getHeight(); if (self::$maxsize && $origWidth * $origHeight > self::$maxsize) { // if we're using GD we need to check the image will fit in memory $this->debugmessages[] = "GD: {$input} may exceed available memory ** Skipping **"; return false; } /* source crop (start) */ if (isset($options['sw']) || isset($options['sh'])) { if (empty($options['sw']) || $options['sw'] > $origWidth) { $newWidth = $origWidth; } else { $newWidth = $options['sw'] < 1 ? round($origWidth * $options['sw']) : $options['sw']; // sw < 1 is a %, >= 1 in px } if (empty($options['sh']) || $options['sh'] > $origHeight) { $newHeight = $origHeight; } else { $newHeight = $options['sh'] < 1 ? round($origHeight * $options['sh']) : $options['sh']; } if ($newWidth !== $origWidth || $newHeight !== $origHeight) { // only if something will actually be cropped if (empty($options['sx'])) { $cropStartX = isset($options['sx']) ? $options['sx'] : (int) (($origWidth - $newWidth) / 2); // 0 or center } else { $cropStartX = $options['sx'] < 1 ? round($origWidth * $options['sx']) : $options['sx']; if ($cropStartX + $newWidth > $origWidth) { $cropStartX = $origWidth - $newWidth; } // crop box can't go past the right edge } if (empty($options['sy'])) { $cropStartY = isset($options['sy']) ? $options['sy'] : (int) (($origHeight - $newHeight) / 2); } else { $cropStartY = $options['sy'] < 1 ? round($origHeight * $options['sy']) : $options['sy']; if ($cropStartY + $newHeight > $origHeight) { $cropStartY = $origHeight - $newHeight; } } $scBox = new Box($newWidth, $newHeight); $origWidth = $newWidth; // update input dimensions to the new cropped size $origHeight = $newHeight; } } $origAR = $origWidth / $origHeight; // original image aspect ratio $origAspect = round($origAR, 2); /* input dimensions */ // use width/height if specified if (isset($options['w'])) { $width = $options['w']; } if (isset($options['h'])) { $height = $options['h']; } // override with any orientation-specific dimensions if ($origAspect > 1) { // landscape if (isset($options['wl'])) { $width = $options['wl']; } if (isset($options['hl'])) { $height = $options['hl']; } } elseif ($origAspect < 1) { // portrait if (isset($options['wp'])) { $width = $options['wp']; } if (isset($options['hp'])) { $height = $options['hp']; } } else { // square if (isset($options['ws'])) { $width = $options['ws']; } if (isset($options['hs'])) { $height = $options['hs']; } } // fill in a missing dimension $bothDims = true; if (empty($width)) { if (empty($height)) { $height = $origHeight; $width = $origWidth; } else { $width = $height * $origAR; } $bothDims = false; } if (empty($height)) { $height = $width / $origAR; $bothDims = false; } $newAR = $width / $height; /* scale */ if (empty($options['scale'])) { $requestedMP = $width * $height; // we'll need this for quality scaling } else { $requestedMP = $width * $options['scale'] * $height * $options['scale']; if (empty($options['aoe'])) { // if aoe is off, cap scale so image isn't enlarged $hScale = $origHeight / $height; $wScale = $origWidth / $width; $wRequested = $width * $options['scale']; $hRequested = $height * $options['scale']; $options['scale'] = $hScale > 1 && $wScale > 1 ? min($hScale, $wScale, $options['scale']) : 1; } $options['w'] = $width *= $options['scale']; $options['h'] = $height *= $options['scale']; } if (empty($options['zc']) || !$bothDims) { /* non-zc sizing */ $newAspect = round($newAR, 2); // ignore small differences if ($newAspect < $origAspect) { // Make sure AR doesn't change. Smaller dimension... if ($origWidth < $options['w'] && empty($options['aoe'])) { $options['w'] = $width = $origWidth; $options['h'] = $width / $newAR; } $height = $width / $origAR; } elseif ($newAspect > $origAspect) { // ...limits larger if ($origHeight < $options['h'] && empty($options['aoe'])) { $options['h'] = $height = $origHeight; $options['w'] = $height * $newAR; } $width = $height * $origAR; } $width = round($width); // clean up $height = round($height); /* far */ if (!empty($options['far']) && $bothDims) { $options['w'] = round($options['w']); $options['h'] = round($options['h']); if ($options['w'] > $width || $options['h'] > $height) { $farPoint = Reductionist::startPoint($options['far'], array($options['w'], $options['h']), array($width, $height)); $farBox = new Box($options['w'], $options['h']); $this->width = $options['w']; $this->height = $options['h']; } } } else { /* zc */ if (empty($options['aoe'])) { // if the crop box is bigger than the original image, scale it down if ($width > $origWidth) { $height = $origWidth / $newAR; $width = $origWidth; } if ($height > $origHeight) { $width = $origHeight * $newAR; $height = $origHeight; } } // make sure final image will cover the crop box $width = round($width); $height = round($height); $newWidth = round($height * $origAR); $newHeight = round($width / $origAR); if ($newWidth > $width) { // needs horizontal cropping $newHeight = $height; } elseif ($newHeight > $height) { // needs vertical cropping $newWidth = $width; } else { // no cropping needed, same AR $newWidth = $width; $newHeight = $height; } $cropStart = Reductionist::startPoint($options['zc'], array($newWidth, $newHeight), array($width, $height)); $cropBox = new Box($width, $height); $this->width = $width; $this->height = $height; $width = $newWidth; $height = $newHeight; } /* source crop (finish) */ if (isset($scBox)) { $scale = max($width / $origWidth, $height / $origHeight); if ($scale <= 0.5 && $this->gLib && $image->getFormat() === IMG_JPG) { $image->resize(new Box(round($inputParams['width'] * $scale), round($inputParams['height'] * $scale))); $scStart = new \Imagine\Image\Point(round($cropStartX * $scale), round($cropStartY * $scale)); $scBox = new Box(round($scBox->getWidth() * $scale), round($scBox->getHeight() * $scale)); } else { $scStart = new \Imagine\Image\Point($cropStartX, $cropStartY); } $image->crop($scStart, $scBox); if (abs($scBox->getWidth() - $width) == 1) { // snap a 1px rounding error to the source crop box $this->width = $width = $scBox->getWidth(); } if (abs($scBox->getHeight() - $height) == 1) { $this->height = $height = $scBox->getHeight(); } } /* resize, aoe */ if ($didScale = $width < $origWidth && $height < $origHeight || !empty($options['aoe'])) { $imgBox = new Box($width, $height); $image->resize($imgBox); } elseif (isset($options['qmax']) && empty($options['aoe']) && isset($options['q']) && $outputIsJpg) { // undersized input image. We'll increase q towards qmax depending on how much it's undersized $sizeRatio = $requestedMP / (isset($cropBox) ? $this->width * $this->height : $width * $height); if ($sizeRatio >= 2) { $options['q'] = $options['qmax']; } elseif ($sizeRatio > 1) { $options['q'] += round(($options['qmax'] - $options['q']) * ($sizeRatio - 1)); } } /* crop */ if (isset($cropBox)) { $image->crop($cropStart, $cropBox); } /* filters (start) */ if (!empty($options['fltr'])) { if (!is_array($options['fltr'])) { $options['fltr'] = array($options['fltr']); // in case somebody did fltr= instead of fltr[]= } $transformation = new \Imagine\Filter\Transformation($this->imagine); $filterlog = array($this->debug); foreach ($options['fltr'] as $fltr) { $filter = explode('|', $fltr); if ($filter[0] === 'usm') { // right now only unsharp mask is implemented, sort of $image->effects()->sharpen(); // radius, amount and threshold are ignored! } elseif ($filter[0] === 'wmt' || $filter[0] === 'wmi') { $doApply = true; $transformation->add(new Filter\Watermark($filter, $filterlog)); } } } /* bg */ if ($hasBG = isset($options['bg']) && !$outputIsJpg || isset($farBox)) { if (self::$palette === null) { self::$palette = new \Imagine\Image\Palette\RGB(); } if (isset($options['bg'])) { $bgColor = explode('/', $options['bg']); $bgColor[1] = isset($bgColor[1]) ? $bgColor[1] : 100; } else { $bgColor = array(array(255, 255, 255), 100); } $backgroundColor = self::$palette->color($bgColor[0], 100 - $bgColor[1]); if (isset($cropBox)) { $bgBox = $cropBox; } elseif (isset($farBox)) { $bgBox = $farBox; } elseif (isset($imgBox)) { $bgBox = $imgBox; } else { $bgBox = new Box($width, $height); } $image = $this->imagine->create($bgBox, self::$palette->color($bgColor[0], 100 - $bgColor[1]))->paste($this->gLib ? $image->getImage() : $image, isset($farPoint) ? $farPoint : new \Imagine\Image\Point(0, 0)); } /* filters (finish) */ if (isset($transformation) && !empty($doApply)) { // apply any filters try { $transformation->apply($image); } catch (\Exception $e) { $this->debugmessages[] = $e->getMessage(); } } /* debug info */ if ($this->debug) { $debugTime = microtime(true); $this->debugmessages[] = 'Input options:' . self::formatDebugArray($inputParams['options']); // print all options, stripping off array() $changed = array(); // note any options which may have been changed during processing foreach (array('w', 'h', 'scale', 'q') as $opt) { if (isset($inputParams['options'][$opt]) && $inputParams['options'][$opt] != $options[$opt]) { $changed[$opt] = $options[$opt]; } } if ($changed) { $this->debugmessages[] = 'Modified options:' . self::formatDebugArray($changed, true); } $this->debugmessages[] = "Original - w: {$inputParams['width']} | h: {$inputParams['height']} " . sprintf("(%2.2f MP)", $inputParams['width'] * $inputParams['height'] / 1000000.0); if (isset($image->prescalesize)) { $this->debugmessages[] = "JPEG prescale - w: {$image->prescalesize[0]} | h: {$image->prescalesize[1]} " . sprintf("(%2.2f MP)", $image->prescalesize[0] * $image->prescalesize[1] / 1000000.0); } if (isset($scBox)) { $this->debugmessages[] = "Source area - start: {$scStart} | box: {$scBox}"; } if (isset($wRequested)) { $this->debugmessages[] = "Requested - w: " . round($wRequested) . ' | h: ' . round($hRequested); } if (!isset($wRequested) || !$didScale) { $this->debugmessages[] = "New - w: {$width} | h: {$height}" . ($didScale ? '' : ' [Not scaled: same size or insufficient input resolution]'); } if (isset($farPoint)) { $this->debugmessages[] = "FAR - start: {$farPoint} | box: {$options['w']}x{$options['h']} px"; } if (isset($cropBox)) { $this->debugmessages[] = "ZC - start: {$cropStart} | box: {$cropBox}"; } if ($hasBG) { $this->debugmessages[] = "Background color: {$bgColor[0]} | opacity: {$bgColor[1]}"; } $debugTime = microtime(true) - $debugTime; } if (isset($filterlog[1])) { // add any filter debug output unset($filterlog[0]); $this->debugmessages = array_merge($this->debugmessages, $filterlog); } /* save */ $outputOpts = array('jpeg_quality' => empty($options['q']) ? $this->defaultQuality : (int) $options['q'], 'format' => empty($options['f']) ? $outputType : $options['f']); $image->save($output, $outputOpts); if (!$this->width) { $this->width = $width; } if (!$this->height) { $this->height = $height; } } catch (\Imagine\Exception\Exception $e) { $this->debugmessages[] = "Input file: {$input}"; $this->debugmessages[] = 'Input options: ' . self::formatDebugArray($inputParams['options']); $this->debugmessages[] = "*** Error *** {$e->getMessage()}"; return false; } /* debug info (timing) */ if ($this->debug) { $this->debugmessages[] = "Wrote {$output}"; $this->debugmessages[] = "Dimensions: {$this->width}x{$this->height} px"; $this->debugmessages[] = 'Execution time: ' . round((microtime(true) - $startTime - $debugTime) * 1000.0) . ' ms'; } return true; }
public function apply(ImageInterface $image) { $imagine = $this->getImagine(); $class = get_class($image); $isRImage = strpos($class, 'RImage'); $isGmagick = strpos($class, 'Gmagick'); /* Unfortunately Gmagick doesn't support opacity, so that's out for text and watermark images. Also watermark images need opacity in order to be rotated */ if ($this->opt[0] === 'wmt') { /* Text Watermark */ $p = array('text' => empty($this->opt[1]) ? '' : $this->opt[1], 'fontsize' => empty($this->opt[2]) ? 12 : (int) $this->opt[2], 'alignment' => empty($this->opt[3]) ? 'C' : $this->opt[3], 'color' => empty($this->opt[4]) ? '000' : $this->opt[4], 'opacity' => empty($this->opt[6]) ? 100 : (int) $this->opt[6], 'margin' => empty($this->opt[7]) ? 3 : (double) $this->opt[7], 'angle' => empty($this->opt[8]) ? 0 : (double) $this->opt[8], 'bgcolor' => empty($this->opt[9]) ? null : $this->opt[9], 'bgopacity' => empty($this->opt[10]) ? 100 : (int) $this->opt[10]); if (empty($this->opt[5]) || null === ($p['fontfile'] = Reductionist::findFile($this->opt[5]))) { // font file $p['fontfile'] = realpath(__DIR__ . '/../resources/FiraSansOT-Medium.otf'); // default to included Fira Sans } if ($this->debug) { $this->debugmessages[] = 'Filter :: Text Watermark' . Reductionist::formatDebugArray($p); } if (empty($p['text'])) { // no text, so quit return $image; } $alpha = $isGmagick ? 0 : 100 - $p['opacity']; try { // Set up font and bounding box $font = $imagine->font($p['fontfile'], $p['fontsize'], self::$rgb->color($p['color'], $alpha)); $wmBox = $font->box($p['text'], $p['bgcolor'] === null ? $p['angle'] : 0); $wmWidth = $wmBox->getWidth(); $wmHeight = $wmBox->getHeight(); $imgSize = $image->getSize(); $imgWidth = $imgSize->getWidth(); $imgHeight = $imgSize->getHeight(); // Calculate bg box padding, or text margin if ($p['margin'] < 1) { // as a percent of image dimensions $paddingX = round($imgWidth * $p['margin']); $paddingY = round($imgHeight * $p['margin']); } else { // as an explicit pixel value $paddingX = $paddingY = (int) $p['margin']; } $wmbgWidth = $wmWidth + 2 * $paddingX; // bg box dimensions $wmbgHeight = $wmHeight + 2 * $paddingY; // Check box size and reduce margin as needed to fit text into image area if ($wmbgWidth > $imgWidth) { $paddingX = max(round(($imgWidth - $wmWidth) / 2), 0); $wmbgWidth = $wmWidth + 2 * $paddingX; if ($this->debug) { $this->debugmessages[] = ":: Text watermark overflow: horizontal margin reduced to {$paddingX}px"; } } if ($wmbgHeight > $imgHeight) { $paddingY = max(round(($imgHeight - $wmHeight) / 2), 0); $wmbgHeight = $wmHeight + 2 * $paddingY; if ($this->debug) { $this->debugmessages[] = ":: Text watermark overflow: vertical margin reduced to {$paddingY}px"; } } $doRotate = !$isGmagick && $p['angle'] % 360; if ($doRotate && $p['bgcolor'] === null && $isRImage) { // if text will be rotated, use a transparent bg for better positioning $p['bgcolor'] = array(255, 255, 255); $p['bgopacity'] = 0; } if ($p['bgcolor']) { // if we have a bg color, add a bg box $wmbg = $imagine->create(new Box($wmbgWidth, $wmbgHeight), self::$rgb->color($p['bgcolor'], 100 - $p['bgopacity'])); $wmbg->draw()->text($p['text'], $font, new Point($paddingX, $paddingY), 0); // add text if ($doRotate) { $wmbg->rotate($p['angle'], self::$rgb->color(array(255, 255, 255), 100)); $wmbgSize = $wmbg->getSize(); $wmbgWidth = $wmbgSize->getWidth(); $wmbgHeight = $wmbgSize->getHeight(); } if ($wmbgWidth > $imgWidth || $wmbgHeight > $imgHeight) { // if the box overflows the image... $wmbgWidth = $wmbgWidth > $imgWidth ? $imgWidth : $wmbgWidth; $wmbgHeight = $wmbgHeight > $imgHeight ? $imgHeight : $wmbgHeight; $wmbg->crop(new Point(0, 0), new Box($wmbgWidth, $wmbgHeight)); } $wmbgStartPoint = Reductionist::startPoint($p['alignment'], array($imgWidth, $imgHeight), array($wmbgWidth, $wmbgHeight)); if ($this->debug) { $this->debugmessages[] = ":: Text watermark with background: {$wmbgWidth}x{$wmbgHeight} px @ {$wmbgStartPoint}"; } $image->paste($isRImage ? $wmbg->getImage() : $wmbg, $wmbgStartPoint); // add to image } else { // otherwise simply add text $wmbgStartPoint = Reductionist::startPoint($p['alignment'], array($imgWidth, $imgHeight), array($wmbgWidth, $wmbgHeight)); $wmStartPoint = new Point($wmbgStartPoint->getX() + $paddingX, $wmbgStartPoint->getY() + $paddingY); if ($this->debug) { $this->debugmessages[] = ":: Text watermark: {$wmBox} @ {$wmStartPoint}"; } $image->draw()->text($p['text'], $font, $wmStartPoint, $p['angle']); // add to Image } } catch (\Exception $e) { $this->debugmessages[] = '*** Text Watermark Error: ' . $e->getMessage(); return $image; } } elseif ($this->opt[0] === 'wmi') { /* Image Watermark */ $file = empty($this->opt[1]) ? null : Reductionist::findFile($this->opt[1]); $p = array('file' => $file, 'alignment' => empty($this->opt[2]) ? 'C' : $this->opt[2], 'opacity' => empty($this->opt[3]) ? 100 : (int) $this->opt[3], 'x' => empty($this->opt[4]) ? 0 : (double) $this->opt[4], 'y' => empty($this->opt[5]) ? 0 : (double) $this->opt[5], 'angle' => empty($this->opt[6]) ? 0 : (double) $this->opt[6]); if ($this->debug) { $this->debugmessages[] = 'Filter :: Image Watermark' . Reductionist::formatDebugArray($p); } if ($file === null) { $this->debugmessages[] = '*** Image Watermark Error: ' . (empty($this->opt[1]) ? 'no image specified' : "{$this->opt[1]} not found"); return $image; } try { $imgSize = $image->getSize(); $imgWidth = $imgSize->getWidth(); $imgHeight = $imgSize->getHeight(); $wm = $imagine->open($p['file']); $wmSize = $wm->getSize(); $wmWidth = $wmSize->getWidth(); $wmHeight = $wmSize->getHeight(); $doRotate = !$isGmagick && $p['angle'] % 360; if ($doRotate) { // calculate bounding box for rotated image $rads = deg2rad($p['angle']); $sin = sin($rads); $cos = cos($rads); $rotWidth = ceil(abs($wmWidth * $cos) + abs($wmHeight * $sin)); $rotHeight = ceil(abs($wmWidth * $sin) + abs($wmHeight * $cos)); $imgSize = new Box((int) ($imgWidth * $wmWidth / $rotWidth) - 1, (int) ($imgHeight * $wmHeight / $rotHeight) - 1); $wmWidth = $rotWidth; $wmHeight = $rotHeight; } if ($wmWidth > $imgWidth || $wmHeight > $imgHeight) { // scale watermark down if it's bigger than the image $wm->thumbnail($imgSize); $wmSize = $wm->getSize(); if ($this->debug) { if (!empty($wm->prescalesize)) { $this->debugmessages[] = ":: Image watermark prescale: " . $wm->getPrescaleSize(); } $this->debugmessages[] = ":: Image watermark size reduced to {$wmSize}"; } } if ($isRImage && !$isGmagick && $p['opacity'] < 100) { // for Imagick we can easily reduce transparency try { $wm->fade($p['opacity'] / 100); } catch (\Exception $e) { // maybe. fallback for ImageMagick < 6.3.1 $a = round((100 - $p['opacity']) * 2.55); // calculate alpha (0:opaque - 255:transparent) $mask = $imagine->create($wmSize, self::$rgb->color(array($a, $a, $a))); $wm->applyMask($mask->getImage()); } } // Rotation if ($doRotate) { $wm->rotate($p['angle'], self::$rgb->color(array(255, 255, 255), 100)); $wmSize = $wm->getSize(); if ($wmSize->getWidth() > $imgWidth || $wmSize->getHeight() > $imgHeight) { // one more check. Shouldn't be necessary, but it sometimes is $wm = $wm->thumbnail(new Box($imgWidth, $imgHeight)); $wmSize = $wm->getSize(); if ($this->debug) { $this->debugmessages[] = ":: Image watermark size reduced to {$wmSize}"; } } } $wmWidth = $wmSize->getWidth(); $wmHeight = $wmSize->getHeight(); // Margin if ($p['x']) { if ($p['x'] < 1) { $p['x'] = round($imgWidth * $p['x']); } if (2 * $p['x'] + $wmWidth - $imgWidth > 0) { // reduce if necessary $p['x'] = (int) (($imgWidth - $wmWidth) / 2); if ($this->debug) { $this->debugmessages[] = ":: Image watermark X margin reduced to {$p['x']} px"; } } } if ($p['y']) { if ($p['y'] < 1) { $p['y'] = round($imgHeight * $p['y']); } if (2 * $p['y'] + $wmHeight - $imgHeight > 0) { $p['y'] = (int) (($imgHeight - $wmHeight) / 2); if ($this->debug) { $this->debugmessages[] = ":: Image watermark Y margin reduced to {$p['y']} px"; } } } $wmStartPoint = Reductionist::startPoint($p['alignment'], array($imgWidth, $imgHeight), array($wmWidth + 2 * $p['x'], $wmHeight + 2 * $p['y'])); if ($p['x'] || $p['y']) { // adjust paste point for margins $wmStartPoint = new Point($wmStartPoint->getX() + $p['x'], $wmStartPoint->getY() + $p['y']); if ($this->debug) { $this->debugmessages[] = ":: Image watemark margins: X {$p['x']} px, Y {$p['y']} px"; } } if ($this->debug) { $this->debugmessages[] = ":: Image watermark: {$wmSize} @ {$wmStartPoint}"; } if ($isRImage) { $image->paste($wm->getImage(), $wmStartPoint); } elseif ($p['opacity'] >= 100) { // GD $image->paste($wm, $wmStartPoint); } else { // GD: paste with opacity $orig = $image->getGdResource(); $img = imagecreatetruecolor($wmWidth, $wmHeight); imagecopy($img, $orig, 0, 0, $wmStartPoint->getX(), $wmStartPoint->getY(), $wmWidth, $wmHeight); imagecopy($img, $wm->getGdResource(), 0, 0, 0, 0, $wmWidth, $wmHeight); imagecopymerge($orig, $img, $wmStartPoint->getX(), $wmStartPoint->getY(), 0, 0, $wmWidth, $wmHeight, $p['opacity']); imagedestroy($img); unset($orig); } } catch (\Exception $e) { $this->debugmessages[] = '*** Image Watermark Error: ' . $e->getMessage(); return $image; } } return $image; }