diff --git a/Readme.md b/Readme.md index 5a9e72e..8b0131a 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ [QOI Image]((https://github.com/phoboslab/qoi)) encoder and decoder written in pure PHP. -It can encode up to 5 MiB/s +It can encode and decode a few megabytes per second with a small memory footprint. ## Usage @@ -13,8 +13,8 @@ Convert a PNG file using a stream : ```php use MKCG\Image\QOI\Codec; +use MKCG\Image\QOI\Driver\Dynamic; use MKCG\Image\QOI\Writer\StreamWriter; -use MKCG\Image\QOI\Writer\Driver\Dynamic; $inputFilepath = "/foobar.png"; $outputFilepath = "/foobar.qoi"; @@ -37,8 +37,8 @@ Convert a PNG file in-memory : ```php use MKCG\Image\QOI\Codec; +use MKCG\Image\QOI\Driver\Dynamic; use MKCG\Image\QOI\Writer\InMemoryWriterFactory; -use MKCG\Image\QOI\Writer\Driver\Dynamic; $inputFilepath = "/foobar.png"; $outputFilepath = "/foobar.qoi"; @@ -98,16 +98,62 @@ if ($outputFile) { ```php use MKCG\Image\QOI\Codec; +function createFileIterator($filepath): \Generator +{ + $bytes = file_get_contents($filepath); + + for ($i = 0; $i < strlen($bytes); $i++) { + yield $bytes[$i]; + } +}; + +$filepath = "/input.qoi"; + $reader = Codec::decode(createFileIterator($filepath)); + +$width = $reader->descriptor->width; +$height = $reader->descriptor->height; +$channels = $reader->descriptor->channels; + $pixels = iterator_to_array($reader->iterator); ``` +### Convert a QOI image + +```php +use MKCG\Image\QOI\Codec; +use MKCG\Image\QOI\Format; +use MKCG\Image\QOI\Driver\Dynamic; + +function createFileIterator($filepath): \Generator +{ + $handler = fopen($filepath, 'r'); + + while (($bytes = fread($handler, 8192)) !== false) { + for ($i = 0; $i < strlen($bytes); $i++) { + yield $bytes[$i]; + } + } + + fclose($handler); +}; + +$filepath = "/input.qoi"; + +$reader = Codec::decode(createFileIterator($filepath)); +Dynamic::convertInto($reader, "/output.png", Format::PNG); + +// Important: you need to create another reader +$reader = Codec::decode(createFileIterator($filepath)); +Dynamic::convertInto($reader, "/output.jpg", Format::JPG); +``` + ## Drivers | Name | Requirements | Description | | ------- | ----------------------------- | ------------------------------------------------------------ | -| Dynamic | one of : ext-imagick, ext-gd | Load the image using the appropriate PHP image extension | -| Gd | ext-gd | | +| Dynamic | one of : ext-imagick, ext-gd | Manipulates images using the appropriate PHP image extension | +| Gd | ext-gd | gd use only 7 bits for the alpha channel instead of 8 | | Imagick | ext-imagick | imagick must have been compiled against ImageMagick >= 6.4.0 | diff --git a/src/Writer/Driver/DriverException.php b/src/Driver/DriverException.php similarity index 96% rename from src/Writer/Driver/DriverException.php rename to src/Driver/DriverException.php index f1901e0..84bf405 100644 --- a/src/Writer/Driver/DriverException.php +++ b/src/Driver/DriverException.php @@ -24,6 +24,6 @@ * SOFTWARE. */ -namespace MKCG\Image\QOI\Writer\Driver; +namespace MKCG\Image\QOI\Driver; class DriverException extends \Exception; diff --git a/src/Writer/Driver/Dynamic.php b/src/Driver/Dynamic.php similarity index 75% rename from src/Writer/Driver/Dynamic.php rename to src/Driver/Dynamic.php index e464b13..ba0f805 100644 --- a/src/Writer/Driver/Dynamic.php +++ b/src/Driver/Dynamic.php @@ -24,36 +24,37 @@ * SOFTWARE. */ -namespace MKCG\Image\QOI\Writer\Driver; +namespace MKCG\Image\QOI\Driver; use MKCG\Image\QOI\Context; +use MKCG\Image\QOI\Format; class Dynamic { public static function loadFromFile(string $filepath): ?Context { - $fallbacks = [ - static::useImagick(...), - static::useGdImage(...) - ]; + $function = match(true) { + class_exists("\Imagick") => static::useImagick(...), + class_exists("\GdImage") => static::useGdImage(...), + default => throw new \Exception() + }; - foreach ($fallbacks as $fallback) { - $output = $fallback($filepath); + return $function($filepath); + } - if ($output !== null) { - return $output; - } - } + public static function convertInto(Context $reader, string $filepath, Format $format): void + { + $function = match(true) { + class_exists("\Imagick") => Imagick::convertInto(...), + class_exists("\GdImage") => GdImage::convertInto(...), + default => throw new \Exception(), + }; - return null; + $function($reader, $filepath, $format); } private static function useGdImage(string $filepath): ?Context { - if (!class_exists("\GdImage")) { - return null; - } - try { $image = GdImage::loadFromFile($filepath); $descriptor = GdImage::createImageDescriptor($image, $filepath); @@ -68,10 +69,6 @@ private static function useGdImage(string $filepath): ?Context private static function useImagick(string $filepath): ?Context { - if (!class_exists("\Imagick")) { - return null; - } - try { $image = Imagick::loadFromFile($filepath); $descriptor = Imagick::createImageDescriptor($image, $filepath); diff --git a/src/Writer/Driver/GdImage.php b/src/Driver/GdImage.php similarity index 71% rename from src/Writer/Driver/GdImage.php rename to src/Driver/GdImage.php index 34ea24a..24a0959 100644 --- a/src/Writer/Driver/GdImage.php +++ b/src/Driver/GdImage.php @@ -24,10 +24,12 @@ * SOFTWARE. */ -namespace MKCG\Image\QOI\Writer\Driver; +namespace MKCG\Image\QOI\Driver; use MKCG\Image\QOI\ImageDescriptor; use MKCG\Image\QOI\Colorspace; +use MKCG\Image\QOI\Context; +use MKCG\Image\QOI\Format; class GdImage { @@ -98,4 +100,43 @@ public static function createIterator(\GdImage $image, ImageDescriptor $descript } } } + + public static function convertInto(Context $reader, string $filepath, Format $format): void + { + $image = imagecreatetruecolor($reader->descriptor->width, $reader->descriptor->height); + + if (!$image) { + throw new \Exception(); + } + + imagecolorallocate($image, 0, 0, 0); + + $x = 0; + $y = 0; + + foreach ($reader->iterator as $pixel) { + $color = match ($reader->descriptor->channels) { + // @see: https://github.com/php/php-src/blob/2f85d79165ad5744cc411194c159f1ce43e1ec0a/ext/gd/libgd/gd_png.c#L265 + 4 => imagecolorallocatealpha($image, $pixel[0], $pixel[1], $pixel[2], 127 - ($pixel[3] >> 1)), + default => imagecolorallocate($image, $pixel[0], $pixel[1], $pixel[2]), + }; + + imagesetpixel($image, $x, $y, $color); + + $x++; + + if ($x == $reader->descriptor->width) { + $y++; + $x = 0; + } + } + + $saved = match ($format) { + Format::AVIF => imageavif($image, $filepath), + Format::BMP => imagebmp($image, $filepath), + Format::JPG => imagejpeg($image, $filepath), + Format::PNG => imagepng($image, $filepath), + default => throw new \Exception() + }; + } } diff --git a/src/Writer/Driver/Imagick.php b/src/Driver/Imagick.php similarity index 68% rename from src/Writer/Driver/Imagick.php rename to src/Driver/Imagick.php index 6ca799b..53def5f 100644 --- a/src/Writer/Driver/Imagick.php +++ b/src/Driver/Imagick.php @@ -24,10 +24,12 @@ * SOFTWARE. */ -namespace MKCG\Image\QOI\Writer\Driver; +namespace MKCG\Image\QOI\Driver; use MKCG\Image\QOI\ImageDescriptor; use MKCG\Image\QOI\Colorspace; +use MKCG\Image\QOI\Context; +use MKCG\Image\QOI\Format; class Imagick { @@ -99,4 +101,50 @@ public static function createPixelIterator(\Imagick $image, int $channels): iter $iterator->syncIterator(); } } + + public static function convertInto(Context $reader, string $filepath, Format $format): void + { + $image = new \Imagick; + + $image->newImage( + $reader->descriptor->width, + $reader->descriptor->height, + new \ImagickPixel('black'), + $format->value + ); + + $image->setImageAlphaChannel( + match ($reader->descriptor->channels) { + 4 => \Imagick::ALPHACHANNEL_ACTIVATE, + default => \Imagick::ALPHACHANNEL_DEACTIVATE, + } + ); + + $pixelIterator = $image->getPixelIterator(); + + foreach ($pixelIterator as $row => $rowPixels) { + foreach ($rowPixels as $col => $pixel) { + $px = $reader->iterator->current(); + + if ($px === null) { + throw new \Exception(); + } + + $pixel->setColorValue(\Imagick::COLOR_RED, $px[0] / 255); + $pixel->setColorValue(\Imagick::COLOR_GREEN, $px[1] / 255); + $pixel->setColorValue(\Imagick::COLOR_BLUE, $px[2] / 255); + $pixel->setColorValue(\Imagick::COLOR_ALPHA, $px[3] / 255); + + $reader->iterator->next(); + } + + $pixelIterator->syncIterator(); + } + + if ($reader->iterator->current() !== null) { + throw new \Exception(); + } + + $image->writeImage($filepath); + } } diff --git a/src/Format.php b/src/Format.php new file mode 100644 index 0000000..3f91c09 --- /dev/null +++ b/src/Format.php @@ -0,0 +1,34 @@ +