packages feed

tttool-1.8: src/OidCode.hs

{-# LANGUAGE BangPatterns, TupleSections, FlexibleContexts #-}

module OidCode (genRawPixels, oidSVGPattern, writeRawPNG, writeRawSVG, tilePixelSize, DPI(..), PixelSize(..)) where

import Data.Word
import Data.Bits
import Data.Functor
import qualified Data.ByteString.Lazy as B
import Data.Monoid
import Control.Monad
import Codec.Picture
import Codec.Picture.Types
import Codec.Picture.Metadata
import Control.Monad.ST
import Control.Applicative
import Data.Vector.Storable.ByteString
import qualified Data.ByteString.Base64.Lazy
import qualified Data.Text as T
import qualified Data.Text.Encoding as T

import Utils
import Types

import qualified Text.Blaze.Svg as S
import qualified Text.Blaze.Svg11 as S
import qualified Text.Blaze.Svg11.Attributes as A
import Text.Blaze.Svg11 ((!))
import Text.Blaze.Svg.Renderer.Utf8
import Control.Arrow


-- Image generation

checksum :: Word16 -> Word16
checksum dec = c3
  where
    c1  =       (((dec >> 2) ^ (dec >> 8) ^ (dec >> 12) ^ (dec >> 14)) & 0x01) << 1
    c2 = c1 .|. (((dec) ^ (dec >> 4) ^ (dec >>  6) ^ (dec >> 10)) & 0x01)
    c3 = c2  ^  0x02

    (>>) = shiftR
    (<<) = shiftL
    (^) = xor
    (&) = (.&.)

oidSVG :: Conf -> Bool -> String -> Word16 -> S.Svg
oidSVG conf usePNG title code =
    S.docTypeSvg ! A.version (S.toValue "1.1")
                 ! A.width (S.toValue "1mm")
                 ! A.height (S.toValue "1mm")
                 ! A.viewbox (S.toValue "0 0 48 48") $ do
        S.defs (oidSVGPattern conf usePNG title code)
        S.rect ! A.width (S.toValue "48") ! A.height (S.toValue "48")
               ! A.fill (S.toValue $ "url(#"++title++")")

-- Create an OID pattern with the given id of the given code
-- This assumes 48 dots per mm.
oidSVGPattern :: Conf -> Bool -> String -> Word16 -> S.Svg
oidSVGPattern conf usePNG title code =
  S.pattern ! A.width (S.toValue "48")
            ! A.height (S.toValue "48")
            ! A.id_ (S.toValue title)
            ! A.patternunits (S.toValue "userSpaceOnUse") $
  oidSVGPatternContent conf usePNG title code

oidSVGPatternContent :: Conf -> Bool -> String -> Word16 -> S.Svg
oidSVGPatternContent conf True title code =
    S.image ! A.width (S.toValue "48")
            ! A.height (S.toValue "48")
            ! A.xlinkHref (S.toValue (T.pack "data:image/png;base64," <> pngBase64))
  where
    png = genRawPNG 48 48 (conf { cDPI = 1200 }) title code
    pngBase64 = T.decodeUtf8 $ B.toStrict $
        Data.ByteString.Base64.Lazy.encode png

oidSVGPatternContent _conf False _title code = f (0,0)
  where
    quart 8 = checksum code
    quart n = (code `div` 4^n) `mod` 4

    f :: (Integer, Integer) -> S.Svg
    f = mconcat $ map position $
        zip (flip (,) <$> [3,2,1] <*> [3,2,1])
            [ value (quart n) | n <- [0..8] ] ++
        [ (p, plain) | p <- [(0,0), (1,0), (2,0), (3,0), (0,1), (0,3) ] ] ++
        [ ((0,2), special) ]

    -- pixel = S.rect ! A.width (S.toValue "2") ! A.height (S.toValue "2") ! pos (7,7)
    pixel (x,y) = S.path ! A.d path
      where path = S.mkPath $ do
                S.m (x+5) (y+5)
                S.hr 2
                S.vr 2
                S.hr (-2)
                S.z

    plain = pixel
    value 0 = at (2,2)   plain
    value 1 = at (-2,2)  plain
    value 2 = at (-2,-2) plain
    value 3 = at (2,-2)  plain
    special = at (3,0)   plain

    position ((n,m), p) = at (n*12, m*12) p

    -- Drawing combinators
    at (x, y) f = f . ((+x) *** (+y))

writeRawSVG :: Conf -> Bool -> String -> Word16 -> FilePath -> IO ()
writeRawSVG conf usePNG title code filename =
    B.writeFile filename $
    renderSvg (oidSVG conf usePNG title code)

type DPI = Int
type PixelSize = Int

imageFromBlackPixels :: ColorConvertible PixelYA8 p => Int -> Int -> [(Int, Int)] -> Image p
imageFromBlackPixels width height pixels = runST $ do
    i <- createMutableImage width height background
    forM_ pixels $ \(x,y) -> do
        writePixel i x y black
    freezeImage i
  where
    black =      promotePixel $ PixelYA8 minBound maxBound
    background = promotePixel $ PixelYA8 maxBound minBound

-- | Size of one tile, in pixels
tilePixelSize :: DPI -> PixelSize -> Int
tilePixelSize dpi _ps = width
  where
    spacePerPoint = dpi `div2` 100
    width  = 4*spacePerPoint

-- | Renders a single OID Image, returns its dimensions and the black pixels therein
singeOidImage :: Conf -> Word16 -> ((Int, Int), [(Int, Int)])
singeOidImage conf code = ((width, height), pixels)
  where
    dpi = cDPI conf
    ps = cPixelSize conf

    spacePerPoint = dpi `div2` 100
    width  = 4*spacePerPoint
    height = 4*spacePerPoint

    pixels = mconcat $ map position $
        zip (flip (,) <$> [3,2,1] <*> [3,2,1])
            [ value (quart n) | n <- [0..8] ] ++
        [ (p, centeredDot) | p <- [(0,0), (1,0), (2,0), (3,0), (0,1), (0,3) ] ] ++
        [ ((0,2), special) ]


    quart 8 = checksum code
    quart n = (code `div` 4^n) `mod` 4


    dot = [(x,y) | x <- [1..ps], y <- [1..ps]]

    centeredDot | xshift < 0 || yshift < 0 = error "Dots too large. Try a smaller pixel size"
                | otherwise = at (xshift, yshift) dot
      where xshift = (spacePerPoint - ps) `div` 2 - 1
            yshift = (spacePerPoint - ps) `div` 2 - 1

    -- | how many pixels to shift dots horizontally
    s  = dpi `div2` 600
    -- | how many pixels to shift the special dot horizontally
    ss = dpi `div2` 400

    value 0 = at ( s, s) centeredDot
    value 1 = at (-s, s) centeredDot
    value 2 = at (-s,-s) centeredDot
    value 3 = at ( s,-s) centeredDot
    special = at (ss,0)  centeredDot

    position ((n,m), p) = at (n*spacePerPoint, m*spacePerPoint) p

    -- Drawing combinators
    at (x, y) = map (\(x', y') -> (x + x', y + y'))

-- integer division rounded up
x `div2` y = ((x-1) `div` y) + 1

oidImage :: ColorConvertible PixelYA8 p => Int -> Int -> Conf -> Word16 -> Image p
oidImage w h conf code =
    imageFromBlackPixels w h tiledPixels
  where
    ((cw,ch), pixels) = singeOidImage conf code

    tiledPixels =
        [ (x',y')
        | (x,y) <- pixels
        , x' <- [x,x + cw..w-1]
        , y' <- [y,y + ch..h-1]
        ]
-- Width and height in pixels
genRawPixels :: Int -> Int -> Conf -> Word16 -> B.ByteString
genRawPixels w h conf code =
    -- All very shaky here, but it seems to work
    B.fromStrict $
    vectorToByteString $
    imageData $
    (oidImage w h conf code :: Image PixelRGB8)


writeRawPNG :: Int -> Int -> Conf -> String -> Word16 -> FilePath -> IO ()
writeRawPNG w h conf title code filename =
    B.writeFile filename $
    genRawPNG w h conf title code

genRawPNG :: Int -> Int -> Conf -> String -> Word16 -> B.ByteString
genRawPNG w h conf title code =
    encodePngWithMetadata metadata $
    (oidImage w h conf code :: Image PixelYA8)
  where
    metadata = mconcat
        [ singleton DpiX (fromIntegral (cDPI conf))
        , singleton DpiY (fromIntegral (cDPI conf))
        , singleton Title title
        , singleton Software $ "tttool " ++ tttoolVersion
        ]