aboutsummaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/ImageSize.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text/Pandoc/ImageSize.hs')
-rw-r--r--src/Text/Pandoc/ImageSize.hs547
1 files changed, 0 insertions, 547 deletions
diff --git a/src/Text/Pandoc/ImageSize.hs b/src/Text/Pandoc/ImageSize.hs
deleted file mode 100644
index cc22c06ca..000000000
--- a/src/Text/Pandoc/ImageSize.hs
+++ /dev/null
@@ -1,547 +0,0 @@
-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, CPP #-}
-{-# OPTIONS_GHC -fno-warn-type-defaults #-}
-{-
- Copyright (C) 2011-2016 John MacFarlane <jgm@berkeley.edu>
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but WITHOUT
- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- more details.
-
- You should have received a copy of the GNU General Public License along
- with this program; if not, write to the Free Software Foundation, Inc., 59
- Temple Place, Suite 330, Boston, MA 02111-1307 USA
--}
-
-{- |
-Module : Text.Pandoc.ImageSize
-Copyright : Copyright (C) 2011-2016 John MacFarlane
-License : GNU GPL, version 2 or above
-
-Maintainer : John MacFarlane <jgm@berkeley.edu>
-Stability : alpha
-Portability : portable
-
-Functions for determining the size of a PNG, JPEG, or GIF image.
--}
-module Text.Pandoc.ImageSize ( ImageType(..)
- , imageType
- , imageSize
- , sizeInPixels
- , sizeInPoints
- , desiredSizeInPoints
- , Dimension(..)
- , Direction(..)
- , dimension
- , inInch
- , inPoints
- , numUnit
- , showInInch
- , showInPixel
- , showFl
- ) where
-import Data.ByteString (ByteString, unpack)
-import qualified Data.ByteString.Char8 as B
-import qualified Data.ByteString.Lazy as BL
-import Data.Char (isDigit)
-import Control.Monad
-import Data.Bits
-import Data.Binary
-import Data.Binary.Get
-import Text.Pandoc.Shared (safeRead)
-import Data.Default (Default)
-import Numeric (showFFloat)
-import Text.Pandoc.Definition
-import Text.Pandoc.Options
-import qualified Data.Map as M
-import Control.Monad.Except
-import Data.Maybe (fromMaybe)
-
--- quick and dirty functions to get image sizes
--- algorithms borrowed from wwwis.pl
-
-data ImageType = Png | Gif | Jpeg | Pdf | Eps deriving Show
-data Direction = Width | Height
-instance Show Direction where
- show Width = "width"
- show Height = "height"
-
-data Dimension = Pixel Integer
- | Centimeter Double
- | Inch Double
- | Percent Double
-instance Show Dimension where
- show (Pixel a) = show a ++ "px"
- show (Centimeter a) = showFl a ++ "cm"
- show (Inch a) = showFl a ++ "in"
- show (Percent a) = show a ++ "%"
-
-data ImageSize = ImageSize{
- pxX :: Integer
- , pxY :: Integer
- , dpiX :: Integer
- , dpiY :: Integer
- } deriving (Read, Show, Eq)
-instance Default ImageSize where
- def = ImageSize 300 200 72 72
-
-showFl :: (RealFloat a) => a -> String
-showFl a = showFFloat (Just 5) a ""
-
-imageType :: ByteString -> Maybe ImageType
-imageType img = case B.take 4 img of
- "\x89\x50\x4e\x47" -> return Png
- "\x47\x49\x46\x38" -> return Gif
- "\xff\xd8\xff\xe0" -> return Jpeg -- JFIF
- "\xff\xd8\xff\xe1" -> return Jpeg -- Exif
- "%PDF" -> return Pdf
- "%!PS"
- | (B.take 4 $ B.drop 1 $ B.dropWhile (/=' ') img) == "EPSF"
- -> return Eps
- _ -> mzero
-
-imageSize :: ByteString -> Either String ImageSize
-imageSize img =
- case imageType img of
- Just Png -> mbToEither "could not determine PNG size" $ pngSize img
- Just Gif -> mbToEither "could not determine GIF size" $ gifSize img
- Just Jpeg -> jpegSize img
- Just Eps -> mbToEither "could not determine EPS size" $ epsSize img
- Just Pdf -> Left "could not determine PDF size" -- TODO
- Nothing -> Left "could not determine image type"
- where mbToEither msg Nothing = Left msg
- mbToEither _ (Just x) = Right x
-
-defaultSize :: (Integer, Integer)
-defaultSize = (72, 72)
-
-sizeInPixels :: ImageSize -> (Integer, Integer)
-sizeInPixels s = (pxX s, pxY s)
-
--- | Calculate (height, width) in points using the image file's dpi metadata,
--- using 72 Points == 1 Inch.
-sizeInPoints :: ImageSize -> (Double, Double)
-sizeInPoints s = (pxXf * 72 / dpiXf, pxYf * 72 / dpiYf)
- where
- pxXf = fromIntegral $ pxX s
- pxYf = fromIntegral $ pxY s
- dpiXf = fromIntegral $ dpiX s
- dpiYf = fromIntegral $ dpiY s
-
--- | Calculate (height, width) in points, considering the desired dimensions in the
--- attribute, while falling back on the image file's dpi metadata if no dimensions
--- are specified in the attribute (or only dimensions in percentages).
-desiredSizeInPoints :: WriterOptions -> Attr -> ImageSize -> (Double, Double)
-desiredSizeInPoints opts attr s =
- case (getDim Width, getDim Height) of
- (Just w, Just h) -> (w, h)
- (Just w, Nothing) -> (w, w / ratio)
- (Nothing, Just h) -> (h * ratio, h)
- (Nothing, Nothing) -> sizeInPoints s
- where
- ratio = fromIntegral (pxX s) / fromIntegral (pxY s)
- getDim dir = case (dimension dir attr) of
- Just (Percent _) -> Nothing
- Just dim -> Just $ inPoints opts dim
- Nothing -> Nothing
-
-inPoints :: WriterOptions -> Dimension -> Double
-inPoints opts dim = 72 * inInch opts dim
-
-inInch :: WriterOptions -> Dimension -> Double
-inInch opts dim =
- case dim of
- (Pixel a) -> fromIntegral a / (fromIntegral $ writerDpi opts)
- (Centimeter a) -> a * 0.3937007874
- (Inch a) -> a
- (Percent _) -> 0
-
--- | Convert a Dimension to a String denoting its equivalent in inches, for example "2.00000".
--- Note: Dimensions in percentages are converted to the empty string.
-showInInch :: WriterOptions -> Dimension -> String
-showInInch _ (Percent _) = ""
-showInInch opts dim = showFl $ inInch opts dim
-
--- | Convert a Dimension to a String denoting its equivalent in pixels, for example "600".
--- Note: Dimensions in percentages are converted to the empty string.
-showInPixel :: WriterOptions -> Dimension -> String
-showInPixel opts dim =
- case dim of
- (Pixel a) -> show a
- (Centimeter a) -> show (floor $ dpi * a * 0.3937007874 :: Int)
- (Inch a) -> show (floor $ dpi * a :: Int)
- (Percent _) -> ""
- where
- dpi = fromIntegral $ writerDpi opts
-
--- | Maybe split a string into a leading number and trailing unit, e.g. "3cm" to Just (3.0, "cm")
-numUnit :: String -> Maybe (Double, String)
-numUnit s =
- let (nums, unit) = span (\c -> isDigit c || ('.'==c)) s
- in case safeRead nums of
- Just n -> Just (n, unit)
- Nothing -> Nothing
-
--- | Read a Dimension from an Attr attribute.
--- `dimension Width attr` might return `Just (Pixel 3)` or for example `Just (Centimeter 2.0)`, etc.
-dimension :: Direction -> Attr -> Maybe Dimension
-dimension dir (_, _, kvs) =
- case dir of
- Width -> extractDim "width"
- Height -> extractDim "height"
- where
- extractDim key =
- case lookup key kvs of
- Just str ->
- case numUnit str of
- Just (num, unit) -> toDim num unit
- Nothing -> Nothing
- Nothing -> Nothing
- toDim a "cm" = Just $ Centimeter a
- toDim a "mm" = Just $ Centimeter (a / 10)
- toDim a "in" = Just $ Inch a
- toDim a "inch" = Just $ Inch a
- toDim a "%" = Just $ Percent a
- toDim a "px" = Just $ Pixel (floor a::Integer)
- toDim a "" = Just $ Pixel (floor a::Integer)
- toDim _ _ = Nothing
-
-epsSize :: ByteString -> Maybe ImageSize
-epsSize img = do
- let ls = takeWhile ("%" `B.isPrefixOf`) $ B.lines img
- let ls' = dropWhile (not . ("%%BoundingBox:" `B.isPrefixOf`)) ls
- case ls' of
- [] -> mzero
- (x:_) -> case B.words x of
- (_:_:_:ux:uy:[]) -> do
- ux' <- safeRead $ B.unpack ux
- uy' <- safeRead $ B.unpack uy
- return ImageSize{
- pxX = ux'
- , pxY = uy'
- , dpiX = 72
- , dpiY = 72 }
- _ -> mzero
-
-pngSize :: ByteString -> Maybe ImageSize
-pngSize img = do
- let (h, rest) = B.splitAt 8 img
- guard $ h == "\x8a\x4d\x4e\x47\x0d\x0a\x1a\x0a" ||
- h == "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
- let (i, rest') = B.splitAt 4 $ B.drop 4 rest
- guard $ i == "MHDR" || i == "IHDR"
- let (sizes, rest'') = B.splitAt 8 rest'
- (x,y) <- case map fromIntegral $ unpack $ sizes of
- ([w1,w2,w3,w4,h1,h2,h3,h4] :: [Integer]) -> return
- ((shift w1 24) + (shift w2 16) + (shift w3 8) + w4,
- (shift h1 24) + (shift h2 16) + (shift h3 8) + h4)
- _ -> Nothing -- "PNG parse error"
- let (dpix, dpiy) = findpHYs rest''
- return $ ImageSize { pxX = x, pxY = y, dpiX = dpix, dpiY = dpiy }
-
-findpHYs :: ByteString -> (Integer, Integer)
-findpHYs x =
- if B.null x || "IDAT" `B.isPrefixOf` x
- then (72,72) -- default, no pHYs
- else if "pHYs" `B.isPrefixOf` x
- then let [x1,x2,x3,x4,y1,y2,y3,y4,u] = map fromIntegral
- $ unpack $ B.take 9 $ B.drop 4 x
- factor = if u == 1 -- dots per meter
- then \z -> z * 254 `div` 10000
- else const 72
- in ( factor $ (shift x1 24) + (shift x2 16) + (shift x3 8) + x4,
- factor $ (shift y1 24) + (shift y2 16) + (shift y3 8) + y4 )
- else findpHYs $ B.drop 1 x -- read another byte
-
-gifSize :: ByteString -> Maybe ImageSize
-gifSize img = do
- let (h, rest) = B.splitAt 6 img
- guard $ h == "GIF87a" || h == "GIF89a"
- case map fromIntegral $ unpack $ B.take 4 rest of
- [w2,w1,h2,h1] -> return ImageSize {
- pxX = shift w1 8 + w2,
- pxY = shift h1 8 + h2,
- dpiX = 72,
- dpiY = 72
- }
- _ -> Nothing -- "GIF parse error"
-
-jpegSize :: ByteString -> Either String ImageSize
-jpegSize img =
- let (hdr, rest) = B.splitAt 4 img
- in if B.length rest < 14
- then Left "unable to determine JPEG size"
- else case hdr of
- "\xff\xd8\xff\xe0" -> jfifSize rest
- "\xff\xd8\xff\xe1" -> exifSize rest
- _ -> Left "unable to determine JPEG size"
-
-jfifSize :: ByteString -> Either String ImageSize
-jfifSize rest =
- let [dpiDensity,dpix1,dpix2,dpiy1,dpiy2] = map fromIntegral
- $ unpack $ B.take 5 $ B.drop 9 $ rest
- factor = case dpiDensity of
- 1 -> id
- 2 -> \x -> (x * 254 `div` 10)
- _ -> const 72
- dpix = factor (shift dpix1 8 + dpix2)
- dpiy = factor (shift dpiy1 8 + dpiy2)
- in case findJfifSize rest of
- Left msg -> Left msg
- Right (w,h) -> Right $ ImageSize { pxX = w
- , pxY = h
- , dpiX = dpix
- , dpiY = dpiy }
-
-findJfifSize :: ByteString -> Either String (Integer,Integer)
-findJfifSize bs =
- let bs' = B.dropWhile (=='\xff') $ B.dropWhile (/='\xff') bs
- in case B.uncons bs' of
- Just (c,bs'') | c >= '\xc0' && c <= '\xc3' ->
- case map fromIntegral $ unpack $ B.take 4 $ B.drop 3 bs'' of
- [h1,h2,w1,w2] -> Right (shift w1 8 + w2, shift h1 8 + h2)
- _ -> Left "JFIF parse error"
- Just (_,bs'') ->
- case map fromIntegral $ unpack $ B.take 2 bs'' of
- [c1,c2] ->
- let len = shift c1 8 + c2
- -- skip variables
- in findJfifSize $ B.drop len bs''
- _ -> Left "JFIF parse error"
- Nothing -> Left "Did not find JFIF length record"
-
-runGet' :: Get (Either String a) -> BL.ByteString -> Either String a
-runGet' p bl =
-#if MIN_VERSION_binary(0,7,0)
- case runGetOrFail p bl of
- Left (_,_,msg) -> Left msg
- Right (_,_,x) -> x
-#else
- runGet p bl
-#endif
-
-
-exifSize :: ByteString -> Either String ImageSize
-exifSize bs = runGet' header $ bl
- where bl = BL.fromChunks [bs]
- header = runExceptT $ exifHeader bl
--- NOTE: It would be nicer to do
--- runGet ((Just <$> exifHeader) <|> return Nothing)
--- which would prevent pandoc from raising an error when an exif header can't
--- be parsed. But we only get an Alternative instance for Get in binary 0.6,
--- and binary 0.5 ships with ghc 7.6.
-
-exifHeader :: BL.ByteString -> ExceptT String Get ImageSize
-exifHeader hdr = do
- _app1DataSize <- lift getWord16be
- exifHdr <- lift getWord32be
- unless (exifHdr == 0x45786966) $ throwError "Did not find exif header"
- zeros <- lift getWord16be
- unless (zeros == 0) $ throwError "Expected zeros after exif header"
- -- beginning of tiff header -- we read whole thing to use
- -- in getting data from offsets:
- let tiffHeader = BL.drop 8 hdr
- byteAlign <- lift getWord16be
- let bigEndian = byteAlign == 0x4d4d
- let (getWord16, getWord32, getWord64) =
- if bigEndian
- then (getWord16be, getWord32be, getWord64be)
- else (getWord16le, getWord32le, getWord64le)
- let getRational = do
- num <- getWord32
- den <- getWord32
- return $ fromIntegral num / fromIntegral den
- tagmark <- lift getWord16
- unless (tagmark == 0x002a) $ throwError "Failed alignment sanity check"
- ifdOffset <- lift getWord32
- lift $ skip (fromIntegral ifdOffset - 8) -- skip to IDF
- numentries <- lift getWord16
- let ifdEntry :: ExceptT String Get (TagType, DataFormat)
- ifdEntry = do
- tag <- fromMaybe UnknownTagType . flip M.lookup tagTypeTable
- <$> lift getWord16
- dataFormat <- lift getWord16
- numComponents <- lift getWord32
- (fmt, bytesPerComponent) <-
- case dataFormat of
- 1 -> return (UnsignedByte <$> getWord8, 1)
- 2 -> return (AsciiString <$>
- getLazyByteString
- (fromIntegral numComponents), 1)
- 3 -> return (UnsignedShort <$> getWord16, 2)
- 4 -> return (UnsignedLong <$> getWord32, 4)
- 5 -> return (UnsignedRational <$> getRational, 8)
- 6 -> return (SignedByte <$> getWord8, 1)
- 7 -> return (Undefined <$> getLazyByteString
- (fromIntegral numComponents), 1)
- 8 -> return (SignedShort <$> getWord16, 2)
- 9 -> return (SignedLong <$> getWord32, 4)
- 10 -> return (SignedRational <$> getRational, 8)
- 11 -> return (SingleFloat <$> getWord32 {- TODO -}, 4)
- 12 -> return (DoubleFloat <$> getWord64 {- TODO -}, 8)
- _ -> throwError $ "Unknown data format " ++ show dataFormat
- let totalBytes = fromIntegral $ numComponents * bytesPerComponent
- payload <- if totalBytes <= 4 -- data is right here
- then lift $ fmt <* skip (4 - totalBytes)
- else do -- get data from offset
- offs <- lift getWord32
- let bytesAtOffset =
- BL.take (fromIntegral totalBytes)
- $ BL.drop (fromIntegral offs) tiffHeader
- case runGet' (Right <$> fmt) bytesAtOffset of
- Left msg -> throwError msg
- Right x -> return x
- return (tag, payload)
- entries <- sequence $ replicate (fromIntegral numentries) ifdEntry
- subentries <- case lookup ExifOffset entries of
- Just (UnsignedLong offset') -> do
- pos <- lift bytesRead
- lift $ skip (fromIntegral offset' - (fromIntegral pos - 8))
- numsubentries <- lift getWord16
- sequence $
- replicate (fromIntegral numsubentries) ifdEntry
- _ -> return []
- let allentries = entries ++ subentries
- (wdth, hght) <- case (lookup ExifImageWidth allentries,
- lookup ExifImageHeight allentries) of
- (Just (UnsignedLong w), Just (UnsignedLong h)) ->
- return (fromIntegral w, fromIntegral h)
- _ -> return defaultSize
- -- we return a default width and height when
- -- the exif header doesn't contain these
- let resfactor = case lookup ResolutionUnit allentries of
- Just (UnsignedShort 1) -> (100 / 254)
- _ -> 1
- let xres = maybe 72 (\(UnsignedRational x) -> floor $ x * resfactor)
- $ lookup XResolution allentries
- let yres = maybe 72 (\(UnsignedRational x) -> floor $ x * resfactor)
- $ lookup YResolution allentries
- return $ ImageSize{
- pxX = wdth
- , pxY = hght
- , dpiX = xres
- , dpiY = yres }
-
-data DataFormat = UnsignedByte Word8
- | AsciiString BL.ByteString
- | UnsignedShort Word16
- | UnsignedLong Word32
- | UnsignedRational Rational
- | SignedByte Word8
- | Undefined BL.ByteString
- | SignedShort Word16
- | SignedLong Word32
- | SignedRational Rational
- | SingleFloat Word32
- | DoubleFloat Word64
- deriving (Show)
-
-data TagType = ImageDescription
- | Make
- | Model
- | Orientation
- | XResolution
- | YResolution
- | ResolutionUnit
- | Software
- | DateTime
- | WhitePoint
- | PrimaryChromaticities
- | YCbCrCoefficients
- | YCbCrPositioning
- | ReferenceBlackWhite
- | Copyright
- | ExifOffset
- | ExposureTime
- | FNumber
- | ExposureProgram
- | ISOSpeedRatings
- | ExifVersion
- | DateTimeOriginal
- | DateTimeDigitized
- | ComponentConfiguration
- | CompressedBitsPerPixel
- | ShutterSpeedValue
- | ApertureValue
- | BrightnessValue
- | ExposureBiasValue
- | MaxApertureValue
- | SubjectDistance
- | MeteringMode
- | LightSource
- | Flash
- | FocalLength
- | MakerNote
- | UserComment
- | FlashPixVersion
- | ColorSpace
- | ExifImageWidth
- | ExifImageHeight
- | RelatedSoundFile
- | ExifInteroperabilityOffset
- | FocalPlaneXResolution
- | FocalPlaneYResolution
- | FocalPlaneResolutionUnit
- | SensingMethod
- | FileSource
- | SceneType
- | UnknownTagType
- deriving (Show, Eq, Ord)
-
-tagTypeTable :: M.Map Word16 TagType
-tagTypeTable = M.fromList
- [ (0x010e, ImageDescription)
- , (0x010f, Make)
- , (0x0110, Model)
- , (0x0112, Orientation)
- , (0x011a, XResolution)
- , (0x011b, YResolution)
- , (0x0128, ResolutionUnit)
- , (0x0131, Software)
- , (0x0132, DateTime)
- , (0x013e, WhitePoint)
- , (0x013f, PrimaryChromaticities)
- , (0x0211, YCbCrCoefficients)
- , (0x0213, YCbCrPositioning)
- , (0x0214, ReferenceBlackWhite)
- , (0x8298, Copyright)
- , (0x8769, ExifOffset)
- , (0x829a, ExposureTime)
- , (0x829d, FNumber)
- , (0x8822, ExposureProgram)
- , (0x8827, ISOSpeedRatings)
- , (0x9000, ExifVersion)
- , (0x9003, DateTimeOriginal)
- , (0x9004, DateTimeDigitized)
- , (0x9101, ComponentConfiguration)
- , (0x9102, CompressedBitsPerPixel)
- , (0x9201, ShutterSpeedValue)
- , (0x9202, ApertureValue)
- , (0x9203, BrightnessValue)
- , (0x9204, ExposureBiasValue)
- , (0x9205, MaxApertureValue)
- , (0x9206, SubjectDistance)
- , (0x9207, MeteringMode)
- , (0x9208, LightSource)
- , (0x9209, Flash)
- , (0x920a, FocalLength)
- , (0x927c, MakerNote)
- , (0x9286, UserComment)
- , (0xa000, FlashPixVersion)
- , (0xa001, ColorSpace)
- , (0xa002, ExifImageWidth)
- , (0xa003, ExifImageHeight)
- , (0xa004, RelatedSoundFile)
- , (0xa005, ExifInteroperabilityOffset)
- , (0xa20e, FocalPlaneXResolution)
- , (0xa20f, FocalPlaneYResolution)
- , (0xa210, FocalPlaneResolutionUnit)
- , (0xa217, SensingMethod)
- , (0xa300, FileSource)
- , (0xa301, SceneType)
- ]