qoi_encoder.sf 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/ruby
  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. require("Imager")
  8. func qoi_encoder (img) {
  9. define(
  10. QOI_OP_RGB = 0b1111_1110,
  11. QOI_OP_RGBA = 0b1111_1111,
  12. QOI_OP_DIFF = 0b01_000_000,
  13. QOI_OP_RUN = 0b11_000_000,
  14. QOI_OP_LUMA = 0b10_000_000,
  15. )
  16. var width = img.getwidth
  17. var height = img.getheight
  18. var channels = img.getchannels
  19. var colorspace = 0
  20. say [width, height, channels, colorspace]
  21. var *bytes = unpack('C*', 'qoif')
  22. bytes << unpack('C4', pack('N', width))
  23. bytes << unpack('C4', pack('N', height))
  24. bytes << channels
  25. bytes << colorspace
  26. var run = 0
  27. var (R_, G_, B_, A_) = (0, 0, 0, 255)
  28. var colors = 64.of { [0, 0, 0, 0] }
  29. for y in (0 ..^ height) {
  30. var line = img.getscanline(y => y).bytes
  31. line.each_slice(4, {|R,G,B,A|
  32. if ((R == R_) &&
  33. (G == G_) &&
  34. (B == B_) &&
  35. (A == A_)
  36. ) {
  37. if (++run == 62) {
  38. bytes << (QOI_OP_RUN | (run - 1))
  39. run = 0
  40. }
  41. }
  42. else {
  43. if (run > 0) {
  44. bytes << (QOI_OP_RUN | (run - 1))
  45. run = 0
  46. }
  47. var hash = sum(3*R, 5*G, 7*B, 11*A)%64
  48. var index_px = colors[hash]
  49. if ((R == index_px[0]) &&
  50. (G == index_px[1]) &&
  51. (B == index_px[2]) &&
  52. (A == index_px[3])
  53. ) {
  54. bytes << hash
  55. }
  56. else {
  57. colors[hash] = [R, G, B, A]
  58. if (A == A_) {
  59. var vr = (R - R_)
  60. var vg = (G - G_)
  61. var vb = (B - B_)
  62. var vg_r = (vr - vg)
  63. var vg_b = (vb - vg)
  64. if (vr.is_between(-2, 1) && vg.is_between(-2, 1) && vb.is_between(-2, 1)) {
  65. bytes << (QOI_OP_DIFF | ((vr + 2) << 4) | ((vg + 2) << 2) | (vb + 2))
  66. }
  67. elsif (vg_r.is_between(-8, 7) && vg.is_between(-32, 31) && vg_b.is_between(-8, 7)) {
  68. bytes << (QOI_OP_LUMA | (vg + 32))
  69. bytes << (((vg_r + 8) << 4) | (vg_b + 8))
  70. }
  71. else {
  72. bytes << (QOI_OP_RGB, R, G, B)
  73. }
  74. }
  75. else {
  76. bytes << (QOI_OP_RGBA, R, G, B, A)
  77. }
  78. }
  79. }
  80. (R_, G_, B_, A_) = (R, G, B, A)
  81. })
  82. }
  83. if (run > 0) {
  84. bytes << (QOI_OP_RUN | (run - 1))
  85. }
  86. bytes << 7.of { 0x00 }...
  87. bytes << 0x01
  88. return bytes
  89. }
  90. ARGV || do {
  91. STDERR.say("usage: #{__MAIN__} [input.png] [output.qoi]")
  92. Sys.exit(2)
  93. }
  94. var in_file = File(ARGV[0])
  95. var out_file = File(ARGV[1] \\ "#{in_file}.qoi")
  96. var img = %O'Imager'.new(file => in_file) || die "Can't read image: #{in_file}"
  97. var bytes = qoi_encoder(img)
  98. out_file.write(pack('C*', bytes...), ':raw') || die "Can't open file <<#{out_file}>> for writing: #{$!}"