| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\Image; |
| 44 | use Std\Lib\Image\{Bitmap, Png, Bmp, Targa, Gif, Jpeg, Webp}; |
| 45 | |
| 46 | $test = new Tester([ |
| 47 | 'colorOutput' => true, |
| 48 | 'verboseOutput' => true |
| 49 | ]); |
| 50 | |
| 51 | // テスト用の小さい画像を作る(Imageのパスはルートからの相対パス) |
| 52 | $rel = '/data/lbimg_'.bin2hex(random_bytes(6)); |
| 53 | $dir = ROOT.$rel; |
| 54 | mkdir($dir, 0700, true); |
| 55 | |
| 56 | $chunk = fn(string $t, string $d) => pack('N', strlen($d)).$t.$d.pack('N', crc32($t.$d)); |
| 57 | $raw = ''; |
| 58 | for ($y = 0; $y < 2; $y++) $raw .= "\0".str_repeat("\xff\x00\x00", 3); |
| 59 | file_put_contents("{$dir}/red.png", "\x89PNG\r\n\x1a\n".$chunk('IHDR', pack('NNCCCCC', 3, 2, 8, 2, 0, 0, 0)) |
| 60 | .$chunk('IDAT', gzcompress($raw)).$chunk('IEND', '')); |
| 61 | file_put_contents("{$dir}/a.gif", "GIF89a".pack('vv', 4, 5)."\x80\x00\x00\x00\x00\x00\xff\xff\xff" |
| 62 | ."\x2c".pack('vvvv', 0, 0, 4, 5)."\x00\x02\x02\x44\x01\x00\x3b"); |
| 63 | file_put_contents("{$dir}/a.jpg", "\xff\xd8\xff\xe0".pack('n', 16)."JFIF\0\x01\x01\x00\x00\x01\x00\x01\x00\x00" |
| 64 | ."\xff\xc0".pack('n', 17)."\x08".pack('nn', 6, 7)."\x03\x01\x22\x00\x02\x11\x01\x03\x11\x01\xff\xd9"); |
| 65 | file_put_contents("{$dir}/a.tga", pack('CCCvvCvvvvCC', 0, 0, 2, 0, 0, 0, 0, 0, 2, 3, 24, 0).str_repeat("\x00\x00\xff", 6)); |
| 66 | file_put_contents("{$dir}/a.webp", base64_decode('UklGRkwBAABXRUJQVlA4TEABAAAvE8ADAM2VIaL/MQAIwv+4hRjw/ocANSBo2zZmMP5wfxQpxEaSFEkFc0z+O3nPuDNVTiLJdq2pavRQ+Pyjh34QkBQk/z6Og1CbtgEDnpxyCOWacx4555JbuTfncuo9zT2Xcu86ykUAiVREGAJKIGa5DrM4IpgiCSIQQKkAOo4F+WEPUkgxGzs2CAjCLOA4szH4htnzjwBmMijB1IC6DjCYlLcQqIgS6APQYTKxNC+IM6HQDCkBUKcxkcWB/HAARI0phCdxHQJMdqxQxDcGiGAWXYg80NOICGjMwnwAzpDiVS8CqdMIsCCDaQFmcYKr7c4AejOmvo49kAX4XRwuTCALOY6qQQApoFD5MSYChxEMECvmnx1BhJzoYfEGKSDHZQwmJiAEFBGACEOeiOPB/+MIZmOABBFjtJpvNBac/X8cAA==')); |
| 67 | file_put_contents("{$dir}/fake.png", 'これは画像ではない'); |
| 68 | file_put_contents("{$dir}/big.png", "\x89PNG\r\n\x1a\n".str_repeat("\0", 3000)); |
| 69 | |
| 70 | $test->describe('画像', function($test) use ($rel): void { |
| 71 | $test->it('PNGの情報を読めるはず', function($test) use ($rel): void { |
| 72 | $img = new Image("{$rel}/red.png"); |
| 73 | $test->assertEquals('image/png', $img->type); |
| 74 | $test->assertEquals('red', $img->filename); |
| 75 | $test->assertEquals('png', $img->extension); |
| 76 | $test->assertEquals(filesize(ROOT."{$rel}/red.png"), $img->bytes); |
| 77 | $test->assertEquals(3, $img->fullInfo->width); |
| 78 | $test->assertEquals(2, $img->fullInfo->height); |
| 79 | $test->assertEquals('image/png', $img->fullInfo->type); |
| 80 | }); |
| 81 | |
| 82 | $test->it('GIF・JPEG・TGA・WebPの大きさを読めるはず', function($test) use ($rel): void { |
| 83 | foreach (['a.gif' => ['image/gif', 4, 5], 'a.jpg' => ['image/jpeg', 7, 6], 'a.tga' => ['image/x-tga', 2, 3], 'a.webp' => ['image/webp', 20, 16]] as $f => [$type, $w, $h]) { |
| 84 | $img = new Image("{$rel}/{$f}"); |
| 85 | $test->assertEquals($type, $img->type, "{$f}の種類"); |
| 86 | $test->assertEquals($w, $img->fullInfo->width, "{$f}の横"); |
| 87 | $test->assertEquals($h, $img->fullInfo->height, "{$f}の丈"); |
| 88 | } |
| 89 | $test->assertEquals('VP8L', (new Image("{$rel}/a.webp"))->fullInfo->compression); |
| 90 | }); |
| 91 | |
| 92 | $test->it('大きさを読み易い形式にするはず', function($test) use ($rel): void { |
| 93 | $test->assertEquals('41 B', (new Image("{$rel}/a.jpg"))->size); |
| 94 | $test->assertEquals('2.94 KiB', (new Image("{$rel}/big.png"))->size); |
| 95 | }); |
| 96 | |
| 97 | $test->it('無いファイルは何もしないはず', function($test) use ($rel): void { |
| 98 | $img = new Image("{$rel}/none.png"); |
| 99 | $test->assertEquals('UNSUPPORTED', $img->type); |
| 100 | $test->assertEquals(0, $img->bytes); |
| 101 | // テンプレートがfullInfoを読んでも落ちない事 |
| 102 | $test->assertEquals(0, $img->fullInfo->width); |
| 103 | $test->assertEquals('UNSUPPORTED', $img->fullInfo->type); |
| 104 | }); |
| 105 | |
| 106 | $test->it('未対応の形式は例外のはず', function($test) use ($rel): void { |
| 107 | // 拡張子ではなく中身で判断する |
| 108 | $test->assertThrows(fn() => new Image("{$rel}/fake.png"), \RuntimeException::class); |
| 109 | }); |
| 110 | }); |
| 111 | |
| 112 | $test->describe('画素の読み書き', function($test): void { |
| 113 | // テスト用の画素:塗り分け(2色、16色、灰色、半透明) |
| 114 | $make = function(string $kind, int $w = 13, int $h = 7): Bitmap { |
| 115 | $b = new Bitmap($w, $h); |
| 116 | for ($y = 0; $y < $h; $y++) for ($x = 0; $x < $w; $x++) { |
| 117 | match ($kind) { |
| 118 | 'two' => ($x + $y) % 2 ? $b->set($x, $y, 0, 0, 0) : $b->set($x, $y, 255, 255, 255), |
| 119 | 'sixteen' => $b->set($x, $y, ($x % 4) * 80, ($y % 4) * 80, 40), |
| 120 | 'gray' => $b->set($x, $y, ($x * 19 + $y * 7) & 255, ($x * 19 + $y * 7) & 255, ($x * 19 + $y * 7) & 255), |
| 121 | 'rgb' => $b->set($x, $y, ($x * 19) & 255, ($y * 37) & 255, ($x * $y * 11) & 255), |
| 122 | 'rgba' => $b->set($x, $y, ($x * 19) & 255, ($y * 37) & 255, ($x * $y * 11) & 255, ($x * 23 + $y) & 255), |
| 123 | }; |
| 124 | } |
| 125 | return $b; |
| 126 | }; |
| 127 | |
| 128 | $test->it('PNGは色に合わせて一番小さい形式で書き、そのまま読み戻せるはず', function($test) use ($make): void { |
| 129 | // 色類とビット深さ(IHDRの25・24バイト目) |
| 130 | foreach (['two' => [3, 1], 'sixteen' => [3, 4], 'rgb' => [2, 8], 'rgba' => [6, 8]] as $kind => [$ctype, $depth]) { |
| 131 | // 256色以下はパレットになるので、RGBは大きくする |
| 132 | $b = str_starts_with($kind, 'rgb') ? $make($kind, 40, 20) : $make($kind); |
| 133 | $png = (new Png())->encode($b); |
| 134 | $test->assertEquals($ctype, ord($png[25]), "{$kind}の色類"); |
| 135 | $test->assertEquals($depth, ord($png[24]), "{$kind}のビット深さ"); |
| 136 | $test->assertEquals($b->data, (new Png())->decode($png)->data, "{$kind}の画素"); |
| 137 | } |
| 138 | // 257色以上の灰色はグレースケール |
| 139 | $g = new Bitmap(300, 1); |
| 140 | for ($x = 0; $x < 300; $x++) $g->set($x, 0, $x % 256, $x % 256, $x % 256); |
| 141 | $g->set(299, 0, 1, 1, 1, 0); |
| 142 | $test->assertEquals(4, ord((new Png())->encode($g)[25])); |
| 143 | }); |
| 144 | |
| 145 | $test->it('他のソフトが書いたPNG(Adam7・16ビット・複数のIDAT、2ビットの灰色とtRNS)を読めるはず', function($test): void { |
| 146 | // Pythonで書いた画像と、仕様通りに計算した画素 |
| 147 | $a = (new Png())->decode(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAUAAAADEAYAAAF8oSktAAAARUlEQVR42gF/AID/ATkADM6M0H1iBNbLcN3l/o5vBNgGEEUPQS/4AZYbuShicyOc4RV6jmAsa7Y+tjiOxhKWPAJySEdxNHosg2/8MHdoAAAARUlEQVTxdx8NlmXOBKUuLT5gsPxX0TRDWr8dWK4DA1NRvthgrjWNHCc8Ag1VPO0yCC+LZ9tZnaKZ3CYt/v2Q0+fcfpgdOR3QNgufp1a3AAAAAElFTkSuQmCC')); |
| 148 | $test->assertEquals('390c8c7d7247342cd8100f2f6f770d65d670e58e0351d8ae8e4f6eac342fc231b7b08716eb3fc12896b96223177494287733c28ee8ba53bdb56b8824', bin2hex($a->data)); |
| 149 | $b = (new Png())->decode(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAUAAAADAgAAAAA07YKFAAAAAnRSTlMAApidrBQAAAAISURBVHjaYwnPZApswiXMeQAAAAlJREFUYGo7AAALNwLgcgWJwgAAAABJRU5ErkJggg==')); |
| 150 | $test->assertEquals('555555ff555555ff555555ffffffffffffffffffaaaaaa00aaaaaa00aaaaaa00000000ff555555ff000000ffaaaaaa00ffffffffaaaaaa00000000ff', bin2hex($b->data)); |
| 151 | }); |
| 152 | |
| 153 | $test->it('壊れたPNGは例外のはず', function($test) use ($make): void { |
| 154 | $png = (new Png())->encode($make('rgb')); |
| 155 | $bad = $png; |
| 156 | $bad[40] = chr(ord($bad[40]) ^ 1); // IDATの中(CRCが合わなくなる) |
| 157 | $test->assertThrows(fn() => (new Png())->decode($bad), \RuntimeException::class); |
| 158 | $test->assertThrows(fn() => (new Png())->decode(substr($png, 0, 60)), \RuntimeException::class); |
| 159 | $test->assertThrows(fn() => (new Png())->decode('GIF89a'), \RuntimeException::class); |
| 160 | }); |
| 161 | |
| 162 | $test->it('APNGのフレーム・遅延・繰り返しを読み書き出来るはず', function($test): void { |
| 163 | $a = Bitmap::fill(4, 3, 255, 0, 0); |
| 164 | $a->frames = [ |
| 165 | ['bitmap' => Bitmap::fill(4, 3, 255, 0, 0), 'delay' => 100], |
| 166 | ['bitmap' => Bitmap::fill(4, 3, 0, 0, 255, 128), 'delay' => 250], |
| 167 | ]; |
| 168 | $a->loops = 3; |
| 169 | $b = (new Png())->decode((new Png())->encode($a)); |
| 170 | $test->assertCount(2, $b->frames); |
| 171 | $test->assertEquals([100, 250], array_column($b->frames, 'delay')); |
| 172 | $test->assertEquals(3, $b->loops); |
| 173 | $test->assertEquals([0, 0, 255, 128], $b->frames[1]['bitmap']->get(2, 1)); |
| 174 | }); |
| 175 | |
| 176 | $test->it('アルファで重ねられるはず', function($test): void { |
| 177 | $bg = Bitmap::fill(2, 1, 255, 0, 0); |
| 178 | $bg->draw(Bitmap::fill(1, 1, 0, 0, 255, 128), 1, 0); |
| 179 | $test->assertEquals([255, 0, 0, 255], $bg->get(0, 0)); |
| 180 | $test->assertEquals([127, 0, 128, 255], $bg->get(1, 0)); |
| 181 | }); |
| 182 | |
| 183 | $test->it('BMPを読み書き出来るはず(パレット、24ビット、アルファ付き32ビット、RLE4)', function($test) use ($make): void { |
| 184 | foreach (['two' => 1, 'sixteen' => 4, 'rgb' => 24, 'rgba' => 32] as $kind => $bits) { |
| 185 | $b = str_starts_with($kind, 'rgb') ? $make($kind, 40, 20) : $make($kind); |
| 186 | $bmp = (new Bmp())->encode($b); |
| 187 | $test->assertEquals($bits, unpack('v', substr($bmp, 28, 2))[1], "{$kind}のビット深さ"); |
| 188 | $test->assertEquals($b->data, (new Bmp())->decode($bmp)->data, "{$kind}の画素"); |
| 189 | } |
| 190 | // RLE4(繰り返し、そのままの画素、デルタ、行と画像の終わり)。飛ばした画素は透明 |
| 191 | $r = (new Bmp())->decode(base64_decode('Qk2MAAAAAAAAAHYAAAAoAAAABwAAAAQAAAABAAQAAgAAABYAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAA/wAA/wAA/wAAAEBAQABQUFAAYGBgAHBwcACAgIAAkJCQAKCgoACwsLAAwMDAANDQ0ADg4OAA8PDwAAcSAAAABTEjQAACVQAAAAIDAQT/AAE=')); |
| 192 | $test->assertEquals([0, 0, 0, 0], $r->get(0, 0)); |
| 193 | $test->assertEquals([240, 240, 240, 255], $r->get(3, 0)); |
| 194 | $test->assertEquals([0, 0, 0, 0], $r->get(3, 1)); |
| 195 | $test->assertEquals([0, 0, 255, 255], $r->get(0, 2)); |
| 196 | $test->assertEquals([80, 80, 80, 255], $r->get(6, 2)); |
| 197 | $test->assertEquals([0, 255, 0, 255], $r->get(1, 3)); |
| 198 | }); |
| 199 | |
| 200 | $test->it('TGAを読み書き出来るはず(左下の原点、行を跨ぐRLE)', function($test) use ($make): void { |
| 201 | foreach (['rgb', 'rgba'] as $kind) { |
| 202 | $b = $make($kind); |
| 203 | $test->assertEquals($b->data, (new Targa())->decode((new Targa())->encode($b))->data, "{$kind}(RLE)"); |
| 204 | $test->assertEquals($b->data, (new Targa())->decode((new Targa())->encode($b, ['rle' => false]))->data, "{$kind}(無圧縮)"); |
| 205 | } |
| 206 | $t = (new Targa())->decode(base64_decode('AAACAAAAAAAAAAAAAwACABgA////AAAAAP//AAD/AP8A/wAA')); |
| 207 | $test->assertEquals('ff0000ff00ff00ff0000ffffffffffff000000ffffff00ff', bin2hex($t->data)); |
| 208 | $t = (new Targa())->decode(base64_decode('AAAKAAAAAAAAAAAAAwACACAoAQAA//8A/wCAgv8AAAAAAP///w==')); |
| 209 | $test->assertEquals('ff0000ff00ff00800000ff000000ff000000ff00ffff00ff', bin2hex($t->data)); |
| 210 | }); |
| 211 | |
| 212 | $test->it('GIFを読み書き出来るはず(パレット、透明、256色を超える減色)', function($test) use ($make): void { |
| 213 | foreach (['two', 'sixteen', 'gray'] as $kind) { |
| 214 | $b = $make($kind); |
| 215 | $test->assertEquals($b->data, (new Gif())->decode((new Gif())->encode($b))->data, $kind); |
| 216 | } |
| 217 | // 透明はアルファ128未満、GIFに半透明は無い |
| 218 | $t = $make('two'); |
| 219 | $t->set(1, 1, 9, 9, 9, 0); |
| 220 | $t->set(2, 1, 9, 9, 9, 200); |
| 221 | $d = (new Gif())->decode((new Gif())->encode($t)); |
| 222 | $test->assertEquals(0, $d->get(1, 1)[3]); |
| 223 | $test->assertEquals([9, 9, 9, 255], $d->get(2, 1)); |
| 224 | // 減色:大きさと大体の色は保つ |
| 225 | $rgb = $make('rgb', 40, 20); |
| 226 | $q = (new Gif())->decode((new Gif())->encode($rgb, ['dither' => true])); |
| 227 | $diff = 0; |
| 228 | for ($i = 0; $i < strlen($rgb->data); $i++) $diff += abs(ord($rgb->data[$i]) - ord($q->data[$i])); |
| 229 | $test->assertTrue($diff / strlen($rgb->data) < 16, '平均の差'); |
| 230 | }); |
| 231 | |
| 232 | $test->it('他のソフトが書いたGIF(インターレース、オフセットと廃棄方法のあるアニメーション)を読めるはず', function($test): void { |
| 233 | // Pillowが書いたインターレースの3x9:真ん中の列は3行ずらした色 |
| 234 | $cols = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [0, 255, 255], [255, 0, 255], [255, 255, 255], [0, 0, 0], [128, 128, 128]]; |
| 235 | $i = (new Gif())->decode(base64_decode('R0lGODdhAwAJAIMAAP//////AICAgAD//wD/AP8A//8AAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAwAJAAAIIQANBDBAYACBAwUOBAAQYACCAQUEFABgAAACAggEHBAQEAA7')); |
| 236 | for ($y = 0; $y < 9; $y++) { |
| 237 | $test->assertEquals([...$cols[$y], 255], $i->get(0, $y), "{$y}行目"); |
| 238 | $test->assertEquals([...$cols[($y + 3) % 9], 255], $i->get(1, $y), "{$y}行目の真ん中"); |
| 239 | } |
| 240 | // ImageMagickが書いた6x5:紺の背景、赤(前に戻す)、黄(背景に戻す)、緑。画素はChromiumと同じ |
| 241 | $a = (new Gif())->decode(base64_decode('R0lGODlhBgAFAPAAAAAAgAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQECgAAACH/C0ltYWdlTWFnaWNrDmdhbW1hPTAuNDU0NTQ1ACwAAAAABgAFAAACBYSPqWsFACH5BAwUAAAAIf8LSW1hZ2VNYWdpY2sOZ2FtbWE9MC40NTQ1NDUALAEAAQACAAIAgP8AAAAAAAIChFEAIfkECB4AAAAh/wtJbWFnZU1hZ2ljaw5nYW1tYT0wLjQ1NDU0NQAsAwACAAIAAgCA//8AAAAAAgKEUQAh+QQEBQAAACH/C0ltYWdlTWFnaWNrDmdhbW1hPTAuNDU0NTQ1ACwAAAMAAgACAIAA/wAAAAACAoRRADs=')); |
| 242 | $test->assertCount(4, $a->frames); |
| 243 | $test->assertEquals([100, 200, 300, 50], array_column($a->frames, 'delay')); |
| 244 | $test->assertEquals(0, $a->loops); |
| 245 | $f = array_column($a->frames, 'bitmap'); |
| 246 | $test->assertEquals([255, 0, 0, 255], $f[1]->get(1, 1)); |
| 247 | $test->assertEquals([0, 0, 128, 255], $f[2]->get(1, 1), '赤は前に戻した'); |
| 248 | $test->assertEquals([255, 255, 0, 255], $f[2]->get(3, 2)); |
| 249 | $test->assertEquals([0, 0, 0, 0], $f[3]->get(3, 2), '黄は背景(透明)に戻した'); |
| 250 | $test->assertEquals([0, 255, 0, 255], $f[3]->get(0, 3)); |
| 251 | }); |
| 252 | |
| 253 | $test->it('GIFのアニメーションを書けるはず', function($test): void { |
| 254 | $a = Bitmap::fill(5, 4, 255, 0, 0); |
| 255 | $a->frames = [ |
| 256 | ['bitmap' => Bitmap::fill(5, 4, 255, 0, 0), 'delay' => 100], |
| 257 | ['bitmap' => Bitmap::fill(5, 4, 0, 0, 255), 'delay' => 250], |
| 258 | ]; |
| 259 | $a->loops = 0; |
| 260 | $b = (new Gif())->decode((new Gif())->encode($a)); |
| 261 | $test->assertEquals([100, 250], array_column($b->frames, 'delay')); |
| 262 | $test->assertEquals(0, $b->loops); |
| 263 | $test->assertEquals([0, 0, 255, 255], $b->frames[1]['bitmap']->get(4, 3)); |
| 264 | }); |
| 265 | |
| 266 | $test->it('他のソフトが書いたJPEG(プログレッシブ+リスタート、CMYK、グレースケール、Exifの向き)を読めるはず', function($test): void { |
| 267 | // Pillow(libjpeg-turbo)で書いて、libjpeg-turboで読んだ画素(逆DCTの計算の違いで±3まで) |
| 268 | $cases = [ |
| 269 | 'prog' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wgARCAAHAAkDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAYH/8QAFQEBAQAAAAAAAAAAAAAAAAAAAwT/3QAEAAH/2gAMAwEAAhADEAAAAZ/WyUf/xAAYEAACAwAAAAAAAAAAAAAAAAAAAwYHF//aAAgBAQABBQKI0+g//9DH0H//xAAZEQABBQAAAAAAAAAAAAAAAAAGAAEHFSP/2gAIAQMBAT8Bj81IaNtV/8QAFxEBAAMAAAAAAAAAAAAAAAAABAATMf/aAAgBAgEBPwELk07P/8QAFxABAQEBAAAAAAAAAAAAAAAAAgAjM//aAAgBAQAGPwI5m//Q5m//xAAXEAADAQAAAAAAAAAAAAAAAAAAAdHx/9oACAEBAAE/Iaaj/9DZR//aAAwDAQACAAMAAAAQ+//EABgRAAIDAAAAAAAAAAAAAAAAAABRMUFx/9oACAEDAQE/EJPprT//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPxCC/wD/xAAWEAEBAQAAAAAAAAAAAAAAAAAAAXH/2gAIAQEAAT8Qrxr/0C3/2Q==', '000100ff110708ff2f0618ff490627ff640736ff810645ff9e0758ffb60764ffcf0b71ff091e0fff1c231bff39242bff54243aff6e234aff8a235affa9236affc22478ffd92883ff0a4322ff1b472cff37493dff53494aff6e495bff89486affa8477affc14987ffd84c93ff096732ff1d6c3dff386e4cff556e5bff6e6d6bff8b6c7bffa96d89ffc36e97ffd971a4ff0a8a3fff1b8e49ff388f58ff558f67ff6f8f78ff8c8f88ffaa8f98ffc38fa5ffd893b0ff0ead50ff1fb25aff3db369ff59b278ff74b289ff90b299ffafb2a9ffc7b3b5ffddb7c2ff11cd5fff25d16bff40d37aff5dd389ff78d39aff93d1a8ffb1d1b9ffcbd3c6ffe1d6d4ff', 9, 7], |
| 270 | 'cmyk' => ['/9j/7gAOQWRvYmUAZAAAAAAA/9sAQwACAQEBAQECAQEBAgICAgIEAwICAgIFBAQDBAYFBgYGBQYGBgcJCAYHCQcGBggLCAkKCgoKCgYICwwLCgwJCgoK/8AAFAgABwAJBEMRAE0RAFkRAEsRAP/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/aAA4EQwBNAFkASwAAPwDwf/ggD/zBP+2f9Kxf+IaHwh/0BdN/7+R/4143/wAE7v2AtL/0H/RLf+H+Ja/fyv34o/4hofCH/QF03/v5H/jX6H/8MBaX/wA+lv8A99LRX//Z', '000000ff1c000eff38001eff54002cff70003cff8c004affa8005affc40068ffe00078ff002410ff1c241eff38242eff54243cff70244cff8c245affa8246affc42478ffe02488ff00481eff1c482cff38483cff54484aff70485aff8c4868ffa84878ffc44886ffe04896ff006c2eff1c6c3cff386c4cff546c5aff706c6aff8c6c78ffa86c88ffc46c96ffe06ca6ff00903cff1c904aff38905aff549068ff709078ff8c9086ffa89096ffc490a4ffe090b4ff00b44bff1cb45aff38b469ff54b478ff70b487ff8cb496ffa8b4a5ffc4b4b4ffe0b4c3ff00d85bff1cd869ff38d879ff54d887ff70d897ff8cd8a5ffa8d8b5ffc4d8c3ffe0d8d3ff', 9, 7], |
| 271 | 'gray' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/wAALCAAHAAkBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/AOA/ZH/4I/aAfsv/ABLrTt/GtfW3/Dn3Qv8AoH2n/fa1/9k=', '000000ff0a0a0aff141414ff1e1e1eff282828ff323232ff3d3d3dff464646ff515151ff161616ff202020ff2b2b2bff353535ff3e3e3eff484848ff535353ff5d5d5dff676767ff2e2e2eff373737ff424242ff4c4c4cff565656ff5f5f5fff6a6a6aff747474ff7e7e7eff454545ff4f4f4fff5a5a5aff646464ff6d6d6dff777777ff828282ff8c8c8cff969696ff5b5b5bff646464ff6f6f6fff797979ff838383ff8d8d8dff989898ffa1a1a1ffabababff737373ff7c7c7cff878787ff919191ff9b9b9bffa5a5a5ffb0b0b0ffb9b9b9ffc4c4c4ff888888ff929292ff9d9d9dffa7a7a7ffb1b1b1ffbababaffc5c5c5ffcfcfcfffd9d9d9ff', 9, 7], |
| 272 | 'o6' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAACAAMDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDzv9vn4I/Be3/a9+N1nb/CL4YQWeneINI0rT7SHwD4UitrDS9K8J+H9N0vTbKBNJWK1sNN061trCws4Ejt7Oyt4LW3jjghjjX/AG+/ZL8TcR5N+zP+g7RyjiDO8qo5j9G7wz4lzCjlua4/A0sdxHxfkGH4r4sz/GU8LiKUMVnfFHFGc5xxJxFmtdTx2dZ/muZZxmVfE5jjsViav+4/7LLwQ8FvFf6IFDxD8UvCHwv8SuP+KfpQfT/x/E3HPH/AHCnGPGHEWOp/T8+k3g6eNz3ibiLKcxzrN8XDCYbDYWGIzDG4itHDYehQjNUqVOMf/9k=', 'ffff00fffe0000ff00ffffff01ff00ffff00feff0000feff', 2, 3], |
| 273 | ]; |
| 274 | foreach ($cases as $name => [$b64, $hex, $w, $h]) { |
| 275 | $b = (new Jpeg())->decode(base64_decode($b64)); |
| 276 | $test->assertEquals([$w, $h], [$b->width, $b->height], "{$name}の大きさ"); |
| 277 | $exp = hex2bin($hex); |
| 278 | $max = 0; |
| 279 | for ($i = 0; $i < strlen($exp); $i++) $max = max($max, abs(ord($exp[$i]) - ord($b->data[$i]))); |
| 280 | $test->assertTrue($max <= 3, "{$name}の画素(差{$max})"); |
| 281 | } |
| 282 | // 向き6(時計回りに90度):3x2が2x3になる |
| 283 | $info = (new Jpeg())->decode(base64_decode($cases['o6'][0])); |
| 284 | $test->assertEquals(2, $info->width); |
| 285 | }); |
| 286 | |
| 287 | $test->it('JPEGを書けるはず(品質、4:2:0・4:4:4、グレースケール、透明は白)', function($test) use ($make): void { |
| 288 | // 滑らかなグラデーション(画素毎に色が変わる画像は4:2:0で色差が失われるのが正しい) |
| 289 | $b = new Bitmap(40, 20); |
| 290 | for ($y = 0; $y < 20; $y++) for ($x = 0; $x < 40; $x++) $b->set($x, $y, $x * 6, $y * 12, ($x + $y) * 4); |
| 291 | $hi = (new Jpeg())->encode($b, ['quality' => 95]); |
| 292 | $lo = (new Jpeg())->encode($b, ['quality' => 30]); |
| 293 | $test->assertTrue(strlen($lo) < strlen($hi), '品質が低いと小さい'); |
| 294 | $err = function(Bitmap $a, Bitmap $c): float { |
| 295 | $s = 0; |
| 296 | for ($i = 0; $i < strlen($a->data); $i++) if ($i % 4 !== 3) $s += abs(ord($a->data[$i]) - ord($c->data[$i])); |
| 297 | return $s / (strlen($a->data) * 3 / 4); |
| 298 | }; |
| 299 | $test->assertTrue($err($b, (new Jpeg())->decode($hi)) < 2, '品質95の平均の差'); |
| 300 | $test->assertTrue($err($b, (new Jpeg())->decode((new Jpeg())->encode($b, ['quality' => 95, 'subsampling' => '444']))) < 1.5, '4:4:4の平均の差'); |
| 301 | // 灰色はグレースケール(SOF0の成分の数が1) |
| 302 | $g = (new Jpeg())->encode($make('gray')); |
| 303 | $test->assertEquals(1, ord($g[strpos($g, "\xff\xc0") + 9])); |
| 304 | // 透明は白と合成 |
| 305 | $t = Bitmap::fill(16, 16, 0, 0, 0, 0); |
| 306 | $test->assertTrue(min((new Jpeg())->decode((new Jpeg())->encode($t))->get(8, 8)) >= 250); |
| 307 | }); |
| 308 | |
| 309 | $test->it('libwebpが書いたWebP(非可逆、可逆、アルファ、アニメーション)を読めるはず', function($test): void { |
| 310 | // Pillow(libwebp 1.6)で書いて、libwebpで読んだ画素のMD5(計算はlibwebpと全く同じなので一致する) |
| 311 | $cases = [ |
| 312 | 'ly' => ['UklGRqYAAABXRUJQVlA4IJoAAADQBACdASoYABQAPrVOoEqnJCMhsBgIAOAWiUAWHYHlTnwr36n6ZUj8wDDu9+DCyAD+opZKvMHS233rVxNfgljnxvWSmFqCXbB3yF9HmOg8rtcoGdyLn5z6+4VxeQ1ImRbIwaoZ1v7erNwZKFXCzKxRsQdQwWWn+vJ3DfH+/zPcdEROIODEUzTDqVxI6LKvZMfVtHqlKUQ+AYQA', 24, 20, ['3d2d40581e10cc86445dd4d4f2c935c4']], |
| 313 | 'll' => ['UklGRkwBAABXRUJQVlA4TEABAAAvE8ADAM2VIaL/MQAIwv+4hRjw/ocANSBo2zZmMP5wfxQpxEaSFEkFc0z+O3nPuDNVTiLJdq2pavRQ+Pyjh34QkBQk/z6Og1CbtgEDnpxyCOWacx4555JbuTfncuo9zT2Xcu86ykUAiVREGAJKIGa5DrM4IpgiCSIQQKkAOo4F+WEPUkgxGzs2CAjCLOA4szH4htnzjwBmMijB1IC6DjCYlLcQqIgS6APQYTKxNC+IM6HQDCkBUKcxkcWB/HAARI0phCdxHQJMdqxQxDcGiGAWXYg80NOICGjMwnwAzpDiVS8CqdMIsCCDaQFmcYKr7c4AejOmvo49kAX4XRwuTCALOY6qQQApoFD5MSYChxEMECvmnx1BhJzoYfEGKSDHZQwmJiAEFBGACEOeiOPB/+MIZmOABBFjtJpvNBac/X8cAA==', 20, 16, ['d815286545f188d51ae2c856a6f5082c']], |
| 314 | 'lya' => ['UklGRmgAAABXRUJQVlA4WAoAAAAQAAAADwAACwAAQUxQSCQAAAABR6CQbQTI3/aO4IeIiBTxMMxGUhmWZVmW5fzfBxDR/4Bb8xdWUDggHgAAAHABAJ0BKhAADAACwEwllAJ0AUAAAP72JivN6Z4AAA==', 16, 12, ['34aea5dccb0104704648d56751219bbc']], |
| 315 | 'an' => ['UklGRtQBAABXRUJQVlA4WAoAAAASAAAADwAACwAAQU5JTQYAAAAAAAAAAgBBTk1GogAAAAAAAAAAAA8AAAsAACgAAAJWUDggigAAAFABAJ0BKhAADAACQDglsAAEdAAA/u1jqjpe1yd//xMetXZf+DXz/HzO0n/0HuFcl/EhnxTn0nP3xTxDvrfSTJaHOCrPsAzkZfKf/73SWFW+zS0Fi24G8cxE7+8Xr6jlWkP/8zQiYwzd5n+5wb3KX//1em9W3/J3X/v/+P+P+f+ftLXAlKa01l4AAEFOTUaSAAAAAAAAAQAACQAABQAAMgAAAkFMUEgTAAAAAQ9wrdKICCEQSDLYX3iGiP5HBgBWUDggXgAAAFABAJ0BKgoABgACQDglsAAEdAAA/vCixaMTQv+HtdHh7XR4e193///iY5/Aj+ii1+QeONk0LN/+qGF+r/Q18Gx+bdzX//6WH/fFx/Y3//8SF//pYf98XH9jf/w/gABBTk1GZAAAAAIAAAEAAAkAAAUAADwAAABWUDggTAAAAFQBAJ0BKgoABgAAADglsAAEdAAA/vCI1w8Uzl4f69KERUrb//bMkc3+Sn6WUnj11///Uaf9i2f+Jb5n//Ehf/6WH/cWz/wQ//D+AAA=', 16, 12, ['878e0516e278e5b3287d4fe34e020d74', 'cc6eb9ea67b8b238e6d1e7044cf59124', '832f71c95b62e156655925ddaaa6fb29']], |
| 316 | 'anl' => ['UklGRtIDAABXRUJQVlA4WAoAAAASAAAADwAACwAAQU5JTQYAAAAAAAAAAABBTk1GSAIAAAAAAAAAAA8AAAsAACgAAAJWUDhMLwIAAC8PwAIA5wTXtq3j1XfOd37EnW3bTqo8bkZqO9e+nd3aeg03jSRJcjhy3jX3vINwOjcCABDK2fZlY7L5017QI9pba7Jtu/mP7N+VAggPiRZCnD9nY0uZiENYUe56Az+mHi568cJxoJPx50YAgP9I523pQhmiiS7eEslAh83E8MDLXwAE+4mGHg7bQIg4zW+M4zpSVOBrr6MixkcscsFC/CYgB+hi0UWRE/hDD8AFtTGQ73CSKwSbdJKn454AAj39hwKghyxkRwizV1cdp99eRQWaAC8MBFCmtRBCQQTYQX+is0FrdS7ohnz02rIL89la7yAoaKeWGrdpPS1uiDWrqSxNsPQTgoGRJ8bvqyXZBnpieLOxlOWg9PEwFGpHop9+XRYKXxQR5yXYtAZLDqlEgLQ7thIoTAjv6rs1yrCq7mnctZUJQT77expvpuH21wiPwTG61STFmDFEfp1RlarMPRNIx+UE2Y5J8KObbp9zSP+W4f66fxos8vC74/rkOX8MQAwAsIx0tm3btm2+bdu2bWbcDBHR//j4/v/pItKVwc67HSsVifj6/Xsi4YSK8tbbgUmCQQEAAAAa+9zi5c1CXCuDswOvkyNH66OtGry5++wWOfLjSxNwOpETSEO6yHQHzCGwmBun/WvdNlhtUcnPr++vnGQ8BdvX01ute8ODs9tDDDS70hy+2C81/NF2xmBO8vUnt6vvD8szMd9Ygecqclcej+fPPl/2DqdSRrEnQAMAQU5NRqgAAAAAAAABAAAJAAAFAAAyAAACVlA4TJAAAAAvCUABEMegNpKtBHeNfkZKLfTfACVYam2ojSS1eQMRKXIdqAH1X5oYBQBYaHztZZ1ZRt3VwREkEEji3G/PQB9UXzEDhfNHQXwCSI/HhxHHRG2fsq3hTalWKAblr3vV7ogag6NBxwo6J26DE8kUFLSRpOwzM/rXuiIi+h/nzwQAwN1V8hte8SrGcdM2cxCxEwBBTk1GpgAAAAIAAAEAAAkAAAUAADwAAABWUDhMjgAAAC8JQAEAx8A2tq1EOAUwZHTg0j4dEK9Gm/k24G0oaNuG6dkFNXBjzji2rTS4QxWfAui/DYdIA1klW5n5D3uQQlMwpriI0A4+cGEaoOEmB6jsfCQjs4sfmcHJGyaDCvnjpSKafeKmPv3ocuU7KGgjSXlmxvWvdUVE9D+kDgkAAOFqZzvX5vFL5nON4td8JAA=', 16, 12, ['8dba6b986075f2fe3083ea5a7feba53a', 'b2a89c12a21cceb0aefe1858efcf0b98', '61d35682a51b88e281f8aa058aa7d441']], |
| 317 | ]; |
| 318 | foreach ($cases as $name => [$b64, $w, $h, $md5]) { |
| 319 | $b = (new Webp())->decode(base64_decode($b64)); |
| 320 | $test->assertEquals([$w, $h], [$b->width, $b->height], "{$name}の大きさ"); |
| 321 | $frames = $b->frames ? array_column($b->frames, 'bitmap') : [$b]; |
| 322 | $test->assertEquals($md5, array_map(fn($f) => md5($f->data), $frames), "{$name}の画素"); |
| 323 | } |
| 324 | $an = (new Webp())->decode(base64_decode($cases['an'][0])); |
| 325 | $test->assertEquals([40, 50, 60], array_column($an->frames, 'delay')); |
| 326 | $test->assertEquals(2, $an->loops); |
| 327 | }); |
| 328 | |
| 329 | $test->it('WebPを書けるはず(可逆はそのまま、非可逆は品質、アルファは正確、アニメーション)', function($test) use ($make): void { |
| 330 | foreach (['two', 'sixteen', 'gray', 'rgba'] as $kind) { |
| 331 | $b = $kind === 'rgba' ? $make($kind, 40, 20) : $make($kind); |
| 332 | $ll = (new Webp())->encode($b, ['lossless' => true]); |
| 333 | $test->assertEquals('VP8L', substr($ll, 12, 4), "{$kind}は可逆"); |
| 334 | $test->assertEquals($b->data, (new Webp())->decode($ll)->data, "{$kind}の画素"); |
| 335 | } |
| 336 | $g = new Bitmap(40, 20); |
| 337 | for ($y = 0; $y < 20; $y++) for ($x = 0; $x < 40; $x++) $g->set($x, $y, $x * 6, $y * 12, ($x + $y) * 4); |
| 338 | $hi = (new Webp())->encode($g, ['quality' => 95]); |
| 339 | $lo = (new Webp())->encode($g, ['quality' => 30]); |
| 340 | $test->assertTrue(strlen($lo) < strlen($hi), '品質が低いと小さい'); |
| 341 | $test->assertEquals('VP8 ', substr($hi, 12, 4), '不透明ならVP8だけ'); |
| 342 | $err = 0; |
| 343 | $back = (new Webp())->decode($hi); |
| 344 | for ($i = 0; $i < strlen($g->data); $i++) if ($i % 4 !== 3) $err += abs(ord($g->data[$i]) - ord($back->data[$i])); |
| 345 | $test->assertTrue($err / (40 * 20 * 3) < 2, '品質95の平均の差'); |
| 346 | // アルファがあればVP8XとALPH(アルファは可逆) |
| 347 | $t = $make('rgba', 40, 20); |
| 348 | $ly = (new Webp())->encode($t, ['quality' => 50]); |
| 349 | $test->assertEquals('VP8X', substr($ly, 12, 4)); |
| 350 | $back = (new Webp())->decode($ly); |
| 351 | $alpha = fn(string $d) => implode('', array_map(fn($i) => $d[$i], range(3, strlen($d) - 1, 4))); |
| 352 | $test->assertEquals($alpha($t->data), $alpha($back->data), 'アルファ'); |
| 353 | // アニメーション |
| 354 | $a = Bitmap::fill(5, 4, 255, 0, 0); |
| 355 | $a->frames = [ |
| 356 | ['bitmap' => Bitmap::fill(5, 4, 255, 0, 0), 'delay' => 100], |
| 357 | ['bitmap' => Bitmap::fill(5, 4, 0, 0, 255, 128), 'delay' => 250], |
| 358 | ]; |
| 359 | $a->loops = 3; |
| 360 | foreach ([true, false] as $lossless) { |
| 361 | $b = (new Webp())->decode((new Webp())->encode($a, ['lossless' => $lossless])); |
| 362 | $test->assertEquals([100, 250], array_column($b->frames, 'delay')); |
| 363 | $test->assertEquals(3, $b->loops); |
| 364 | $test->assertEquals(128, $b->frames[1]['bitmap']->get(2, 2)[3]); |
| 365 | if ($lossless) $test->assertEquals([0, 0, 255, 128], $b->frames[1]['bitmap']->get(4, 3)); |
| 366 | } |
| 367 | }); |
| 368 | |
| 369 | $test->it('壊れたWebPは例外のはず', function($test): void { |
| 370 | $ly = base64_decode('UklGRqYAAABXRUJQVlA4IJoAAADQBACdASoYABQAPrVOoEqnJCMhsBgIAOAWiUAWHYHlTnwr36n6ZUj8wDDu9+DCyAD+opZKvMHS233rVxNfgljnxvWSmFqCXbB3yF9HmOg8rtcoGdyLn5z6+4VxeQ1ImRbIwaoZ1v7erNwZKFXCzKxRsQdQwWWn+vJ3DfH+/zPcdEROIODEUzTDqVxI6LKvZMfVtHqlKUQ+AYQA'); |
| 371 | $short = substr($ly, 0, 120); |
| 372 | $short = substr_replace(substr_replace($short, pack('V', strlen($short) - 8), 4, 4), pack('V', strlen($short) - 20), 16, 4); |
| 373 | $test->assertThrows(fn() => (new Webp())->decode($short), \RuntimeException::class); |
| 374 | $test->assertThrows(fn() => (new Webp())->decode('RIFF'.pack('V', 17).'WEBPVP8L'.pack('V', 5)."\x2e\0\0\0\0"), \RuntimeException::class); |
| 375 | $test->assertThrows(fn() => (new Webp())->decode('RIFF'.pack('V', 4).'WEBP'), \RuntimeException::class); |
| 376 | }); |
| 377 | |
| 378 | $test->it('未対応のJPEG(算術符号)は例外のはず', function($test): void { |
| 379 | $sof9 = "\xff\xd8\xff\xc9".pack('n', 11)."\x08".pack('nn', 8, 8)."\x01\x01\x11\x00\xff\xd9"; |
| 380 | $test->assertThrows(fn() => (new Jpeg())->decode($sof9), \RuntimeException::class); |
| 381 | }); |
| 382 | |
| 383 | $test->it('中身で形式を判断して読めるはず', function($test) use ($make): void { |
| 384 | $b = $make('rgba'); |
| 385 | foreach (['image/png', 'image/bmp', 'image/gif'] as $type) { |
| 386 | $data = Image::encode($b, $type); |
| 387 | $test->assertEquals($type, Image::detect($data)); |
| 388 | if ($type !== 'image/gif') $test->assertEquals($b->data, Image::decode($data)->data); |
| 389 | } |
| 390 | $test->assertEquals('image/webp', Image::detect('RIFF'.pack('V', 100).'WEBPVP8L')); |
| 391 | $test->assertEquals($b->data, Image::decode(Image::encode($b, 'image/webp', ['lossless' => true]))->data); |
| 392 | $test->assertEquals('image/x-tga', Image::detect("\0\0\2", 'a.TGA')); |
| 393 | $test->assertNull(Image::detect('これは画像ではない')); |
| 394 | $test->assertThrows(fn() => Image::decode('これは画像ではない'), \RuntimeException::class); |
| 395 | }); |
| 396 | |
| 397 | $test->it('大き過ぎる画像は読まないはず', function($test): void { |
| 398 | $huge = "\x89PNG\r\n\x1a\n".pack('N', 13).'IHDR'.pack('NNCCCCC', 60000, 60000, 8, 6, 0, 0, 0); |
| 399 | $huge .= pack('N', crc32(substr($huge, 12, 17))); |
| 400 | $test->assertThrows(fn() => (new Png())->decode($huge), \RuntimeException::class); |
| 401 | }); |
| 402 | }); |
| 403 | |
| 404 | foreach (glob("{$dir}/*") ?: [] as $f) @unlink($f); |
| 405 | @rmdir($dir); |
| 406 | |
| 407 | $test->printSummary(); |