| 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\Lib\Image; |
| 39 | |
| 40 | /** |
| 41 | * BMP(Windows・OS/2) |
| 42 | * |
| 43 | * ヘッダー:OS/2 1.x(12バイト)、OS/2 2.x(16~64バイト)、BITMAPINFOHEADER(40)、V2(52)、V3(56)、V4(108)、V5(124) |
| 44 | * 画素:1・2・4・8ビット(パレット)、16・24・32ビット(既定の並び又はビットフィールド) |
| 45 | * 圧縮:無し、RLE8、RLE4、BITFIELDS、ALPHABITFIELDS、OS/2のRLE24、埋め込んだPNG・JPEG |
| 46 | * 下から上(既定)と上から下(丈が負) |
| 47 | */ |
| 48 | class Bmp implements ImageInterface { |
| 49 | private const RGB = 0, RLE8 = 1, RLE4 = 2, BITFIELDS = 3, JPEG = 4, PNG = 5, ALPHABITFIELDS = 6; |
| 50 | |
| 51 | public function parse(string $file): \stdClass { |
| 52 | $h = $this->header((string)file_get_contents($file, false, null, 0, 1024)); |
| 53 | return (object)[ |
| 54 | 'width' => $h['width'], |
| 55 | 'height' => $h['height'], |
| 56 | 'bitDepth' => $h['bits'], |
| 57 | 'headerSize' => $h['dib'], |
| 58 | 'compression' => $h['comp'], |
| 59 | 'topDown' => $h['topDown'], |
| 60 | 'colors' => $h['colors'], |
| 61 | 'hasAlpha' => $h['masks'][3] !== 0, |
| 62 | ]; |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * ヘッダーを読む |
| 67 | */ |
| 68 | private function header(string $d): array { |
| 69 | if (strlen($d) < 26 || substr($d, 0, 2) !== 'BM') throw new \RuntimeException('BMPではありません。'); |
| 70 | $offset = unpack('V', substr($d, 10, 4))[1]; |
| 71 | $dib = unpack('V', substr($d, 14, 4))[1]; |
| 72 | if (strlen($d) < 14 + min($dib, 64)) throw new \RuntimeException('BMPのヘッダーが途中で切れています。'); |
| 73 | $os2 = false; |
| 74 | if ($dib === 12) { |
| 75 | // OS/2 1.x:大きさは16ビット、パレットは3バイト |
| 76 | $h = unpack('vwidth/vheight/vplanes/vbits', substr($d, 18, 8)); |
| 77 | $h += ['comp' => self::RGB, 'colors' => 0]; |
| 78 | $os2 = true; |
| 79 | } else if ($dib >= 16) { |
| 80 | $h = unpack('lwidth/lheight/vplanes/vbits', substr($d, 18, 12)); |
| 81 | $more = $dib >= 40 ? unpack('Vcomp/Vsize/lxppm/lyppm/Vcolors', substr($d, 30, 20)) : ['comp' => 0, 'colors' => 0]; |
| 82 | $h += $more; |
| 83 | // OS/2 2.xは圧縮の番号が違う(3 = ハフマン、4 = RLE24) |
| 84 | $os2 = $dib === 16 || $dib === 64; |
| 85 | } else { |
| 86 | throw new \RuntimeException("未対応のBMPのヘッダーです({$dib}バイト)。"); |
| 87 | } |
| 88 | $topDown = $h['height'] < 0; |
| 89 | $h['height'] = abs($h['height']); |
| 90 | Bitmap::check($h['width'], $h['height']); |
| 91 | if (!in_array($h['bits'], [1, 2, 4, 8, 16, 24, 32], true) && !in_array($h['comp'], [self::JPEG, self::PNG], true)) { |
| 92 | throw new \RuntimeException("未対応のBMPのビット深さです({$h['bits']})。"); |
| 93 | } |
| 94 | |
| 95 | // ビットフィールド:V2以上はヘッダーの中、40バイトのヘッダーは後ろに続く |
| 96 | $masks = [0, 0, 0, 0]; |
| 97 | $palAt = 14 + $dib; |
| 98 | if ($h['comp'] === self::BITFIELDS && !$os2 || $h['comp'] === self::ALPHABITFIELDS) { |
| 99 | $n = $h['comp'] === self::ALPHABITFIELDS ? 4 : 3; |
| 100 | if ($dib >= 52) { |
| 101 | $masks = array_values(unpack('V4', substr($d, 54, 16) ?: str_repeat("\0", 16))); |
| 102 | if ($dib < 56) $masks[3] = 0; |
| 103 | } else { |
| 104 | $masks = array_values(unpack("V{$n}", substr($d, $palAt, $n * 4))); |
| 105 | if ($n === 3) $masks[] = 0; |
| 106 | $palAt += $n * 4; |
| 107 | } |
| 108 | } else if ($h['bits'] === 16) { |
| 109 | $masks = [0x7c00, 0x03e0, 0x001f, 0]; |
| 110 | } else if ($h['bits'] === 32) { |
| 111 | $masks = [0xff0000, 0xff00, 0xff, 0]; |
| 112 | // V3以上でアルファのマスクがあれば使う |
| 113 | if ($dib >= 56) $masks[3] = unpack('V', substr($d, 66, 4))[1]; |
| 114 | } |
| 115 | |
| 116 | $entry = $dib === 12 ? 3 : 4; |
| 117 | $colors = $h['bits'] <= 8 ? ($h['colors'] ?: 1 << $h['bits']) : 0; |
| 118 | return [ |
| 119 | 'offset' => $offset, 'dib' => $dib, 'width' => $h['width'], 'height' => $h['height'], 'bits' => $h['bits'], |
| 120 | 'comp' => $h['comp'], 'os2' => $os2, 'topDown' => $topDown, 'masks' => $masks, |
| 121 | 'colors' => min($colors, 256), 'palAt' => $palAt, 'entry' => $entry, |
| 122 | ]; |
| 123 | } |
| 124 | |
| 125 | public function decode(string $data): Bitmap { |
| 126 | $h = $this->header($data); |
| 127 | $w = $h['width']; |
| 128 | $ht = $h['height']; |
| 129 | $comp = $h['comp']; |
| 130 | $pixels = substr($data, $h['offset']); |
| 131 | |
| 132 | // 埋め込んだPNG・JPEG(OS/2の4はRLE24) |
| 133 | if ($comp === self::PNG && !$h['os2']) return (new Png())->decode($pixels); |
| 134 | if ($comp === self::JPEG && !$h['os2']) return (new Jpeg())->decode($pixels); |
| 135 | if ($h['os2'] && $comp === 3) throw new \RuntimeException('OS/2のハフマン圧縮のBMPは未対応です。'); |
| 136 | |
| 137 | // パレット => RGBA |
| 138 | $lut = null; |
| 139 | if ($h['bits'] <= 8) { |
| 140 | $lut = []; |
| 141 | for ($i = 0; $i < 256; $i++) { |
| 142 | $e = $i < $h['colors'] ? substr($data, $h['palAt'] + $i * $h['entry'], 3) : ''; |
| 143 | $lut[chr($i)] = strlen($e) === 3 ? $e[2].$e[1].$e[0]."\xff" : "\0\0\0\xff"; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | if ($comp === self::RLE8 || $comp === self::RLE4 || ($h['os2'] && $comp === 4)) { |
| 148 | $rows = $this->rle($pixels, $w, $ht, $comp === self::RLE4 ? 4 : ($comp === self::RLE8 ? 8 : 24), $lut); |
| 149 | } else { |
| 150 | $stride = (($h['bits'] * $w + 31) >> 5) * 4; |
| 151 | if (strlen($pixels) < $stride * $ht) throw new \RuntimeException('BMPの画素データが足りません。'); |
| 152 | $rows = []; |
| 153 | $alphaSeen = false; |
| 154 | for ($y = 0; $y < $ht; $y++) { |
| 155 | $row = substr($pixels, $y * $stride, $stride); |
| 156 | $rows[] = $this->row($row, $w, $h, $lut); |
| 157 | } |
| 158 | // 32ビットでアルファのマスクが無い場合:全部0なら不透明(Windowsはアルファを使わない)、それ以外は使う |
| 159 | if ($h['bits'] === 32 && $h['masks'][3] === 0 && $comp !== self::BITFIELDS) { |
| 160 | foreach ($rows as $y => $r) { |
| 161 | $raw = substr($pixels, $y * $stride, $w * 4); |
| 162 | for ($i = 3; $i < $w * 4; $i += 4) if ($raw[$i] !== "\0") { |
| 163 | $alphaSeen = true; |
| 164 | break 2; |
| 165 | } |
| 166 | } |
| 167 | if ($alphaSeen) { |
| 168 | foreach ($rows as $y => $r) { |
| 169 | $raw = substr($pixels, $y * $stride, $w * 4); |
| 170 | for ($i = 3; $i < $w * 4; $i += 4) $r[$i] = $raw[$i]; |
| 171 | $rows[$y] = $r; |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | if (!$h['topDown']) $rows = array_reverse($rows); |
| 177 | return new Bitmap($w, $ht, implode('', $rows)); |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * 圧縮していない1行をRGBAにする |
| 182 | */ |
| 183 | private function row(string $row, int $w, array $h, ?array $lut): string { |
| 184 | $bits = $h['bits']; |
| 185 | if ($lut !== null) { |
| 186 | if ($bits < 8) { |
| 187 | static $unpack = []; |
| 188 | if (!isset($unpack[$bits])) { |
| 189 | $n = 8 / $bits; |
| 190 | $mask = (1 << $bits) - 1; |
| 191 | for ($b = 0; $b < 256; $b++) { |
| 192 | $s = ''; |
| 193 | for ($k = $n - 1; $k >= 0; $k--) $s .= chr(($b >> ($k * $bits)) & $mask); |
| 194 | $unpack[$bits][chr($b)] = $s; |
| 195 | } |
| 196 | } |
| 197 | $row = strtr($row, $unpack[$bits]); |
| 198 | } |
| 199 | return strtr(substr($row, 0, $w), $lut); |
| 200 | } |
| 201 | if ($bits === 24) return preg_replace('/(.)(.)(.)/s', '$3$2$1'."\xff", substr($row, 0, $w * 3)); |
| 202 | $masks = $h['masks']; |
| 203 | if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0]) return preg_replace('/(.)(.)(.)./s', '$3$2$1'."\xff", substr($row, 0, $w * 4)); |
| 204 | if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0xff000000]) return preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', substr($row, 0, $w * 4)); |
| 205 | |
| 206 | // ビットフィールド(16・32ビット) |
| 207 | static $cache = []; |
| 208 | $key = implode(',', $masks); |
| 209 | if (!isset($cache[$key])) { |
| 210 | $cache[$key] = array_map(function($m) { |
| 211 | if ($m === 0) return null; |
| 212 | $shift = 0; |
| 213 | while ((($m >> $shift) & 1) === 0) $shift++; |
| 214 | $max = $m >> $shift; |
| 215 | return [$shift, $max]; |
| 216 | }, $masks); |
| 217 | } |
| 218 | $f = $cache[$key]; |
| 219 | $vals = array_values(unpack($bits === 16 ? 'v*' : 'V*', substr($row, 0, $w * ($bits >> 3)))); |
| 220 | $out = ''; |
| 221 | foreach ($vals as $v) { |
| 222 | for ($c = 0; $c < 4; $c++) { |
| 223 | if ($f[$c] === null) { |
| 224 | $out .= $c === 3 ? "\xff" : "\0"; |
| 225 | continue; |
| 226 | } |
| 227 | [$shift, $max] = $f[$c]; |
| 228 | $out .= chr(intdiv((($v >> $shift) & $max) * 255 + ($max >> 1), $max)); |
| 229 | } |
| 230 | } |
| 231 | return $out; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * RLE8・RLE4・RLE24(OS/2)を展開する。飛ばした画素(デルタ、行・画像の終わり)は透明 |
| 236 | * |
| 237 | * @return array 行(下から上) |
| 238 | */ |
| 239 | private function rle(string $d, int $w, int $h, int $bits, ?array $lut): array { |
| 240 | $rows = array_fill(0, $h, array_fill(0, $w, "\0\0\0\0")); |
| 241 | $len = strlen($d); |
| 242 | $p = 0; |
| 243 | $x = $y = 0; |
| 244 | $px = function(string $idx) use ($lut, $bits): string { |
| 245 | return $bits === 24 ? $idx[2].$idx[1].$idx[0]."\xff" : $lut[$idx]; |
| 246 | }; |
| 247 | while ($p + 1 < $len && $y < $h) { |
| 248 | $n = ord($d[$p]); |
| 249 | $c = $d[$p + 1]; |
| 250 | $p += 2; |
| 251 | if ($n > 0) { |
| 252 | // 繰り返し |
| 253 | if ($bits === 24) { |
| 254 | $c = substr($d, $p - 1, 3); |
| 255 | $p += 1; |
| 256 | $v = $px($c); |
| 257 | for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; |
| 258 | } else if ($bits === 8) { |
| 259 | $v = $px($c); |
| 260 | for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; |
| 261 | } else { |
| 262 | $hi = $px(chr(ord($c) >> 4)); |
| 263 | $lo = $px(chr(ord($c) & 15)); |
| 264 | for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $i % 2 ? $lo : $hi; |
| 265 | } |
| 266 | continue; |
| 267 | } |
| 268 | $cmd = ord($c); |
| 269 | if ($cmd === 0) { |
| 270 | $x = 0; |
| 271 | $y++; |
| 272 | } else if ($cmd === 1) { |
| 273 | break; |
| 274 | } else if ($cmd === 2) { |
| 275 | if ($p + 1 >= $len) break; |
| 276 | $x += ord($d[$p]); |
| 277 | $y += ord($d[$p + 1]); |
| 278 | $p += 2; |
| 279 | } else { |
| 280 | // そのままの画素(2バイト境界に揃える) |
| 281 | $bytes = $bits === 24 ? $cmd * 3 : ($bits === 8 ? $cmd : ($cmd + 1) >> 1); |
| 282 | $chunk = substr($d, $p, $bytes); |
| 283 | $p += $bytes + ($bytes & 1); |
| 284 | for ($i = 0; $i < $cmd && $x < $w; $i++) { |
| 285 | if ($bits === 24) $v = $px(substr($chunk, $i * 3, 3)); |
| 286 | else if ($bits === 8) $v = $px($chunk[$i] ?? "\0"); |
| 287 | else { |
| 288 | $b = ord($chunk[$i >> 1] ?? "\0"); |
| 289 | $v = $px(chr($i % 2 ? $b & 15 : $b >> 4)); |
| 290 | } |
| 291 | $rows[$y][$x++] = $v; |
| 292 | } |
| 293 | } |
| 294 | } |
| 295 | return array_map(fn($r) => implode('', $r), $rows); |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * 画素をBMPにする |
| 300 | * |
| 301 | * 不透明で256色以下なら1・4・8ビットのパレット、不透明なら24ビット、 |
| 302 | * 透明があれば32ビット(BITMAPV4HEADERのアルファのマスク)。下から上の順。 |
| 303 | */ |
| 304 | public function encode(Bitmap $img, array $opt = []): string { |
| 305 | $w = $img->width; |
| 306 | $h = $img->height; |
| 307 | $opaque = $img->isOpaque(); |
| 308 | $colors = []; |
| 309 | if ($opaque) { |
| 310 | for ($y = 0; $y < $h && count($colors) <= 256; $y++) $colors += array_flip(str_split($img->row($y), 4)); |
| 311 | } |
| 312 | |
| 313 | $palette = ''; |
| 314 | if ($opaque && count($colors) <= 256) { |
| 315 | $n = count($colors); |
| 316 | $bits = $n <= 2 ? 1 : ($n <= 16 ? 4 : 8); |
| 317 | $map = []; |
| 318 | foreach (array_keys($colors) as $i => $c) { |
| 319 | $map[$c] = chr($i); |
| 320 | $palette .= $c[2].$c[1].$c[0]."\0"; |
| 321 | } |
| 322 | $dib = pack('VllvvVVllVV', 40, $w, $h, 1, $bits, self::RGB, 0, 2835, 2835, $n, 0); |
| 323 | } else if ($opaque) { |
| 324 | $bits = 24; |
| 325 | $dib = pack('VllvvVVllVV', 40, $w, $h, 1, 24, self::RGB, 0, 2835, 2835, 0, 0); |
| 326 | } else { |
| 327 | $bits = 32; |
| 328 | // V4:ビットフィールド(BGRA)+sRGB |
| 329 | $dib = pack('VllvvVVllVV', 108, $w, $h, 1, 32, self::BITFIELDS, 0, 2835, 2835, 0, 0) |
| 330 | .pack('VVVV', 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000).'BGRs'.str_repeat("\0", 48); |
| 331 | } |
| 332 | |
| 333 | $stride = (($bits * $w + 31) >> 5) * 4; |
| 334 | $body = ''; |
| 335 | for ($y = $h - 1; $y >= 0; $y--) { |
| 336 | $row = $img->row($y); |
| 337 | $line = match ($bits) { |
| 338 | 24 => preg_replace('/(.)(.)(.)./s', '$3$2$1', $row), |
| 339 | 32 => preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $row), |
| 340 | default => strtr($row, $map), |
| 341 | }; |
| 342 | if ($bits < 8) { |
| 343 | $per = 8 / $bits; |
| 344 | $packed = ''; |
| 345 | foreach (str_split($line, $per) as $g) { |
| 346 | $b = 0; |
| 347 | for ($k = 0; $k < $per; $k++) $b = ($b << $bits) | ($k < strlen($g) ? ord($g[$k]) : 0); |
| 348 | $packed .= chr($b); |
| 349 | } |
| 350 | $line = $packed; |
| 351 | } |
| 352 | $body .= str_pad($line, $stride, "\0"); |
| 353 | } |
| 354 | $offset = 14 + strlen($dib) + strlen($palette); |
| 355 | return 'BM'.pack('VvvV', $offset + strlen($body), 0, 0, $offset).$dib.$palette.$body; |
| 356 | } |
| 357 | } |