qzst_decoder.pl 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. #!/usr/bin/perl
  2. # Implementation of the QZST decoder (QOI + Zstandard compression), 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. use IO::Uncompress::UnZstd qw(unzstd $UnZstdError);
  11. sub qzst_decoder ($bytes) {
  12. my sub invalid() {
  13. die "Not a QZST image";
  14. }
  15. my $index = 0;
  16. join('', map { $bytes->[$index++] } 1 .. 4) eq 'qzst' or invalid();
  17. my $width = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  18. my $height = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  19. my $channels = ord $bytes->[$index++];
  20. my $colorspace = ord $bytes->[$index++];
  21. ($width > 0 and $height > 0) or invalid();
  22. ($channels > 0 and $channels <= 4) or invalid();
  23. ($colorspace == 0 or $colorspace == 1) or invalid();
  24. ord(pop(@$bytes)) == 0x01 or invalid();
  25. for (1 .. 7) {
  26. ord(pop(@$bytes)) == 0x00 or invalid();
  27. }
  28. say "[$width, $height, $channels, $colorspace]";
  29. my $len = unpack('N', join('', map { $bytes->[$index++] } 1 .. 4));
  30. scalar(@$bytes) - $index == $len or invalid();
  31. splice(@$bytes, 0, $index);
  32. unzstd(\join('', @$bytes), \my $qoi_data)
  33. or die "unzstd failed: $UnZstdError\n";
  34. $index = 0;
  35. @$bytes = unpack('C*', $qoi_data);
  36. my $img = 'Imager'->new(
  37. xsize => $width,
  38. ysize => $height,
  39. channels => $channels,
  40. );
  41. my $run = 0;
  42. my @px = (0, 0, 0, 255);
  43. my @pixels;
  44. my @colors = (map { [0, 0, 0, 0] } 1 .. 64);
  45. while (1) {
  46. if ($run > 0) {
  47. --$run;
  48. }
  49. else {
  50. my $byte = $bytes->[$index++] // last;
  51. if ($byte == 0b11_11_11_10) { # OP RGB
  52. $px[0] = $bytes->[$index++];
  53. $px[1] = $bytes->[$index++];
  54. $px[2] = $bytes->[$index++];
  55. }
  56. elsif ($byte == 0b11_11_11_11) { # OP RGBA
  57. $px[0] = $bytes->[$index++];
  58. $px[1] = $bytes->[$index++];
  59. $px[2] = $bytes->[$index++];
  60. $px[3] = $bytes->[$index++];
  61. }
  62. elsif (($byte >> 6) == 0b00) { # OP INDEX
  63. @px = @{$colors[$byte]};
  64. }
  65. elsif (($byte >> 6) == 0b01) { # OP DIFF
  66. my $dr = (($byte & 0b00_11_00_00) >> 4) - 2;
  67. my $dg = (($byte & 0b00_00_11_00) >> 2) - 2;
  68. my $db = (($byte & 0b00_00_00_11) >> 0) - 2;
  69. ($px[0] += $dr) %= 256;
  70. ($px[1] += $dg) %= 256;
  71. ($px[2] += $db) %= 256;
  72. }
  73. elsif (($byte >> 6) == 0b10) { # OP LUMA
  74. my $byte2 = $bytes->[$index++];
  75. my $dg = ($byte & 0b00_111_111) - 32;
  76. my $dr_dg = ($byte2 >> 4) - 8;
  77. my $db_dg = ($byte2 & 0b0000_1111) - 8;
  78. my $dr = $dr_dg + $dg;
  79. my $db = $db_dg + $dg;
  80. ($px[0] += $dr) %= 256;
  81. ($px[1] += $dg) %= 256;
  82. ($px[2] += $db) %= 256;
  83. }
  84. elsif (($byte >> 6) == 0b11) { # OP RUN
  85. $run = ($byte & 0b00_111_111);
  86. }
  87. $colors[($px[0] * 3 + $px[1] * 5 + $px[2] * 7 + $px[3] * 11) % 64] = [@px];
  88. }
  89. push @pixels, @px;
  90. }
  91. foreach my $row (0 .. $height - 1) {
  92. my @line = splice(@pixels, 0, 4 * $width);
  93. $img->setscanline(y => $row, pixels => pack("C*", @line));
  94. }
  95. return $img;
  96. }
  97. @ARGV || do {
  98. say STDERR "usage: $0 [input.qzst] [output.png]";
  99. exit(2);
  100. };
  101. my $in_file = $ARGV[0];
  102. my $out_file = $ARGV[1] // "$in_file.png";
  103. my @chars = do {
  104. open(my $fh, '<:raw', $in_file)
  105. or die "Can't open file <<$in_file>> for reading: $!";
  106. local $/;
  107. split(//, scalar <$fh>);
  108. };
  109. my $img = qzst_decoder(\@chars);
  110. $img->write(file => $out_file, type => 'png');