Compare commits

...

5 Commits

Author SHA1 Message Date
1569873cbb Update README 2024-12-30 11:09:50 +01:00
5e4180a4a2 Test mono / color images (WIP) 2024-12-27 16:26:52 +01:00
19df4cc926 Crazy SVG conversion... 2024-12-26 17:03:59 +01:00
da712b0e77 Test FitsHeader#keyword method 2024-12-25 13:02:28 +01:00
109ae71059 Separate multi-block comments; update README 2024-12-24 15:01:55 +01:00
11 changed files with 19805 additions and 18 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
# Vim stuff
*.swp
*.swo
composer.lock composer.lock
vendor/ vendor/
docs/ docs/
*.svg

View File

@@ -11,13 +11,15 @@ Why not? But seriously, this doesn't make any sense, you should never use it in
## Usage ## Usage
**NOTE: Not added to Packagist yet...**
Add the package with Composer: Add the package with Composer:
``` ```
composer require dumbastro/fits-php composer require dumbastro/fits-php
``` ```
then use classes from the `Dumbastro\FitsPhp` namespace. More info in the documentation. then use classes from the `Dumbastro\FitsPhp` namespace.
### Examples ### Examples
@@ -72,8 +74,10 @@ echo $blob->bitpix->type(); //int32
## TODO ## TODO
- [x] Read keywords from the FITS header
- [x] Separate the main data table (actual image data) from the header (partly done? Who knows...) - [x] Separate the main data table (actual image data) from the header (partly done? Who knows...)
- [x] Read keywords from the FITS header (COMMENT keywords could be buggy)
- [ ] History keywords?
- [ ] FITS extensions?
- [ ] Actually display the image in the standard output - [ ] Actually display the image in the standard output
- [ ] Save the image to PNG and JPG - [ ] Save the image to PNG and/or JPG
- [ ] Manipulate the bits using basic processing algorithms?? - [ ] Manipulate the bits using basic processing algorithms??

View File

@@ -13,7 +13,8 @@
"phpunit/phpunit": "^11.3" "phpunit/phpunit": "^11.3"
}, },
"scripts": { "scripts": {
"run-pstan": "vendor/bin/phpstan analyze src/ --level 6" "run-pstan": "vendor/bin/phpstan analyze src/ --level 6",
"run-tests": "vendor/bin/phpunit tests/ --display-warnings"
}, },
"license": "MIT", "license": "MIT",
"autoload": { "autoload": {

View File

@@ -86,8 +86,9 @@ class Fits
{ {
$naxis1 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS1')); $naxis1 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS1'));
$naxis2 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS2')); $naxis2 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS2'));
$naxis3 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS3'));
$blobEnd = $naxis1 * $naxis2; $blobEnd = $naxis1 * $naxis2 * $naxis3;
return substr( return substr(
$this->contents, $this->contents,

View File

@@ -28,6 +28,8 @@ class FitsHeader
* *
* From the spec: each keyword record, including * From the spec: each keyword record, including
* any comments, is at most 80 bytes long * any comments, is at most 80 bytes long
* @todo Comments and keyword values could span more
than one 80-bytes block...
* @return Keyword[] * @return Keyword[]
*/ */
private function readKeywords(): array private function readKeywords(): array
@@ -50,6 +52,11 @@ class FitsHeader
$name = $keyVal[0]; $name = $keyVal[0];
$value = $keyVal[1] ?? ''; $value = $keyVal[1] ?? '';
if (str_starts_with($name, 'COMMENT')) {
$value = explode('COMMENT', $name)[1];
$name = 'COMMENT';
}
$keywords[] = new Keyword( $keywords[] = new Keyword(
name : $name, name : $name,
value : $value, value : $value,

View File

@@ -13,6 +13,10 @@ class ImageBlob
private FitsHeader $header; private FitsHeader $header;
public readonly Bitpix $bitpix; public readonly Bitpix $bitpix;
public readonly int $dataBits; public readonly int $dataBits;
public readonly int $width;
public readonly int $height;
public readonly ?int $naxis3;
public readonly bool $isColor;
/** /**
* @throws InvalidBitpixValue * @throws InvalidBitpixValue
@@ -24,18 +28,57 @@ class ImageBlob
$this->blob = $blob; $this->blob = $blob;
$bitpix = (int) $this->header->keyword('BITPIX')->value; $bitpix = (int) $this->header->keyword('BITPIX')->value;
$this->bitpix = Bitpix::tryFrom($bitpix) ?? throw new InvalidBitpixValue($bitpix); $this->bitpix = Bitpix::tryFrom($bitpix) ?? throw new InvalidBitpixValue($bitpix);
$naxis1 = (int) trim($this->header->keyword('NAXIS1')->value); $this->width = (int) trim($this->header->keyword('NAXIS1')->value);
$naxis2 = (int) trim($this->header->keyword('NAXIS2')->value); $this->height = (int) trim($this->header->keyword('NAXIS2')->value);
$this->dataBits = abs($this->bitpix->value) * $naxis1 * $naxis2; $naxis3 = null;
$dataBits = abs($this->bitpix->value) * $this->width * $this->height;
$naxis = (int) trim($this->header->keyword('NAXIS')->value);
// Color image (right?)
if ($naxis === 3) {
$naxis3 = (int) trim($this->header->keyword('NAXIS3')->value);
$dataBits *= $naxis3;
}
$this->naxis3 = $naxis3 ?? 1;
$this->dataBits = $dataBits;
$this->isColor = $this->naxis3 === 3;
} }
/** /**
* Returns a generator that yields image data * Returns a generator that yields image data
* byte by byte * pixel by pixel
*@todo Conversion from 16 to 8-bit?
* This won't work with mono images...
*/ */
public function dataBytes(): \Generator public function pixels(): \Generator
{ {
$n = 0;
$pixel = [];
$pixBytes = abs($this->bitpix->value) / 8;
for ($i = 0; $i < strlen($this->blob); $i++) { for ($i = 0; $i < strlen($this->blob); $i++) {
yield $this->blob[$i]; $value = unpack(
format: 'C',
string: $this->blob[$i]
)[1];
//$value = floor($value / 256);
if ($i + $pixBytes <= strlen($this->blob) - 1) {
$value += unpack(
format: 'C',
string: $this->blob[$i + $pixBytes - 1]
)[1];
}
$pixel[$n] = $value;
$n++;
if ($n === $this->naxis3) {
$n = 0;
yield $pixel;
}
} }
} }
/** /**
@@ -48,4 +91,42 @@ class ImageBlob
$gdImg = imagecreatefromstring($this->blob); $gdImg = imagecreatefromstring($this->blob);
return imagepng($gdImg, quality: $quality); return imagepng($gdImg, quality: $quality);
} }
/**
* Convert to SVG for display
* @todo This assumes RGB and produces a gigantic file...
* Note: pixels are treated as SVG rectangles, RGB values are
extracted from bit values... (?!)
*/
public function toSVG(): string
{
$svg = <<<SVG
<svg version="1.1"
width="{$this->width}"
height="{$this->height}"
xmlns="http://www.w3.org/2000/svg">
SVG;
$x = 1;
$y = 1;
$cols = 1;
// Build SVG using 1x1 rectangles
foreach ($this->pixels() as $k => $pixel) {
// A pixel is a 3-element array if the image is RGB
[$r, $g, $b] = $pixel;
$r = $r / 2;
$g = $g / 2;
$b = $b / 2;
// Change row after reaching image width
if ($x === $this->width + 1) {
$y++;
$x = 1;
}
$svg .= "<rect x=\"$x\" y=\"$y\" width=\"1\" height=\"1\" fill=\"rgb($r, $g, $b)\" />\n";
$x++;
}
$svg .= '</svg>';
return $svg;
}
} }

View File

@@ -6,19 +6,54 @@ use PHPUnit\Framework\TestCase;
final class FitsHeaderTest extends TestCase final class FitsHeaderTest extends TestCase
{ {
public function testKeywordValue(): void private Dumbastro\FitsPhp\Fits $fits;
{ private Dumbastro\FitsPhp\FitsHeader $header;
$fits = new Dumbastro\FitsPhp\Fits(__DIR__ . '/test_orion.fit');
$header = new Dumbastro\FitsPhp\FitsHeader($fits->headerBlock);
protected function setUp(): void
{
$this->fits = new Dumbastro\FitsPhp\Fits(__DIR__ . '/test_orion.fit');
$this->header = new Dumbastro\FitsPhp\FitsHeader($this->fits->headerBlock);
}
public function testKeywordValues(): void
{
$this->assertSame( $this->assertSame(
trim($header->getKeywordValue('BITPIX')), trim($this->header->getKeywordValue('BITPIX')),
'16' '16'
); );
$this->assertSame( $this->assertSame(
trim($header->getKeywordValue('NAXIS')), trim($this->header->getKeywordValue('NAXIS')),
'3' '3'
); );
$this->assertSame(
trim($this->header->getKeywordValue('STACKCNT')),
'86'
);
} }
public function testKeyword(): void
{
$keyword = $this->header->keyword('NAXIS1');
// Probably useless... (covered by PHPStan)
$this->assertInstanceOf(Dumbastro\FitsPhp\Keyword::class, $keyword);
$this->assertEquals($keyword->value, 2448);
$keyword = $this->header->keyword('EQUINOX');
$this->assertEquals(trim($keyword->value), '2000.');
}
/**
* @todo This fails for COMMENT keywords
public function testToString(): void
{
$headerBlock = $this->fits->headerBlock;
$headerString = $this->header->toString();
$this->assertEquals($headerBlock, $headerString);
}
*/
} }

44
tests/ImageBlobTest.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ImageBlobTest extends TestCase
{
private Dumbastro\FitsPhp\ImageBlob $imageBlob;
private Dumbastro\FitsPhp\Fits $fits;
private Dumbastro\FitsPhp\FitsHeader $header;
protected function setUp(): void
{
$this->fits = new Dumbastro\FitsPhp\Fits(__DIR__ . '/test_orion.fit');
$this->header = $this->fits->header();
$blob = $this->fits->imageBlob;
$this->imageBlob = new Dumbastro\FitsPhp\ImageBlob($this->header, $blob);
}
public function testBitpixValue(): void
{
$this->assertSame($this->imageBlob->bitpix->value, 16);
}
public function testDataBitsLength(): void
{
$this->assertSame($this->imageBlob->dataBits, 16*2448*1669*3);
}
public function testIsColor(): void
{
$mono = new Dumbastro\FitsPhp\Fits(__DIR__ . '/test_mono.fit');
$imageBlob = new Dumbastro\FitsPhp\ImageBlob($mono->header(), $mono->imageBlob);
$this->assertFalse($imageBlob->isColor);
$this->assertTrue($this->imageBlob->isColor);
$this->assertEquals($imageBlob->naxis3, 1);
}
}

2
tests/test_m52.fit Normal file

File diff suppressed because one or more lines are too long

BIN
tests/test_mono.fit Normal file

Binary file not shown.

19607
tests/test_orion_8bit.fit Normal file

File diff suppressed because one or more lines are too long