Add image blob and bitpix enum

This commit is contained in:
Nicolò P. 2024-10-08 08:16:35 +02:00
parent b56beba4ce
commit 366521853c
7 changed files with 171 additions and 15 deletions

43
src/Bitpix.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Dumbastro\FitsPhp;
/**
* Valid BITPIX values
*/
enum Bitpix: int
{
case Uint8 = 8;
case Uint16 = 16;
case Uint32 = 32;
case Uint64 = 64;
case Float32 = -32;
case Float64 = -64;
public function type(): string
{
return match($this) {
Bitpix::Uint8 => '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',
};
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Dumbastro\FitsPhp\Exceptions;
class InvalidBitpixValue extends \Exception
{
public function __construct(int $bitpix)
{
$this->message = "The value $bitpix is not a valid BITPIX value";
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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

View File

@ -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;
}
}

51
src/ImageBlob.php Normal file
View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Dumbastro\FitsPhp;
use Dumbastro\FitsPhp\Exceptions\InvalidBitpixValue;
use Dumbastro\FitsPhp\Bitpix;
class ImageBlob
{
private string $blob;
private FitsHeader $header;
public readonly Bitpix $bitpix;
public readonly int $dataBits;
/**
* @throws InvalidBitpixValue
* @todo Don't assume that NAXIS = 2...
*/
public function __construct(FitsHeader $header, string $blob)
{
$this->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);
}
}