packages feed

JuicyPixels-blp-1.0.0.0: src/Codec/Picture/Blp/Internal/Encoder.hs

{-|
Building and serialising BLP1 files: palette quantisation, the mip chain, the
shared JPEG header and the binary layout.

This is an internal module: its API may change between minor versions.
-}
module Codec.Picture.Blp.Internal.Encoder(
    encodeBlp
  , blpEncoder
  , toBlpStruct
  , toBlp2Struct
  , encodeBlp2
  , blp2Encoder
  , shrinkTo
  , createMipMaps
  , createMipMapsIndexed
  , scanHeader
  ) where

import Codec.Picture
import Codec.Picture.ColorQuant
import Codec.Picture.Jpg
import Codec.Picture.Types
import Data.Binary.Put
import Data.ByteString.Lazy (ByteString, toStrict)
import Data.Foldable (traverse_)
import Data.List (sortOn)
import Data.Word

import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BI
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Storable.Mutable as MVS

import Codec.Picture.Blp.Internal.Convert
import Codec.Picture.Blp.Internal.Data
import Codec.Picture.Blp.Internal.Dxt (DxtFormat(..), encodeDxt)
import Codec.Picture.Blp.Types

-- | Serialise a BLP structure. The content type follows from 'blpExt'; at
-- most 16 mip levels are written, and unused slots are zero.
encodeBlp :: BlpStruct -> ByteString
encodeBlp = runPut . blpEncoder

-- | Raw encoder for BLP
blpEncoder :: BlpStruct -> Put
blpEncoder BlpStruct{..} = do
  putByteString "BLP1"
  putWord32le $ case blpExt of
    BlpJpeg {} -> 0
    BlpPaletted {} -> 1
  putWord32le blpAlphaBits
  putWord32le blpWidth
  putWord32le blpHeight
  putWord32le blpExtra
  putWord32le blpHasMipmaps
  traverse_ putWord32le . slots $ take (length levels) (scanl (+) contentStart levelSizes)
  traverse_ putWord32le $ slots levelSizes
  case blpExt of
    BlpJpeg {..} -> do
      putWord32le . fromIntegral $ BS.length blpJpegHeader
      putByteString blpJpegHeader
    BlpPaletted {..} ->
      traverse_ putRgba8 . V.take 256 $ blpPalette <> V.replicate 256 (PixelRGBA8 0 0 0 0)
  traverse_ putByteString levels
  where
    headerSize = 156
    levels = take 16 $ case blpExt of
      BlpJpeg {..} -> blpJpegData
      BlpPaletted {..} -> blpPalettedMipMaps
    levelSizes = BS.length <$> levels
    contentStart = case blpExt of
      BlpJpeg {..} -> headerSize + 4 + BS.length blpJpegHeader
      BlpPaletted {} -> headerSize + 4 * 256
    slots xs = take 16 $ (fromIntegral <$> xs) ++ repeat 0
    putRgba8 (PixelRGBA8 r g b a) = putWord8 r >> putWord8 g >> putWord8 b >> putWord8 a

-- | Return 'True' if given picture format has alpha channel
hasAlpha :: DynamicImage -> Bool
hasAlpha img = case img of
  ImageYA8 _ -> True
  ImageYA16 _ -> True
  ImageRGBA8 _ -> True
  ImageRGBA16 _ -> True
  _ -> False

-- | Build the BLP structure for an image. The number of mip levels is clamped
-- to between 1 and the image's full chain, at most 16. The image's size is
-- not checked, and 'blpMaxSize' is applied by the caller (see 'shrinkTo').
toBlpStruct :: BlpEncodeOptions -> DynamicImage -> Either BlpEncodeError BlpStruct
toBlpStruct BlpEncodeOptions{..} img = do
  ext <- case blpFormat of
    BlpFormatJpeg quality -> Right $ toBlpJpg (fromIntegral quality) numLevels alpha rgba
    BlpFormatPaletted _ -> Right $ toBlpPaletted blpDither alpha numLevels (convertRGB8 img) rgba
    other -> Left $ BlpUnsupportedFormat Blp1 other
  return BlpStruct {
      blpAlphaBits = if alpha then 8 else 0
    , blpWidth = fromIntegral width
    , blpHeight = fromIntegral height
    -- Readers that follow the old description of the format pick alpha by
    -- this field (3 or 4 with alpha, 5 without), so keep it consistent.
    , blpExtra = if alpha then 3 else 5
    -- WorldEdit writes 5; a single level says it has no mipmaps.
    , blpHasMipmaps = if numLevels > 1 then 5 else 0
    , blpExt = ext
    }
  where
    -- JPEG keeps alpha only if some pixel is not fully opaque.
    alpha = case blpFormat of
      BlpFormatJpeg _ -> hasAlpha img && anyTransparent rgba
      BlpFormatPaletted withAlpha -> withAlpha
      _ -> False
    -- Only built when needed: paletted textures without alpha go from RGB.
    rgba = convertRGBA8 img
    width = dynamicMap imageWidth img
    height = dynamicMap imageHeight img
    numLevels = max 1 $ min (mipChainLength width height) blpMipLevels

-- | Build the BLP2 structure for an image, as 'toBlpStruct' does for BLP1.
-- The header values follow files from World of Warcraft: alpha depth 0 or
-- 1 for DXT1, 8 for DXT3, DXT5 and BGRA, 8 or 0 for a palette; alpha type
-- 0, 1 and 7 for the DXT formats and 8 otherwise.
toBlp2Struct :: BlpEncodeOptions -> DynamicImage -> Either BlpEncodeError Blp2Struct
toBlp2Struct BlpEncodeOptions{..} img = case blpFormat of
  BlpFormatPaletted withAlpha ->
    let (palette, mips) = quantiseLevels blpDither withAlpha numLevels (convertRGB8 img) rgba
    in Right $ struct (if withAlpha then 8 else 0) 8 (Blp2Paletted palette) mips
  BlpFormatDxt1 ->
    let punch = hasAlpha img && anyBelowHalf rgba
    in Right $ struct (if punch then 1 else 0) 0 (Blp2Dxt (Dxt1 punch)) (encodeDxt (Dxt1 punch) <$> levels)
  BlpFormatDxt3 -> Right $ struct 8 1 (Blp2Dxt Dxt3) (encodeDxt Dxt3 <$> levels)
  BlpFormatDxt5 -> Right $ struct 8 7 (Blp2Dxt Dxt5) (encodeDxt Dxt5 <$> levels)
  BlpFormatBgra -> Right $ struct 8 8 Blp2Bgra (bgraBytes <$> levels)
  other -> Left $ BlpUnsupportedFormat Blp2 other
  where
    rgba = convertRGBA8 img
    width = dynamicMap imageWidth img
    height = dynamicMap imageHeight img
    numLevels = max 1 $ min (mipChainLength width height) blpMipLevels
    levels = take numLevels (createMipMaps rgba)
    struct depth alphaType content mips = Blp2Struct {
        blp2AlphaDepth = depth
      , blp2AlphaType = alphaType
      , blp2HasMips = if numLevels > 1 then 1 else 0
      , blp2Width = fromIntegral width
      , blp2Height = fromIntegral height
      , blp2Content = content
      , blp2MipMaps = mips
      }

-- | Whether any pixel's alpha is below 128, so DXT1 needs 1-bit alpha.
anyBelowHalf :: Image PixelRGBA8 -> Bool
anyBelowHalf img = VS.any id $ VS.generate (imageWidth img * imageHeight img) (\i -> VS.unsafeIndex (imageData img) (4 * i + 3) < 128)

-- | Pixels as B, G, R and A.
bgraBytes :: Image PixelRGBA8 -> BS.ByteString
bgraBytes img = BS.pack . concat $ [[b, g, r, a] | PixelRGBA8 r g b a <- pixels]
  where pixels = [pixelAt img x y | y <- [0 .. imageHeight img - 1], x <- [0 .. imageWidth img - 1]]

-- | Serialise a BLP2 structure. At most 16 mip levels are written, unused
-- slots are zero, and the palette is zeros unless the content is paletted.
encodeBlp2 :: Blp2Struct -> ByteString
encodeBlp2 = runPut . blp2Encoder

-- | Raw encoder for BLP2.
blp2Encoder :: Blp2Struct -> Put
blp2Encoder Blp2Struct{..} = do
  putByteString "BLP2"
  putWord32le 1
  putWord8 $ case blp2Content of
    Blp2Paletted _ -> 1
    Blp2Dxt _ -> 2
    Blp2Bgra -> 3
  putWord8 blp2AlphaDepth
  putWord8 blp2AlphaType
  putWord8 blp2HasMips
  putWord32le blp2Width
  putWord32le blp2Height
  traverse_ putWord32le . slots $ take (length levels) (scanl (+) contentStart levelSizes)
  traverse_ putWord32le $ slots levelSizes
  traverse_ putRgba8 . V.take 256 $ palette <> V.replicate 256 (PixelRGBA8 0 0 0 0)
  traverse_ putByteString levels
  where
    contentStart = 148 + 4 * 256
    levels = take 16 blp2MipMaps
    levelSizes = BS.length <$> levels
    palette = case blp2Content of
      Blp2Paletted p -> p
      _ -> V.empty
    slots xs = take 16 $ (fromIntegral <$> xs) ++ repeat 0
    putRgba8 (PixelRGBA8 r g b a) = putWord8 r >> putWord8 g >> putWord8 b >> putWord8 a

-- | Whether any pixel is less than fully opaque.
anyTransparent :: Image PixelRGBA8 -> Bool
anyTransparent img = go 3
  where
    src = imageData img
    go i
      | i >= VS.length src = False
      | VS.unsafeIndex src i /= 255 = True
      | otherwise = go (i + 4)

-- | Halve an image, as its mipmaps are made, until its larger side is at most
-- the given number of pixels. An image without alpha stays without alpha.
shrinkTo :: Int -> DynamicImage -> DynamicImage
shrinkTo limit img
  | max width height <= limit = img
  | otherwise = case dropWhile tooLarge (createMipMaps (convertRGBA8 img)) of
      level : _ -> if hasAlpha img then ImageRGBA8 level else ImageRGB8 (convertRGB8 (ImageRGBA8 level))
      [] -> img
  where
    width = dynamicMap imageWidth img
    height = dynamicMap imageHeight img
    tooLarge level = max (imageWidth level) (imageHeight level) > limit

-- | Scale image to form the sequence of mipmaps. The first element is always the original picture.
--
-- Each level averages 2x2 blocks; on an odd or one-pixel side the last block
-- reuses the edge pixel. Colour is weighted by alpha, so the colour of
-- transparent pixels does not bleed into their neighbours; alpha itself is
-- a plain average. Rounding is to the nearest, ties to even.
createMipMaps :: Image PixelRGBA8 -> [Image PixelRGBA8]
createMipMaps img = img : go img
  where
    go i | imageWidth i <= 1 && imageHeight i <= 1 = []
         | otherwise = let i' = halveRGBA8 i in i' : go i'

-- | One step of 'createMipMaps': halve both sides, but not below one pixel.
-- On a side that is already one pixel (or odd) the second sample of a block
-- is clamped to the edge.
halveRGBA8 :: Image PixelRGBA8 -> Image PixelRGBA8
halveRGBA8 (Image w h src) = Image w' h' $ VS.create $ do
  dst <- MVS.new (w' * h' * 4)
  let pixel x y = do
        let x0 = 2 * x
            x1 = min (w - 1) (x0 + 1)
            y0 = 2 * y
            y1 = min (h - 1) (y0 + 1)
            o00 = (y0 * w + x0) * 4
            o10 = (y0 * w + x1) * 4
            o01 = (y1 * w + x0) * 4
            o11 = (y1 * w + x1) * 4
            at o = fromIntegral (VS.unsafeIndex src o) :: Int
            a00 = at (o00 + 3)
            a10 = at (o10 + 3)
            a01 = at (o01 + 3)
            a11 = at (o11 + 3)
            alphaSum = a00 + a10 + a01 + a11
            -- Colour weighted by alpha, or a plain average if all four are
            -- transparent.
            channel c
              | alphaSum == 0 = roundDiv (at (o00 + c) + at (o10 + c) + at (o01 + c) + at (o11 + c)) 4
              | otherwise = roundDiv (at (o00 + c) * a00 + at (o10 + c) * a10 + at (o01 + c) * a01 + at (o11 + c) * a11) alphaSum
            out = (y * w' + x) * 4
        MVS.unsafeWrite dst out (channel 0)
        MVS.unsafeWrite dst (out + 1) (channel 1)
        MVS.unsafeWrite dst (out + 2) (channel 2)
        MVS.unsafeWrite dst (out + 3) (roundDiv alphaSum 4)
      loop y x
        | y >= h' = return ()
        | x >= w' = loop (y + 1) 0
        | otherwise = pixel x y >> loop y (x + 1)
  loop 0 0
  return dst
  where
    w' = max 1 (w `div` 2)
    h' = max 1 (h `div` 2)
    -- Nearest, ties to even.
    roundDiv :: Int -> Int -> Word8
    roundDiv n d = let
      (q, r) = n `divMod` d
      in fromIntegral $ if 2 * r > d || (2 * r == d && odd q) then q + 1 else q

-- | Scale image to form the sequence of mipmaps, down to 1x1. The first
-- element is always the original picture. Each level takes the top-left pixel
-- of every 2x2 block, as averaging palette indices makes no sense.
createMipMapsIndexed :: Pixel a => Image a -> [Image a]
createMipMapsIndexed img = img : go img
  where
    power2Scale i = generateImage (\x y -> pixelAt i (x*2) (y*2)) (downgrade $ imageWidth i) (downgrade $ imageHeight i)
    downgrade v = max 1 $ v `div` 2
    go i | imageWidth i <= 1 && imageHeight i <= 1 = []
         | otherwise = let i' = power2Scale i in i' : go i'

-- | Convert picture to BLP JPEG and create mipmaps
toBlpJpg :: Word8 -> Int -> Bool -> Image PixelRGBA8 -> BlpExt
toBlpJpg quality numLevels keepAlpha img = BlpJpeg {
    blpJpegHeader = header
  , blpJpegData = BS.drop (BS.length header) <$> streams
  }
  where
    opaque = if keepAlpha then id else opaqueRGBA8
    levels = take numLevels . createMipMaps . opaque $ img
    streams = toStrict . encodeDirectJpegAtQualityWithMetadata quality mempty . toBlpCMYK8 <$> levels
    header = scanHeader 624 streams

-- | Manually scan shared prefix of each mipmap
scanHeader :: Int -> [BS.ByteString] -> BS.ByteString
scanHeader _ [] = mempty
scanHeader maxheader mipmaps@(m0 : _) = BS.take common m0
  where
    -- Each mipmap keeps at least one byte of its own: faked mipmaps are
    -- copies of a real one, and a header swallowing a whole stream would
    -- leave a zero-size slot that readers skip.
    limit = min maxheader (minimum (BS.length <$> mipmaps) - 1)
    common = length . takeWhile sameAt $ [0 .. limit - 1]
    -- Safe to index: every stream is longer than the limit.
    sameAt i = all (\m -> BS.index m i == BS.index m0 i) mipmaps

-- | Quantise to a 256 colour palette, with or without dithering.
makePallette :: Bool -> Image PixelRGB8 -> (Image Pixel8, Palette)
makePallette dither = palettize defaultPaletteOptions { enableImageDithering = dither }

-- | Convert palette to format that we need for BLP
convertPalette :: Palette -> V.Vector PixelRGBA8
convertPalette palette = V.generate (imageWidth palette) $ \i ->
  let PixelRGB8 r g b = pixelAt palette i 0 in PixelRGBA8 b g r 255

-- | Convert indexed image to raw bytestring
convertIndexed :: Image Pixel8 -> BS.ByteString
convertIndexed img = let
  (fptr, l) = VS.unsafeToForeignPtr0 . imageData $ img
  -- Shares the image buffer without copying; the ByteString keeps it alive.
  in BI.fromForeignPtr fptr 0 l

-- | Quantise a picture to a 256 colour palette and create mipmaps, with an
-- 8-bit alpha list after each index list when asked to keep alpha.
toBlpPaletted :: Bool -> Bool -> Int -> Image PixelRGB8 -> Image PixelRGBA8 -> BlpExt
toBlpPaletted dither keepAlpha numLevels rgb rgba =
  uncurry BlpPaletted (quantiseLevels dither keepAlpha numLevels rgb rgba)

-- | The palette and the raw mip levels of 'toBlpPaletted', shared with BLP2.
quantiseLevels :: Bool -> Bool -> Int -> Image PixelRGB8 -> Image PixelRGBA8 -> (V.Vector PixelRGBA8, [BS.ByteString])
quantiseLevels dither keepAlpha numLevels rgb rgba =
  ( convertPalette palette
  , if keepAlpha
      then zipWith (<>) (convertIndexed <$> mipmaps) (convertIndexed <$> alphaMaps)
      else convertIndexed <$> mipmaps
  )
  where
    img'    :: Image Pixel8
    palette :: Palette
    -- Fully transparent pixels are made black, so the colours hidden under
    -- them take no palette entries.
    (img', palette) = makePallette dither $ if keepAlpha then fillTransparent rgba else rgb

    -- Smaller levels are averaged in RGBA and mapped to the nearest palette
    -- entry; picking one index per block would sample the dithering noise.
    mipmaps :: [Image Pixel8]
    mipmaps = take numLevels $ img' : (nearestIndices palette <$> drop 1 (createMipMaps averaged))
    averaged = if keepAlpha then rgba else opaqueRGBA8 rgba

    -- Alpha is coverage, not an index, so its mipmaps are averaged.
    alphaMaps :: [Image Pixel8]
    alphaMaps = take numLevels $ extractComponent PlaneAlpha <$> createMipMaps rgba

-- | The RGB of an image, with every fully transparent pixel made black.
-- Whatever colour those pixels had is invisible, and leaving it in would
-- spend palette entries on it; a single colour costs at most one entry.
fillTransparent :: Image PixelRGBA8 -> Image PixelRGB8
fillTransparent = pixelMap $ \(PixelRGBA8 r g b a) -> if a == 0 then PixelRGB8 0 0 0 else PixelRGB8 r g b

-- | Index of the palette entry nearest to each pixel's colour (squared RGB
-- distance; alpha is stored separately; ties go to the lower index).
--
-- The entries are searched in order of red, outwards from the pixel's red,
-- and a direction stops once the red difference alone is no better than the
-- best match. That gives the same answer as trying every entry, with far
-- fewer tries.
nearestIndices :: Palette -> Image PixelRGBA8 -> Image Pixel8
nearestIndices palette img = pixelMap nearest img
  where
    entries = imageData palette
    count = VS.length entries `div` 3
    channel i k = fromIntegral (VS.unsafeIndex entries (i * 3 + k)) :: Int
    -- Entry indices sorted by red, and their reds.
    byRed = VU.fromList . map snd . sortOn fst $ [((channel i 0, i), i) | i <- [0 .. count - 1]]
    reds = VU.map (`channel` 0) byRed
    nearest (PixelRGBA8 r8 g8 b8 _) = fromIntegral . snd $ down (start - 1) (up start (maxBound, 0))
      where
        r = fromIntegral r8 :: Int
        g = fromIntegral g8 :: Int
        b = fromIntegral b8 :: Int
        -- First position whose red is at least the pixel's.
        start = search 0 count
        search lo hi
          | lo >= hi = lo
          | VU.unsafeIndex reds mid < r = search (mid + 1) hi
          | otherwise = search lo mid
          where mid = (lo + hi) `div` 2
        consider j best@(bestDist, bestIndex) = let
          i = VU.unsafeIndex byRed j
          dr = channel i 0 - r
          dg = channel i 1 - g
          db = channel i 2 - b
          dist = dr * dr + dg * dg + db * db
          in if dist < bestDist || (dist == bestDist && i < bestIndex) then (dist, i) else best
        up j best@(bestDist, _)
          | j >= count = best
          | let dr = VU.unsafeIndex reds j - r, dr * dr > bestDist = best
          | otherwise = up (j + 1) (consider j best)
        down j best@(bestDist, _)
          | j < 0 = best
          | let dr = r - VU.unsafeIndex reds j, dr * dr > bestDist = best
          | otherwise = down (j - 1) (consider j best)