qhi_decoder.pl 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/perl
  2. # Implementation of the QHI decoder (QOI+Huffman coding), generating a PNG file.
  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. sub huffman_decode ($bits, $hash) {
  11. local $" = '|';
  12. $bits =~ s/(@{[sort { length($a) <=> length($b) } keys %{$hash}]})/$hash->{$1}/gr; # very fast
  13. }
  14. sub qhi_decoder ($bytes) {
  15. my sub invalid() {
  16. die "Not a QHIF image";
  17. }
  18. my $index = 0;
  19. join('', map { $bytes->[$index++] } 1 .. 4) eq 'qhif' or invalid();
  20. my $width = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  21. my $height = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  22. my $channels = ord $bytes->[$index++];
  23. my $colorspace = ord $bytes->[$index++];
  24. ($width > 0 and $height > 0) or invalid();
  25. ($channels > 0 and $channels <= 4) or invalid();
  26. ($colorspace == 0 or $colorspace == 1) or invalid();
  27. ord(pop(@$bytes)) == 0x01 or invalid();
  28. for (1 .. 7) {
  29. ord(pop(@$bytes)) == 0x00 or invalid();
  30. }
  31. say "[$width, $height, $channels, $colorspace]";
  32. my $img = 'Imager'->new(
  33. xsize => $width,
  34. ysize => $height,
  35. channels => $channels,
  36. );
  37. my $run = 0;
  38. my @px = (0, 0, 0, 255);
  39. my @pixels;
  40. my @colors = (map { [0, 0, 0, 0] } 1 .. 64);
  41. my @codes;
  42. my $codes_len = 0;
  43. foreach my $c (0 .. 255) {
  44. my $l = ord($bytes->[$index++]);
  45. if ($l > 0) {
  46. $codes_len += $l;
  47. push @codes, [$c, $l];
  48. }
  49. }
  50. my $codes_bin = '';
  51. while (length($codes_bin) < $codes_len) {
  52. $codes_bin .= unpack('B*', $bytes->[$index++] // last);
  53. }
  54. my %rev_dict;
  55. foreach my $pair (@codes) {
  56. my $code = substr($codes_bin, 0, $pair->[1], '');
  57. $rev_dict{$code} = chr($pair->[0]);
  58. }
  59. my $enc_len = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  60. splice(@$bytes, 0, $index);
  61. if ($enc_len > 0) {
  62. @$bytes = unpack("C*", huffman_decode(unpack("B" . $enc_len, join('', @$bytes)), \%rev_dict));
  63. }
  64. else {
  65. @$bytes = ();
  66. }
  67. $index = 0;
  68. while (1) {
  69. if ($run > 0) {
  70. --$run;
  71. }
  72. else {
  73. my $byte = $bytes->[$index++] // last;
  74. if ($byte == 0b11_11_11_10) { # OP RGB
  75. $px[0] = $bytes->[$index++];
  76. $px[1] = $bytes->[$index++];
  77. $px[2] = $bytes->[$index++];
  78. }
  79. elsif ($byte == 0b11_11_11_11) { # OP RGBA
  80. $px[0] = $bytes->[$index++];
  81. $px[1] = $bytes->[$index++];
  82. $px[2] = $bytes->[$index++];
  83. $px[3] = $bytes->[$index++];
  84. }
  85. elsif (($byte >> 6) == 0b00) { # OP INDEX
  86. @px = @{$colors[$byte]};
  87. }
  88. elsif (($byte >> 6) == 0b01) { # OP DIFF
  89. my $dr = (($byte & 0b00_11_00_00) >> 4) - 2;
  90. my $dg = (($byte & 0b00_00_11_00) >> 2) - 2;
  91. my $db = (($byte & 0b00_00_00_11) >> 0) - 2;
  92. ($px[0] += $dr) %= 256;
  93. ($px[1] += $dg) %= 256;
  94. ($px[2] += $db) %= 256;
  95. }
  96. elsif (($byte >> 6) == 0b10) { # OP LUMA
  97. my $byte2 = $bytes->[$index++];
  98. my $dg = ($byte & 0b00_111_111) - 32;
  99. my $dr_dg = ($byte2 >> 4) - 8;
  100. my $db_dg = ($byte2 & 0b0000_1111) - 8;
  101. my $dr = $dr_dg + $dg;
  102. my $db = $db_dg + $dg;
  103. ($px[0] += $dr) %= 256;
  104. ($px[1] += $dg) %= 256;
  105. ($px[2] += $db) %= 256;
  106. }
  107. elsif (($byte >> 6) == 0b11) { # OP RUN
  108. $run = ($byte & 0b00_111_111);
  109. }
  110. $colors[($px[0] * 3 + $px[1] * 5 + $px[2] * 7 + $px[3] * 11) % 64] = [@px];
  111. }
  112. push @pixels, @px;
  113. }
  114. foreach my $row (0 .. $height - 1) {
  115. my @line = splice(@pixels, 0, 4 * $width);
  116. $img->setscanline(y => $row, pixels => pack("C*", @line));
  117. }
  118. return $img;
  119. }
  120. @ARGV || do {
  121. say STDERR "usage: $0 [input.qhi] [output.png]";
  122. exit(2);
  123. };
  124. my $in_file = $ARGV[0];
  125. my $out_file = $ARGV[1] // "$in_file.png";
  126. my @chars = do {
  127. open(my $fh, '<:raw', $in_file)
  128. or die "Can't open file <<$in_file>> for reading: $!";
  129. local $/;
  130. split(//, scalar <$fh>);
  131. };
  132. my $img = qhi_decoder(\@chars);
  133. $img->write(file => $out_file, type => 'png');