/ Gists / Image pipe for #nettefw templates
On gists

Image pipe for #nettefw templates

Nette Nette-Tricks
Image pipe for #nettefw templates

config.neon Raw #

nette:
	latte:
		macros: [Img\ImgMacro]

services:
	imageStorage: Img\ImageStorage(%tempDir%)
	imagePipe: Img\ImagePipe(%wwwDir%)

Size.php Raw #

<?php

namespace Img;

use Nette;



/**
 * @author Filip Procházka <filip@prochazka.su>
 *
 * @property float|int $width
 * @property float|int $height
 * @property-read float|int $height
 */
class Size extends Nette\Object
{

	/**
	 * @var float|int
	 */
	private $width;

	/**
	 * @var float|int
	 */
	private $height;



	/**
	 * @param float|int $width
	 * @param float|int $height
	 */
	public function __construct($width, $height)
	{
		$this->width = $width;
		$this->height = $height;
	}



	/**
	 * @return float|int
	 */
	public function getHeight()
	{
		return $this->height;
	}



	/**
	 * @return float|int
	 */
	public function getWidth()
	{
		return $this->width;
	}



	/**
	 * @param string $file
	 *
	 * @return Size
	 */
	public static function fromFile($file)
	{
		list($width, $height) = @getimagesize($file);
		return new Size($width, $height);
	}

}

ImgMacro.php Raw #

<?php

namespace Img;

use Kdyby;
use Nette;
use Nette\Latte\PhpWriter;
use Nette\Latte\MacroNode;
use Nette\Latte\Compiler;



/**
 * @author Filip Procházka <filip@prochazka.su>
 */
class ImgMacro extends Nette\Latte\Macros\MacroSet
{

	/**
	 * @var bool
	 */
	private $isUsed = FALSE;



	/**
	 * @param \Nette\Latte\Compiler $compiler
	 *
	 * @return ImgMacro|\Nette\Latte\Macros\MacroSet
	 */
	public static function install(Compiler $compiler)
	{
		$me = new static($compiler);
		// todo: předání parametrů s velikostí

		/**
		 * {img} je obecně pro jakýkoliv obrázek, který je veřejný na webu.
		 */
		$me->addMacro('img', array($me, 'macroImg'), NULL, array($me, 'macroAttrImg'));

		/**
		 * {tempImg} je kvůli obrázkům, které nejsou "schválené" a je pravděpodobné, 
		 * že se budou mazat. Tak aby byly ve zvláštní složce, kterou může promazávat robot.
		 * Pokud nic takového nepotřebujete, používejte prostě {img}.
		 */
		$me->addMacro('tempImg', array($me, 'macroImgTemp'), NULL, array($me, 'macroAttrImgTemp'));

		return $me;
	}



	/**
	 * @param \Nette\Latte\MacroNode $node
	 * @param \Nette\Latte\PhpWriter $writer
	 * @return string
	 */
	public function macroImg(MacroNode $node, PhpWriter $writer)
	{
		$this->isUsed = TRUE;
		return $writer->write('echo %escape($_imagePipe->request(%node.word))');
	}



	/**
	 * @param \Nette\Latte\MacroNode $node
	 * @param \Nette\Latte\PhpWriter $writer
	 * @return string
	 */
	public function macroAttrImg(MacroNode $node, PhpWriter $writer)
	{
		$this->isUsed = TRUE;
		return $writer->write('?>src="<?php echo %escape($_imagePipe->request(%node.word))?>" <?php');
	}



	/**
	 * @param \Nette\Latte\MacroNode $node
	 * @param \Nette\Latte\PhpWriter $writer
	 * @return string
	 */
	public function macroImgTemp(MacroNode $node, PhpWriter $writer)
	{
		$this->isUsed = TRUE;
		return $writer->write('echo %escape($_imagePipe->request(%node.word, array(), $_imagePipe::TEMP))');
	}



	/**
	 * @param \Nette\Latte\MacroNode $node
	 * @param \Nette\Latte\PhpWriter $writer
	 * @return string
	 */
	public function macroAttrImgTemp(MacroNode $node, PhpWriter $writer)
	{
		$this->isUsed = TRUE;
		return $writer->write('?>src="<?php echo %escape($_imagePipe->request(%node.word, array(), $_imagePipe::TEMP))?>" <?php');
	}



	/**
	 */
	public function initialize()
	{
		$this->isUsed = FALSE;
	}



	/**
	 * Finishes template parsing.
	 * @return array(prolog, epilog)
	 */
	public function finalize()
	{
		if (!$this->isUsed) {
			return array();
		}

		return array(
			get_called_class() . '::validateTemplateParams($template);',
			NULL
		);
	}



	/**
	 * @param \Nette\Templating\Template $template
	 * @throws \Nette\InvalidStateException
	 */
	public static function validateTemplateParams(Nette\Templating\Template $template)
	{
		$params = $template->getParameters();
		if (!isset($params['_imagePipe']) || !$params['_imagePipe'] instanceof ImagePipe) {
			$where = isset($params['control']) ?
				" of component " . get_class($params['control']) . '(' . $params['control']->getName() . ')'
				: NULL;

			throw new Nette\InvalidStateException(
				'Please provide an instanceof Img\\ImagePipe ' .
				'as a parameter $_imagePipe to template' . $where
			);
		}
	}

}

ImageStorage.php Raw #

<?php

namespace Img;

use Kdyby;
use Nette;
use Nette\Http\FileUpload;
use Nette\Utils\Finder;
use Nette\Utils\Strings;



/**
 * @author Filip Procházka <filip.prochazka@kdyby.org>
 */
class ImageStorage extends Nette\Object
{

	/**
	 * @var string
	 */
	private $imagesDir;



	/**
	 * @param string $dir
	 */
	public function __construct($dir)
	{
		$dir .= '/images';
		if (!is_dir($dir)) {
			umask(0);
			mkdir($dir, 0777);
		}

		$this->imagesDir = $dir;
	}



	/**
	 * @param FileUpload $file
	 * @return Image
	 * @throws \Nette\InvalidArgumentException
	 */
	public function upload(FileUpload $file)
	{
		if (!$file->isOk() || !$file->isImage()) {
			throw new Nette\InvalidArgumentException;
		}

		do {
			$name = Strings::random(10) . '.' . $file->getSanitizedName();
		} while (file_exists($path = $this->imagesDir . DIRECTORY_SEPARATOR . $name));

		$file->move($path);
		return new Image($path);
	}



	/**
	 * @param string $content
	 * @param string $filename
	 * @return Image
	 */
	public function save($content, $filename)
	{
		do {
			$name = Strings::random(10) . '.' . $filename;
		} while (file_exists($path = $this->imagesDir . DIRECTORY_SEPARATOR . $name));

		file_put_contents($path, $content);
		return new Image($path);
	}



	/**
	 * @return string
	 */
	public function getImagesDir()
	{
		return $this->imagesDir;
	}



	/**
	 * @param $param
	 * @throws FileNotFoundException
	 * @return string
	 */
	public function find($param)
	{
		foreach (Finder::findFiles($param)->from($this->imagesDir) as $file) {
			/** @var \SplFileInfo $file */
			return $file->getPathname();
		}

		throw new FileNotFoundException("File $param not found.");
	}

}



/**
 * @author Filip Procházka <filip.prochazka@kdyby.org>
 */
class FileNotFoundException extends \RuntimeException
{

}

ImagePipe.php Raw #

<?php

namespace Img;

use Kdyby;
use Nette\Http\Request;
use Nette;



/**
 * @author Filip Procházka <filip@prochazka.su>
 */
class ImagePipe extends Nette\Object
{

	const TEMP = 1;

	/**
	 * @var string
	 */
	private $assetsDir;

	/**
	 * @var string
	 */
	private $wwwDir;

	/**
	 * @var string
	 */
	private $baseUrl;



	/**
	 * @param string $wwwDir
	 * @param \Nette\Http\Request $httpRequest
	 * @param string $assetsDir
	 *
	 * @throws \Nette\IOException
	 */
	public function __construct($wwwDir, Request $httpRequest, $assetsDir = 'assets')
	{
		// public assets dir
		$assetsDir = $wwwDir . '/' . $assetsDir;
		if (!is_dir($assetsDir) || !is_writable($assetsDir)) {
			throw new Nette\IOException("Directory $assetsDir is not writable.");
		}
		$this->wwwDir = $wwwDir;
		$this->assetsDir = $assetsDir;

		// for temporary files
		if (!is_dir($tempDir = ($this->assetsDir . '/temp'))) {
			self::mkdir($tempDir);
		}

		// base of public url
		$this->baseUrl = rtrim($httpRequest->url->baseUrl, '/');
	}



	/**
	 * @param string|Image $sourceImage
	 * @param array $size
	 * @param int $flags
	 *
	 * @return string
	 * @throws \Nette\IOException
	 */
	public function request($sourceImage, array $size = array(), $flags = 0)
	{
		if (!$sourceImage instanceof Image) {
			$sourceImage = new Image($sourceImage);
		}

		$image = NULL;
		/** @var Nette\Image $image */
		if ($sourceImage->size->width > 500 || $sourceImage->size->height > 500) { // w & h
			$image = Nette\Image::fromFile($sourceImage->file)->resize(500, 500);
		} // elseif($size) todo: zpracování velikosti


		// figure out public directory
		$dir = $flags & self::TEMP ? $this->assetsDir . '/temp' : $this->assetsDir;
		$dir .= '/' . (implode('x', $size) ? : 'original');
		if (!is_dir($dir)) {
			self::mkdir($dir);
		}

		// filename
		$targetPath = $dir . '/' . basename($sourceImage->file);

		// continue processing only if newer version is available
		if (file_exists($targetPath) && filectime($targetPath) < filectime($sourceImage->file)) {
			return $this->publicPath($targetPath);
		}

		// copy source file
		if ($image) {
			if (!$image->save($targetPath)) {
				throw new Nette\IOException("Cannot resize $sourceImage->file to $targetPath");
			}

		} else {
			if (!@copy($sourceImage->file, $targetPath)) {
				throw new Nette\IOException("Cannot copy $sourceImage->file to $targetPath");
			}
		}

		return $this->publicPath($targetPath);
	}



	/**
	 * @param string $file
	 * @return string
	 */
	private function publicPath($file)
	{
		return $this->baseUrl . str_replace($this->wwwDir, '', $file);
	}



	/**
	 * @param string $dir
	 *
	 * @throws \Nette\IOException
	 * @return void
	 */
	private static function mkdir($dir)
	{
		$oldMask = umask(0);
		@mkdir($dir, 0777);
		@chmod($dir, 0777);
		umask($oldMask);

		if (!is_dir($dir) || !is_writable($dir)) {
			throw new Nette\IOException("Please create writable directory $dir.");
		}
	}

}

Image.php Raw #

<?php

namespace Img;

use Nette;



/**
 * @author Filip Procházka <filip@prochazka.su>
 *
 * @property-read string $file
 * @property \Size $size
 * @property-read \Size $size
 */
class Image extends Nette\Object
{

	/**
	 * @var string
	 */
	private $file;

	/**
	 * @var \Size
	 */
	private $size;



	/**
	 * @param string $file
	 */
	public function __construct($file)
	{
		$this->file = $file;
		$this->size = Size::fromFile($file);
	}



	/**
	 * @return bool
	 */
	public function exists()
	{
		return file_exists($this->file);
	}



	/**
	 * @return float|int
	 */
	public function getFile()
	{
		return $this->file;
	}



	/**
	 * @return \Size
	 */
	public function getSize()
	{
		return $this->size;
	}

}

FooPresenter.php Raw #

<?php

/**
 * @author Filip Procházka <filip@prochazka.su>
 */
class FooPresenter extends BasePresenter
{

	/**
	 * @var \Img\ImageStorage
	 */
	private $imageStorage;



	/**
	 * @param \Img\ImageStorage $storage
	 */
	public function injectImageStorage(\Img\ImageStorage $storage)
	{
		$this->imageStorage = $storage;
	}



	public function renderDefault()
	{
		$this->template->uploadedImg = new Img\Image(...); // z databáze
	}



	/**
	 * @param string $name
	 * @return \Nette\Application\UI\Form
	 */
	protected function createComponentUpload($name)
	{
		$form = new UI\Form;

		$form->addUpload('file')
			->addRule(UI\Form::IMAGE, 'Obrázek musí být JPEG, PNG nebo GIF.')
			->addRule(UI\Form::FILLED, 'Zvolte prosím obrázek.');

		$form->addSubmit('upload', 'Upload image')
			->setValidationScope(FALSE)
			->onClick[] = $this->processUpload;

		return $form;
	}



	/**
	 * @param \Nette\Forms\Controls\SubmitButton $button
	 */
	public function processUpload(SubmitButton $button)
	{
		try {
			$image = $this->imageStorage->upload($button->form['file']->value);

		} catch (\Exception $e) {
			$button->form->addError($e->getMessage());
			return;
		}

		// $this->flashMessage('Huray!');
		// $this->redirect('this');
	}

}

Foo.default.latte Raw #

<img n:tempImg="$uploadedImg" alt="" /> nebo <img n:img="$uploadedImg" alt="" />

BasePresenter.php Raw #

<?php

/**
 * @author Filip Procházka <filip@prochazka.su>
 */
abstract class BasePresenter extends Nette\Application\UI\Presenter
{

	/**
	 * @var \Img\ImagePipe
	 */
	private $imgPipe;



	/**
	 * @param \Img\ImagePipe $imgPipe
	 */
	public function injectImgPipe(\Img\ImagePipe $imgPipe)
	{
		$this->imgPipe = $imgPipe;
	}



	/**
	 * @param string $class
	 *
	 * @return Nette\Templating\FileTemplate
	 */
	protected function createTemplate($class = NULL)
	{
		$tmp = parent::createTemplate($class);
		/** @var \Nette\Templating\FileTemplate|\stdClass $tmp */
		$tmp->_imagePipe = $this->imgPipe;
		return $tmp;
	}

}