diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Revision history for louis
+
+## 0.1.0.0 -- 2019-09-22
+
+* First version.
+* You can braillize files, ByteStrings-s, and DynamicImage-s of JuicyPixels.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2019 Alexey Kutepov
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/louis.cabal b/louis.cabal
new file mode 100644
--- /dev/null
+++ b/louis.cabal
@@ -0,0 +1,71 @@
+cabal-version:       2.4
+
+-- Initial package description 'louis.cabal' generated by 'cabal init'.
+-- For further documentation, see http://haskell.org/cabal/users-guide/
+
+-- The name of the package.
+name:                louis
+
+-- The package version.  See the Haskell package versioning policy (PVP)
+-- for standards guiding when and how versions should be incremented.
+-- https://pvp.haskell.org
+-- PVP summary:      +-+------- breaking API changes
+--                   | | +----- non-breaking API additions
+--                   | | | +--- code changes with no API change
+version:             0.1.0.0
+
+-- A short (one-line) description of the package.
+synopsis:            Turning images into text using Braille font
+
+-- A longer description of the package.
+-- description:
+
+-- A URL where users can report bugs.
+-- bug-reports:
+
+-- The license under which the package is released.
+license:             MIT
+
+-- The file containing the license text.
+license-file:        LICENSE
+
+-- The package author(s).
+author:              rexim
+
+-- An email address to which users can send suggestions, bug reports, and
+-- patches.
+maintainer:          reximkut@gmail.com
+
+-- A copyright notice.
+-- copyright:
+
+category:            Graphics
+
+-- Extra files to be distributed with the package, such as examples or a
+-- README.
+extra-source-files:  CHANGELOG.md
+
+
+library
+  -- Modules exported by the library.
+  exposed-modules: Louis
+
+  -- Modules included in this library but not exported.
+  -- other-modules:
+
+  -- LANGUAGE extensions used by modules in this package.
+  -- other-extensions:
+
+  -- Other library packages from which modules are imported.
+  build-depends:       base >= 4.8 && < 6
+                     , JuicyPixels >= 3.3 && < 3.4
+                     , vector >= 0.10 && < 0.13
+                     , text >= 1.2 && < 1.3
+                     , bytestring >=0.9 && <0.11
+
+  -- Directories containing source files.
+  hs-source-dirs:      src
+
+  -- Base language which the package is written in.
+  default-language:    Haskell2010
+
diff --git a/src/Louis.hs b/src/Louis.hs
new file mode 100644
--- /dev/null
+++ b/src/Louis.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE BinaryLiterals #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Module      : Louis
+-- Copyright   : (c) Alexey Kutepov 2019
+-- License     : MIT
+-- Maintainer  : tsodingbiz@gmail.com
+-- Portability : portable
+--
+-- >>> import Louis
+-- >>> import qualified Data.Text as T
+-- >>> putStrLn . T.unpack . T.unlines =<< braillizeFile "image.png"
+-- ⠀⠀⠀⡸⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⠀⢀⣴⣶⣶⣶⣶⣶⣶⣦⣬⣉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⠀⣸⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⡆⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⢿⣿⣿⣿⣿
+-- ⠀⣿⣿⣿⣿⣿⣿⣿⣿⠁⢠⣿⠡⠿⠿⠿⠿⣿⣿⣿⢃⣶⣾⣿⣷⣶⣶⣤⣍⡛
+-- ⡀⢻⣿⣿⣿⣿⣿⣿⣿⣤⣾⠋⠐⠲⠶⣦⣤⣌⡙⠋⢸⣿⠋⠙⣿⣿⣿⣿⣿⣿
+-- ⣿⣦⣭⣉⣉⣙⣛⣋⣉⣉⣅⣾⣿⣿⣷⣾⣿⣿⣿⡇⣿⡏⠀⢰⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢹⣿⣾⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⡙⠛⢿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⡝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣍⣛⠻⠿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣶⣶⣶⣦⡉⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣯⣾⣿⣿⣿⣿⣇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+-- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
+
+module Louis
+  ( braillizeDynamicImage
+  , braillizeByteString
+  , braillizeFile
+  ) where
+
+import Data.Word
+import Data.Char
+import Data.Bits
+import Codec.Picture
+import qualified Data.Vector.Storable as V
+import Data.List
+import qualified Data.Text as T
+import Data.Functor.Compose
+import qualified Data.ByteString as BS
+
+type Chunk = Word8
+
+renderChunk :: Chunk -> Char
+renderChunk x = chr (bgroup * groupSize + boffset + ord '⠀')
+  where
+    bgroup =
+      let b1 = (x .&. 0b00001000) `shiftR` 3
+          b2 = (x .&. 0b10000000) `shiftR` 6
+       in fromIntegral (b1 .|. b2)
+    boffset =
+      let b1 = (x .&. 0b00000111)
+          b2 = (x .&. 0b01110000) `shiftR` 1
+       in fromIntegral (b1 .|. b2)
+    groupSize = 64
+
+chunkifyGreyScale :: Image Pixel8 -> [[Chunk]]
+chunkifyGreyScale img =
+  [ [chunkAt (i * 2, j * 4) | i <- [0 .. chunksWidth - 1]]
+  | j <- [0 .. chunksHeight - 1]
+  ]
+  where
+    width = imageWidth img
+    height = imageHeight img
+    chunksWidth = width `div` 2
+    chunksHeight = height `div` 4
+    squashBits :: [Word8] -> Word8
+    squashBits = foldl' (\acc x -> shiftL acc 1 .|. x) 0
+    threshold =
+      let imgData = imageData img
+       in round $
+          (/ (fromIntegral $ V.length imgData)) $
+          V.foldl' (+) (0.0 :: Float) $ V.map fromIntegral imgData
+    k :: Pixel8 -> Word8
+    k x
+      | x < threshold = 0
+      | otherwise = 1
+    f :: (Int, Int) -> Word8
+    f (x, y)
+      | 0 <= x && x < width && 0 <= y && y < height = k $ pixelAt img x y
+      | otherwise = 0
+    chunkAt :: (Int, Int) -> Chunk
+    chunkAt (x, y) =
+      squashBits $ reverse [f (i + x, j + y) | i <- [0, 1], j <- [0 .. 3]]
+
+greyScaleImage :: DynamicImage -> Image Pixel8
+greyScaleImage = pixelMap greyScalePixel . convertRGBA8
+  -- reference: https://www.mathworks.com/help/matlab/ref/rgb2gray.html
+  where
+    greyScalePixel :: PixelRGBA8 -> Pixel8
+    greyScalePixel (PixelRGBA8 r g b a) = k
+      where
+        k = round ((r' * 0.299 + g' * 0.587 + b' * 0.114) * a')
+        r' = fromIntegral r :: Float
+        g' = fromIntegral g :: Float
+        b' = fromIntegral b :: Float
+        a' = (fromIntegral a :: Float) / 255.0
+
+braillizeGreyScale :: Image Pixel8 -> [T.Text]
+braillizeGreyScale =
+  map T.pack . getCompose . fmap renderChunk . Compose . chunkifyGreyScale
+
+resizeImageWidth :: Pixel a => Int -> Image a -> Image a
+resizeImageWidth width' image
+  | width /= width' =
+    let ratio :: Float
+        ratio = fromIntegral width' / fromIntegral width
+        height' = floor (fromIntegral height * ratio)
+        y_interval :: Float
+        y_interval = fromIntegral height / fromIntegral height'
+        x_interval :: Float
+        x_interval = fromIntegral width / fromIntegral width'
+        resizedData =
+          [ imgData V.! idx
+          | y <- [0 .. (height' - 1)]
+          , x <- [0 .. (width' - 1)]
+          , let idx =
+                  floor (fromIntegral y * y_interval) * width +
+                  floor (fromIntegral x * x_interval)
+          ]
+     in Image width' height' $ V.fromList resizedData
+  | otherwise = image
+  where
+    width = imageWidth image
+    height = imageHeight image
+    imgData = imageData image
+
+braillizeDynamicImage :: DynamicImage -> [T.Text]
+braillizeDynamicImage = braillizeGreyScale . resizeImageWidth 60 . greyScaleImage
+
+braillizeByteString :: BS.ByteString -> Either String [T.Text]
+braillizeByteString bytes = braillizeDynamicImage <$> decodeImage bytes
+
+braillizeFile :: FilePath -> IO [T.Text]
+braillizeFile filePath = do
+  bytes <- BS.readFile filePath
+  either error return $ braillizeByteString bytes
