qoi_encoder.pl 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/usr/bin/perl
  2. # Implementation of the QOI encoder.
  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_encoder ($img) {
  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 @bytes = unpack('C*', 'qoif');
  25. push @bytes, unpack('C4', pack('N', $width));
  26. push @bytes, unpack('C4', pack('N', $height));
  27. push @bytes, $channels;
  28. push @bytes, $colorspace;
  29. my $run = 0;
  30. my @px = (0, 0, 0, 255);
  31. my @prev_px = @px;
  32. my @colors = (map { [0, 0, 0, 0] } 1 .. 64);
  33. foreach my $y (0 .. $height - 1) {
  34. my @line = unpack('C*', scalar $img->getscanline(y => $y));
  35. my $line_len = scalar(@line);
  36. for (my $i = 0 ; $i < $line_len ; $i += 4) {
  37. @px = splice(@line, 0, 4);
  38. if ( $px[0] == $prev_px[0]
  39. and $px[1] == $prev_px[1]
  40. and $px[2] == $prev_px[2]
  41. and $px[3] == $prev_px[3]) {
  42. if (++$run == 62) {
  43. push @bytes, QOI_OP_RUN | ($run - 1);
  44. $run = 0;
  45. }
  46. }
  47. else {
  48. if ($run > 0) {
  49. push @bytes, (QOI_OP_RUN | ($run - 1));
  50. $run = 0;
  51. }
  52. my $hash = ($px[0] * 3 + $px[1] * 5 + $px[2] * 7 + $px[3] * 11) % 64;
  53. my $index_px = $colors[$hash];
  54. if ( $px[0] == $index_px->[0]
  55. and $px[1] == $index_px->[1]
  56. and $px[2] == $index_px->[2]
  57. and $px[3] == $index_px->[3]) { # OP INDEX
  58. push @bytes, $hash;
  59. }
  60. else {
  61. $colors[$hash] = [@px];
  62. if ($px[3] == $prev_px[3]) {
  63. my $vr = $px[0] - $prev_px[0];
  64. my $vg = $px[1] - $prev_px[1];
  65. my $vb = $px[2] - $prev_px[2];
  66. my $vg_r = $vr - $vg;
  67. my $vg_b = $vb - $vg;
  68. if ( $vr > -3
  69. and $vr < 2
  70. and $vg > -3
  71. and $vg < 2
  72. and $vb > -3
  73. and $vb < 2) {
  74. push(@bytes, QOI_OP_DIFF | (($vr + 2) << 4) | (($vg + 2) << 2) | ($vb + 2));
  75. }
  76. elsif ( $vg_r > -9
  77. and $vg_r < 8
  78. and $vg > -33
  79. and $vg < 32
  80. and $vg_b > -9
  81. and $vg_b < 8) {
  82. push(@bytes, QOI_OP_LUMA | ($vg + 32));
  83. push(@bytes, (($vg_r + 8) << 4) | ($vg_b + 8));
  84. }
  85. else {
  86. push(@bytes, QOI_OP_RGB, $px[0], $px[1], $px[2]);
  87. }
  88. }
  89. else {
  90. push(@bytes, QOI_OP_RGBA, $px[0], $px[1], $px[2], $px[3]);
  91. }
  92. }
  93. }
  94. @prev_px = @px;
  95. }
  96. }
  97. if ($run > 0) {
  98. push(@bytes, QOI_OP_RUN | ($run - 1));
  99. }
  100. push(@bytes, (0x00) x 7);
  101. push(@bytes, 0x01);
  102. return \@bytes;
  103. }
  104. @ARGV || do {
  105. say STDERR "usage: $0 [input.png] [output.qoi]";
  106. exit(2);
  107. };
  108. my $in_file = $ARGV[0];
  109. my $out_file = $ARGV[1] // "$in_file.qoi";
  110. my $img = 'Imager'->new(file => $in_file)
  111. or die "Can't read image: $in_file";
  112. my $bytes = qoi_encoder($img);
  113. open(my $fh, '>:raw', $out_file)
  114. or die "Can't open file <<$out_file>> for writing: $!";
  115. print $fh pack('C*', @$bytes);
  116. close $fh;