QOIWriter.cpp 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /*
  2. * Copyright (c) 2022, Olivier De Cannière <olivier.decanniere96@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "QOIWriter.h"
  7. #include <AK/DeprecatedString.h>
  8. #include <AK/Endian.h>
  9. namespace Gfx {
  10. static constexpr Array<u8, 4> qoi_magic_bytes = { 'q', 'o', 'i', 'f' };
  11. static constexpr Array<u8, 8> qoi_end_marker = { 0, 0, 0, 0, 0, 0, 0, 1 };
  12. enum class Colorspace {
  13. sRGB,
  14. Linear,
  15. };
  16. enum class Channels {
  17. RGB,
  18. RGBA,
  19. };
  20. ErrorOr<ByteBuffer> QOIWriter::encode(Bitmap const& bitmap)
  21. {
  22. QOIWriter writer;
  23. TRY(writer.add_header(bitmap.width(), bitmap.height(), Channels::RGBA, Colorspace::sRGB));
  24. Color previous_pixel = { 0, 0, 0, 255 };
  25. bool creating_run = false;
  26. int run_length = 0;
  27. for (auto y = 0; y < bitmap.height(); y++) {
  28. for (auto x = 0; x < bitmap.width(); x++) {
  29. auto pixel = bitmap.get_pixel(x, y);
  30. // Check for at most 62 consecutive identical pixels.
  31. if (pixel == previous_pixel) {
  32. if (!creating_run) {
  33. creating_run = true;
  34. run_length = 0;
  35. writer.insert_into_running_array(pixel);
  36. }
  37. run_length++;
  38. // If the run reaches a maximum length of 62 or if this is the last pixel then create the chunk.
  39. if (run_length == 62 || (y == bitmap.height() - 1 && x == bitmap.width() - 1)) {
  40. TRY(writer.add_run_chunk(run_length));
  41. creating_run = false;
  42. }
  43. continue;
  44. }
  45. // Run ended with the previous pixel. Create a chunk for it and continue processing this pixel.
  46. if (creating_run) {
  47. TRY(writer.add_run_chunk(run_length));
  48. creating_run = false;
  49. }
  50. // Check if the pixel matches a pixel in the running array.
  51. auto index = pixel_hash_function(pixel);
  52. auto& array_pixel = writer.running_array[index];
  53. if (array_pixel == pixel) {
  54. TRY(writer.add_index_chunk(index));
  55. previous_pixel = pixel;
  56. continue;
  57. }
  58. writer.running_array[index] = pixel;
  59. // Check if pixel can be expressed as a difference of the previous pixel.
  60. if (pixel.alpha() == previous_pixel.alpha()) {
  61. int red_difference = pixel.red() - previous_pixel.red();
  62. int green_difference = pixel.green() - previous_pixel.green();
  63. int blue_difference = pixel.blue() - previous_pixel.blue();
  64. int relative_red_difference = red_difference - green_difference;
  65. int relative_blue_difference = blue_difference - green_difference;
  66. if (red_difference > -3 && red_difference < 2
  67. && green_difference > -3 && green_difference < 2
  68. && blue_difference > -3 && blue_difference < 2) {
  69. TRY(writer.add_diff_chunk(red_difference, green_difference, blue_difference));
  70. previous_pixel = pixel;
  71. continue;
  72. }
  73. if (relative_red_difference > -9 && relative_red_difference < 8
  74. && green_difference > -33 && green_difference < 32
  75. && relative_blue_difference > -9 && relative_blue_difference < 8) {
  76. TRY(writer.add_luma_chunk(relative_red_difference, green_difference, relative_blue_difference));
  77. previous_pixel = pixel;
  78. continue;
  79. }
  80. TRY(writer.add_rgb_chunk(pixel.red(), pixel.green(), pixel.blue()));
  81. previous_pixel = pixel;
  82. continue;
  83. }
  84. previous_pixel = pixel;
  85. // Write full color values.
  86. TRY(writer.add_rgba_chunk(pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()));
  87. }
  88. }
  89. TRY(writer.add_end_marker());
  90. return ByteBuffer::copy(writer.m_data);
  91. }
  92. ErrorOr<void> QOIWriter::add_header(u32 width, u32 height, Channels channels = Channels::RGBA, Colorspace color_space = Colorspace::sRGB)
  93. {
  94. // FIXME: Handle RGB and all linear channels.
  95. if (channels == Channels::RGB || color_space == Colorspace::Linear)
  96. TODO();
  97. TRY(m_data.try_append(qoi_magic_bytes.data(), sizeof(qoi_magic_bytes)));
  98. auto big_endian_width = AK::convert_between_host_and_big_endian(width);
  99. TRY(m_data.try_append(bit_cast<u8*>(&big_endian_width), sizeof(width)));
  100. auto big_endian_height = AK::convert_between_host_and_big_endian(height);
  101. TRY(m_data.try_append(bit_cast<u8*>(&big_endian_height), sizeof(height)));
  102. // Number of channels: 3 = RGB, 4 = RGBA.
  103. TRY(m_data.try_append(4));
  104. // Colorspace: 0 = sRGB, 1 = all linear channels.
  105. TRY(m_data.try_append(color_space == Colorspace::sRGB ? 0 : 1));
  106. return {};
  107. }
  108. ErrorOr<void> QOIWriter::add_rgb_chunk(u8 r, u8 g, u8 b)
  109. {
  110. constexpr static u8 rgb_tag = 0b1111'1110;
  111. TRY(m_data.try_append(rgb_tag));
  112. TRY(m_data.try_append(r));
  113. TRY(m_data.try_append(g));
  114. TRY(m_data.try_append(b));
  115. return {};
  116. }
  117. ErrorOr<void> QOIWriter::add_rgba_chunk(u8 r, u8 g, u8 b, u8 a)
  118. {
  119. constexpr static u8 rgba_tag = 0b1111'1111;
  120. TRY(m_data.try_append(rgba_tag));
  121. TRY(m_data.try_append(r));
  122. TRY(m_data.try_append(g));
  123. TRY(m_data.try_append(b));
  124. TRY(m_data.try_append(a));
  125. return {};
  126. }
  127. ErrorOr<void> QOIWriter::add_index_chunk(unsigned int index)
  128. {
  129. constexpr static u8 index_tag = 0b0000'0000;
  130. u8 chunk = index_tag | index;
  131. TRY(m_data.try_append(chunk));
  132. return {};
  133. }
  134. ErrorOr<void> QOIWriter::add_diff_chunk(i8 red_difference, i8 green_difference, i8 blue_difference)
  135. {
  136. constexpr static u8 diff_tag = 0b0100'0000;
  137. u8 bias = 2;
  138. u8 red = red_difference + bias;
  139. u8 green = green_difference + bias;
  140. u8 blue = blue_difference + bias;
  141. u8 chunk = diff_tag | (red << 4) | (green << 2) | blue;
  142. TRY(m_data.try_append(chunk));
  143. return {};
  144. }
  145. ErrorOr<void> QOIWriter::add_luma_chunk(i8 relative_red_difference, i8 green_difference, i8 relative_blue_difference)
  146. {
  147. constexpr static u8 luma_tag = 0b1000'0000;
  148. u8 green_bias = 32;
  149. u8 red_blue_bias = 8;
  150. u8 chunk1 = luma_tag | (green_difference + green_bias);
  151. u8 chunk2 = ((relative_red_difference + red_blue_bias) << 4) | (relative_blue_difference + red_blue_bias);
  152. TRY(m_data.try_append(chunk1));
  153. TRY(m_data.try_append(chunk2));
  154. return {};
  155. }
  156. ErrorOr<void> QOIWriter::add_run_chunk(unsigned run_length)
  157. {
  158. constexpr static u8 run_tag = 0b1100'0000;
  159. int bias = -1;
  160. u8 chunk = run_tag | (run_length + bias);
  161. TRY(m_data.try_append(chunk));
  162. return {};
  163. }
  164. ErrorOr<void> QOIWriter::add_end_marker()
  165. {
  166. TRY(m_data.try_append(qoi_end_marker.data(), sizeof(qoi_end_marker)));
  167. return {};
  168. }
  169. u32 QOIWriter::pixel_hash_function(Color pixel)
  170. {
  171. return (pixel.red() * 3 + pixel.green() * 5 + pixel.blue() * 7 + pixel.alpha() * 11) % 64;
  172. }
  173. void QOIWriter::insert_into_running_array(Color pixel)
  174. {
  175. auto index = pixel_hash_function(pixel);
  176. running_array[index] = pixel;
  177. }
  178. }