header((string)file_get_contents($file, false, null, 0, 1024)); return (object)[ 'width' => $h['width'], 'height' => $h['height'], 'bitDepth' => $h['bits'], 'headerSize' => $h['dib'], 'compression' => $h['comp'], 'topDown' => $h['topDown'], 'colors' => $h['colors'], 'hasAlpha' => $h['masks'][3] !== 0, ]; } /** * ヘッダーを読む */ private function header(string $d): array { if (strlen($d) < 26 || substr($d, 0, 2) !== 'BM') throw new \RuntimeException('BMPではありません。'); $offset = unpack('V', substr($d, 10, 4))[1]; $dib = unpack('V', substr($d, 14, 4))[1]; if (strlen($d) < 14 + min($dib, 64)) throw new \RuntimeException('BMPのヘッダーが途中で切れています。'); $os2 = false; if ($dib === 12) { // OS/2 1.x:大きさは16ビット、パレットは3バイト $h = unpack('vwidth/vheight/vplanes/vbits', substr($d, 18, 8)); $h += ['comp' => self::RGB, 'colors' => 0]; $os2 = true; } else if ($dib >= 16) { $h = unpack('lwidth/lheight/vplanes/vbits', substr($d, 18, 12)); $more = $dib >= 40 ? unpack('Vcomp/Vsize/lxppm/lyppm/Vcolors', substr($d, 30, 20)) : ['comp' => 0, 'colors' => 0]; $h += $more; // OS/2 2.xは圧縮の番号が違う(3 = ハフマン、4 = RLE24) $os2 = $dib === 16 || $dib === 64; } else { throw new \RuntimeException("未対応のBMPのヘッダーです({$dib}バイト)。"); } $topDown = $h['height'] < 0; $h['height'] = abs($h['height']); Bitmap::check($h['width'], $h['height']); if (!in_array($h['bits'], [1, 2, 4, 8, 16, 24, 32], true) && !in_array($h['comp'], [self::JPEG, self::PNG], true)) { throw new \RuntimeException("未対応のBMPのビット深さです({$h['bits']})。"); } // ビットフィールド:V2以上はヘッダーの中、40バイトのヘッダーは後ろに続く $masks = [0, 0, 0, 0]; $palAt = 14 + $dib; if ($h['comp'] === self::BITFIELDS && !$os2 || $h['comp'] === self::ALPHABITFIELDS) { $n = $h['comp'] === self::ALPHABITFIELDS ? 4 : 3; if ($dib >= 52) { $masks = array_values(unpack('V4', substr($d, 54, 16) ?: str_repeat("\0", 16))); if ($dib < 56) $masks[3] = 0; } else { $masks = array_values(unpack("V{$n}", substr($d, $palAt, $n * 4))); if ($n === 3) $masks[] = 0; $palAt += $n * 4; } } else if ($h['bits'] === 16) { $masks = [0x7c00, 0x03e0, 0x001f, 0]; } else if ($h['bits'] === 32) { $masks = [0xff0000, 0xff00, 0xff, 0]; // V3以上でアルファのマスクがあれば使う if ($dib >= 56) $masks[3] = unpack('V', substr($d, 66, 4))[1]; } $entry = $dib === 12 ? 3 : 4; $colors = $h['bits'] <= 8 ? ($h['colors'] ?: 1 << $h['bits']) : 0; return [ 'offset' => $offset, 'dib' => $dib, 'width' => $h['width'], 'height' => $h['height'], 'bits' => $h['bits'], 'comp' => $h['comp'], 'os2' => $os2, 'topDown' => $topDown, 'masks' => $masks, 'colors' => min($colors, 256), 'palAt' => $palAt, 'entry' => $entry, ]; } public function decode(string $data): Bitmap { $h = $this->header($data); $w = $h['width']; $ht = $h['height']; $comp = $h['comp']; $pixels = substr($data, $h['offset']); // 埋め込んだPNG・JPEG(OS/2の4はRLE24) if ($comp === self::PNG && !$h['os2']) return (new Png())->decode($pixels); if ($comp === self::JPEG && !$h['os2']) return (new Jpeg())->decode($pixels); if ($h['os2'] && $comp === 3) throw new \RuntimeException('OS/2のハフマン圧縮のBMPは未対応です。'); // パレット => RGBA $lut = null; if ($h['bits'] <= 8) { $lut = []; for ($i = 0; $i < 256; $i++) { $e = $i < $h['colors'] ? substr($data, $h['palAt'] + $i * $h['entry'], 3) : ''; $lut[chr($i)] = strlen($e) === 3 ? $e[2].$e[1].$e[0]."\xff" : "\0\0\0\xff"; } } if ($comp === self::RLE8 || $comp === self::RLE4 || ($h['os2'] && $comp === 4)) { $rows = $this->rle($pixels, $w, $ht, $comp === self::RLE4 ? 4 : ($comp === self::RLE8 ? 8 : 24), $lut); } else { $stride = (($h['bits'] * $w + 31) >> 5) * 4; if (strlen($pixels) < $stride * $ht) throw new \RuntimeException('BMPの画素データが足りません。'); $rows = []; $alphaSeen = false; for ($y = 0; $y < $ht; $y++) { $row = substr($pixels, $y * $stride, $stride); $rows[] = $this->row($row, $w, $h, $lut); } // 32ビットでアルファのマスクが無い場合:全部0なら不透明(Windowsはアルファを使わない)、それ以外は使う if ($h['bits'] === 32 && $h['masks'][3] === 0 && $comp !== self::BITFIELDS) { foreach ($rows as $y => $r) { $raw = substr($pixels, $y * $stride, $w * 4); for ($i = 3; $i < $w * 4; $i += 4) if ($raw[$i] !== "\0") { $alphaSeen = true; break 2; } } if ($alphaSeen) { foreach ($rows as $y => $r) { $raw = substr($pixels, $y * $stride, $w * 4); for ($i = 3; $i < $w * 4; $i += 4) $r[$i] = $raw[$i]; $rows[$y] = $r; } } } } if (!$h['topDown']) $rows = array_reverse($rows); return new Bitmap($w, $ht, implode('', $rows)); } /** * 圧縮していない1行をRGBAにする */ private function row(string $row, int $w, array $h, ?array $lut): string { $bits = $h['bits']; if ($lut !== null) { if ($bits < 8) { static $unpack = []; if (!isset($unpack[$bits])) { $n = 8 / $bits; $mask = (1 << $bits) - 1; for ($b = 0; $b < 256; $b++) { $s = ''; for ($k = $n - 1; $k >= 0; $k--) $s .= chr(($b >> ($k * $bits)) & $mask); $unpack[$bits][chr($b)] = $s; } } $row = strtr($row, $unpack[$bits]); } return strtr(substr($row, 0, $w), $lut); } if ($bits === 24) return preg_replace('/(.)(.)(.)/s', '$3$2$1'."\xff", substr($row, 0, $w * 3)); $masks = $h['masks']; if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0]) return preg_replace('/(.)(.)(.)./s', '$3$2$1'."\xff", substr($row, 0, $w * 4)); if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0xff000000]) return preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', substr($row, 0, $w * 4)); // ビットフィールド(16・32ビット) static $cache = []; $key = implode(',', $masks); if (!isset($cache[$key])) { $cache[$key] = array_map(function($m) { if ($m === 0) return null; $shift = 0; while ((($m >> $shift) & 1) === 0) $shift++; $max = $m >> $shift; return [$shift, $max]; }, $masks); } $f = $cache[$key]; $vals = array_values(unpack($bits === 16 ? 'v*' : 'V*', substr($row, 0, $w * ($bits >> 3)))); $out = ''; foreach ($vals as $v) { for ($c = 0; $c < 4; $c++) { if ($f[$c] === null) { $out .= $c === 3 ? "\xff" : "\0"; continue; } [$shift, $max] = $f[$c]; $out .= chr(intdiv((($v >> $shift) & $max) * 255 + ($max >> 1), $max)); } } return $out; } /** * RLE8・RLE4・RLE24(OS/2)を展開する。飛ばした画素(デルタ、行・画像の終わり)は透明 * * @return array 行(下から上) */ private function rle(string $d, int $w, int $h, int $bits, ?array $lut): array { $rows = array_fill(0, $h, array_fill(0, $w, "\0\0\0\0")); $len = strlen($d); $p = 0; $x = $y = 0; $px = function(string $idx) use ($lut, $bits): string { return $bits === 24 ? $idx[2].$idx[1].$idx[0]."\xff" : $lut[$idx]; }; while ($p + 1 < $len && $y < $h) { $n = ord($d[$p]); $c = $d[$p + 1]; $p += 2; if ($n > 0) { // 繰り返し if ($bits === 24) { $c = substr($d, $p - 1, 3); $p += 1; $v = $px($c); for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; } else if ($bits === 8) { $v = $px($c); for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; } else { $hi = $px(chr(ord($c) >> 4)); $lo = $px(chr(ord($c) & 15)); for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $i % 2 ? $lo : $hi; } continue; } $cmd = ord($c); if ($cmd === 0) { $x = 0; $y++; } else if ($cmd === 1) { break; } else if ($cmd === 2) { if ($p + 1 >= $len) break; $x += ord($d[$p]); $y += ord($d[$p + 1]); $p += 2; } else { // そのままの画素(2バイト境界に揃える) $bytes = $bits === 24 ? $cmd * 3 : ($bits === 8 ? $cmd : ($cmd + 1) >> 1); $chunk = substr($d, $p, $bytes); $p += $bytes + ($bytes & 1); for ($i = 0; $i < $cmd && $x < $w; $i++) { if ($bits === 24) $v = $px(substr($chunk, $i * 3, 3)); else if ($bits === 8) $v = $px($chunk[$i] ?? "\0"); else { $b = ord($chunk[$i >> 1] ?? "\0"); $v = $px(chr($i % 2 ? $b & 15 : $b >> 4)); } $rows[$y][$x++] = $v; } } } return array_map(fn($r) => implode('', $r), $rows); } /** * 画素をBMPにする * * 不透明で256色以下なら1・4・8ビットのパレット、不透明なら24ビット、 * 透明があれば32ビット(BITMAPV4HEADERのアルファのマスク)。下から上の順。 */ public function encode(Bitmap $img, array $opt = []): string { $w = $img->width; $h = $img->height; $opaque = $img->isOpaque(); $colors = []; if ($opaque) { for ($y = 0; $y < $h && count($colors) <= 256; $y++) $colors += array_flip(str_split($img->row($y), 4)); } $palette = ''; if ($opaque && count($colors) <= 256) { $n = count($colors); $bits = $n <= 2 ? 1 : ($n <= 16 ? 4 : 8); $map = []; foreach (array_keys($colors) as $i => $c) { $map[$c] = chr($i); $palette .= $c[2].$c[1].$c[0]."\0"; } $dib = pack('VllvvVVllVV', 40, $w, $h, 1, $bits, self::RGB, 0, 2835, 2835, $n, 0); } else if ($opaque) { $bits = 24; $dib = pack('VllvvVVllVV', 40, $w, $h, 1, 24, self::RGB, 0, 2835, 2835, 0, 0); } else { $bits = 32; // V4:ビットフィールド(BGRA)+sRGB $dib = pack('VllvvVVllVV', 108, $w, $h, 1, 32, self::BITFIELDS, 0, 2835, 2835, 0, 0) .pack('VVVV', 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000).'BGRs'.str_repeat("\0", 48); } $stride = (($bits * $w + 31) >> 5) * 4; $body = ''; for ($y = $h - 1; $y >= 0; $y--) { $row = $img->row($y); $line = match ($bits) { 24 => preg_replace('/(.)(.)(.)./s', '$3$2$1', $row), 32 => preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $row), default => strtr($row, $map), }; if ($bits < 8) { $per = 8 / $bits; $packed = ''; foreach (str_split($line, $per) as $g) { $b = 0; for ($k = 0; $k < $per; $k++) $b = ($b << $bits) | ($k < strlen($g) ? ord($g[$k]) : 0); $packed .= chr($b); } $line = $packed; } $body .= str_pad($line, $stride, "\0"); } $offset = 14 + strlen($dib) + strlen($palette); return 'BM'.pack('VvvV', $offset + strlen($body), 0, 0, $offset).$dib.$palette.$body; } }