6

I'm using the Sonata Media Bundle and I need to code a specific behavior for the image resizer, because the default SimpleResizer and SquareResizer classes don't fit my needs.

I would like a simple image resizer that lets me resize the image exactly if I specify both the width and height parameters. I would also like that it can fallback on the simple resizer behavior if I don't specify a height parameter.

I've just searched for the docs but I wasn't able to find a solution.

Francesco Casula
  • 26,184
  • 15
  • 132
  • 131

1 Answers1

14

First of all you have to create a resizer service in your bundle in order to put it in the Sonata Media Bundle configuration.

# Acme/Bundle/CoreBundle/Resources/config/services.yml

services:
    sonata.media.resizer.custom:
        class: Acme\Bundle\CoreBundle\Resizer\CustomResizer
        arguments: [@sonata.media.adapter.image.gd, 'outbound', @sonata.media.metadata.proxy]

The second service argument, in this case, must be 'outbound'. Allowed parameters are ImageInterface::THUMBNAIL_INSET and ImageInterface::THUMBNAIL_OUTBOUND.

Now the Acme\Bundle\CoreBundle\Resizer\CustomResizer code:

<?php

    namespace Acme\Bundle\CoreBundle\Resizer;

    use Imagine\Image\ImagineInterface;
    use Imagine\Image\Box;
    use Gaufrette\File;
    use Sonata\MediaBundle\Model\MediaInterface;
    use Sonata\MediaBundle\Resizer\ResizerInterface;
    use Imagine\Image\ImageInterface;
    use Imagine\Exception\InvalidArgumentException;
    use Sonata\MediaBundle\Metadata\MetadataBuilderInterface;

    class CustomResizer implements ResizerInterface
    {
        protected $adapter;
        protected $mode;
        protected $metadata;

        /**
         * @param ImagineInterface $adapter
         * @param string $mode
         */
        public function __construct(ImagineInterface $adapter, $mode, MetadataBuilderInterface $metadata)
        {
            $this->adapter = $adapter;
            $this->mode = $mode;
            $this->metadata = $metadata;
        }

        /**
         * {@inheritdoc}
         */
        public function resize(MediaInterface $media, File $in, File $out, $format, array $settings)
        {
            if (!(isset($settings['width']) && $settings['width']))
                throw new \RuntimeException(sprintf('Width parameter is missing in context "%s" for provider "%s"', $media->getContext(), $media->getProviderName()));

            $image = $this->adapter->load($in->getContent());

            $content = $image
                       ->thumbnail($this->getBox($media, $settings), $this->mode)
                       ->get($format, array('quality' => $settings['quality']));

            $out->setContent($content, $this->metadata->get($media, $out->getName()));
        }

        /**
         * {@inheritdoc}
         */
        public function getBox(MediaInterface $media, array $settings)
        {
            $size = $media->getBox();
            $hasWidth = isset($settings['width']) && $settings['width'];
            $hasHeight = isset($settings['height']) && $settings['height'];

            if (!$hasWidth && !$hasHeight)
                throw new \RuntimeException(sprintf('Width/Height parameter is missing in context "%s" for provider "%s". Please add at least one parameter.', $media->getContext(), $media->getProviderName()));

            if ($hasWidth && $hasHeight)
                return new Box($settings['width'], $settings['height']);

            if (!$hasHeight)
                $settings['height'] = intval($settings['width'] * $size->getHeight() / $size->getWidth());

            if (!$hasWidth)
                $settings['width'] = intval($settings['height'] * $size->getWidth() / $size->getHeight());

            return $this->computeBox($media, $settings);
        }

        /**
         * @throws InvalidArgumentException
         *
         * @param MediaInterface $media
         * @param array $settings
         *
         * @return Box
         */
        private function computeBox(MediaInterface $media, array $settings)
        {
            if ($this->mode !== ImageInterface::THUMBNAIL_INSET && $this->mode !== ImageInterface::THUMBNAIL_OUTBOUND)
                throw new InvalidArgumentException('Invalid mode specified');

            $size = $media->getBox();

            $ratios = [
                $settings['width'] / $size->getWidth(),
                $settings['height'] / $size->getHeight()
            ];

            if ($this->mode === ImageInterface::THUMBNAIL_INSET)
                $ratio = min($ratios);
            else
                $ratio = max($ratios);

            return $size->scale($ratio);
        }
    }

Well done. Your service is defined. You have to link it in the app/config.yml and all is done. I've included the whole sonata_media configuration in order to provide a good example, but remember you only need the last three lines.

sonata_media:
    default_context: default
    db_driver: doctrine_orm # or doctrine_mongodb, doctrine_phpcr
    contexts:
        default:  # the default context is mandatory
            providers:
                - sonata.media.provider.dailymotion
                - sonata.media.provider.youtube
                - sonata.media.provider.image
                - sonata.media.provider.file

            formats:
                small: { width: 100, height: 100, quality: 70 }
                big:   { width: 500, height: 300, quality: 70 }
            download:
                strategy: sonata.media.security.public_strategy
    cdn:
        server:
            path: /uploads/media # http://media.sonata-project.org/
    filesystem:
        local:
            directory:  %kernel.root_dir%/../web/uploads/media
            create:     true
    providers:
        image:
            resizer: sonata.media.resizer.custom # THIS IS OUR NEW RESIZER SERVICE
Francesco Casula
  • 26,184
  • 15
  • 132
  • 131
  • Thanks @fra_casula, very informative and helpful explanation. In addition to your question and answer, I also have need to add custom area for my resizer. Think like Facebook's profile photo editor, which you can set the specific bounds after uploading your photo with an user interface. Are there any ways to achieve this? Thanks. – bateristt Jun 28 '13 at 11:09
  • In this specific case, you need to upload the image before any crop/resize through a user interface. So in the first upload probably you don't want any resizer. After the first upload you can use a jquery plugin (like Jcrop) to provide an user interface and, if you use the SonataAdminBundle you can specify a custom CRUD Controller in order to handle this type of customizations. Finally consider that the resizer is even a service, so try to pass the @service_container at your service in order to catch the request and read the resize data provided from jcrop. – Francesco Casula Jun 28 '13 at 14:16
  • I was trying to create a custom resizer too. A zoom-crop style resizer. And your example worked like a charm. First i copy pasted it, and planned to do code on top of it. But seems like you already implemented a zoom-crop resizer here ? – Sarim Mar 31 '14 at 20:52
  • 1
    You can make width an optional value by calculating it from the original aspect ratio applied to the height. In this way yo can substitute the "RuntimeException" line (38) with: `$settings['width'] = $settings['height']*$media->getWidth()/$media->getHeight();` – jonaguera Apr 12 '16 at 12:05
  • In `public function resize()` instead `if (!(isset($settings['width']) && $settings['width']))` ===> **`if (!(isset($settings['width']) && $settings['width']) && !(isset($settings['height']) && $settings['height']))`** – rrr_2010 Jun 22 '16 at 16:20
  • Is it possible to use this in Controller? I have tired like this , `$resizer = $this->container->get('sonata.media.resizer.custom');` `$media= $this->em->createQuery("SELECT a FROM ApplicationSonataMediaBundle:Media a")->getSingleResult();` `$resizer->resize($media,$inFile,$outFile,$format,$setting);` What should I put into $inFile,$outFile,$format,$setting?? – whitebear Feb 25 '17 at 14:12