HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
png.hpp
1// Copyright Take Vos 2020-2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "../file/file.hpp"
8#include "../utility/utility.hpp"
9#include "../image/image.hpp"
10#include "../geometry/geometry.hpp"
11#include "../container/container.hpp"
12#include "../parser/parser.hpp"
13#include "../macros.hpp"
14#include "zlib.hpp"
15#include <span>
16#include <vector>
17#include <cstddef>
18#include <cstdint>
19#include <numeric>
20#include <filesystem>
21#include <memory>
22
23hi_export_module(hikogui.codec.png);
24
25hi_export namespace hi { inline namespace v1 {
26
27hi_export class png {
28public:
29 [[nodiscard]] png(file_view view) : _view(std::move(view))
30 {
31 std::size_t offset = 0;
32
33 auto const bytes = as_bstring_view(_view);
34 read_header(bytes, offset);
35 read_chunks(bytes, offset);
36 }
37
38 [[nodiscard]] png(std::filesystem::path const& path) : png(file_view{path}) {}
39
40 [[nodiscard]] std::size_t width() const noexcept
41 {
42 return _width;
43 }
44
45 [[nodiscard]] std::size_t height() const noexcept
46 {
47 return _height;
48 }
49
50 void decode_image(pixmap_span<sfloat_rgba16> image) const
51 {
52 // There is a filter selection byte in front of every line.
53 auto const image_data_size = _stride * _height;
54
55 auto image_data = decompress_IDATs(image_data_size);
56 hi_check(ssize(image_data) == image_data_size, "Uncompressed image data has incorrect size.");
57
58 unfilter_lines(image_data);
59
60 data_to_image(image_data, image);
61 }
62
63 [[nodiscard]] static pixmap<sfloat_rgba16> load(std::filesystem::path const& path)
64 {
65 auto const png_data = png(file_view{path});
66 auto image = pixmap<sfloat_rgba16>{png_data.width(), png_data.height()};
67 png_data.decode_image(image);
68 return image;
69 }
70
71private:
72 struct PNGHeader {
73 uint8_t signature[8];
74 };
75
76 struct ChunkHeader {
77 big_uint32_buf_t length;
78 uint8_t type[4];
79 };
80
81 struct IHDR {
82 big_uint32_buf_t width;
83 big_uint32_buf_t height;
84 uint8_t bit_depth;
85 uint8_t color_type;
86 uint8_t compression_method;
87 uint8_t filter_method;
88 uint8_t interlace_method;
89 };
90
91 struct gAMA {
92 big_uint32_buf_t gamma;
93 };
94
95 struct cHRM {
96 big_uint32_buf_t white_point_x;
97 big_uint32_buf_t white_point_y;
98 big_uint32_buf_t red_x;
99 big_uint32_buf_t red_y;
100 big_uint32_buf_t green_x;
101 big_uint32_buf_t green_y;
102 big_uint32_buf_t blue_x;
103 big_uint32_buf_t blue_y;
104 };
105
106 struct sRGB {
107 uint8_t rendering_intent;
108 };
109
113 matrix3 _color_to_sRGB = {};
114
117 std::vector<float> _transfer_function;
118
119 int _width = 0;
120 int _height = 0;
121 int _bit_depth = 0;
122 int _color_type = 0;
123 int _compression_method = 0;
124 int _filter_method = 0;
125 int _interlace_method = 0;
126
127 bool _has_alpha;
128 bool _is_palletted;
129 bool _is_color;
130 int _samples_per_pixel = 0;
131 int _bits_per_pixel = 0;
132 int _bytes_per_pixel = 0;
133 int _bytes_per_line = 0;
134 int _stride = 0;
135
139
142 file_view _view;
143
144 static std::string read_string(std::span<std::byte const> bytes)
145 {
146 std::string r;
147
148 for (ssize_t i = 0; i != ssize(bytes); ++i) {
149 auto const c = static_cast<char>(bytes[i]);
150 if (c == 0) {
151 return r;
152 } else {
153 r += c;
154 }
155 }
156 throw parse_error("string is not null terminated.");
157 }
158
159 static uint8_t paeth_predictor(uint8_t _a, uint8_t _b, uint8_t _c) noexcept
160 {
161 auto const a = static_cast<int>(_a);
162 auto const b = static_cast<int>(_b);
163 auto const c = static_cast<int>(_c);
164
165 auto const p = a + b - c;
166 auto const pa = std::abs(p - a);
167 auto const pb = std::abs(p - b);
168 auto const pc = std::abs(p - c);
169
170 if (pa <= pb && pa <= pc) {
171 return narrow_cast<uint8_t>(a);
172 } else if (pb <= pc) {
173 return narrow_cast<uint8_t>(b);
174 } else {
175 return narrow_cast<uint8_t>(c);
176 }
177 }
178
179 static uint16_t get_sample(std::span<std::byte const> bytes, ssize_t& offset, bool two_bytes)
180 {
181 uint16_t value = static_cast<uint8_t>(bytes[offset++]);
182 if (two_bytes) {
183 value <<= 8;
184 value |= static_cast<uint8_t>(bytes[offset++]);
185 }
186 return value;
187 }
188
189 void read_header(std::span<std::byte const> bytes, std::size_t& offset)
190 {
191 auto const png_header = make_placement_ptr<PNGHeader>(bytes, offset);
192
193 auto const valid_signature = png_header->signature[0] == 137 && png_header->signature[1] == 80 &&
194 png_header->signature[2] == 78 && png_header->signature[3] == 71 && png_header->signature[4] == 13 &&
195 png_header->signature[5] == 10 && png_header->signature[6] == 26 && png_header->signature[7] == 10;
196
197 hi_check(valid_signature, "invalid PNG file signature");
198 }
199
200 void read_chunks(std::span<std::byte const> bytes, std::size_t& offset)
201 {
202 auto IHDR_bytes = std::span<std::byte const>{};
203 auto cHRM_bytes = std::span<std::byte const>{};
204 auto gAMA_bytes = std::span<std::byte const>{};
205 auto iCCP_bytes = std::span<std::byte const>{};
206 auto sRGB_bytes = std::span<std::byte const>{};
207 bool has_IEND = false;
208
209 while (!has_IEND) {
210 auto const header = make_placement_ptr<ChunkHeader>(bytes, offset);
211 auto const length = narrow_cast<ssize_t>(*header->length);
212 hi_check(length < 0x8000'0000, "Chunk length must be smaller than 2GB");
213 hi_check(offset + length + ssizeof(uint32_t) <= bytes.size(), "Chuck extents beyond file.");
214
215 switch (fourcc(header->type)) {
216 case fourcc("IDAT"):
217 _idat_chunk_data.push_back(bytes.subspan(offset, length));
218 break;
219
220 case fourcc("IHDR"):
221 IHDR_bytes = bytes.subspan(offset, length);
222 break;
223
224 case fourcc("cHRM"):
225 cHRM_bytes = bytes.subspan(offset, length);
226 break;
227
228 case fourcc("gAMA"):
229 gAMA_bytes = bytes.subspan(offset, length);
230 break;
231
232 case fourcc("iCCP"):
233 iCCP_bytes = bytes.subspan(offset, length);
234 break;
235
236 case fourcc("sRGB"):
237 sRGB_bytes = bytes.subspan(offset, length);
238 break;
239
240 case fourcc("IEND"):
241 has_IEND = true;
242 break;
243
244 default:;
245 }
246
247 // Skip over the data, and extract the crc32.
248 offset += length;
249 [[maybe_unused]] auto const crc = make_placement_ptr<big_uint32_buf_t>(bytes, offset);
250 }
251
252 hi_check(!IHDR_bytes.empty(), "Missing IHDR chunk.");
253 read_IHDR(IHDR_bytes);
254 if (!cHRM_bytes.empty()) {
255 read_cHRM(cHRM_bytes);
256 }
257 if (!gAMA_bytes.empty()) {
258 read_gAMA(gAMA_bytes);
259 }
260
261 // Will override cHRM and gAMA chunks.
262 if (!iCCP_bytes.empty()) {
263 read_iCCP(iCCP_bytes);
264 }
265
266 // Will override cHRM, gAMA and ICCP chunks.
267 if (!sRGB_bytes.empty()) {
268 read_sRGB(sRGB_bytes);
269 }
270 }
271
272 void read_IHDR(std::span<std::byte const> bytes)
273 {
274 auto const ihdr = make_placement_ptr<IHDR>(bytes);
275
276 _width = *ihdr->width;
277 _height = *ihdr->height;
278 _bit_depth = ihdr->bit_depth;
279 _color_type = ihdr->color_type;
280 _compression_method = ihdr->compression_method;
281 _filter_method = ihdr->filter_method;
282 _interlace_method = ihdr->interlace_method;
283
284 hi_check(_width <= 16384, "PNG width too large.");
285 hi_check(_height <= 16384, "PNG height too large.");
286 hi_check(_bit_depth == 8 || _bit_depth == 16, "PNG only bit depth of 8 or 16 is implemented.");
287 hi_check(_compression_method == 0, "Only deflate/inflate compression is allowed.");
288 hi_check(_filter_method == 0, "Only adaptive filtering is allowed.");
289 hi_check(_interlace_method == 0, "Only non interlaced PNG are implemented.");
290
291 _is_palletted = (_color_type & 1) != 0;
292 _is_color = (_color_type & 2) != 0;
293 _has_alpha = (_color_type & 4) != 0;
294 hi_check((_color_type & 0xf8) == 0, "Invalid color type");
295 hi_check(!_is_palletted, "Paletted images are not supported");
296
297 if (_is_palletted) {
298 _samples_per_pixel = 1;
299 } else {
300 _samples_per_pixel = static_cast<int>(_has_alpha);
301 _samples_per_pixel += _is_color ? 3 : 1;
302 }
303
304 _bits_per_pixel = _samples_per_pixel * _bit_depth;
305 _bytes_per_line = (_bits_per_pixel * _width + 7) / 8;
306 _stride = _bytes_per_line + 1;
307 _bytes_per_pixel = std::max(1, _bits_per_pixel / 8);
308
309 generate_sRGB_transfer_function();
310 }
311
312 void read_cHRM(std::span<std::byte const> bytes)
313 {
314 auto const chrm = make_placement_ptr<cHRM>(bytes);
315
316 auto const color_to_XYZ = color_primaries_to_RGBtoXYZ(
317 narrow_cast<float>(*chrm->white_point_x) / 100'000.0f,
318 narrow_cast<float>(*chrm->white_point_y) / 100'000.0f,
319 narrow_cast<float>(*chrm->red_x) / 100'000.0f,
320 narrow_cast<float>(*chrm->red_y) / 100'000.0f,
321 narrow_cast<float>(*chrm->green_x) / 100'000.0f,
322 narrow_cast<float>(*chrm->green_y) / 100'000.0f,
323 narrow_cast<float>(*chrm->blue_x) / 100'000.0f,
324 narrow_cast<float>(*chrm->blue_y) / 100'000.0f);
325
326 _color_to_sRGB = XYZ_to_sRGB * color_to_XYZ;
327 }
328
329 void read_gAMA(std::span<std::byte const> bytes)
330 {
331 auto const gama = make_placement_ptr<gAMA>(bytes);
332 auto const gamma = narrow_cast<float>(*gama->gamma) / 100'000.0f;
333 hi_check(gamma != 0.0f, "Gamma value can not be zero");
334
335 generate_gamma_transfer_function(1.0f / gamma);
336 }
337
338 void read_iCCP(std::span<std::byte const> bytes)
339 {
340 auto profile_name = read_string(bytes);
341
342 if (profile_name == "ITUR_2100_PQ_FULL") {
343 // The official rule here is to ignore everything in the ICC profile and
344 // create the conversion matrix and transfer function from scratch.
345
346 _color_to_sRGB = XYZ_to_sRGB * Rec2100_to_XYZ;
347 generate_Rec2100_transfer_function();
348 return;
349 }
350 }
351
352 void read_sRGB(std::span<std::byte const> bytes)
353 {
354 auto const srgb = make_placement_ptr<sRGB>(bytes);
355 auto const rendering_intent = srgb->rendering_intent;
356 hi_check(rendering_intent <= 3, "Invalid rendering intent");
357
358 _color_to_sRGB = {};
359 generate_sRGB_transfer_function();
360 }
361
362 void generate_sRGB_transfer_function() noexcept
363 {
364 auto const value_range = _bit_depth == 8 ? 256 : 65536;
365 auto const value_range_f = narrow_cast<float>(value_range);
366 for (int i = 0; i != value_range; ++i) {
367 auto u = narrow_cast<float>(i) / value_range_f;
368 _transfer_function.push_back(sRGB_gamma_to_linear(u));
369 }
370 }
371
372 void generate_Rec2100_transfer_function() noexcept
373 {
374 // SDR brightness is 80 cd/m2. Rec2100/PQ brightness is 10,000 cd/m2.
375 constexpr float hdr_multiplier = 10'000.0f / 80.0f;
376
377 auto const value_range = _bit_depth == 8 ? 256 : 65536;
378 auto const value_range_f = narrow_cast<float>(value_range);
379 for (int i = 0; i != value_range; ++i) {
380 auto u = narrow_cast<float>(i) / value_range_f;
381 _transfer_function.push_back(Rec2100_gamma_to_linear(u) * hdr_multiplier);
382 }
383 }
384
385 void generate_gamma_transfer_function(float gamma) noexcept
386 {
387 auto const value_range = _bit_depth == 8 ? 256 : 65536;
388 auto const value_range_f = narrow_cast<float>(value_range);
389 for (int i = 0; i != value_range; ++i) {
390 auto u = narrow_cast<float>(i) / value_range_f;
391 _transfer_function.push_back(powf(u, gamma));
392 }
393 }
394
395 [[nodiscard]] bstring decompress_IDATs(std::size_t image_data_size) const
396 {
397 if (ssize(_idat_chunk_data) == 1) {
398 return zlib_decompress(_idat_chunk_data[0], image_data_size);
399 } else {
400 // Merge all idat chunks together.
401 auto const compressed_data_size =
402 std::accumulate(_idat_chunk_data.cbegin(), _idat_chunk_data.cend(), ssize_t{0}, [](auto const& a, auto const& b) {
403 return a + ssize(b);
404 });
405
406 bstring compressed_data;
407 compressed_data.reserve(compressed_data_size);
408 for (auto const chunk_data : _idat_chunk_data) {
409 std::copy(chunk_data.begin(), chunk_data.end(), std::back_inserter(compressed_data));
410 }
411
412 return zlib_decompress(compressed_data, image_data_size);
413 }
414 }
415
416 void unfilter_lines(bstring& image_data) const
417 {
418 auto const image_bytes = std::span(reinterpret_cast<uint8_t *>(image_data.data()), image_data.size());
419 auto zero_line = std::vector<uint8_t>(_bytes_per_line, uint8_t{0});
420
421 auto prev_line = std::span(zero_line.data(), zero_line.size());
422 for (auto y = 0_uz; y != _height; ++y) {
423 auto const line = image_bytes.subspan(y * _stride, _stride);
424 unfilter_line(line, prev_line);
425 prev_line = line.subspan(1, _bytes_per_line);
426 }
427 }
428
429 void unfilter_line(std::span<uint8_t> line, std::span<uint8_t const> prev_line) const
430 {
431 switch (line[0]) {
432 case 0:
433 return;
434 case 1:
435 return unfilter_line_sub(line.subspan(1, _bytes_per_line), prev_line);
436 case 2:
437 return unfilter_line_up(line.subspan(1, _bytes_per_line), prev_line);
438 case 3:
439 return unfilter_line_average(line.subspan(1, _bytes_per_line), prev_line);
440 case 4:
441 return unfilter_line_paeth(line.subspan(1, _bytes_per_line), prev_line);
442 default:
443 throw parse_error("Unknown line-filter type");
444 }
445 }
446
447 void unfilter_line_sub(std::span<uint8_t> line, std::span<uint8_t const> prev_line) const noexcept
448 {
449 for (int i = 0; i != _bytes_per_line; ++i) {
450 auto const j = i - _bytes_per_pixel;
451
452 uint8_t prev_raw = j >= 0 ? line[j] : 0;
453 line[i] += prev_raw;
454 }
455 }
456
457 void unfilter_line_up(std::span<uint8_t> line, std::span<uint8_t const> prev_line) const noexcept
458 {
459 for (int i = 0; i != _bytes_per_line; ++i) {
460 line[i] += prev_line[i];
461 }
462 }
463
464 void unfilter_line_average(std::span<uint8_t> line, std::span<uint8_t const> prev_line) const noexcept
465 {
466 for (int i = 0; i != _bytes_per_line; ++i) {
467 auto const j = i - _bytes_per_pixel;
468
469 uint8_t prev_raw = j >= 0 ? line[j] : 0;
470 line[i] += (prev_raw + prev_line[i]) / 2;
471 }
472 }
473
474 void unfilter_line_paeth(std::span<uint8_t> line, std::span<uint8_t const> prev_line) const noexcept
475 {
476 for (int i = 0; i != _bytes_per_line; ++i) {
477 auto const j = i - _bytes_per_pixel;
478
479 uint8_t const up = prev_line[i];
480 uint8_t const left = j >= 0 ? line[j] : 0;
481 uint8_t const left_up = j >= 0 ? prev_line[j] : 0;
482 line[i] += paeth_predictor(left, up, left_up);
483 }
484 }
485
486 void data_to_image(bstring bytes, pixmap_span<sfloat_rgba16> image) const noexcept
487 {
488 auto bytes_span = std::span(bytes);
489
490 for (int y = 0; y != _height; ++y) {
491 int inv_y = _height - y - 1;
492
493 auto bytes_line = bytes_span.subspan(inv_y * _stride + 1, _bytes_per_line);
494 auto pixel_line = image[y];
495 data_to_image_line(bytes_line, pixel_line);
496 }
497 }
498
499 void data_to_image_line(std::span<std::byte const> bytes, std::span<sfloat_rgba16> line) const noexcept
500 {
501 auto const alpha_mul = _bit_depth == 16 ? 1.0f / 65535.0f : 1.0f / 255.0f;
502 for (int x = 0; x != _width; ++x) {
503 auto const value = extract_pixel_from_line(bytes, x);
504
505 auto const linear_RGB =
506 f32x4{_transfer_function[value.x()], _transfer_function[value.y()], _transfer_function[value.z()], 1.0f};
507
508 auto const linear_sRGB_color = _color_to_sRGB * linear_RGB;
509 auto const alpha = static_cast<float>(value.w()) * alpha_mul;
510
511 // pre-multiply the alpha for use in texture-maps.
512 line[x] = linear_sRGB_color * f32x4::broadcast(alpha);
513 }
514 }
515
516 u16x4 extract_pixel_from_line(std::span<std::byte const> bytes, int x) const noexcept
517 {
518 hi_axiom(_bit_depth == 8 or _bit_depth == 16);
519 hi_axiom(not _is_palletted);
520
521 uint16_t r = 0;
522 uint16_t g = 0;
523 uint16_t b = 0;
524 uint16_t a = 0;
525
526 ssize_t offset = x * _bytes_per_pixel;
527 if (_is_color) {
528 r = get_sample(bytes, offset, _bit_depth == 16);
529 g = get_sample(bytes, offset, _bit_depth == 16);
530 b = get_sample(bytes, offset, _bit_depth == 16);
531 } else {
532 r = g = b = get_sample(bytes, offset, _bit_depth == 16);
533 }
534 if (_has_alpha) {
535 a = get_sample(bytes, offset, _bit_depth == 16);
536 } else {
537 a = (_bit_depth == 16) ? 65535 : 255;
538 }
539
540 return u16x4{r, g, b, a};
541 }
542};
543
544}} // namespace hi::v1
Defines the file class.
constexpr matrix3 XYZ_to_sRGB
Matrix to convert XYZ to sRGB.
Definition sRGB.hpp:39
constexpr matrix3 color_primaries_to_RGBtoXYZ(float wx, float wy, float rx, float ry, float gx, float gy, float bx, float by) noexcept
Create a color space conversion matrix.
Definition color_space.hpp:36
constexpr matrix3 Rec2100_to_XYZ
Rec.2100 to XYZ color space conversion matrix.
Definition Rec2100.hpp:24
float sRGB_gamma_to_linear(float u) noexcept
sRGB gamma to linear transfer function.
Definition sRGB.hpp:71
float Rec2100_gamma_to_linear(float N) noexcept
Rec.2100 gamma to linear transfer function.
Definition Rec2100.hpp:56
@ left
Align the text to the left side.
The HikoGUI namespace.
Definition array_generic.hpp:20
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Definition png.hpp:27
Map a file into virtual memory.
Definition file_view.hpp:39
A 2D or 3D homogenius matrix for transforming homogenious vectors and points.
Definition matrix3.hpp:36
A non-owning 2D pixel-based image.
Definition pixmap_span.hpp:34
A 2D pixel-based image.
Definition pixmap.hpp:38
Exception thrown during parsing on an error.
Definition exception_intf.hpp:48
T accumulate(T... args)
T back_inserter(T... args)
T cbegin(T... args)
T copy(T... args)
T cend(T... args)
T max(T... args)
T move(T... args)
T push_back(T... args)
T reserve(T... args)