Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/Driver/GdResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
use Horde\Image\Filter\Grayscale;
use Horde\Image\Filter\Negate;
use Horde\Image\Filter\Sepia;
use Horde\Image\Filter\Threshold;
use Horde\Image\Geometry\Rectangle;
use Horde\Image\Geometry\Size;
use GdImage;

final class GdResource implements ImageResource
final class GdResource implements ImageResource, PixelReader
{
public function __construct(
private GdImage $gd,
Expand All @@ -46,6 +47,22 @@ public function gd(): GdImage
return $this->gd;
}

/** @return array{int, int, int} */
public function getPixelRgb(int $x, int $y): array
{
$rgb = imagecolorat($this->gd, $x, $y);
if ($rgb === false) {
return [0, 0, 0];
}
return [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF];
}

public function getLuminance(int $x, int $y): int
{
[$r, $g, $b] = $this->getPixelRgb($x, $y);
return (int) round(0.299 * $r + 0.587 * $g + 0.114 * $b);
}

public function size(): Size
{
return new Size((float) imagesx($this->gd), (float) imagesy($this->gd));
Expand Down Expand Up @@ -157,10 +174,30 @@ private function applyFilter(Filter $filter): void
),
$filter instanceof Gamma => imagegammacorrect($this->gd, 1.0, $filter->gamma),
$filter instanceof Sepia => $this->applySepiaFilter($filter->threshold),
$filter instanceof Threshold => $this->applyThreshold($filter->level),
default => throw new DriverException('Filter not supported by GdDriver: ' . $filter::class),
};
}

private function applyThreshold(int $level): void
{
$w = imagesx($this->gd);
$h = imagesy($this->gd);
$black = imagecolorallocate($this->gd, 0, 0, 0);
$white = imagecolorallocate($this->gd, 255, 255, 255);

for ($y = 0; $y < $h; $y++) {
for ($x = 0; $x < $w; $x++) {
$rgb = imagecolorat($this->gd, $x, $y);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
$lum = (int) round(0.299 * $r + 0.587 * $g + 0.114 * $b);
imagesetpixel($this->gd, $x, $y, $lum < $level ? $black : $white);
}
}
}

private function applySepiaFilter(float $threshold): void
{
imagefilter($this->gd, IMG_FILTER_GRAYSCALE);
Expand Down
18 changes: 17 additions & 1 deletion src/Driver/ImagickResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
use Horde\Image\Filter\Pixelate;
use Horde\Image\Filter\Sepia;
use Horde\Image\Filter\Sharpen;
use Horde\Image\Filter\Threshold;
use Horde\Image\Geometry\Rectangle;
use Horde\Image\Geometry\Size;
use Imagick;
use ImagickPixel;

final class ImagickResource implements ImageResource
final class ImagickResource implements ImageResource, PixelReader
{
public function __construct(
private Imagick $imagick,
Expand Down Expand Up @@ -117,6 +118,7 @@ public function apply(Filter $filter): static
),
$filter instanceof Negate => $clone->imagick->negateImage(false),
$filter instanceof Pixelate => $clone->applyPixelate($filter->size),
$filter instanceof Threshold => $clone->imagick->thresholdImage($filter->level * 257),
$filter instanceof Modulate => $clone->imagick->modulateImage(
$filter->brightness,
$filter->saturation,
Expand Down Expand Up @@ -147,6 +149,20 @@ public function imagick(): Imagick
return $this->imagick;
}

/** @return array{int, int, int} */
public function getPixelRgb(int $x, int $y): array
{
$pixel = $this->imagick->getImagePixelColor($x, $y);
$color = $pixel->getColor();
return [(int) $color['r'], (int) $color['g'], (int) $color['b']];
}

public function getLuminance(int $x, int $y): int
{
[$r, $g, $b] = $this->getPixelRgb($x, $y);
return (int) round(0.299 * $r + 0.587 * $g + 0.114 * $b);
}

public function withImagick(Imagick $imagick): static
{
$clone = clone $this;
Expand Down
29 changes: 29 additions & 0 deletions src/Driver/PixelReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/**
* Contract for per-pixel read access to image data.
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*/

namespace Horde\Image\Driver;

interface PixelReader
{
/**
* @return array{int, int, int} RGB values 0-255
*/
public function getPixelRgb(int $x, int $y): array;

/**
* BT.601 luminance: 0.299*R + 0.587*G + 0.114*B
*
* @return int Luminance 0-255
*/
public function getLuminance(int $x, int $y): int;
}
14 changes: 13 additions & 1 deletion src/Driver/PngResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use Horde\Image\Geometry\Rectangle;
use Horde\Image\Geometry\Size;

final class PngResource implements ImageResource
final class PngResource implements ImageResource, PixelReader
{
/** @var array<int, array<int, array{int, int, int}>> row => col => [r, g, b] */
private array $pixels;
Expand Down Expand Up @@ -74,6 +74,18 @@ public function getPixel(int $x, int $y): array
return [0, 0, 0];
}

/** @return array{int, int, int} */
public function getPixelRgb(int $x, int $y): array
{
return $this->getPixel($x, $y);
}

public function getLuminance(int $x, int $y): int
{
[$r, $g, $b] = $this->getPixel($x, $y);
return (int) round(0.299 * $r + 0.587 * $g + 0.114 * $b);
}

public function resize(Size $size): static
{
$newW = max(1, (int) $size->width);
Expand Down
27 changes: 27 additions & 0 deletions src/Filter/Threshold.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/**
* Binarize an image by thresholding pixel luminance.
*
* Pixels with luminance below the threshold become black (0,0,0),
* pixels at or above become white (255,255,255).
*
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*/

namespace Horde\Image\Filter;

final readonly class Threshold implements Filter
{
/**
* @param int $level 0-255; pixels below become black, at or above become white
*/
public function __construct(
public int $level = 128,
) {}
}
Loading