summaryrefslogtreecommitdiffstats
path: root/src/Sixel.hs
blob: 9bda65a0b567db829e7dcbe5de7c6ff8f87004e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Sixel
    ( PaletteColor
    , RGBColor
    , render
    ) where

import Data.Bits (shiftL)
import Data.Bool (bool)
import Data.ByteString (ByteString)
import Data.Word (Word8)
import Pager.Types (Geometry(..))
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as ByteString.Char8
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.List.Split as List (chunksOf)
import qualified Foreign.C.Types
import qualified Numeric.Probability.Trace as List (zipListWith)
import qualified Test.Speculate.Utils as Tuple (uncurry6)


showByteString :: Show a => a -> ByteString
showByteString =
    ByteString.Char8.pack . show


type Bit = Foreign.C.Types.CBool

type Sixel = (Bit, Bit, Bit, Bit, Bit, Bit)

-- TODO rename to RegisteredColor, IndexedColor?
type PaletteColor = Word8

type RGBColor = (Word8, Word8, Word8)


render :: Geometry -> [RGBColor] -> [PaletteColor] -> ByteString
render (Geometry x y width _) rgbColors canvas =
    "\ESCP0;0;q" <> colors <> sixeldata <> "\ESC\\"
  where
    bandsToSkip = floor (fromIntegral y / (6 :: Double))

    paddedCanvas =
      if y < 0 then
        drop (-(fromIntegral y) * fromIntegral width) canvas
      else
        let
          paddingTop = replicate (fromIntegral offsetY * fromIntegral width) 0
          offsetY = y - bandsToSkip * 6
        in
        paddingTop <> canvas

    palette = map fromIntegral [0..length rgbColors - 1]
    colors = mconcat $ map (uncurry setColorMapRegister) (zip palette rgbColors)
    channels = splitChannels palette paddedCanvas
    bitmaps = map (toScanlines width) channels
    images = map (map (runLengthEncode . toByteString) . toSixels) bitmaps
    sixeldata = skipBands (fromIntegral bandsToSkip) <> overstrikeBands (map (map (shiftX x)) images)


skipBands :: Int -> ByteString
skipBands n =
    ByteString.replicate n newline
  where
    newline = fromIntegral (Char.ord '-')


shiftX :: Int -> ByteString -> ByteString
shiftX x s =
    "!" <> showByteString x <> "?" <> s


setColorMapRegister :: PaletteColor -> RGBColor -> ByteString
setColorMapRegister i _rgbColor256@(r256,g256,b256) =
    "#" <> mconcat (List.intersperse ";" (map showByteString [i, 2, r100, g100, b100]))
  where
    (r100,g100,b100) = (to100 r256, to100 g256, to100 b256)
    to100 = round . (*(100/256 :: Double)) . fromIntegral


useColorMapRegister :: PaletteColor -> ByteString -> ByteString
useColorMapRegister color s =
    "#" <> showByteString color <> s


-- TODO what's the correct name?
-- TODO reword: channels :: [BitArray Int]
-- TODO reword:  XXX channels must be sorted by color index (to compute sixeldata)
splitChannels :: [PaletteColor] -> [PaletteColor] -> [[Bit]]
splitChannels channels canvas =
    map (flip getChannel canvas) channels
  where
    getChannel :: PaletteColor -> [PaletteColor] -> [Bit]
    getChannel color = map (bool 0 1 . (==color))


toScanlines :: Int -> [Bit] -> [[Bit]]
toScanlines width =
    List.chunksOf (fromIntegral width)


-- TODO maybe use BitArray
-- Turn scanlines into sixelbands.
-- Empty lines will be added as necessary to construct the final band.
toSixels :: [[Bit]] -> [[Sixel]]
toSixels =
    map (Tuple.uncurry6 List.zip6) . rec
  where
    rec :: [[Bit]] -> [([Bit],[Bit],[Bit],[Bit],[Bit],[Bit])]
    rec (a:b:c:d:e:f:rest) = (a,b,c,d,e,f) : rec rest
    rec (a:b:c:d:e:[])     = (a,b,c,d,e,z) : []
    rec (a:b:c:d:[])       = (a,b,c,d,z,z) : []
    rec (a:b:c:[])         = (a,b,c,z,z,z) : []
    rec (a:b:[])           = (a,b,z,z,z,z) : []
    rec (a:[])             = (a,z,z,z,z,z) : []
    rec ([])               = []
    z = repeat 0


toByteString :: [Sixel] -> ByteString
toByteString =
    ByteString.pack . map ((+63) . toWord8)


toWord8 :: Sixel -> Word8
toWord8 (a, b, c, d, e, f) =
  shiftL (fromIntegral a) 0 +
  shiftL (fromIntegral b) 1 +
  shiftL (fromIntegral c) 2 +
  shiftL (fromIntegral d) 3 +
  shiftL (fromIntegral e) 4 +
  shiftL (fromIntegral f) 5


runLengthEncode :: ByteString -> ByteString
runLengthEncode input =
    case ByteString.uncons input of
      Just (c, input') ->
        let
          (c_last, n_last, out) = ByteString.foldl f (c, 1, "") input'
        in
        encode c_last n_last out

      Nothing ->
        input
  where
    f :: (Word8, Int, ByteString) -> Word8 -> (Word8, Int, ByteString)
    f (c, n, out) c_in =
      if c_in == c then
        (c, n + 1, out)
      else
        (c_in, 1, encode c n out)

    encode :: Word8 -> Int -> ByteString -> ByteString
    encode c n output =
        if n > 3 then
          output <> "!" <> showByteString n <> ByteString.singleton c
        else
          output <> ByteString.replicate n c


overstrikeBands :: [[ByteString]] -> ByteString
overstrikeBands =
    mconcat . List.intersperse "-" . List.zipListWith overstrikeBand


-- #gitginore TODO use Vector.imap instead of map zip [0..]?
overstrikeBand :: [ByteString] -> ByteString
overstrikeBand =
    mconcat . List.intersperse "$" . map (uncurry useColorMapRegister) . zip [0..]