packages feed

JuicyPixels-blp-0.4.0.0: src/Codec/Picture/Blp/Internal/Convert.hs

{-|
Pixel conversions between the BGRA planes of BLP1 JPEG content and
JuicyPixels images.

This is an internal module: its API may change between minor versions.
-}
module Codec.Picture.Blp.Internal.Convert(
    fromBlpJpeg
  , jpegAlphaIsUsable
  , toBlpCMYK8
  , opaqueRGBA8
  ) where

import Codec.Picture
import Data.Bits ((.&.))
import Data.Word

import qualified Data.Vector.Storable as VS

-- | Convert a decoded mipmap of a JPEG compressed BLP1 into an image that can
-- be written out directly.
--
-- The four components of a BLP1 JPEG are the raw B, G, R and A planes of the
-- texture. The stream carries no Adobe marker, so a JPEG decoder has nothing
-- to go on and reports it as CMYK; running the generic CMYK conversion over
-- it applies an ink model to data that is not ink, and multiplies every
-- colour channel by the alpha plane. Map the planes straight across instead.
--
-- The 'Bool' says whether the alpha plane should be honoured; see
-- 'jpegAlphaIsUsable'. When it is 'False' the texture is made fully opaque.
-- Any other kind of JPEG (greyscale, YCbCr) is converted to RGBA as usual.
fromBlpJpeg :: Bool -> DynamicImage -> DynamicImage
fromBlpJpeg useAlpha i = case i of
  ImageCMYK8 p -> ImageRGBA8 $ swapRedBlue (not useAlpha) p
  _ -> ImageRGBA8 $ convertRGBA8 i

-- | Whether the alpha plane of a decoded BLP1 JPEG mipmap carries information.
--
-- Warcraft III authoring tools disagree about the alpha plane of a JPEG
-- compressed BLP1. Some set the alpha flag and fill the plane with 0x00,
-- others with 0xFF, and both mean the same thing: the texture is opaque and
-- the plane was never populated. Taking either at face value gives a wrong
-- answer for half the files in the wild, so a plane that is entirely 0x00 or
-- entirely 0xFF is treated as carrying no information at all.
--
-- Any other plane, including one that is uniformly semi-transparent, is used
-- as it stands.
jpegAlphaIsUsable :: DynamicImage -> Bool
jpegAlphaIsUsable i = case i of
  ImageCMYK8 p
    | VS.null planes -> False
    | otherwise -> varies 7 || (a0 /= 0 && a0 /= 255)
    where
      -- The fourth component of every pixel; stops at the first that differs.
      planes = imageData p
      a0 = VS.unsafeIndex planes 3
      varies j
        | j >= VS.length planes = False
        | VS.unsafeIndex planes j /= a0 = True
        | otherwise = varies (j + 4)
  _ -> True

-- | Reinterpret the four planes a JPEG decoder labelled CMYK as the BGRA they
-- actually are, optionally making every pixel opaque. This inverts
-- 'toBlpCMYK8'.
swapRedBlue :: Bool -> Image PixelCMYK8 -> Image PixelRGBA8
swapRedBlue opaque img = Image (imageWidth img) (imageHeight img) (swapBytes opaque (imageData img))

-- | Swap the first and third byte of every four, optionally setting the
-- fourth to 255.
swapBytes :: Bool -> VS.Vector Word8 -> VS.Vector Word8
swapBytes opaque src = VS.generate (VS.length src) $ \i -> case i .&. 3 of
  0 -> VS.unsafeIndex src (i + 2)
  2 -> VS.unsafeIndex src (i - 2)
  3 | opaque -> 255
  _ -> VS.unsafeIndex src i

-- | Set the alpha of every pixel to 255.
opaqueRGBA8 :: Image PixelRGBA8 -> Image PixelRGBA8
opaqueRGBA8 img = img { imageData = VS.imap (\i v -> if i .&. 3 == 3 then 255 else v) (imageData img) }

-- | Lay out the B, G, R and A planes of a texture as the four components of
-- a BLP1 JPEG. The encoder labels them CMYK only because that is the
-- four-component JPEG it can write; this is the exact inverse of
-- 'swapRedBlue'.
toBlpCMYK8 :: Image PixelRGBA8 -> Image PixelCMYK8
toBlpCMYK8 img = Image (imageWidth img) (imageHeight img) (swapBytes False (imageData img))