|
4 | 4 |
|
5 | 5 | module Bot.Asciify |
6 | 6 | ( asciifyReaction |
7 | | - , asciifyFile |
8 | 7 | ) where |
9 | 8 |
|
10 | 9 | import Bot.BttvFfz |
11 | 10 | import Bot.Replies |
12 | | -import Codec.Picture |
13 | 11 | import Control.Applicative |
14 | | -import Data.Bits |
15 | | -import qualified Data.ByteString as BS |
16 | 12 | import qualified Data.ByteString.Lazy as BSL |
17 | | -import Data.Char |
18 | 13 | import Data.Functor |
19 | | -import Data.Functor.Compose |
20 | | -import Data.List |
21 | 14 | import qualified Data.Map as M |
22 | 15 | import Data.Maybe |
23 | 16 | import Data.Proxy |
24 | 17 | import qualified Data.Text as T |
25 | 18 | import Data.Time |
26 | 19 | import Data.Time.Extra |
27 | | -import qualified Data.Vector.Storable as V |
28 | | -import Data.Word |
29 | 20 | import Effect |
30 | 21 | import Entity |
| 22 | +import Louis |
31 | 23 | import Property |
32 | 24 | import Reaction |
33 | 25 | import Text.InterpolatedString.QM |
34 | 26 | import Transport |
35 | 27 |
|
36 | | -type Chunk = Word8 |
37 | | - |
38 | | -renderChunk :: Chunk -> Char |
39 | | -renderChunk x = chr (bgroup * groupSize + boffset + ord '⠀') |
40 | | - where |
41 | | - bgroup = |
42 | | - let b1 = (x .&. 0b00001000) `shiftR` 3 |
43 | | - b2 = (x .&. 0b10000000) `shiftR` 6 |
44 | | - in fromIntegral (b1 .|. b2) |
45 | | - boffset = |
46 | | - let b1 = (x .&. 0b00000111) |
47 | | - b2 = (x .&. 0b01110000) `shiftR` 1 |
48 | | - in fromIntegral (b1 .|. b2) |
49 | | - groupSize = 64 |
50 | | - |
51 | | -chunkifyGreyScale :: Image Pixel8 -> [[Chunk]] |
52 | | -chunkifyGreyScale img = |
53 | | - [ [chunkAt (i * 2, j * 4) | i <- [0 .. chunksWidth - 1]] |
54 | | - | j <- [0 .. chunksHeight - 1] |
55 | | - ] |
56 | | - where |
57 | | - width = imageWidth img |
58 | | - height = imageHeight img |
59 | | - chunksWidth = width `div` 2 |
60 | | - chunksHeight = height `div` 4 |
61 | | - squashBits :: [Word8] -> Word8 |
62 | | - squashBits = foldl' (\acc x -> shiftL acc 1 .|. x) 0 |
63 | | - threshold = |
64 | | - let imgData = imageData img |
65 | | - in round $ |
66 | | - (/ (fromIntegral $ V.length imgData)) $ |
67 | | - V.foldl' (+) (0.0 :: Float) $ V.map fromIntegral imgData |
68 | | - k :: Pixel8 -> Word8 |
69 | | - k x |
70 | | - | x < threshold = 0 |
71 | | - | otherwise = 1 |
72 | | - f :: (Int, Int) -> Word8 |
73 | | - f (x, y) |
74 | | - | 0 <= x && x < width && 0 <= y && y < height = k $ pixelAt img x y |
75 | | - | otherwise = 0 |
76 | | - chunkAt :: (Int, Int) -> Chunk |
77 | | - chunkAt (x, y) = |
78 | | - squashBits $ reverse [f (i + x, j + y) | i <- [0, 1], j <- [0 .. 3]] |
79 | | - |
80 | | -greyScaleImage :: DynamicImage -> Image Pixel8 |
81 | | -greyScaleImage = pixelMap greyScalePixel . convertRGBA8 |
82 | | - -- reference: https://www.mathworks.com/help/matlab/ref/rgb2gray.html |
83 | | - where |
84 | | - greyScalePixel :: PixelRGBA8 -> Pixel8 |
85 | | - greyScalePixel (PixelRGBA8 r g b a) = k |
86 | | - where |
87 | | - k = round ((r' * 0.299 + g' * 0.587 + b' * 0.114) * a') |
88 | | - r' = fromIntegral r :: Float |
89 | | - g' = fromIntegral g :: Float |
90 | | - b' = fromIntegral b :: Float |
91 | | - a' = (fromIntegral a :: Float) / 255.0 |
92 | | - |
93 | | -asciifyGreyScale :: Image Pixel8 -> [T.Text] |
94 | | -asciifyGreyScale = |
95 | | - map T.pack . getCompose . fmap renderChunk . Compose . chunkifyGreyScale |
96 | | - |
97 | | -resizeImageWidth :: Pixel a => Int -> Image a -> Image a |
98 | | -resizeImageWidth width' image |
99 | | - | width /= width' = |
100 | | - let ratio :: Float |
101 | | - ratio = fromIntegral width' / fromIntegral width |
102 | | - height' = floor (fromIntegral height * ratio) |
103 | | - y_interval :: Float |
104 | | - y_interval = fromIntegral height / fromIntegral height' |
105 | | - x_interval :: Float |
106 | | - x_interval = fromIntegral width / fromIntegral width' |
107 | | - resizedData = |
108 | | - [ imgData V.! idx |
109 | | - | y <- [0 .. (height' - 1)] |
110 | | - , x <- [0 .. (width' - 1)] |
111 | | - , let idx = |
112 | | - floor (fromIntegral y * y_interval) * width + |
113 | | - floor (fromIntegral x * x_interval) |
114 | | - ] |
115 | | - in Image width' height' $ V.fromList resizedData |
116 | | - | otherwise = image |
117 | | - where |
118 | | - width = imageWidth image |
119 | | - height = imageHeight image |
120 | | - imgData = imageData image |
121 | | - |
122 | | -asciifyDynamicImage :: DynamicImage -> [T.Text] |
123 | | -asciifyDynamicImage = asciifyGreyScale . resizeImageWidth 60 . greyScaleImage |
124 | | - |
125 | | -asciifyFile :: FilePath -> IO [T.Text] |
126 | | -asciifyFile filePath = do |
127 | | - bytes <- BS.readFile filePath |
128 | | - either error return $ asciifyByteString bytes |
129 | | - |
130 | | -asciifyByteString :: BS.ByteString -> Either String [T.Text] |
131 | | -asciifyByteString bytes = asciifyDynamicImage <$> decodeImage bytes |
132 | | - |
133 | 28 | newtype AsciifyState = AsciifyState |
134 | 29 | { asciifyStateLastUsed :: UTCTime |
135 | 30 | } |
@@ -175,7 +70,7 @@ asciifyReaction = |
175 | 70 | replyOnNothing "Such emote does not exist" $ |
176 | 71 | asciifyCooldown $ |
177 | 72 | byteStringHttpRequestReaction $ |
178 | | - cmapR (asciifyByteString . BSL.toStrict) $ |
| 73 | + cmapR (braillizeByteString . BSL.toStrict) $ |
179 | 74 | eitherReaction (Reaction (logMsg . T.pack . messageContent)) $ |
180 | 75 | dupCmapR |
181 | 76 | (\Message { messageSender = Sender {senderChannel = channel} |
|
0 commit comments