From e8001ac543a83ab519ff515688eeb9a43d705cae Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 21 Jul 2024 10:35:34 +0200 Subject: [PATCH] Refactor File::class to use file pointer internally --- src/EncodedImage.php | 41 --------- src/File.php | 90 +++++++++++++++++-- tests/Traits/CanDetectInterlacedPng.php | 9 +- .../Drivers/Gd/Encoders/PngEncoderTest.php | 4 +- .../Imagick/Encoders/PngEncoderTest.php | 4 +- tests/Unit/EncodedImageTest.php | 60 ------------- tests/Unit/FileTest.php | 39 ++++++++ 7 files changed, 128 insertions(+), 119 deletions(-) delete mode 100644 tests/Unit/EncodedImageTest.php diff --git a/src/EncodedImage.php b/src/EncodedImage.php index 2caadcc9..e98c51fb 100644 --- a/src/EncodedImage.php +++ b/src/EncodedImage.php @@ -8,45 +8,4 @@ class EncodedImage extends File implements EncodedImageInterface { - /** - * Create new instance - * - * @param string $data - * @param string $mediaType - */ - public function __construct( - protected string $data, - protected string $mediaType = 'application/octet-stream' - ) { - } - - /** - * {@inheritdoc} - * - * @see EncodedImageInterface::mediaType() - */ - public function mediaType(): string - { - return $this->mediaType; - } - - /** - * {@inheritdoc} - * - * @see EncodedImageInterface::mimetype() - */ - public function mimetype(): string - { - return $this->mediaType(); - } - - /** - * {@inheritdoc} - * - * @see EncodedImageInterface::toDataUri() - */ - public function toDataUri(): string - { - return sprintf('data:%s;base64,%s', $this->mediaType, base64_encode($this->data)); - } } diff --git a/src/File.php b/src/File.php index fe5eda8c..b2ee898f 100644 --- a/src/File.php +++ b/src/File.php @@ -4,6 +4,7 @@ namespace Intervention\Image; +use Intervention\Image\Exceptions\InputException; use Intervention\Image\Exceptions\NotWritableException; use Intervention\Image\Interfaces\FileInterface; use Intervention\Image\Traits\CanBuildFilePointer; @@ -12,13 +13,27 @@ class File implements FileInterface { use CanBuildFilePointer; + /** + * Data stream + * + * @var resource $stream + */ + protected mixed $stream; + /** * Create new instance * - * @param string $data + * @param mixed $data + * @throws InputException + * @return void */ - public function __construct(protected string $data) + public function __construct(mixed $data) { + $this->stream = match (true) { + is_resource($data) => $data, + is_string($data) => $this->buildFilePointer($data), + default => throw new InputException('Argument #1 ($data) must be of type string or resource.'), + }; } /** @@ -42,13 +57,30 @@ public function save(string $filepath): void ); } - // write data - $saved = @file_put_contents($filepath, (string) $this); - if ($saved === false) { + // create output file pointer + if (!$output = fopen($filepath, 'a')) { throw new NotWritableException( - "Can't write image data to path ({$filepath})." + "Can't write image to path. File path is not writable." ); } + + // create source file pointer + $source = $this->toFilePointer(); + + while (!feof($source)) { + $buffer = fread($source, 8192); + + if ($buffer === false) { + throw new NotWritableException( + "Can't write image to path. Unable to read source." + ); + } + + // write buffer to output + fwrite($output, $buffer); + } + + fclose($output); } /** @@ -58,7 +90,9 @@ public function save(string $filepath): void */ public function toString(): string { - return $this->data; + rewind($this->stream); + + return stream_get_contents($this->stream); } /** @@ -68,7 +102,9 @@ public function toString(): string */ public function toFilePointer() { - return $this->buildFilePointer($this->toString()); + rewind($this->stream); + + return $this->stream; } /** @@ -78,7 +114,43 @@ public function toFilePointer() */ public function size(): int { - return mb_strlen($this->data); + return fstat($this->stream)['size']; + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::mediaType() + */ + public function mediaType(): string + { + $detected = mime_content_type($this->stream); + + if ($detected === false) { + return 'application/x-empty'; + } + + return $detected; + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::mimetype() + */ + public function mimetype(): string + { + return $this->mediaType(); + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::toDataUri() + */ + public function toDataUri(): string + { + return sprintf('data:%s;base64,%s', $this->mediaType(), base64_encode((string) $this)); } /** diff --git a/tests/Traits/CanDetectInterlacedPng.php b/tests/Traits/CanDetectInterlacedPng.php index 19fffe46..30f3ef30 100644 --- a/tests/Traits/CanDetectInterlacedPng.php +++ b/tests/Traits/CanDetectInterlacedPng.php @@ -4,6 +4,7 @@ namespace Intervention\Image\Tests\Traits; +use Intervention\Image\EncodedImage; use Intervention\Image\Traits\CanBuildFilePointer; trait CanDetectInterlacedPng @@ -13,14 +14,12 @@ trait CanDetectInterlacedPng /** * Checks if the given image data is interlaced encoded PNG format * - * @param string $imagedata + * @param EncodedImage $image * @return bool */ - private function isInterlacedPng(string $imagedata): bool + private function isInterlacedPng(EncodedImage $image): bool { - $f = $this->buildFilePointer($imagedata); - $contents = fread($f, 32); - fclose($f); + $contents = fread($image->toFilePointer(), 32); return ord($contents[28]) != 0; } diff --git a/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php index 9c96591a..1859c21e 100644 --- a/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php +++ b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php @@ -23,7 +23,7 @@ public function testEncode(): void $encoder = new PngEncoder(); $result = $encoder->encode($image); $this->assertMediaType('image/png', (string) $result); - $this->assertFalse($this->isInterlacedPng((string) $result)); + $this->assertFalse($this->isInterlacedPng($result)); } public function testEncodeInterlaced(): void @@ -32,6 +32,6 @@ public function testEncodeInterlaced(): void $encoder = new PngEncoder(interlaced: true); $result = $encoder->encode($image); $this->assertMediaType('image/png', (string) $result); - $this->assertTrue($this->isInterlacedPng((string) $result)); + $this->assertTrue($this->isInterlacedPng($result)); } } diff --git a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php index 503320b7..51a822b7 100644 --- a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php +++ b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php @@ -23,7 +23,7 @@ public function testEncode(): void $encoder = new PngEncoder(); $result = $encoder->encode($image); $this->assertMediaType('image/png', (string) $result); - $this->assertFalse($this->isInterlacedPng((string) $result)); + $this->assertFalse($this->isInterlacedPng($result)); } public function testEncodeInterlaced(): void @@ -32,6 +32,6 @@ public function testEncodeInterlaced(): void $encoder = new PngEncoder(interlaced: true); $result = $encoder->encode($image); $this->assertMediaType('image/png', (string) $result); - $this->assertTrue($this->isInterlacedPng((string) $result)); + $this->assertTrue($this->isInterlacedPng($result)); } } diff --git a/tests/Unit/EncodedImageTest.php b/tests/Unit/EncodedImageTest.php deleted file mode 100644 index 15bab41b..00000000 --- a/tests/Unit/EncodedImageTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertInstanceOf(EncodedImage::class, $image); - } - - public function testSave(): void - { - $image = new EncodedImage('foo', 'bar'); - $path = __DIR__ . '/foo.tmp'; - $this->assertFalse(file_exists($path)); - $image->save($path); - $this->assertTrue(file_exists($path)); - $this->assertEquals('foo', file_get_contents($path)); - unlink($path); - } - - public function testToDataUri(): void - { - $image = new EncodedImage('foo', 'bar'); - $this->assertEquals('data:bar;base64,Zm9v', $image->toDataUri()); - } - - public function testToString(): void - { - $image = new EncodedImage('foo', 'bar'); - $this->assertEquals('foo', (string) $image); - } - - public function testMediaType(): void - { - $image = new EncodedImage('foo'); - $this->assertEquals('application/octet-stream', $image->mediaType()); - - $image = new EncodedImage('foo', 'image/jpeg'); - $this->assertEquals('image/jpeg', $image->mediaType()); - } - - public function testMimetype(): void - { - $image = new EncodedImage('foo'); - $this->assertEquals('application/octet-stream', $image->mimetype()); - - $image = new EncodedImage('foo', 'image/jpeg'); - $this->assertEquals('image/jpeg', $image->mimetype()); - } -} diff --git a/tests/Unit/FileTest.php b/tests/Unit/FileTest.php index 7d407907..5d228d46 100644 --- a/tests/Unit/FileTest.php +++ b/tests/Unit/FileTest.php @@ -4,17 +4,31 @@ namespace Intervention\Image\Tests\Unit; +use Intervention\Image\Exceptions\InputException; use PHPUnit\Framework\Attributes\CoversClass; use Intervention\Image\File; use Intervention\Image\Tests\BaseTestCase; +use Intervention\Image\Traits\CanBuildFilePointer; +use stdClass; #[CoversClass(\Intervention\Image\File::class)] final class FileTest extends BaseTestCase { + use CanBuildFilePointer; + public function testConstructor(): void { $file = new File('foo'); $this->assertInstanceOf(File::class, $file); + + $file = new File($this->buildFilePointer('foo')); + $this->assertInstanceOf(File::class, $file); + } + + public function testConstructorUnkownType(): void + { + $this->expectException(InputException::class); + new File(new stdClass()); } public function testSave(): void @@ -46,4 +60,29 @@ public function testSize(): void $file = new File('foo'); $this->assertEquals(3, $file->size()); } + + public function testToDataUri(): void + { + $image = new File('foo'); + $this->assertEquals('data:text/plain;base64,Zm9v', $image->toDataUri()); + } + + public function testMimetype(): void + { + $image = new File('foo'); + $this->assertEquals('text/plain', $image->mimetype()); + $this->assertEquals('text/plain', $image->mediaType()); + + $image = new File($this->getTestResourceData()); + $this->assertEquals('image/jpeg', $image->mimetype()); + $this->assertEquals('image/jpeg', $image->mediaType()); + + $image = new File("\x000\x001"); + $this->assertEquals('application/octet-stream', $image->mimetype()); + $this->assertEquals('application/octet-stream', $image->mediaType()); + + $image = new File(''); + $this->assertEquals('application/x-empty', $image->mimetype()); + $this->assertEquals('application/x-empty', $image->mediaType()); + } }