qzst_encoder.pl 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/perl
  2. # Variation of the QOI encoder, combined with Zstandard compression.
  3. # See also:
  4. # https://qoiformat.org/
  5. # https://github.com/phoboslab/qoi
  6. use 5.020;
  7. use warnings;
  8. use Imager;
  9. use experimental qw(signatures);
  10. use IO::Compress::Zstd qw(zstd $ZstdError);
  11. sub qzst_encoder ($img, $out_fh) {
  12. use constant {
  13. QOI_OP_RGB => 0b1111_1110,
  14. QOI_OP_RGBA => 0b1111_1111,
  15. QOI_OP_DIFF => 0b01_000_000,
  16. QOI_OP_RUN => 0b11_000_000,
  17. QOI_OP_LUMA => 0b10_000_000,
  18. };
  19. my $width = $img->getwidth;
  20. my $height = $img->getheight;
  21. my $channels = $img->getchannels;
  22. my $colorspace = 0;
  23. say "[$width, $height, $channels, $colorspace]";
  24. my @header = unpack('C*', 'qzst');
  25. push @header, unpack('C4', pack('N', $width));
  26. push @header, unpack('C4', pack('N', $height));
  27. push @header, $channels;
  28. push @header, $colorspace;
  29. my $qoi_data = '';
  30. my $run = 0;
  31. my @px = (0, 0, 0, 255);
  32. my @prev_px = @px;
  33. my @colors = (map { [0, 0, 0, 0] } 1 .. 64);
  34. foreach my $y (0 .. $height - 1) {
  35. my @line = unpack('C*', scalar $img->getscanline(y => $y));
  36. my $line_len = scalar(@line);
  37. for (my $i = 0 ; $i < $line_len ; $i += 4) {
  38. @px = splice(@line, 0, 4);
  39. if ( $px[0] == $prev_px[0]
  40. and $px[1] == $prev_px[1]
  41. and $px[2] == $prev_px[2]
  42. and $px[3] == $prev_px[3]) {
  43. if (++$run == 62) {
  44. $qoi_data .= chr(QOI_OP_RUN | ($run - 1));
  45. $run = 0;
  46. }
  47. }
  48. else {
  49. if ($run > 0) {
  50. $qoi_data .= chr(QOI_OP_RUN | ($run - 1));
  51. $run = 0;
  52. }
  53. my $hash = ($px[0] * 3 + $px[1] * 5 + $px[2] * 7 + $px[3] * 11) % 64;
  54. my $index_px = $colors[$hash];
  55. if ( $px[0] == $index_px->[0]
  56. and $px[1] == $index_px->[1]
  57. and $px[2] == $index_px->[2]
  58. and $px[3] == $index_px->[3]) { # OP INDEX
  59. $qoi_data .= chr($hash);
  60. }
  61. else {
  62. $colors[$hash] = [@px];
  63. if ($px[3] == $prev_px[3]) {
  64. my $vr = $px[0] - $prev_px[0];
  65. my $vg = $px[1] - $prev_px[1];
  66. my $vb = $px[2] - $prev_px[2];
  67. my $vg_r = $vr - $vg;
  68. my $vg_b = $vb - $vg;
  69. if ( $vr > -3
  70. and $vr < 2
  71. and $vg > -3
  72. and $vg < 2
  73. and $vb > -3
  74. and $vb < 2) {
  75. $qoi_data .= chr(QOI_OP_DIFF | (($vr + 2) << 4) | (($vg + 2) << 2) | ($vb + 2));
  76. }
  77. elsif ( $vg_r > -9
  78. and $vg_r < 8
  79. and $vg > -33
  80. and $vg < 32
  81. and $vg_b > -9
  82. and $vg_b < 8) {
  83. $qoi_data .= join('', chr(QOI_OP_LUMA | ($vg + 32)), chr((($vg_r + 8) << 4) | ($vg_b + 8)));
  84. }
  85. else {
  86. $qoi_data .= join('', chr(QOI_OP_RGB), chr($px[0]), chr($px[1]), chr($px[2]));
  87. }
  88. }
  89. else {
  90. $qoi_data .= join('', chr(QOI_OP_RGBA), chr($px[0]), chr($px[1]), chr($px[2]), chr($px[3]));
  91. }
  92. }
  93. }
  94. @prev_px = @px;
  95. }
  96. }
  97. if ($run > 0) {
  98. $qoi_data .= chr(0b11_00_00_00 | ($run - 1));
  99. }
  100. my @footer;
  101. push(@footer, (0x00) x 7);
  102. push(@footer, 0x01);
  103. # Header
  104. print $out_fh pack('C*', @header);
  105. # Compressed data
  106. zstd(\$qoi_data, \my $zstd_data) or die "zstd failed: $ZstdError\n";
  107. print $out_fh pack("N", length($zstd_data));
  108. print $out_fh $zstd_data;
  109. # Footer
  110. print $out_fh pack('C*', @footer);
  111. }
  112. @ARGV || do {
  113. say STDERR "usage: $0 [input.png] [output.qzst]";
  114. exit(2);
  115. };
  116. my $in_file = $ARGV[0];
  117. my $out_file = $ARGV[1] // "$in_file.qzst";
  118. my $img = 'Imager'->new(file => $in_file)
  119. or die "Can't read image: $in_file";
  120. open(my $out_fh, '>:raw', $out_file)
  121. or die "Can't open file <<$out_file>> for writing: $!";
  122. qzst_encoder($img, $out_fh);