qoi_decoder.pl 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/perl
  2. # Implementation of the QOI decoder (generating a PNG file).
  3. # See also:
  4. # https://qoiformat.org/
  5. # https://github.com/phoboslab/qoi
  6. # https://yewtu.be/watch?v=EFUYNoFRHQI
  7. use 5.020;
  8. use warnings;
  9. use Imager;
  10. use experimental qw(signatures);
  11. sub qoi_decoder ($bytes) {
  12. my sub invalid() {
  13. die "Not a QOIF image";
  14. }
  15. my $index = 0;
  16. pack('C4', map { $bytes->[$index++] } 1 .. 4) eq 'qoif' or invalid();
  17. my $width = unpack('N', pack('C4', map { $bytes->[$index++] } 1 .. 4));
  18. my $height = unpack('N', pack('C4', map { $bytes->[$index++] } 1 .. 4));
  19. my $channels = $bytes->[$index++];
  20. my $colorspace = $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. pop(@$bytes) == 0x01 or invalid();
  25. for (1 .. 7) {
  26. pop(@$bytes) == 0x00 or invalid();
  27. }
  28. say "[$width, $height, $channels, $colorspace]";
  29. my $img = 'Imager'->new(
  30. xsize => $width,
  31. ysize => $height,
  32. channels => $channels,
  33. );
  34. my $run = 0;
  35. my @px = (0, 0, 0, 255);
  36. my @pixels;
  37. my @colors = (map { [0, 0, 0, 0] } 1 .. 64);
  38. while (1) {
  39. if ($run > 0) {
  40. --$run;
  41. }
  42. else {
  43. my $byte = $bytes->[$index++] // last;
  44. if ($byte == 0b11_11_11_10) { # OP RGB
  45. $px[0] = $bytes->[$index++];
  46. $px[1] = $bytes->[$index++];
  47. $px[2] = $bytes->[$index++];
  48. }
  49. elsif ($byte == 0b11_11_11_11) { # OP RGBA
  50. $px[0] = $bytes->[$index++];
  51. $px[1] = $bytes->[$index++];
  52. $px[2] = $bytes->[$index++];
  53. $px[3] = $bytes->[$index++];
  54. }
  55. elsif (($byte >> 6) == 0b00) { # OP INDEX
  56. @px = @{$colors[$byte]};
  57. }
  58. elsif (($byte >> 6) == 0b01) { # OP DIFF
  59. my $dr = (($byte & 0b00_11_00_00) >> 4) - 2;
  60. my $dg = (($byte & 0b00_00_11_00) >> 2) - 2;
  61. my $db = (($byte & 0b00_00_00_11) >> 0) - 2;
  62. ($px[0] += $dr) %= 256;
  63. ($px[1] += $dg) %= 256;
  64. ($px[2] += $db) %= 256;
  65. }
  66. elsif (($byte >> 6) == 0b10) { # OP LUMA
  67. my $byte2 = $bytes->[$index++];
  68. my $dg = ($byte & 0b00_111_111) - 32;
  69. my $dr_dg = ($byte2 >> 4) - 8;
  70. my $db_dg = ($byte2 & 0b0000_1111) - 8;
  71. my $dr = $dr_dg + $dg;
  72. my $db = $db_dg + $dg;
  73. ($px[0] += $dr) %= 256;
  74. ($px[1] += $dg) %= 256;
  75. ($px[2] += $db) %= 256;
  76. }
  77. elsif (($byte >> 6) == 0b11) { # OP RUN
  78. $run = ($byte & 0b00_111_111);
  79. }
  80. $colors[($px[0] * 3 + $px[1] * 5 + $px[2] * 7 + $px[3] * 11) % 64] = [@px];
  81. }
  82. push @pixels, @px;
  83. }
  84. foreach my $row (0 .. $height - 1) {
  85. my @line = splice(@pixels, 0, 4 * $width);
  86. $img->setscanline(y => $row, pixels => pack("C*", @line));
  87. }
  88. return $img;
  89. }
  90. @ARGV || do {
  91. say STDERR "usage: $0 [input.qoi] [output.png]";
  92. exit(2);
  93. };
  94. my $in_file = $ARGV[0];
  95. my $out_file = $ARGV[1] // "$in_file.png";
  96. my @bytes = do {
  97. open(my $fh, '<:raw', $in_file)
  98. or die "Can't open file <<$in_file>> for reading: $!";
  99. local $/;
  100. unpack("C*", scalar <$fh>);
  101. };
  102. my $img = qoi_decoder(\@bytes);
  103. $img->write(file => $out_file, type => 'png');