From dc3dbfb4218e93669092e7b2192384dd367b37f1 Mon Sep 17 00:00:00 2001 From: Andrew Knapp Date: Mon, 9 Mar 2020 16:05:10 -0700 Subject: [PATCH 1/4] Preliminary support for float/double EXIF tags. --- src/Codec/Picture/Metadata/Exif.hs | 6 ++++ src/Codec/Picture/Tiff/Internal/Metadata.hs | 4 +++ src/Codec/Picture/Tiff/Internal/Types.hs | 36 ++++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Codec/Picture/Metadata/Exif.hs b/src/Codec/Picture/Metadata/Exif.hs index a454598..f36cdd1 100644 --- a/src/Codec/Picture/Metadata/Exif.hs +++ b/src/Codec/Picture/Metadata/Exif.hs @@ -199,10 +199,14 @@ data ExifData = ExifNone | ExifLong !Word32 | ExifShort !Word16 + | ExifFloat !Float + | ExifDouble !Double | ExifString !B.ByteString | ExifUndefined !B.ByteString | ExifShorts !(V.Vector Word16) | ExifLongs !(V.Vector Word32) + | ExifFloats !(V.Vector Double) + | ExifDoubles !(V.Vector Double) | ExifRational !Word32 !Word32 | ExifSignedRational !Int32 !Int32 | ExifIFD ![(ExifTag, ExifData)] @@ -215,4 +219,6 @@ instance NFData ExifData where rnf (ExifIFD ifds) = rnf ifds `seq` () rnf (ExifLongs l) = rnf l `seq` () rnf (ExifShorts l) = rnf l `seq` () + rnf (ExifFloats l) = rnf l `seq` () + rnf (ExifDoubles l) = rnf l `seq` () rnf a = a `seq` () diff --git a/src/Codec/Picture/Tiff/Internal/Metadata.hs b/src/Codec/Picture/Tiff/Internal/Metadata.hs index 97f7151..4fa760d 100644 --- a/src/Codec/Picture/Tiff/Internal/Metadata.hs +++ b/src/Codec/Picture/Tiff/Internal/Metadata.hs @@ -44,7 +44,11 @@ typeOfData d = case d of ExifLong _l -> TypeLong ExifLongs _l -> TypeLong ExifShort _s -> TypeShort + ExifFloat _f -> TypeFloat + ExifDouble _d -> TypeDouble ExifShorts _s -> TypeShort + ExifFloats _f -> TypeFloat + ExifDoubles _d -> TypeDouble ExifString _str -> TypeAscii ExifUndefined _undef -> TypeUndefined ExifRational _r1 _r2 -> TypeRational diff --git a/src/Codec/Picture/Tiff/Internal/Types.hs b/src/Codec/Picture/Tiff/Internal/Types.hs index cbd0606..7406cfb 100644 --- a/src/Codec/Picture/Tiff/Internal/Types.hs +++ b/src/Codec/Picture/Tiff/Internal/Types.hs @@ -38,6 +38,8 @@ import Data.Binary( Binary( .. ) ) import Data.Binary.Get( Get , getWord16le, getWord16be , getWord32le, getWord32be + , getFloatle, getFloatbe + , getDoublele, getDoublebe , bytesRead , skip , getByteString @@ -45,6 +47,8 @@ import Data.Binary.Get( Get import Data.Binary.Put( Put , putWord16le, putWord16be , putWord32le, putWord32be + , putFloatle, putFloatbe + , putDoublele, putDoublebe , putByteString ) import Data.Function( on ) @@ -105,6 +109,20 @@ instance BinaryParam Endianness Word32 where getP EndianLittle = getWord32le getP EndianBig = getWord32be +instance BinaryParam Endianness Float where + putP EndianLittle = putFloatle + putP EndianBig = putFloatbe + + getP EndianLittle = getFloatle + getP EndianBig = getFloatbe + +instance BinaryParam Endianness Double where + putP EndianLittle = putDoublele + putP EndianBig = putDoublebe + + getP EndianLittle = getDoublele + getP EndianBig = getDoublebe + instance Binary TiffHeader where put hdr = do let endian = hdrEndianness hdr @@ -215,12 +233,16 @@ instance BinaryParam (Endianness, Int, ImageFileDirectory) ExifData where dump ExifNone = pure () dump (ExifLong _) = pure () dump (ExifShort _) = pure () + dump (ExifFloat _) = pure () + dump (ExifDouble _) = pure () dump (ExifIFD _) = pure () dump (ExifString bstr) = paddWrite bstr dump (ExifUndefined bstr) = paddWrite bstr -- wrong if length == 2 dump (ExifShorts shorts) = V.mapM_ (putP endianness) shorts dump (ExifLongs longs) = V.mapM_ (putP endianness) longs + dump (ExifFloats floats) = V.mapM_ (putP endianness) floats + dump (ExifDoubles doubles) = V.mapM_ (putP endianness) doubles dump (ExifRational a b) = putP endianness a >> putP endianness b dump (ExifSignedRational a b) = putP endianness a >> putP endianness b @@ -282,12 +304,20 @@ instance BinaryParam (Endianness, Int, ImageFileDirectory) ExifData where align ifd $ ExifSignedRational <$> getP EndianLittle <*> getP EndianLittle fetcher ImageFileDirectory { ifdType = TypeShort, ifdCount = 1 } = pure . ExifShort . fromIntegral $ ifdOffset ifd - fetcher ImageFileDirectory { ifdType = TypeShort, ifdCount = count } | count > 2 = + fetcher ImageFileDirectory { ifdType = TypeShort, ifdCount = count } | count > 1 = align ifd $ ExifShorts <$> getVec count getE fetcher ImageFileDirectory { ifdType = TypeLong, ifdCount = 1 } = pure . ExifLong . fromIntegral $ ifdOffset ifd fetcher ImageFileDirectory { ifdType = TypeLong, ifdCount = count } | count > 1 = align ifd $ ExifLongs <$> getVec count getE + fetcher ImageFileDirectory { ifdType = TypeFloat, ifdCount = 1 } = + pure . ExifFloat . fromIntegral $ ifdOffset ifd + fetcher ImageFileDirectory { ifdType = TypeFloat, ifdCount = count } | count > 1 = + align ifd $ ExifFloats <$> getVec count getE + fetcher ImageFileDirectory { ifdType = TypeDouble, ifdCount = 1 } = + pure . ExifDouble . fromIntegral $ ifdOffset ifd + fetcher ImageFileDirectory { ifdType = TypeDouble, ifdCount = count } | count > 1 = + align ifd $ ExifDoubles <$> getVec count getE fetcher _ = pure ExifNone cleanImageFileDirectory :: Endianness -> ImageFileDirectory -> ImageFileDirectory @@ -339,6 +369,10 @@ setupIfdOffsets initialOffset lst = mapAccumL updater startExtended lst updater ix ifd@(ImageFileDirectory { ifdExtended = ExifShorts v }) | V.length v > 2 = ( ix + fromIntegral (V.length v * 2) , ifd { ifdOffset = ix }) + updater ix ifd@(ImageFileDirectory { ifdExtended = ExifFloats v }) = + ( ix + fromIntegral (V.length v * 4) , ifd { ifdOffset = ix } ) + updater ix ifd@(ImageFileDirectory { ifdExtended = ExifDoubles v }) = + ( ix + fromIntegral (V.length v * 8) , ifd { ifdOffset = ix } ) updater ix ifd = (ix, ifd) instance BinaryParam B.ByteString (TiffHeader, [[ImageFileDirectory]]) where From db631a8351233dd8e2afa7ac89507e452b62c65b Mon Sep 17 00:00:00 2001 From: Andrew Knapp Date: Mon, 9 Mar 2020 21:35:23 -0700 Subject: [PATCH 2/4] Update README.md. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9689f6f..4d30091 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,10 @@ Status - Tiff * Reading - 2, 4, 8, 16 int bit depth reading (planar and contiguous for each) - - 32 bit floating point reading + - 16 and 32 bit floating point reading - CMYK, YCbCr, RGB, RGBA, Paletted, Greyscale - - Uncompressed, PackBits, LZW + - Uncompressed, PackBits, LZW, Deflate * Writing - 8 and 16 bits From 06be71a7a2a871b53aff6778eb736fcb9f20bdce Mon Sep 17 00:00:00 2001 From: Andrew Knapp Date: Mon, 24 Aug 2020 15:13:51 -0700 Subject: [PATCH 3/4] Snapshot of reading 16 bit floats to 32 bit floats. --- JuicyPixels.cabal | 3 +- src/Codec/Picture/Tiff.hs | 23 ++++++++++++++++ src/Codec/Picture/Tiff/Internal/Types.hs | 3 ++ src/Codec/Picture/Types.hs | 35 ++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/JuicyPixels.cabal b/JuicyPixels.cabal index 4249252..93313a5 100644 --- a/JuicyPixels.cabal +++ b/JuicyPixels.cabal @@ -138,7 +138,8 @@ Library vector >= 0.10 && < 0.13, primitive >= 0.4, deepseq >= 1.1 && < 1.5, - containers >= 0.4.2 && < 0.7 + containers >= 0.4.2 && < 0.7, + half >= 0.3 && < 0.4 -- Modules not exported by this package. Other-modules: Codec.Picture.BitWriter, diff --git a/src/Codec/Picture/Tiff.hs b/src/Codec/Picture/Tiff.hs index aac6d40..739ff2c 100644 --- a/src/Codec/Picture/Tiff.hs +++ b/src/Codec/Picture/Tiff.hs @@ -1,4 +1,5 @@ {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -39,6 +40,7 @@ import Control.Applicative( (<$>), (<*>), pure ) import Data.Monoid( mempty ) #endif +import Codec.Compression.Zlib( decompress ) import Control.Arrow( first ) import Control.Monad( when, foldM_, unless, forM_ ) import Control.Monad.ST( ST, runST ) @@ -58,6 +60,9 @@ import qualified Data.ByteString.Unsafe as BU import Foreign.Storable( sizeOf ) +import Numeric.Half +import Unsafe.Coerce + import Codec.Picture.Metadata.Exif import Codec.Picture.Metadata( Metadatas ) import Codec.Picture.InternalHelper @@ -206,6 +211,14 @@ uncompressAt CompressionLZW = \str outVec _stride writeIndex (offset, size) -> let toDecode = B.take (fromIntegral size) $ B.drop (fromIntegral offset) str runBoolReader $ decodeLzwTiff toDecode outVec writeIndex return 0 +uncompressAt CompressionDeflate = \str outVec stride writeIndex (offset,size) -> + let decompressed = Lb.toStrict + . decompress + . Lb.fromStrict + . B.take (fromIntegral size) + $ B.drop (fromIntegral offset) str + len = fromIntegral $ B.length decompressed + in copyByteString decompressed outVec stride writeIndex (0,len) uncompressAt _ = error "Unhandled compression" class Unpackable a where @@ -782,9 +795,19 @@ unpack file nfo@TiffInfo { tiffColorspace = TiffMonochrome -- some files are a little bit borked... | lst == V.fromList [8, 8, 8] && all (TiffSampleUint ==) format = pure . TrueColorImage . ImageRGB8 $ gatherStrips (0 :: Word8) file nfo + | lst == V.singleton 16 && all (TiffSampleFloat ==) format = + pure . TrueColorImage . ImageYF . fromHalfToFloat $ gatherStrips (0 :: Word16) file nfo unpack _ _ = Left "Failure to unpack TIFF file" +fromHalfToFloat :: Image Word16 -> Image Float +fromHalfToFloat Image { imageWidth = w, imageHeight = h + , imageData = arr } = Image w h transformed + where transformed = VS.map (fromHalf . word16ToHalf) arr + -- safe under the hood, but CUShort's data constructor isn't exposed + word16ToHalf = unsafeCoerce :: Word16 -> Half + + -- | Decode a tiff encoded image while preserving the underlying -- pixel type (except for Y32 which is truncated to 16 bits). -- diff --git a/src/Codec/Picture/Tiff/Internal/Types.hs b/src/Codec/Picture/Tiff/Internal/Types.hs index 7406cfb..770ae34 100644 --- a/src/Codec/Picture/Tiff/Internal/Types.hs +++ b/src/Codec/Picture/Tiff/Internal/Types.hs @@ -157,6 +157,7 @@ data TiffCompression | CompressionModifiedRLE -- 2 | CompressionLZW -- 5 | CompressionJPEG -- 6 + | CompressionDeflate -- 8 | CompressionPackBit -- 32273 data IfdType @@ -511,6 +512,7 @@ unPackCompression v = case v of 2 -> pure CompressionModifiedRLE 5 -> pure CompressionLZW 6 -> pure CompressionJPEG + 8 -> pure CompressionDeflate 32773 -> pure CompressionPackBit vv -> fail $ "Unknown compression scheme " ++ show vv @@ -520,5 +522,6 @@ packCompression v = case v of CompressionModifiedRLE -> 2 CompressionLZW -> 5 CompressionJPEG -> 6 + CompressionDeflate -> 8 CompressionPackBit -> 32773 diff --git a/src/Codec/Picture/Types.hs b/src/Codec/Picture/Types.hs index 7edb061..4b34117 100644 --- a/src/Codec/Picture/Types.hs +++ b/src/Codec/Picture/Types.hs @@ -124,6 +124,7 @@ import Data.Word( Word8, Word16, Word32, Word64 ) import Data.Vector.Storable ( (!) ) import qualified Data.Vector.Storable as V import qualified Data.Vector.Storable.Mutable as M +import Numeric.Half #include "ConvGraph.hs" @@ -1374,6 +1375,40 @@ instance Pixel PixelF where {-# INLINE unsafeWritePixel #-} unsafeWritePixel = M.unsafeWrite +type PixelH = Half + +instance Pixel PixelH where + type PixelBaseComponent PixelH = Half + + {-# INLINE pixelOpacity #-} + pixelOpacity = const 1.0 + + {-# INLINE mixWith #-} + mixWith f = f 0 + + {-# INLINE colorMap #-} + colorMap f = f + {-# INLINE componentCount #-} + componentCount _ = 1 + {-# INLINE pixelAt #-} + pixelAt (Image { imageWidth = w, imageData = arr }) x y = + arr ! (x + y * w) + + {-# INLINE readPixel #-} + readPixel image@(MutableImage { mutableImageData = arr }) x y = + arr `M.read` mutablePixelBaseIndex image x y + + {-# INLINE writePixel #-} + writePixel image@(MutableImage { mutableImageData = arr }) x y = + arr `M.write` mutablePixelBaseIndex image x y + + {-# INLINE unsafePixelAt #-} + unsafePixelAt = V.unsafeIndex + {-# INLINE unsafeReadPixel #-} + unsafeReadPixel = M.unsafeRead + {-# INLINE unsafeWritePixel #-} + unsafeWritePixel = M.unsafeWrite + instance ColorConvertible PixelF PixelRGBF where {-# INLINE promotePixel #-} promotePixel c = PixelRGBF c c c-- (c / 0.3) (c / 0.59) (c / 0.11) From 73dc60ca7806c1c0845db3f5e50f88dffe87fa14 Mon Sep 17 00:00:00 2001 From: Andrew Knapp Date: Wed, 26 Aug 2020 13:58:28 -0700 Subject: [PATCH 4/4] Support for writing deflated TIFF files and a test. --- src/Codec/Picture/Tiff.hs | 34 +++++++++++++++++++++++++++------- test-src/main.hs | 1 + tests/tiff/float16deflate.tif | Bin 0 -> 34290 bytes 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 tests/tiff/float16deflate.tif diff --git a/src/Codec/Picture/Tiff.hs b/src/Codec/Picture/Tiff.hs index 739ff2c..97a3358 100644 --- a/src/Codec/Picture/Tiff.hs +++ b/src/Codec/Picture/Tiff.hs @@ -32,7 +32,9 @@ module Codec.Picture.Tiff( decodeTiff , decodeTiffWithPaletteAndMetadata , TiffSaveable , encodeTiff + , encodeTiffWithDeflate , writeTiff + , writeTiffWithDeflate ) where #if !MIN_VERSION_base(4,8,0) @@ -40,7 +42,7 @@ import Control.Applicative( (<$>), (<*>), pure ) import Data.Monoid( mempty ) #endif -import Codec.Compression.Zlib( decompress ) +import Codec.Compression.Zlib( compress, decompress ) import Control.Arrow( first ) import Control.Monad( when, foldM_, unless, forM_ ) import Control.Monad.ST( ST, runST ) @@ -914,17 +916,15 @@ instance TiffSaveable PixelYCbCr8 where colorSpaceOfPixel _ = TiffYCbCr subSamplingInfo _ = V.fromListN 2 [1, 1] --- | Transform an image into a Tiff encoded bytestring, ready to be --- written as a file. -encodeTiff :: forall px. (TiffSaveable px) => Image px -> Lb.ByteString -encodeTiff img = runPut $ putP rawPixelData hdr +encodeTiffWithCompression :: forall px. (TiffSaveable px) => TiffCompression -> Image px -> Lb.ByteString +encodeTiffWithCompression compressMethod img = runPut $ putP rawPixelData hdr where intSampleCount = componentCount (undefined :: px) sampleCount = fromIntegral intSampleCount sampleType = undefined :: PixelBaseComponent px pixelData = imageData img - rawPixelData = toByteString pixelData + rawPixelData = tiffCompress compressMethod $ toByteString pixelData width = fromIntegral $ imageWidth img height = fromIntegral $ imageHeight img intSampleSize = sizeOf sampleType @@ -946,7 +946,7 @@ encodeTiff img = runPut $ putP rawPixelData hdr , tiffPlaneConfiguration = PlanarConfigContig , tiffSampleFormat = sampleFormat (undefined :: px) , tiffBitsPerSample = V.replicate intSampleCount bitPerSample - , tiffCompression = CompressionNone + , tiffCompression = compressMethod , tiffStripSize = V.singleton imageSize , tiffOffsets = V.singleton headerSize , tiffPalette = Nothing @@ -956,9 +956,29 @@ encodeTiff img = runPut $ putP rawPixelData hdr , tiffMetadatas = mempty } + tiffCompress CompressionNone img' = img' + tiffCompress CompressionDeflate img' = Lb.toStrict $ compress (Lb.fromStrict img') + tiffCompress _ _ = error "encodeTiffWithCompression: unsupported compression format" + +-- | Transform an image into a Tiff encoded bytestring, ready to be +-- written as a file. +encodeTiff :: (TiffSaveable px) => Image px -> Lb.ByteString +encodeTiff img = encodeTiffWithCompression CompressionNone img +{-# INLINE encodeTiff #-} + +-- | Transform an image into a Tiff encoded bytestring, ready to be +-- written as a file. The raw data is compressed via deflate. +encodeTiffWithDeflate :: (TiffSaveable px) => Image px -> Lb.ByteString +encodeTiffWithDeflate img = encodeTiffWithCompression CompressionDeflate img +{-# INLINE encodeTiffWithDeflate #-} + -- | Helper function to directly write an image as a tiff on disk. writeTiff :: (TiffSaveable pixel) => FilePath -> Image pixel -> IO () writeTiff path img = Lb.writeFile path $ encodeTiff img +-- | Helper function to directly write an image as a deflate-compressed tiff on disk. +writeTiffWithDeflate :: (TiffSaveable pixel) => FilePath -> Image pixel -> IO () +writeTiffWithDeflate path img = Lb.writeFile path $ encodeTiff img + {-# ANN module "HLint: ignore Reduce duplication" #-} diff --git a/test-src/main.hs b/test-src/main.hs index 2a21718..f9c4dcf 100644 --- a/test-src/main.hs +++ b/test-src/main.hs @@ -391,6 +391,7 @@ tiffValidTests = ,"other/butique-YA8.tif" ,"other/butique-YA16.tif" ,"horizontal-difference-lzw.tiff" -- produced by "Grab" on Mac OS X + ,"float16deflate.tif" ,"rad_YF.tif" ,"rad_Y32.tif" ] diff --git a/tests/tiff/float16deflate.tif b/tests/tiff/float16deflate.tif new file mode 100644 index 0000000000000000000000000000000000000000..f9df4bfdf7f839eefbadf71265aaa3121cfa5273 GIT binary patch literal 34290 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 aS-I>xE1!L56|(QFV)mU?%D%I>?EeQJY<{!= literal 0 HcmV?d00001