master
ブランチ master
LittleBeast / src / Std / Lib / Image / Targa.php
11 KB、266行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, 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
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Image;
39
40/**
41 * TGA(Truevision TGA 1.0・2.0)
42 *
43 * 種類:1(パレット)、2(トゥルーカラー)、3(グレースケール)と、そのRLE(9・10・11)
44 * 画素:8・15・16・24・32ビット(グレースケールは8ビット又は16ビット=灰色+アルファ)
45 * パレット:15・16・24・32ビット。原点:左下(既定)・左上・右上・右下
46 * アルファ:属性ビット(記述子の下4ビット)と、TGA 2.0の拡張領域の属性の種類(無し・事前乗算)
47 */
48class Targa implements ImageInterface {
49 private const TYPES = [
50 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE',
51 2 => 'UNCOMPRESSED TRUE COLOR IMAGE',
52 3 => 'UNCOMPRESSED GRAYSCALE IMAGE',
53 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE',
54 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE',
55 11 => 'RUN-LENGTH ENCODED GRAYSCALE IMAGE',
56 ];
57 private const FOOTER = "TRUEVISION-XFILE.\0";
58
59 public function parse(string $file): \stdClass {
60 $d = (string)file_get_contents($file);
61 $h = $this->header($d);
62 return (object)[
63 'width' => $h['width'],
64 'height' => $h['height'],
65 'imageTypeByte' => $h['type'],
66 'imageType' => self::TYPES[$h['type']],
67 'bitDepth' => $h['depth'],
68 'colorMapType' => $h['cmType'],
69 'colorMapLength' => $h['cmLength'],
70 'colorMapBits' => $h['cmBits'],
71 'x' => $h['x'],
72 'y' => $h['y'],
73 'isTopToBottom' => $h['top'],
74 'isRightToLeft' => $h['right'],
75 'alphaBits' => $h['alphaBits'],
76 'hasAlpha' => $h['alpha'],
77 'imageId' => $h['id'],
78 'version' => $h['v2'] ? 2 : 1,
79 ];
80 }
81
82 /**
83 * ヘッダー(とTGA 2.0のフッター・拡張領域)を読む
84 */
85 private function header(string $d): array {
86 if (strlen($d) < 18) throw new \RuntimeException('TGAではありません。');
87 $h = unpack('CidLen/CcmType/Ctype/vcmFirst/vcmLength/CcmBits/vx/vy/vwidth/vheight/Cdepth/Cdesc', $d);
88 if (!isset(self::TYPES[$h['type']])) throw new \RuntimeException("未対応のTGAの種類です({$h['type']})。");
89 $okDepth = match ($h['type'] & 7) {
90 1 => in_array($h['depth'], [8, 16], true) && $h['cmType'] === 1,
91 2 => in_array($h['depth'], [15, 16, 24, 32], true),
92 3 => in_array($h['depth'], [8, 16], true),
93 };
94 if (!$okDepth || ($h['cmType'] === 1 && !in_array($h['cmBits'], [15, 16, 24, 32], true))) {
95 throw new \RuntimeException('TGAのビット深さが不正です。');
96 }
97 Bitmap::check($h['width'], $h['height']);
98
99 $alphaBits = $h['desc'] & 0x0f;
100 // TGA 2.0:拡張領域の「属性の種類」(0 = アルファ無し、1・2 = 未定義、3 = アルファ、4 = 事前乗算したアルファ)
101 $v2 = strlen($d) >= 44 && substr($d, -18) === self::FOOTER;
102 $attr = null;
103 if ($v2) {
104 $ext = unpack('V', substr($d, -26, 4))[1];
105 if ($ext > 0 && $ext + 495 <= strlen($d) - 26 && unpack('v', substr($d, $ext, 2))[1] >= 495) $attr = ord($d[$ext + 494]);
106 }
107 $alpha = $alphaBits > 0 && ($attr === null || $attr >= 3);
108 // パレットの画像は、パレットの項目のアルファを使う
109 if (($h['type'] & 7) === 1) $alpha = $h['cmBits'] === 32 || ($h['cmBits'] === 16 && $alphaBits > 0);
110 if (($h['type'] & 7) === 3 && $h['depth'] === 16) $alpha = true;
111
112 return $h + [
113 'id' => substr($d, 18, $h['idLen']),
114 'top' => ($h['desc'] & 0x20) !== 0,
115 'right' => ($h['desc'] & 0x10) !== 0,
116 'alphaBits' => $alphaBits,
117 'alpha' => $alpha,
118 'premultiplied' => $attr === 4,
119 'v2' => $v2,
120 ];
121 }
122
123 public function decode(string $data): Bitmap {
124 $h = $this->header($data);
125 $w = $h['width'];
126 $ht = $h['height'];
127 $kind = $h['type'] & 7;
128 $pos = 18 + $h['idLen'];
129
130 // パレット
131 $lut = null;
132 if ($h['cmType'] === 1) {
133 $eb = ($h['cmBits'] + 7) >> 3;
134 $cm = substr($data, $pos, $h['cmLength'] * $eb);
135 $pos += $h['cmLength'] * $eb;
136 if ($kind === 1) {
137 $lut = [];
138 $n = $h['depth'] === 16 ? 65536 : 256;
139 $entries = str_split($cm, $eb);
140 foreach ($entries as $i => $e) $lut[$h['cmFirst'] + $i] = $this->pixel($e, $h['cmBits'], $h['alpha']);
141 }
142 }
143
144 $bpp = ($h['depth'] + 7) >> 3;
145 $count = $w * $ht;
146 if ($h['type'] >= 9) {
147 $raw = $this->unrle($data, $pos, $count, $bpp);
148 } else {
149 $raw = substr($data, $pos, $count * $bpp);
150 if (strlen($raw) < $count * $bpp) throw new \RuntimeException('TGAの画素データが足りません。');
151 }
152
153 // RGBAにする
154 if ($kind === 1) {
155 $idx = $h['depth'] === 16 ? array_values(unpack('v*', $raw)) : array_values(unpack('C*', $raw));
156 $black = "\0\0\0".($h['alpha'] ? "\0" : "\xff");
157 $px = implode('', array_map(fn($i) => $lut[$i] ?? $black, $idx));
158 } else if ($kind === 3) {
159 $px = $h['depth'] === 8 ? preg_replace('/(.)/s', '$1$1$1'."\xff", $raw) : preg_replace('/(.)(.)/s', '$1$1$1$2', $raw);
160 } else if ($h['depth'] === 24) {
161 $px = preg_replace('/(.)(.)(.)/s', '$3$2$1'."\xff", $raw);
162 } else if ($h['depth'] === 32) {
163 $px = $h['alpha'] ? preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $raw) : preg_replace('/(.)(.)(.)./s', '$3$2$1'."\xff", $raw);
164 } else {
165 $alpha = $h['alpha'] && $h['depth'] === 16;
166 $px = implode('', array_map(fn($p) => $this->pixel($p, 16, $alpha), str_split($raw, 2)));
167 }
168
169 if ($h['premultiplied']) $px = $this->unpremultiply($px);
170
171 // 向き(既定は左下が原点)
172 $rows = str_split($px, $w * 4);
173 if (!$h['top']) $rows = array_reverse($rows);
174 if ($h['right']) $rows = array_map(fn($r) => implode('', array_reverse(str_split($r, 4))), $rows);
175 return new Bitmap($w, $ht, implode('', $rows));
176 }
177
178 /**
179 * パレットの項目又は15・16ビットの画素をRGBAにする
180 */
181 private function pixel(string $p, int $bits, bool $alpha): string {
182 if ($bits === 24) return $p[2].$p[1].$p[0]."\xff";
183 if ($bits === 32) return $p[2].$p[1].$p[0].($alpha ? $p[3] : "\xff");
184 // A1R5G5B5(リトルエンディアン)
185 $v = ord($p[0]) | (ord($p[1] ?? "\0") << 8);
186 $c = fn($x) => chr(($x << 3) | ($x >> 2));
187 $a = $bits === 16 && $alpha ? ($v & 0x8000 ? "\xff" : "\0") : "\xff";
188 return $c(($v >> 10) & 31).$c(($v >> 5) & 31).$c($v & 31).$a;
189 }
190
191 /**
192 * RLEを展開する(パケットが行を跨いでも良い)
193 */
194 private function unrle(string $d, int $pos, int $count, int $bpp): string {
195 $out = '';
196 $n = 0;
197 $len = strlen($d);
198 while ($n < $count) {
199 if ($pos >= $len) throw new \RuntimeException('TGAのRLEデータが足りません。');
200 $hdr = ord($d[$pos++]);
201 $k = ($hdr & 0x7f) + 1;
202 if ($hdr & 0x80) {
203 $out .= str_repeat(substr($d, $pos, $bpp), $k);
204 $pos += $bpp;
205 } else {
206 $out .= substr($d, $pos, $k * $bpp);
207 $pos += $k * $bpp;
208 }
209 $n += $k;
210 }
211 return substr($out, 0, $count * $bpp);
212 }
213
214 private function unpremultiply(string $px): string {
215 $len = strlen($px);
216 for ($i = 0; $i < $len; $i += 4) {
217 $a = ord($px[$i + 3]);
218 if ($a === 0 || $a === 255) continue;
219 for ($c = 0; $c < 3; $c++) $px[$i + $c] = chr(min(255, intdiv(ord($px[$i + $c]) * 255 + ($a >> 1), $a)));
220 }
221 return $px;
222 }
223
224 /**
225 * 画素をTGAにする(RLEのトゥルーカラー、不透明なら24ビット、それ以外は32ビット、左上が原点、TGA 2.0のフッター付き)
226 *
227 * @param array $opt rle:false で圧縮しない
228 */
229 public function encode(Bitmap $img, array $opt = []): string {
230 $alpha = !$img->isOpaque();
231 $bpp = $alpha ? 4 : 3;
232 $rle = $opt['rle'] ?? true;
233 $raw = $alpha ? preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $img->data) : preg_replace('/(.)(.)(.)./s', '$3$2$1', $img->data);
234 $header = pack('CCCvvCvvvvCC', 0, 0, $rle ? 10 : 2, 0, 0, 0, 0, 0, $img->width, $img->height, $bpp * 8, 0x20 | ($alpha ? 8 : 0));
235 $body = $rle ? $this->rle($raw, $img->width, $bpp) : $raw;
236 return $header.$body.pack('VV', 0, 0).self::FOOTER;
237 }
238
239 /**
240 * RLEにする(TGA 2.0の推奨通り、パケットは行を跨がない)
241 */
242 private function rle(string $raw, int $w, int $bpp): string {
243 $out = '';
244 foreach (str_split($raw, $w * $bpp) as $row) {
245 $px = str_split($row, $bpp);
246 $n = count($px);
247 $i = 0;
248 while ($i < $n) {
249 // 同じ画素の繰り返し
250 $j = $i + 1;
251 while ($j < $n && $j - $i < 128 && $px[$j] === $px[$i]) $j++;
252 if ($j - $i >= 2) {
253 $out .= chr(0x80 | ($j - $i - 1)).$px[$i];
254 $i = $j;
255 continue;
256 }
257 // 繰り返さない画素(次の繰り返しの前まで)
258 $j = $i + 1;
259 while ($j < $n && $j - $i < 128 && !($j + 1 < $n && $px[$j] === $px[$j + 1])) $j++;
260 $out .= chr($j - $i - 1).implode('', array_slice($px, $i, $j - $i));
261 $i = $j;
262 }
263 }
264 return $out;
265 }
266}