From 366521853c16fc2ce84665e85a95a85ab624044f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20P=2E?= Date: Tue, 8 Oct 2024 08:16:35 +0200 Subject: [PATCH] Add image blob and bitpix enum --- src/Bitpix.php | 43 ++++++++++++++++ src/Exceptions/InvalidBitpixValue.php | 13 +++++ ...validFitsException.php => InvalidFits.php} | 2 +- ...validPathException.php => InvalidPath.php} | 2 +- src/Fits.php | 44 +++++++++++----- src/FitsHeader.php | 31 +++++++++++ src/ImageBlob.php | 51 +++++++++++++++++++ 7 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 src/Bitpix.php create mode 100644 src/Exceptions/InvalidBitpixValue.php rename src/Exceptions/{InvalidFitsException.php => InvalidFits.php} (51%) rename src/Exceptions/{InvalidPathException.php => InvalidPath.php} (72%) create mode 100644 src/ImageBlob.php diff --git a/src/Bitpix.php b/src/Bitpix.php new file mode 100644 index 0000000..96bf524 --- /dev/null +++ b/src/Bitpix.php @@ -0,0 +1,43 @@ + 'int8', + Bitpix::Uint16 => 'int16', + Bitpix::Uint32 => 'int32', + Bitpix::Uint64 => 'int64', + Bitpix::Float32 => 'float32', + Bitpix::Float64 => 'float64', + }; + } + + public function toString(): string + { + return match($this) { + Bitpix::Uint8 => 'Character or unsigned binary integer', + Bitpix::Uint16 => '16 bit two\'s complement binary integer', + Bitpix::Uint32 => '32 bit two\'s complement binary integer', + Bitpix::Uint64 => '64 bit two\'s complement binary integer', + Bitpix::Float32 => 'IEEE single-precision floating point', + Bitpix::Float64 => 'IEEE double-precision floating point', + }; + } +} + diff --git a/src/Exceptions/InvalidBitpixValue.php b/src/Exceptions/InvalidBitpixValue.php new file mode 100644 index 0000000..87edaf8 --- /dev/null +++ b/src/Exceptions/InvalidBitpixValue.php @@ -0,0 +1,13 @@ +message = "The value $bitpix is not a valid BITPIX value"; + } +} diff --git a/src/Exceptions/InvalidFitsException.php b/src/Exceptions/InvalidFits.php similarity index 51% rename from src/Exceptions/InvalidFitsException.php rename to src/Exceptions/InvalidFits.php index 55a287b..8137187 100644 --- a/src/Exceptions/InvalidFitsException.php +++ b/src/Exceptions/InvalidFits.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Dumbastro\FitsPhp\Exceptions; -final class InvalidFitsException extends \Exception implements \Throwable +final class InvalidFits extends \Exception implements \Throwable { } diff --git a/src/Exceptions/InvalidPathException.php b/src/Exceptions/InvalidPath.php similarity index 72% rename from src/Exceptions/InvalidPathException.php rename to src/Exceptions/InvalidPath.php index 8563a0f..88b3bfe 100644 --- a/src/Exceptions/InvalidPathException.php +++ b/src/Exceptions/InvalidPath.php @@ -7,6 +7,6 @@ namespace Dumbastro\FitsPhp\Exceptions; /** * @todo Different exception if path is not writable? */ -final class InvalidPathException extends \Exception +final class InvalidPath extends \Exception { } diff --git a/src/Fits.php b/src/Fits.php index f3d06b6..5e51584 100644 --- a/src/Fits.php +++ b/src/Fits.php @@ -5,38 +5,39 @@ declare(strict_types=1); namespace Dumbastro\FitsPhp; use Dumbastro\FitsPhp\Exceptions\{ - InvalidFitsException, - InvalidPathException, + InvalidFits, + InvalidPath, }; class Fits { - /** - * @var resource $byteStream - */ - private $byteStream; + private string $contents; private string $path; + private FitsHeader $fitsHeader; public readonly int $size; public readonly string $headerBlock; + public readonly string $imageBlob; /** - * @throws InvalidFitsException, InvalidPathException + * @throws InvalidFits, InvalidPath * @todo Check path for reading/writing errors */ public function __construct(string $path) { if (! is_readable($path) || ! is_file($path)) { - throw new InvalidPathException("The path '$path' is not readable or is not a file."); + throw new InvalidPath("The path '$path' is not readable or is not a file."); } - $this->byteStream = fopen($path, 'rb'); + $this->contents = file_get_contents($path); $this->path = $path; $this->size = filesize($this->path); if (! $this->validate()) { - throw new InvalidFitsException('The opened file is not a valid FITS image (invalid block size)'); + throw new InvalidFits('The opened file is not a valid FITS image (invalid block size)'); } $this->headerBlock = $this->extractHeader(); + $this->fitsHeader = new FitsHeader($this->headerBlock); + $this->imageBlob = $this->extractImageBlob(); } /** * Validate the given FITS file based on block sizes @@ -66,12 +67,29 @@ class Fits */ private function extractHeader(): string { - $contents = fread($this->byteStream, $this->size); - $end = strpos($contents, 'END'); + $end = strpos($this->contents, 'END'); // Determine minimum integer number of blocks including 'END' position $headerEnd = (($end - ($end % 2880)) / 2880 + 1) * 2880; - return substr($contents, 0, $headerEnd); + return substr($this->contents, 0, $headerEnd); + } + /** + * Extract the FITS image blob as a string; + * it uses the NAXIS1 and NAXIS2 keywords + * to compute the length of the main data table + */ + private function extractImageBlob(): string + { + $naxis1 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS1')); + $naxis2 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS2')); + + $blobEnd = $naxis1 * $naxis2; + + return substr( + $this->contents, + strlen($this->headerBlock) + 1, + $blobEnd + ); } public function writeTo(string $path): void diff --git a/src/FitsHeader.php b/src/FitsHeader.php index f36c071..64a08b8 100644 --- a/src/FitsHeader.php +++ b/src/FitsHeader.php @@ -71,5 +71,36 @@ class FitsHeader return $keywordsString . $blanks; } + /** + * Retrieve a Keyword object base on key name + */ + public function keyword(string $key): ?Keyword + { + $keyword = null; + + foreach ($this->keywords as $k) { + if (trim($k->name) === $key) { + $keyword = $k; + break; + } + } + + return $keyword; + } + /** + * Note: the keyword key string is case-sensitive + */ + public function getKeywordValue(string $key): string + { + $value = ''; + foreach ($this->keywords as $keyword) { + if (trim($keyword->name) === $key) { + $value = $keyword->value; + break; + } + } + + return $value; + } } diff --git a/src/ImageBlob.php b/src/ImageBlob.php new file mode 100644 index 0000000..73192bd --- /dev/null +++ b/src/ImageBlob.php @@ -0,0 +1,51 @@ +header = $header; + $this->blob = $blob; + $bitpix = (int) $this->header->keyword('BITPIX')->value; + $this->bitpix = Bitpix::tryFrom($bitpix) ?? throw new InvalidBitpixValue($bitpix); + $naxis1 = (int) trim($this->header->keyword('NAXIS1')->value); + $naxis2 = (int) trim($this->header->keyword('NAXIS2')->value); + $this->dataBits = abs($this->bitpix->value) * $naxis1 * $naxis2; + } + /** + * Returns a generator that yields image data + * byte by byte + */ + public function dataBytes(): \Generator + { + for ($i = 0; $i < strlen($this->blob); $i++) { + yield $this->blob[$i]; + } + } + /** + * Convert to PNG + * @todo Manipulate bits and add PNG header to blob?? + * Throw exception if gd fails? + */ + public function toPNG(int $quality = -1): bool + { + $gdImg = imagecreatefromstring($this->blob); + return imagepng($gdImg, quality: $quality); + } +}