Skip to content

Commit

Permalink
Convert QOI image into AVIF, BMP, JPG, PNG
Browse files Browse the repository at this point in the history
  • Loading branch information
MKCG committed Jan 1, 2022
1 parent 9d0cdb8 commit 3c30d96
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 28 deletions.
56 changes: 51 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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 |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
* SOFTWARE.
*/

namespace MKCG\Image\QOI\Writer\Driver;
namespace MKCG\Image\QOI\Driver;

class DriverException extends \Exception;
37 changes: 17 additions & 20 deletions src/Writer/Driver/Dynamic.php → src/Driver/Dynamic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
43 changes: 42 additions & 1 deletion src/Writer/Driver/GdImage.php → src/Driver/GdImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
};
}
}
50 changes: 49 additions & 1 deletion src/Writer/Driver/Imagick.php → src/Driver/Imagick.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
}
34 changes: 34 additions & 0 deletions src/Format.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* MIT License
*
* Copyright (c) 2021 Kevin Masseix
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

namespace MKCG\Image\QOI;

enum Format: string {
case AVIF = "avif";
case BMP = "bmp";
case JPG = "jpg";
case PNG = "png";
};

0 comments on commit 3c30d96

Please sign in to comment.