Compare commits
5 Commits
623b939df1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1569873cbb | |||
| 5e4180a4a2 | |||
| 19df4cc926 | |||
| da712b0e77 | |||
| 109ae71059 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
# Vim stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
composer.lock
|
||||
vendor/
|
||||
docs/
|
||||
*.svg
|
||||
|
||||
14
README.md
14
README.md
@@ -11,13 +11,15 @@ Why not? But seriously, this doesn't make any sense, you should never use it in
|
||||
|
||||
## Usage
|
||||
|
||||
**NOTE: Not added to Packagist yet...**
|
||||
|
||||
Add the package with Composer:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
@@ -72,8 +74,10 @@ echo $blob->bitpix->type(); //int32
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Read keywords from the FITS header
|
||||
- [x] Separate the main data table (actual image data) from the header (partly done? Who knows...)
|
||||
- [ ] Actually display the image in the standard output
|
||||
- [ ] Save the image to PNG and JPG
|
||||
- [ ] Manipulate the bits using basic processing algorithms??
|
||||
- [x] Read keywords from the FITS header (COMMENT keywords could be buggy)
|
||||
- [ ] History keywords?
|
||||
- [ ] FITS extensions?
|
||||
- [ ] Actually display the image in the standard output
|
||||
- [ ] Save the image to PNG and/or JPG
|
||||
- [ ] Manipulate the bits using basic processing algorithms??
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"phpunit/phpunit": "^11.3"
|
||||
},
|
||||
"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",
|
||||
"autoload": {
|
||||
|
||||
@@ -86,8 +86,9 @@ class Fits
|
||||
{
|
||||
$naxis1 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS1'));
|
||||
$naxis2 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS2'));
|
||||
$naxis3 = (int)trim($this->fitsHeader->getKeywordValue('NAXIS3'));
|
||||
|
||||
$blobEnd = $naxis1 * $naxis2;
|
||||
$blobEnd = $naxis1 * $naxis2 * $naxis3;
|
||||
|
||||
return substr(
|
||||
$this->contents,
|
||||
|
||||
@@ -28,6 +28,8 @@ class FitsHeader
|
||||
*
|
||||
* From the spec: each keyword record, including
|
||||
* any comments, is at most 80 bytes long
|
||||
* @todo Comments and keyword values could span more
|
||||
than one 80-bytes block...
|
||||
* @return Keyword[]
|
||||
*/
|
||||
private function readKeywords(): array
|
||||
@@ -50,6 +52,11 @@ class FitsHeader
|
||||
$name = $keyVal[0];
|
||||
$value = $keyVal[1] ?? '';
|
||||
|
||||
if (str_starts_with($name, 'COMMENT')) {
|
||||
$value = explode('COMMENT', $name)[1];
|
||||
$name = 'COMMENT';
|
||||
}
|
||||
|
||||
$keywords[] = new Keyword(
|
||||
name : $name,
|
||||
value : $value,
|
||||
|
||||
@@ -13,6 +13,10 @@ class ImageBlob
|
||||
private FitsHeader $header;
|
||||
public readonly Bitpix $bitpix;
|
||||
public readonly int $dataBits;
|
||||
public readonly int $width;
|
||||
public readonly int $height;
|
||||
public readonly ?int $naxis3;
|
||||
public readonly bool $isColor;
|
||||
|
||||
/**
|
||||
* @throws InvalidBitpixValue
|
||||
@@ -24,18 +28,57 @@ class ImageBlob
|
||||
$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;
|
||||
$this->width = (int) trim($this->header->keyword('NAXIS1')->value);
|
||||
$this->height = (int) trim($this->header->keyword('NAXIS2')->value);
|
||||
$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
|
||||
* 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++) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,54 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FitsHeaderTest extends TestCase
|
||||
{
|
||||
public function testKeywordValue(): void
|
||||
private Dumbastro\FitsPhp\Fits $fits;
|
||||
private Dumbastro\FitsPhp\FitsHeader $header;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$fits = new Dumbastro\FitsPhp\Fits(__DIR__ . '/test_orion.fit');
|
||||
$header = new Dumbastro\FitsPhp\FitsHeader($fits->headerBlock);
|
||||
$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(
|
||||
trim($header->getKeywordValue('BITPIX')),
|
||||
trim($this->header->getKeywordValue('BITPIX')),
|
||||
'16'
|
||||
);
|
||||
$this->assertSame(
|
||||
trim($header->getKeywordValue('NAXIS')),
|
||||
trim($this->header->getKeywordValue('NAXIS')),
|
||||
'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
44
tests/ImageBlobTest.php
Normal 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
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
BIN
tests/test_mono.fit
Normal file
Binary file not shown.
19607
tests/test_orion_8bit.fit
Normal file
19607
tests/test_orion_8bit.fit
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user