implemented Canvas and FileWriter for PPM

This commit is contained in:
Mirko Janssen 2019-09-22 11:50:41 +02:00
parent dda4ba31e0
commit 1a3e0d31db
4 changed files with 364 additions and 0 deletions

85
src/Canvas/Canvas.php Normal file
View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Canvas;
use ItsAMirko\RayTracer\Primitives\Color;
use OutOfBoundsException;
class Canvas
{
/**
* @var int
*/
private $width;
/**
* @var int
*/
private $height;
/**
* @var Color[][]
*/
private $pixels;
/**
* Canvas constructor.
*
* @param int $width
* @param int $height
*/
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
/**
* @return int
*/
public function width(): int
{
return $this->width;
}
/**
* @return int
*/
public function height(): int
{
return $this->height;
}
/**
* @return Color[][]
*/
public function pixels(): array
{
return $this->pixels;
}
/**
* @param int $x
* @param int $y
* @param Color $color
*/
public function addPixel(int $x, int $y, Color $color): void
{
if ($x < 0 || $x >= $this->width) {
throw new OutOfBoundsException('Invalid position for x provided. Expected integer between 0 and '
. ($this->width - 1) . ', but got ' . $x);
} elseif ($y < 0 || $y >= $this->height) {
throw new OutOfBoundsException('Invalid position for y provided. Expected integer between 0 and '
. ($this->height - 1) . ', but got ' . $y);
}
$this->pixels[$x][$y] = $color;
}
}

View file

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Canvas;
use League\Flysystem\FilesystemInterface;
use function strlen;
use const PHP_EOL;
/**
* Class CanvasPpmFileWriter
*
* @package ItsAMirko\RayTracer\Canvas
*/
class CanvasPpmFileWriter
{
/**
* Defines the maximal number of chars per line in the file.
*/
private const MAX_CHARS_PER_LINE = 70;
/**
* @var FilesystemInterface
*/
private $filesystem;
/**
* CanvasFileWriter constructor.
*
* @param FilesystemInterface $filesystem
*/
public function __construct(FilesystemInterface $filesystem)
{
$this->filesystem = $filesystem;
}
/**
* @param Canvas $canvas
* @param string $filename
*/
public function createFile(Canvas $canvas, string $filename): void
{
$content = $this->getFileHeader($canvas);
$content .= $this->getImageContent($canvas);
$this->filesystem->put($filename, $content);
}
/**
* @param Canvas $canvas
*
* @return string
*/
private function getFileHeader(Canvas $canvas): string
{
return 'P3' . PHP_EOL . $canvas->width() . ' ' . $canvas->height() . PHP_EOL . '255' . PHP_EOL;
}
/**
* @param Canvas $canvas
*
* @return string
*/
private function getImageContent(Canvas $canvas): string
{
$content = '';
$pixels = $canvas->pixels();
for ($y = 0; $y < $canvas->height(); $y++) {
$rowContent = '';
for ($x = 0; $x < $canvas->width(); $x++) {
foreach (['red', 'green', 'blue'] as $color) {
$colorHex = '0';
if (isset($pixels[$x][$y]) && $color === 'red') {
$colorHex = $pixels[$x][$y]->redAsHex();
} elseif (isset($pixels[$x][$y]) && $color === 'green') {
$colorHex = $pixels[$x][$y]->greenAsHex();
} elseif (isset($pixels[$x][$y]) && $color === 'blue') {
$colorHex = $pixels[$x][$y]->blueAsHex();
}
if (strlen($rowContent . $colorHex) > self::MAX_CHARS_PER_LINE) {
$content .= trim($rowContent) . PHP_EOL;
$rowContent = '';
}
$rowContent .= $colorHex . ' ';
}
}
$content .= trim($rowContent) . PHP_EOL;
}
return $content;
}
}

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Canvas;
use ItsAMirko\RayTracer\Primitives\Color;
use League\Flysystem\FilesystemInterface;
use PHPUnit\Framework\TestCase;
use function explode;
use const PHP_EOL;
class CanvasPpmFileWriterTest extends TestCase
{
/**
* @var FilesystemInterface
*/
private $filesystem;
/**
* @var CanvasPpmFileWriter
*/
private $fileWriter;
protected function setUp(): void
{
parent::setUp();
$this->filesystem = $this->createMock(FilesystemInterface::class);
$this->fileWriter = new CanvasPpmFileWriter($this->filesystem);
}
public function testWritesPPMHeader(): void
{
$expectedFilename = 'my_canvas.ppm';
$expectedContentLines = [
0 => 'P3',
1 => '5 3',
2 => '255',
];
$this->filesystem->expects($this->once())
->method('put')
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
$canvas = new Canvas(5, 3);
$this->fileWriter->createFile($canvas, $expectedFilename);
}
public function testWritesPixels(): void
{
$expectedFilename = 'my_canvas.ppm';
$expectedContentLines = [
3 => '255 0 0 0 0 0 0 0 0 0 0 0 0 0 0',
4 => '0 0 0 0 0 0 0 128 0 0 0 0 0 0 0',
5 => '0 0 0 0 0 0 0 0 0 0 0 0 0 0 255',
];
$this->filesystem->expects($this->once())
->method('put')
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
$canvas = new Canvas(5, 3);
$canvas->addPixel(0, 0, new Color(1.5, 0.0, 0.0));
$canvas->addPixel(2, 1, new Color(0.0, 0.5, 0.0));
$canvas->addPixel(4, 2, new Color(-0.5, 0.0, 1.0));
$this->fileWriter->createFile($canvas, $expectedFilename);
}
public function testSplitsLongLinesOfPixels(): void
{
$expectedFilename = 'my_canvas.ppm';
$expectedContentLines = [
3 => '255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204',
4 => '153 255 204 153 255 204 153 255 204 153 255 204 153',
5 => '255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204',
6 => '153 255 204 153 255 204 153 255 204 153 255 204 153',
];
$this->filesystem->expects($this->once())
->method('put')
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
$canvas = new Canvas(10, 2);
for ($i = 0; $i < 10; $i++) {
$canvas->addPixel($i, 0, new Color(1, 0.8, 0.6));
$canvas->addPixel($i, 1, new Color(1, 0.8, 0.6));
}
$this->fileWriter->createFile($canvas, $expectedFilename);
}
/**
* Returns a callback to check the content of the PPM file
*
* @param array $expectedContentLines
*
* @return callable
*/
private function checkFileContentCallback(array $expectedContentLines)
{
return $this->callback(function (
string $content
) use ($expectedContentLines): bool {
$actualContentLines = explode(PHP_EOL, $content);
foreach ($expectedContentLines as $line => $expectedContent) {
if ($expectedContent !== $actualContentLines[$line]) {
return false;
}
}
return true;
});
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Canvas;
use Exception;
use ItsAMirko\RayTracer\Primitives\Color;
use OutOfBoundsException;
use PHPUnit\Framework\TestCase;
use function random_int;
class CanvasTest extends TestCase
{
/**
* @throws Exception
*/
public function testProvidesDimensions(): void
{
$width = random_int(1, 100);
$height = random_int(1, 100);
$canvas = new Canvas($width, $height);
$this->assertSame($width, $canvas->width());
$this->assertSame($height, $canvas->height());
}
public function testProvidesItsPixels(): void
{
$canvas = new Canvas(2, 2);
$canvas->addPixel(0, 0, new Color(1.0, 0.0, 0.0));
$canvas->addPixel(0, 1, new Color(0.0, 1.0, 0.0));
$canvas->addPixel(1, 1, new Color(0.0, 0.0, 1.0));
$expectedPixels = [
0 => [
0 => new Color(1.0, 0.0, 0.0),
1 => new Color(0.0, 1.0, 0.0),
],
1 => [
1 => new Color(0.0, 0.0, 1.0),
],
];
$this->assertEquals($expectedPixels, $canvas->pixels());
}
public function testThrowsExceptionIfPixelToAddIsOutOfBounds(): void
{
$this->expectException(OutOfBoundsException::class);
$canvas = new Canvas(1, 1);
$canvas->addPixel(1, 1, new Color(1.0, 0.0, 0.0));
}
}