├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Setup.hs ├── default.nix ├── louis.cabal └── src └── Louis.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .ghc.environment.* 2 | dist-newstyle/ 3 | dist/ 4 | *.tar.gz -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for louis 2 | 3 | ## 0.1.0.0 -- 2019-09-22 4 | 5 | * First version. 6 | * You can braillize files, ByteStrings-s, and DynamicImage-s of JuicyPixels. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Alexey Kutepov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tsoding](https://img.shields.io/badge/twitch.tv-tsoding-purple?logo=twitch&style=for-the-badge)](https://www.twitch.tv/tsoding) 2 | 3 | # Louis 4 | 5 | Simple library for braillizing images 6 | 7 | ```console 8 | >>> import Louis 9 | >>> import qualified Data.Text as T 10 | >>> putStrLn . T.unpack . T.unlines =<< braillizeFile "image.png" 11 | ⠀⠀⠀⡸⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 12 | ⠀⢀⣴⣶⣶⣶⣶⣶⣶⣦⣬⣉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 13 | ⠀⣸⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⡆⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⢿⣿⣿⣿⣿ 14 | ⠀⣿⣿⣿⣿⣿⣿⣿⣿⠁⢠⣿⠡⠿⠿⠿⠿⣿⣿⣿⢃⣶⣾⣿⣷⣶⣶⣤⣍⡛ 15 | ⡀⢻⣿⣿⣿⣿⣿⣿⣿⣤⣾⠋⠐⠲⠶⣦⣤⣌⡙⠋⢸⣿⠋⠙⣿⣿⣿⣿⣿⣿ 16 | ⣿⣦⣭⣉⣉⣙⣛⣋⣉⣉⣅⣾⣿⣿⣷⣾⣿⣿⣿⡇⣿⡏⠀⢰⣿⣿⣿⣿⣿⣿ 17 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢹⣿⣾⣿⣿⣿⣿⣿⣿⣿ 18 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⡙⠛⢿⣿⣿⣿⣿⣿ 19 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⡝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣍⣛⠻⠿ 20 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣶⣶⣶⣦⡉⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 21 | ⣿⣿⣿⣿⣿⣿⣿⣿⣯⣾⣿⣿⣿⣿⣇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 22 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 23 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 24 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 25 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 26 | ``` 27 | 28 | ## Support 29 | 30 | You can support my work via 31 | 32 | - Twitch channel: https://www.twitch.tv/subs/tsoding 33 | - Patreon: https://www.patreon.com/tsoding 34 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; { 2 | LouisEnv = stdenv.mkDerivation { 3 | name = "LouisEnv"; 4 | buildInputs = [ ghc 5 | cabal-install 6 | zlib 7 | haskellPackages.ghcid 8 | haskellPackages.hindent 9 | haskellPackages.hlint 10 | ]; 11 | LD_LIBRARY_PATH="${zlib}/lib"; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /louis.cabal: -------------------------------------------------------------------------------- 1 | Cabal-version: >=1.18 2 | 3 | -- Initial package description 'louis.cabal' generated by 'cabal init'. 4 | -- For further documentation, see http://haskell.org/cabal/users-guide/ 5 | 6 | -- The name of the package. 7 | name: louis 8 | 9 | -- The package version. See the Haskell package versioning policy (PVP) 10 | -- for standards guiding when and how versions should be incremented. 11 | -- https://pvp.haskell.org 12 | -- PVP summary: +-+------- breaking API changes 13 | -- | | +----- non-breaking API additions 14 | -- | | | +--- code changes with no API change 15 | version: 0.1.0.2 16 | 17 | -- A short (one-line) description of the package. 18 | synopsis: Turning images into text using Braille font 19 | 20 | -- A longer description of the package. 21 | description: 22 | Turning images into text using Braille font 23 | 24 | ⠀⠀⠀⡸⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 25 | ⠀⢀⣴⣶⣶⣶⣶⣶⣶⣦⣬⣉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 26 | ⠀⣸⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⡆⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⢿⣿⣿⣿⣿ 27 | ⠀⣿⣿⣿⣿⣿⣿⣿⣿⠁⢠⣿⠡⠿⠿⠿⠿⣿⣿⣿⢃⣶⣾⣿⣷⣶⣶⣤⣍⡛ 28 | ⡀⢻⣿⣿⣿⣿⣿⣿⣿⣤⣾⠋⠐⠲⠶⣦⣤⣌⡙⠋⢸⣿⠋⠙⣿⣿⣿⣿⣿⣿ 29 | ⣿⣦⣭⣉⣉⣙⣛⣋⣉⣉⣅⣾⣿⣿⣷⣾⣿⣿⣿⡇⣿⡏⠀⢰⣿⣿⣿⣿⣿⣿ 30 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢹⣿⣾⣿⣿⣿⣿⣿⣿⣿ 31 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⡙⠛⢿⣿⣿⣿⣿⣿ 32 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⡝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣍⣛⠻⠿ 33 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣶⣶⣶⣦⡉⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 34 | ⣿⣿⣿⣿⣿⣿⣿⣿⣯⣾⣿⣿⣿⣿⣇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 35 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 36 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 37 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 38 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 39 | 40 | -- A URL where users can report bugs. 41 | bug-reports: https://github.com/tsoding/louis/issues 42 | 43 | -- The license under which the package is released. 44 | license: MIT 45 | 46 | -- The file containing the license text. 47 | license-file: LICENSE 48 | 49 | -- The package author(s). 50 | author: Alexey Kutepov 51 | 52 | -- An email address to which users can send suggestions, bug reports, and 53 | -- patches. 54 | maintainer: tsodingbiz@gmail.com 55 | 56 | -- A copyright notice. 57 | -- copyright: 58 | 59 | category: Graphics 60 | 61 | -- Extra files to be distributed with the package, such as examples or a 62 | -- README. 63 | extra-source-files: CHANGELOG.md 64 | 65 | build-type: Simple 66 | 67 | library 68 | -- Modules exported by the library. 69 | exposed-modules: Louis 70 | 71 | -- Modules included in this library but not exported. 72 | -- other-modules: 73 | 74 | -- LANGUAGE extensions used by modules in this package. 75 | -- other-extensions: 76 | 77 | -- Other library packages from which modules are imported. 78 | build-depends: base >= 4.8 && < 6 79 | , JuicyPixels >= 3.3 && < 3.4 80 | , vector >= 0.10 && < 0.13 81 | , text >= 1.2 && < 1.3 82 | , bytestring >=0.9 && <0.11 83 | 84 | -- Directories containing source files. 85 | hs-source-dirs: src 86 | 87 | -- Base language which the package is written in. 88 | default-language: Haskell2010 89 | 90 | -------------------------------------------------------------------------------- /src/Louis.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BinaryLiterals #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | -- | 6 | -- Module : Louis 7 | -- Copyright : (c) Alexey Kutepov 2019 8 | -- License : MIT 9 | -- Maintainer : tsodingbiz@gmail.com 10 | -- Portability : portable 11 | -- 12 | -- >>> import Louis 13 | -- >>> import qualified Data.Text as T 14 | -- >>> putStrLn . T.unpack . T.unlines =<< braillizeFile "image.png" 15 | -- ⠀⠀⠀⡸⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 16 | -- ⠀⢀⣴⣶⣶⣶⣶⣶⣶⣦⣬⣉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 17 | -- ⠀⣸⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⡆⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⢿⣿⣿⣿⣿ 18 | -- ⠀⣿⣿⣿⣿⣿⣿⣿⣿⠁⢠⣿⠡⠿⠿⠿⠿⣿⣿⣿⢃⣶⣾⣿⣷⣶⣶⣤⣍⡛ 19 | -- ⡀⢻⣿⣿⣿⣿⣿⣿⣿⣤⣾⠋⠐⠲⠶⣦⣤⣌⡙⠋⢸⣿⠋⠙⣿⣿⣿⣿⣿⣿ 20 | -- ⣿⣦⣭⣉⣉⣙⣛⣋⣉⣉⣅⣾⣿⣿⣷⣾⣿⣿⣿⡇⣿⡏⠀⢰⣿⣿⣿⣿⣿⣿ 21 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢹⣿⣾⣿⣿⣿⣿⣿⣿⣿ 22 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⡙⠛⢿⣿⣿⣿⣿⣿ 23 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⡝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣍⣛⠻⠿ 24 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣶⣶⣶⣦⡉⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 25 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣯⣾⣿⣿⣿⣿⣇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 26 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 27 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 28 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 29 | -- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 30 | 31 | module Louis 32 | ( braillizeDynamicImage 33 | , braillizeByteString 34 | , braillizeFile 35 | ) where 36 | 37 | import Data.Word 38 | import Data.Char 39 | import Data.Bits 40 | import Codec.Picture 41 | import qualified Data.Vector.Storable as V 42 | import Data.List 43 | import qualified Data.Text as T 44 | import Data.Functor.Compose 45 | import qualified Data.ByteString as BS 46 | 47 | type Chunk = Word8 48 | 49 | renderChunk :: Chunk -> Char 50 | renderChunk x = chr (bgroup * groupSize + boffset + ord '⠀') 51 | where 52 | bgroup = 53 | let b1 = (x .&. 0b00001000) `shiftR` 3 54 | b2 = (x .&. 0b10000000) `shiftR` 6 55 | in fromIntegral (b1 .|. b2) 56 | boffset = 57 | let b1 = (x .&. 0b00000111) 58 | b2 = (x .&. 0b01110000) `shiftR` 1 59 | in fromIntegral (b1 .|. b2) 60 | groupSize = 64 61 | 62 | chunkifyGreyScale :: Image Pixel8 -> [[Chunk]] 63 | chunkifyGreyScale img = 64 | [ [chunkAt (i * 2, j * 4) | i <- [0 .. chunksWidth - 1]] 65 | | j <- [0 .. chunksHeight - 1] 66 | ] 67 | where 68 | width = imageWidth img 69 | height = imageHeight img 70 | chunksWidth = width `div` 2 71 | chunksHeight = height `div` 4 72 | squashBits :: [Word8] -> Word8 73 | squashBits = foldl' (\acc x -> shiftL acc 1 .|. x) 0 74 | threshold = 75 | let imgData = imageData img 76 | in round $ 77 | (/ (fromIntegral $ V.length imgData)) $ 78 | V.foldl' (+) (0.0 :: Float) $ V.map fromIntegral imgData 79 | k :: Pixel8 -> Word8 80 | k x 81 | | x < threshold = 0 82 | | otherwise = 1 83 | f :: (Int, Int) -> Word8 84 | f (x, y) 85 | | 0 <= x && x < width && 0 <= y && y < height = k $ pixelAt img x y 86 | | otherwise = 0 87 | chunkAt :: (Int, Int) -> Chunk 88 | chunkAt (x, y) = 89 | squashBits $ reverse [f (i + x, j + y) | i <- [0, 1], j <- [0 .. 3]] 90 | 91 | greyScaleImage :: DynamicImage -> Image Pixel8 92 | greyScaleImage = pixelMap greyScalePixel . convertRGBA8 93 | -- reference: https://www.mathworks.com/help/matlab/ref/rgb2gray.html 94 | where 95 | greyScalePixel :: PixelRGBA8 -> Pixel8 96 | greyScalePixel (PixelRGBA8 r g b a) = k 97 | where 98 | k = round ((r' * 0.299 + g' * 0.587 + b' * 0.114) * a') 99 | r' = fromIntegral r :: Float 100 | g' = fromIntegral g :: Float 101 | b' = fromIntegral b :: Float 102 | a' = (fromIntegral a :: Float) / 255.0 103 | 104 | braillizeGreyScale :: Image Pixel8 -> [T.Text] 105 | braillizeGreyScale = 106 | map T.pack . getCompose . fmap renderChunk . Compose . chunkifyGreyScale 107 | 108 | resizeImageWidth :: Pixel a => Int -> Image a -> Image a 109 | resizeImageWidth width' image 110 | | width /= width' = 111 | let ratio :: Float 112 | ratio = fromIntegral width' / fromIntegral width 113 | height' = floor (fromIntegral height * ratio) 114 | y_interval :: Float 115 | y_interval = fromIntegral height / fromIntegral height' 116 | x_interval :: Float 117 | x_interval = fromIntegral width / fromIntegral width' 118 | resizedData = 119 | [ imgData V.! idx 120 | | y <- [0 .. (height' - 1)] 121 | , x <- [0 .. (width' - 1)] 122 | , let idx = 123 | floor (fromIntegral y * y_interval) * width + 124 | floor (fromIntegral x * x_interval) 125 | ] 126 | in Image width' height' $ V.fromList resizedData 127 | | otherwise = image 128 | where 129 | width = imageWidth image 130 | height = imageHeight image 131 | imgData = imageData image 132 | 133 | braillizeDynamicImage :: DynamicImage -> [T.Text] 134 | braillizeDynamicImage = braillizeGreyScale . resizeImageWidth 60 . greyScaleImage 135 | 136 | braillizeByteString :: BS.ByteString -> Either String [T.Text] 137 | braillizeByteString bytes = braillizeDynamicImage <$> decodeImage bytes 138 | 139 | braillizeFile :: FilePath -> IO [T.Text] 140 | braillizeFile filePath = do 141 | bytes <- BS.readFile filePath 142 | either error return $ braillizeByteString bytes 143 | --------------------------------------------------------------------------------