diff --git a/src/Text/Pandoc/ImageSize.hs b/src/Text/Pandoc/ImageSize.hs index 6e0558f8fbc4..9b7d0967e8e1 100644 --- a/src/Text/Pandoc/ImageSize.hs +++ b/src/Text/Pandoc/ImageSize.hs @@ -14,14 +14,20 @@ Functions for determining the size of a PNG, JPEG, or GIF image. -} module Text.Pandoc.ImageSize ( ImageType(..) , ImageSize(..) + , ImageTransform(..) , imageType , imageSize + , imageTransform , sizeInPixels , sizeInPoints , desiredSizeInPoints + , rotatedDesiredSizeInPoints , Dimension(..) , Direction(..) + , Flip(..) + , Rotate(..) , dimension + , rotateDirection , lengthToDim , scaleDimension , inInch @@ -41,6 +47,7 @@ import Data.Bits ((.&.), shiftR, shiftL) import Data.Word (bitReverse32) import Data.Maybe (isJust, fromJust) import Data.Char (isDigit) +import Data.Maybe (fromMaybe) import Control.Monad import Text.Pandoc.Shared (safeRead) import Data.Default (Default) @@ -56,6 +63,7 @@ import Control.Applicative import qualified Data.Attoparsec.ByteString as AW import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Codec.Picture.Metadata as Metadata +import Codec.Picture.Metadata.Exif (ExifTag(TagOrientation), ExifData(..)) import Codec.Picture (decodeImageWithMetadata) -- quick and dirty functions to get image sizes @@ -84,6 +92,26 @@ instance Show Dimension where show (Percent a) = show a ++ "%" show (Em a) = T.unpack (showFl a) ++ "em" +data Flip = NoFlip | FlipH | FlipV deriving Show + +data Rotate = R0 | R90 | R180 | R270 deriving Show + +-- There's a case to be made that this API is wrong and other code should deal +-- somewhat more directly with the 8 EXIF orientations that actually occur, +-- because as-is we could model transformations that don't occur in nature, +-- like FlipV + R270. But since mirroring and rotating are distinct operations +-- everywhere we happen to deal with it, I don't think anything is gained in +-- practice by making ImageTransform a sum type with 8 fixed cases instead of a +-- product type that theoretically allows for 4 cases we won't encounter. +data ImageTransform = ImageTransform + { tFlip :: Flip, + tRotate :: Rotate + } + deriving (Show) + +instance Default ImageTransform where + def = ImageTransform NoFlip R0 + data ImageSize = ImageSize{ pxX :: Integer , pxY :: Integer @@ -176,13 +204,29 @@ sizeInPoints s = (pxXf * 72 / dpiXf, pxYf * 72 / dpiYf) -- are specified in the attribute (or only dimensions in percentages). desiredSizeInPoints :: WriterOptions -> Attr -> ImageSize -> (Double, Double) desiredSizeInPoints opts attr s = + desiredSizeInPoints' opts attr s ratio + where + ratio = fromIntegral (pxX s) / fromIntegral (pxY s) + +-- | As desiredSizeInPoints, but swapping the width and height dimensions if +-- the indicated rotation is a quarter-turn or three-quarter-turn. +rotatedDesiredSizeInPoints :: WriterOptions -> Attr -> ImageSize -> Rotate -> (Double, Double) +rotatedDesiredSizeInPoints opts attr s r = + desiredSizeInPoints' opts attr s (ratio r) + where + ratio R0 = fromIntegral (pxX s) / fromIntegral (pxY s) + ratio R180 = fromIntegral (pxX s) / fromIntegral (pxY s) + ratio R90 = fromIntegral (pxY s) / fromIntegral (pxX s) + ratio R270 = fromIntegral (pxY s) / fromIntegral (pxX s) + +desiredSizeInPoints' :: WriterOptions -> Attr -> ImageSize -> Double -> (Double, Double) +desiredSizeInPoints' opts attr s ratio = 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, 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 @@ -451,3 +495,32 @@ webpSize opts img = case AW.parseOnly pWebpSize img of Left _ -> Nothing Right sz -> Just sz { dpiX = fromIntegral $ writerDpi opts, dpiY = fromIntegral $ writerDpi opts} + +imageTransform :: ByteString -> ImageTransform +imageTransform img = + case decodeImageWithMetadata img of + Left _ -> def + Right (_, meta) -> + let orient = fromMaybe ExifNone $ Metadata.lookup (Metadata.Exif TagOrientation) meta + in exifToTransform (word orient) + where + word ExifNone = 1 + word (ExifShort w) = w + word _ = 1 + exifToTransform 1 = def + exifToTransform 2 = def{tFlip = FlipH} + exifToTransform 3 = def{tRotate = R180} + exifToTransform 4 = def{tFlip = FlipV} + exifToTransform 5 = def{tFlip = FlipH, tRotate = R270} + exifToTransform 6 = def{tRotate = R90} + exifToTransform 7 = def{tFlip = FlipH, tRotate = R90} + exifToTransform 8 = def{tRotate = R270} + exifToTransform _ = def + +rotateDirection :: Rotate -> Direction -> Direction +rotateDirection R0 x = x +rotateDirection R90 Width = Height +rotateDirection R90 Height = Width +rotateDirection R180 x = x +rotateDirection R270 Width = Height +rotateDirection R270 Height = Width diff --git a/src/Text/Pandoc/Writers/Docx/OpenXML.hs b/src/Text/Pandoc/Writers/Docx/OpenXML.hs index e3900f32f1a3..11da77e804ff 100644 --- a/src/Text/Pandoc/Writers/Docx/OpenXML.hs +++ b/src/Text/Pandoc/Writers/Docx/OpenXML.hs @@ -941,13 +941,24 @@ inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do [extLst]) _ -> return ([("r:embed", T.pack ident)], []) let - (xpt,ypt) = desiredSizeInPoints opts attr - (either (const def) id (imageSize opts img)) + transform = imageTransform img + (xpt,ypt) = rotatedDesiredSizeInPoints opts attr + (either (const def) id (imageSize opts img)) (tRotate transform) -- 12700 emu = 1 pt pageWidthPt = case dimension Width attr of Just (Percent a) -> pageWidth * floor (a * 127) _ -> pageWidth * 12700 (xemu,yemu) = fitToPage (xpt * 12700, ypt * 12700) pageWidthPt + height = case tRotate transform of + R0 -> tshow xemu + R90 -> tshow yemu + R180 -> tshow xemu + R270 -> tshow yemu + width = case tRotate transform of + R0 -> tshow yemu + R90 -> tshow xemu + R180 -> tshow yemu + R270 -> tshow xemu cNvPicPr = mknode "pic:cNvPicPr" [] $ mknode "a:picLocks" [("noChangeArrowheads","1") ,("noChangeAspect","1")] () @@ -962,10 +973,19 @@ inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do , mknode "a:stretch" [] $ mknode "a:fillRect" [] () ] - xfrm = mknode "a:xfrm" [] + xfrmFlip NoFlip = [] + xfrmFlip FlipH = [("flipH", "1")] + xfrmFlip FlipV = [("flipV", "1")] + -- 60,000ths of a degree + xfrmRot R0 = [] + xfrmRot R90 = [("rot", "5400000")] + xfrmRot R180 = [("rot", "10800000")] + xfrmRot R270 = [("rot", "16200000")] + + xfrm = mknode "a:xfrm" ((xfrmFlip (tFlip transform)) <> (xfrmRot (tRotate transform))) [ mknode "a:off" [("x","0"),("y","0")] () - , mknode "a:ext" [("cx",tshow xemu) - ,("cy",tshow yemu)] () ] + , mknode "a:ext" [("cx",height) + ,("cy",width)] () ] prstGeom = mknode "a:prstGeom" [("prst","rect")] $ mknode "a:avLst" [] () ln = mknode "a:ln" [("w","9525")] @@ -986,7 +1006,7 @@ inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do imgElt = mknode "w:r" [] $ mknode "w:drawing" [] $ mknode "wp:inline" [] - [ mknode "wp:extent" [("cx",tshow xemu),("cy",tshow yemu)] () + [ mknode "wp:extent" [("cx",height),("cy",width)] () , mknode "wp:effectExtent" [("b","0"),("l","0"),("r","0"),("t","0")] () , mknode "wp:docPr" diff --git a/src/Text/Pandoc/Writers/LaTeX.hs b/src/Text/Pandoc/Writers/LaTeX.hs index 40cdb73e2393..69210b86cdd8 100644 --- a/src/Text/Pandoc/Writers/LaTeX.hs +++ b/src/Text/Pandoc/Writers/LaTeX.hs @@ -30,6 +30,7 @@ import Control.Monad liftM, when, unless ) +import Control.Monad.Except (catchError) import Data.Containers.ListUtils (nubOrd) import Data.Char (isDigit) import Data.List (intersperse, (\\)) @@ -41,7 +42,7 @@ import qualified Data.Text as T import Network.URI (unEscapeString) import Text.DocTemplates (FromContext(lookupContext), renderTemplate) import Text.Collate.Lang (renderLang, Lang(langLanguage)) -import Text.Pandoc.Class.PandocMonad (PandocMonad, report, toLang) +import Text.Pandoc.Class.PandocMonad (PandocMonad, report, toLang, fetchItem) import Text.Pandoc.Definition import Text.Pandoc.Highlighting (formatLaTeXBlock, formatLaTeXInline, highlight, styleToLaTeX) @@ -1075,13 +1076,15 @@ inlineToLaTeX (Image attr@(_,_,kvs) _ (source, _)) = do modify $ \s -> s{ stGraphics = True , stSVG = stSVG s || isSVG } opts <- gets stOptions - let showDim dir = let d = text (show dir) <> "=" - in case dimension dir attr of + (ImageTransform flp rot) <- catchError ((imageTransform . fst) <$> fetchItem source) (\_ -> return def) + let redir = rotateDirection rot + showDim dir = let d = text (show dir) <> "=" + in case dimension (redir dir) attr of Just (Pixel a) -> [d <> literal (showInInch opts (Pixel a)) <> "in"] Just (Percent a) -> [d <> literal (showFl (a / 100)) <> - case dir of + case (redir dir) of Width -> "\\linewidth" Height -> "\\textheight" ] @@ -1089,9 +1092,9 @@ inlineToLaTeX (Image attr@(_,_,kvs) _ (source, _)) = do [d <> text (show dim)] Nothing -> case dir of - Width | isJust (dimension Height attr) -> + Width | isJust (dimension (redir Height) attr) -> [d <> "\\linewidth"] - Height | isJust (dimension Width attr) -> + Height | isJust (dimension (redir Width) attr) -> [d <> "\\textheight"] _ -> [] optList = showDim Width <> showDim Height <> @@ -1107,6 +1110,21 @@ inlineToLaTeX (Image attr@(_,_,kvs) _ (source, _)) = do source' = if isURI source then source else T.pack $ unEscapeString $ T.unpack source + -- For images in vertically-mirrored exif orientation, \scalebox{1}[-1] + -- will reflect them to below the baseline. It seems like to get the + -- final image to be aligned with the baseline again, we have to rotate + -- it 180 degrees from the default (baseline) origin and then rotate + -- it again about the center so it's facing right side up. In real life + -- where there's only 8 possible EXIF orientations, there's never any + -- other rotation required for a vertical flip, but I think it would + -- still interact correctly if it somehow came up. + flipbox NoFlip x = x + flipbox FlipH x = "\\scalebox{-1}[1]" <> braces x + flipbox FlipV x = "\\rotatebox[origin=c]{180}{\\rotatebox{180}{{\\scalebox{1}[-1]" <> braces x <> "}}}" + rotatebox R0 x = x + rotatebox R90 x = "\\rotatebox[origin=br]{-90}" <> braces x + rotatebox R180 x = "\\rotatebox[origin=c]{180}" <> braces x + rotatebox R270 x = "\\rotatebox[origin=bl]{-270}" <> braces x source'' <- stringToLaTeX URLString source' inHeading <- gets stInHeading return $ @@ -1114,7 +1132,8 @@ inlineToLaTeX (Image attr@(_,_,kvs) _ (source, _)) = do (case dimension Width attr `mplus` dimension Height attr of Just _ -> id Nothing -> ("\\pandocbounded" <>) . braces) - ((if isSVG then "\\includesvg" else "\\includegraphics") <> + ((rotatebox rot . flipbox flp) $ + (if isSVG then "\\includesvg" else "\\includegraphics") <> options <> braces (literal source'')) inlineToLaTeX (Note contents) = do setEmptyLine False diff --git a/src/Text/Pandoc/Writers/ODT.hs b/src/Text/Pandoc/Writers/ODT.hs index 29ee3bd47b86..eb3a1b058246 100644 --- a/src/Text/Pandoc/Writers/ODT.hs +++ b/src/Text/Pandoc/Writers/ODT.hs @@ -257,11 +257,19 @@ transformPicMath opts (Image attr@(id', cls, _) lab (src,t)) = catchError Left msg -> do report $ CouldNotDetermineImageSize src msg return (100, 100) + let (ImageTransform flp rot) = imageTransform img + let xflip NoFlip = ("mirror", "none") + xflip FlipH = ("mirror", "horizontal") + xflip FlipV = ("mirror", "vertical") + let xrotate R0 = ("rotate", "rotate(0)") + xrotate R90 = ("rotate", "rotate(" <> showFl (3*(pi :: Double)/2) <> ")") + xrotate R180 = ("rotate", "rotate(" <> showFl (pi :: Double) <> ")") + xrotate R270 = ("rotate", "rotate(" <> showFl ((pi :: Double) /2) <> ")") let dims = case (getDim Width, getDim Height) of (Just w, Just h) -> [("width", tshow w), ("height", tshow h)] (Just w@(Percent _), Nothing) -> [("rel-width", tshow w),("rel-height", "scale"),("width", tshow ptX <> "pt"),("height", tshow ptY <> "pt")] - (Nothing, Just h@(Percent _)) -> [("rel-width", "scale"),("rel-height", tshow h),("width", tshow ptX <> "pt"),("height", tshow ptY <> "pt")] + (Nothing, Just h@(Percent _)) -> [("rel-width", "scale"),("rel-height", tshow h),("width", tshow ptY <> "pt"),("height", tshow ptY <> "pt")] (Just w@(Inch i), Nothing) -> [("width", tshow w), ("height", tshow (i / ratio) <> "in")] (Nothing, Just h@(Inch i)) -> [("width", tshow (i * ratio) <> "in"), ("height", tshow h)] _ -> [("width", tshow ptX <> "pt"), ("height", tshow ptY <> "pt")] @@ -271,7 +279,7 @@ transformPicMath opts (Image attr@(id', cls, _) lab (src,t)) = catchError Just (Percent i) -> Just $ Percent i Just dim -> Just $ Inch $ inInch opts dim Nothing -> Nothing - let newattr = (id', cls, dims) + let newattr = (id', cls, (xflip flp):(xrotate rot):dims) src' <- if writerLinkImages opts then case T.unpack src of diff --git a/src/Text/Pandoc/Writers/OpenDocument.hs b/src/Text/Pandoc/Writers/OpenDocument.hs index 85df9c70131e..2e2accb32f78 100644 --- a/src/Text/Pandoc/Writers/OpenDocument.hs +++ b/src/Text/Pandoc/Writers/OpenDocument.hs @@ -198,6 +198,15 @@ formulaStyle mt = inTags False "style:style" ,("style:horizontal-rel", "paragraph-content") ,("style:wrap", "none")] +imageStyles :: [Doc Text] +imageStyles = [mirror "horizontal", mirror "vertical"] + where + mirror hv = inTags False "style:style" + [("style:name", "mirror-" <> hv) + ,("style:family", "graphic") + ,("style:parent-style-name", "Graphics")] + $ selfClosingTag "style:graphic-properties" [("style:mirror", hv)] + inBookmarkTags :: Text -> Doc Text -> Doc Text inBookmarkTags ident d = selfClosingTag "text:bookmark-start" [ ("text:name", ident) ] @@ -260,7 +269,7 @@ writeOpenDocument opts (Pandoc meta blocks) = do meta' b <- blocksToOpenDocument opts blocks return (b, m) - let styles = stTableStyles s ++ stParaStyles s ++ formulaStyles ++ + let styles = stTableStyles s ++ stParaStyles s ++ formulaStyles ++ imageStyles ++ map snd (sortBy (flip (comparing fst)) ( Map.elems (stTextStyles s))) listStyle (n,l) = inTags True "text:list-style" @@ -659,6 +668,9 @@ inlineToOpenDocument o ils id' <- gets stImageId modify (\st -> st{ stImageId = id' + 1 }) let getDims [] = [] + getDims (("mirror", "none") :xs) = getDims xs + getDims (("mirror", t) :xs) = ("draw:style-name", "mirror-" <> t) : getDims xs + getDims (("rotate", t) :xs) = ("draw:transform", t) : getDims xs getDims (("width", w) :xs) = ("svg:width", w) : getDims xs getDims (("rel-width", w):xs) = ("style:rel-width", w) : getDims xs getDims (("height", h):xs) = ("svg:height", h) : getDims xs diff --git a/test/command/6792.md b/test/command/6792.md index d0d284f5dfd6..f477fc3e6db7 100644 --- a/test/command/6792.md +++ b/test/command/6792.md @@ -22,6 +22,8 @@ + + diff --git a/test/command/8256.md b/test/command/8256.md index 76398f607ae6..e0cbe8b02a11 100644 --- a/test/command/8256.md +++ b/test/command/8256.md @@ -17,6 +17,8 @@ Testing. + + diff --git a/test/writer.opendocument b/test/writer.opendocument index f10f3be2a0ce..4e5d00622c43 100644 --- a/test/writer.opendocument +++ b/test/writer.opendocument @@ -1017,6 +1017,8 @@ + +