Ejemplo n.º 1
0
 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/';
 }
Ejemplo n.º 2
0
 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;
 }
Ejemplo n.º 3
0
 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;
 }