{-# LANGUAGE PatternGuards #-} {- Copyright (C) 2017-2018 Jesse Rosenthal 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.Writers.Powerpoint.Output Copyright : Copyright (C) 2017-2018 Jesse Rosenthal License : GNU GPL, version 2 or above Maintainer : Jesse Rosenthal Stability : alpha Portability : portable Conversion of Presentation datatype (defined in Text.Pandoc.Writers.Powerpoint.Presentation) to a zip archive. -} module Text.Pandoc.Writers.Powerpoint.Output ( presentationToArchive ) where import Control.Monad.Except (throwError, catchError) import Control.Monad.Reader import Control.Monad.State import Codec.Archive.Zip import Data.Char (toUpper) import Data.List (intercalate, stripPrefix, nub, union, isPrefixOf, intersperse) import Data.Default import Text.Pandoc.Compat.Time (formatTime, defaultTimeLocale) import Data.Time.Clock (UTCTime) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, posixSecondsToUTCTime) import System.FilePath.Posix (splitDirectories, splitExtension, takeExtension) import Text.XML.Light import Text.Pandoc.Definition import qualified Text.Pandoc.UTF8 as UTF8 import Text.Pandoc.Class (PandocMonad) import Text.Pandoc.Error (PandocError(..)) import qualified Text.Pandoc.Class as P import Text.Pandoc.Options import Text.Pandoc.MIME import qualified Data.ByteString.Lazy as BL import Text.Pandoc.Writers.OOXML import qualified Data.Map as M import Data.Maybe (mapMaybe, listToMaybe, fromMaybe, isJust, maybeToList, catMaybes) import Text.Pandoc.ImageSize import Control.Applicative ((<|>)) import System.FilePath.Glob import Text.TeXMath import Text.Pandoc.Writers.Math (convertMath) import Text.Pandoc.Writers.Powerpoint.Presentation import Skylighting (fromColor) -- This populates the global ids map with images already in the -- template, so the ids won't be used by images introduced by the -- user. initialGlobalIds :: Archive -> Archive -> M.Map FilePath Int initialGlobalIds refArchive distArchive = let archiveFiles = filesInArchive refArchive `union` filesInArchive distArchive mediaPaths = filter (isPrefixOf "ppt/media/image") archiveFiles go :: FilePath -> Maybe (FilePath, Int) go fp = do s <- stripPrefix "ppt/media/image" $ fst $ splitExtension fp (n, _) <- listToMaybe $ reads s return (fp, n) in M.fromList $ mapMaybe go mediaPaths getPresentationSize :: Archive -> Archive -> Maybe (Integer, Integer) getPresentationSize refArchive distArchive = do entry <- findEntryByPath "ppt/presentation.xml" refArchive `mplus` findEntryByPath "ppt/presentation.xml" distArchive presElement <- parseXMLDoc $ UTF8.toStringLazy $ fromEntry entry let ns = elemToNameSpaces presElement sldSize <- findChild (elemName ns "p" "sldSz") presElement cxS <- findAttr (QName "cx" Nothing Nothing) sldSize cyS <- findAttr (QName "cy" Nothing Nothing) sldSize (cx, _) <- listToMaybe $ reads cxS :: Maybe (Integer, String) (cy, _) <- listToMaybe $ reads cyS :: Maybe (Integer, String) return (cx `div` 12700, cy `div` 12700) data WriterEnv = WriterEnv { envRefArchive :: Archive , envDistArchive :: Archive , envUTCTime :: UTCTime , envOpts :: WriterOptions , envPresentationSize :: (Integer, Integer) , envSlideHasHeader :: Bool , envInList :: Bool , envInNoteSlide :: Bool , envCurSlideId :: Int -- the difference between the number at -- the end of the slide file name and -- the rId number , envSlideIdOffset :: Int , envContentType :: ContentType , envSlideIdMap :: M.Map SlideId Int -- maps the slide number to the -- corresponding notes id number. If there -- are no notes for a slide, there will be -- no entry in the map for it. , envSpeakerNotesIdMap :: M.Map Int Int } deriving (Show) instance Default WriterEnv where def = WriterEnv { envRefArchive = emptyArchive , envDistArchive = emptyArchive , envUTCTime = posixSecondsToUTCTime 0 , envOpts = def , envPresentationSize = (720, 540) , envSlideHasHeader = False , envInList = False , envInNoteSlide = False , envCurSlideId = 1 , envSlideIdOffset = 1 , envContentType = NormalContent , envSlideIdMap = mempty , envSpeakerNotesIdMap = mempty } data ContentType = NormalContent | TwoColumnLeftContent | TwoColumnRightContent deriving (Show, Eq) data MediaInfo = MediaInfo { mInfoFilePath :: FilePath , mInfoLocalId :: Int , mInfoGlobalId :: Int , mInfoMimeType :: Maybe MimeType , mInfoExt :: Maybe String , mInfoCaption :: Bool } deriving (Show, Eq) data WriterState = WriterState { stLinkIds :: M.Map Int (M.Map Int LinkTarget) -- (FP, Local ID, Global ID, Maybe Mime) , stMediaIds :: M.Map Int [MediaInfo] , stMediaGlobalIds :: M.Map FilePath Int } deriving (Show, Eq) instance Default WriterState where def = WriterState { stLinkIds = mempty , stMediaIds = mempty , stMediaGlobalIds = mempty } type P m = ReaderT WriterEnv (StateT WriterState m) runP :: Monad m => WriterEnv -> WriterState -> P m a -> m a runP env st p = evalStateT (runReaderT p env) st -------------------------------------------------------------------- copyFileToArchive :: PandocMonad m => Archive -> FilePath -> P m Archive copyFileToArchive arch fp = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive case findEntryByPath fp refArchive `mplus` findEntryByPath fp distArchive of Nothing -> fail $ fp ++ " missing in reference file" Just e -> return $ addEntryToArchive e arch alwaysInheritedPatterns :: [Pattern] alwaysInheritedPatterns = map compile [ "docProps/app.xml" , "ppt/slideLayouts/slideLayout*.xml" , "ppt/slideLayouts/_rels/slideLayout*.xml.rels" , "ppt/slideMasters/slideMaster1.xml" , "ppt/slideMasters/_rels/slideMaster1.xml.rels" , "ppt/theme/theme1.xml" , "ppt/theme/_rels/theme1.xml.rels" , "ppt/presProps.xml" , "ppt/viewProps.xml" , "ppt/tableStyles.xml" , "ppt/media/image*" ] -- We only look for these under special conditions contingentInheritedPatterns :: Presentation -> [Pattern] contingentInheritedPatterns pres = [] ++ if presHasSpeakerNotes pres then map compile [ "ppt/notesMasters/notesMaster*.xml" , "ppt/notesMasters/_rels/notesMaster*.xml.rels" , "ppt/theme/theme2.xml" , "ppt/theme/_rels/theme2.xml.rels" ] else [] inheritedPatterns :: Presentation -> [Pattern] inheritedPatterns pres = alwaysInheritedPatterns ++ contingentInheritedPatterns pres patternToFilePaths :: PandocMonad m => Pattern -> P m [FilePath] patternToFilePaths pat = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive let archiveFiles = filesInArchive refArchive `union` filesInArchive distArchive return $ filter (match pat) archiveFiles patternsToFilePaths :: PandocMonad m => [Pattern] -> P m [FilePath] patternsToFilePaths pats = concat <$> mapM patternToFilePaths pats -- Here are the files we'll require to make a Powerpoint document. If -- any of these are missing, we should error out of our build. requiredFiles :: [FilePath] requiredFiles = [ "docProps/app.xml" , "ppt/presProps.xml" , "ppt/slideLayouts/slideLayout1.xml" , "ppt/slideLayouts/_rels/slideLayout1.xml.rels" , "ppt/slideLayouts/slideLayout2.xml" , "ppt/slideLayouts/_rels/slideLayout2.xml.rels" , "ppt/slideLayouts/slideLayout3.xml" , "ppt/slideLayouts/_rels/slideLayout3.xml.rels" , "ppt/slideLayouts/slideLayout4.xml" , "ppt/slideLayouts/_rels/slideLayout4.xml.rels" , "ppt/slideMasters/slideMaster1.xml" , "ppt/slideMasters/_rels/slideMaster1.xml.rels" , "ppt/theme/theme1.xml" , "ppt/viewProps.xml" , "ppt/tableStyles.xml" ] presentationToArchiveP :: PandocMonad m => Presentation -> P m Archive presentationToArchiveP p@(Presentation docProps slides) = do filePaths <- patternsToFilePaths $ inheritedPatterns p -- make sure all required files are available: let missingFiles = filter (\fp -> not (fp `elem` filePaths)) requiredFiles unless (null missingFiles) (throwError $ PandocSomeError $ "The following required files are missing:\n" ++ (unlines $ map (" " ++) missingFiles) ) newArch' <- foldM copyFileToArchive emptyArchive filePaths -- we make a docProps/core.xml entry out of the presentation docprops docPropsEntry <- docPropsToEntry docProps -- we make this ourself in case there's something unexpected in the -- one in the reference doc. relsEntry <- topLevelRelsEntry -- presentation entry and rels. We have to do the rels first to make -- sure we know the correct offset for the rIds. presEntry <- presentationToPresEntry p presRelsEntry <- presentationToRelsEntry p slideEntries <- mapM slideToEntry slides slideRelEntries <- mapM slideToSlideRelEntry slides spkNotesEntries <- catMaybes <$> mapM slideToSpeakerNotesEntry slides spkNotesRelEntries <- catMaybes <$> mapM slideToSpeakerNotesRelEntry slides -- These have to come after everything, because they need the info -- built up in the state. mediaEntries <- makeMediaEntries contentTypesEntry <- presentationToContentTypes p >>= contentTypesToEntry -- fold everything into our inherited archive and return it. return $ foldr addEntryToArchive newArch' $ slideEntries ++ slideRelEntries ++ spkNotesEntries ++ spkNotesRelEntries ++ mediaEntries ++ [contentTypesEntry, docPropsEntry, relsEntry, presEntry, presRelsEntry] makeSlideIdMap :: Presentation -> M.Map SlideId Int makeSlideIdMap (Presentation _ slides) = M.fromList $ (map slideId slides) `zip` [1..] makeSpeakerNotesMap :: Presentation -> M.Map Int Int makeSpeakerNotesMap (Presentation _ slides) = M.fromList $ (mapMaybe f $ slides `zip` [1..]) `zip` [1..] where f (Slide _ _ Nothing, _) = Nothing f (Slide _ _ (Just _), n) = Just n presentationToArchive :: PandocMonad m => WriterOptions -> Presentation -> m Archive presentationToArchive opts pres = do distArchive <- (toArchive . BL.fromStrict) <$> P.readDefaultDataFile "reference.pptx" refArchive <- case writerReferenceDoc opts of Just f -> toArchive <$> P.readFileLazy f Nothing -> (toArchive . BL.fromStrict) <$> P.readDataFile "reference.pptx" utctime <- P.getCurrentTime presSize <- case getPresentationSize refArchive distArchive of Just sz -> return sz Nothing -> throwError $ PandocSomeError $ "Could not determine presentation size" let env = def { envRefArchive = refArchive , envDistArchive = distArchive , envUTCTime = utctime , envOpts = opts , envPresentationSize = presSize , envSlideIdMap = makeSlideIdMap pres , envSpeakerNotesIdMap = makeSpeakerNotesMap pres } let st = def { stMediaGlobalIds = initialGlobalIds refArchive distArchive } runP env st $ presentationToArchiveP pres -------------------------------------------------- -- Check to see if the presentation has speaker notes. This will -- influence whether we import the notesMaster template. presHasSpeakerNotes :: Presentation -> Bool presHasSpeakerNotes (Presentation _ slides) = any isJust $ map slideSpeakerNotes slides curSlideHasSpeakerNotes :: PandocMonad m => P m Bool curSlideHasSpeakerNotes = do sldId <- asks envCurSlideId notesIdMap <- asks envSpeakerNotesIdMap return $ isJust $ M.lookup sldId notesIdMap -------------------------------------------------- getLayout :: PandocMonad m => Layout -> P m Element getLayout layout = do let layoutpath = case layout of (MetadataSlide _ _ _ _) -> "ppt/slideLayouts/slideLayout1.xml" (TitleSlide _) -> "ppt/slideLayouts/slideLayout3.xml" (ContentSlide _ _) -> "ppt/slideLayouts/slideLayout2.xml" (TwoColumnSlide _ _ _) -> "ppt/slideLayouts/slideLayout4.xml" distArchive <- asks envDistArchive root <- case findEntryByPath layoutpath distArchive of Just e -> case parseXMLDoc $ UTF8.toStringLazy $ fromEntry e of Just element -> return $ element Nothing -> throwError $ PandocSomeError $ layoutpath ++ " corrupt in reference file" Nothing -> throwError $ PandocSomeError $ layoutpath ++ " missing in reference file" return root shapeHasId :: NameSpaces -> String -> Element -> Bool shapeHasId ns ident element | Just nvSpPr <- findChild (elemName ns "p" "nvSpPr") element , Just cNvPr <- findChild (elemName ns "p" "cNvPr") nvSpPr , Just nm <- findAttr (QName "id" Nothing Nothing) cNvPr = nm == ident | otherwise = False -- The content shape in slideLayout2 (Title/Content) has id=3 In -- slideLayout4 (two column) the left column is id=3, and the right -- column is id=4. getContentShape :: PandocMonad m => NameSpaces -> Element -> P m Element getContentShape ns spTreeElem | isElem ns "p" "spTree" spTreeElem = do contentType <- asks envContentType let idx = case contentType of NormalContent -> "1" TwoColumnLeftContent -> "1" TwoColumnRightContent -> "2" case getShapeByPlaceHolderIndex ns spTreeElem idx of Just e -> return e Nothing -> throwError $ PandocSomeError $ "Could not find shape for Powerpoint content" getContentShape _ _ = throwError $ PandocSomeError $ "Attempted to find content on non shapeTree" getShapeDimensions :: NameSpaces -> Element -> Maybe ((Integer, Integer), (Integer, Integer)) getShapeDimensions ns element | isElem ns "p" "sp" element = do spPr <- findChild (elemName ns "p" "spPr") element xfrm <- findChild (elemName ns "a" "xfrm") spPr off <- findChild (elemName ns "a" "off") xfrm xS <- findAttr (QName "x" Nothing Nothing) off yS <- findAttr (QName "y" Nothing Nothing) off ext <- findChild (elemName ns "a" "ext") xfrm cxS <- findAttr (QName "cx" Nothing Nothing) ext cyS <- findAttr (QName "cy" Nothing Nothing) ext (x, _) <- listToMaybe $ reads xS (y, _) <- listToMaybe $ reads yS (cx, _) <- listToMaybe $ reads cxS (cy, _) <- listToMaybe $ reads cyS return $ ((x `div` 12700, y `div` 12700), (cx `div` 12700, cy `div` 12700)) | otherwise = Nothing getMasterShapeDimensionsById :: String -> Element -> Maybe ((Integer, Integer), (Integer, Integer)) getMasterShapeDimensionsById ident master = do let ns = elemToNameSpaces master cSld <- findChild (elemName ns "p" "cSld") master spTree <- findChild (elemName ns "p" "spTree") cSld sp <- filterChild (\e -> (isElem ns "p" "sp" e) && (shapeHasId ns ident e)) spTree getShapeDimensions ns sp getContentShapeSize :: PandocMonad m => NameSpaces -> Element -> Element -> P m ((Integer, Integer), (Integer, Integer)) getContentShapeSize ns layout master | isElem ns "p" "sldLayout" layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do sp <- getContentShape ns spTree case getShapeDimensions ns sp of Just sz -> return sz Nothing -> do let mbSz = findChild (elemName ns "p" "nvSpPr") sp >>= findChild (elemName ns "p" "cNvPr") >>= findAttr (QName "id" Nothing Nothing) >>= flip getMasterShapeDimensionsById master case mbSz of Just sz' -> return sz' Nothing -> throwError $ PandocSomeError $ "Couldn't find necessary content shape size" getContentShapeSize _ _ _ = throwError $ PandocSomeError $ "Attempted to find content shape size in non-layout" replaceNamedChildren :: NameSpaces -> String -> String -> [Element] -> Element -> Element replaceNamedChildren ns prefix name newKids element = element { elContent = concat $ fun True $ elContent element } where fun :: Bool -> [Content] -> [[Content]] fun _ [] = [] fun switch ((Elem e) : conts) | isElem ns prefix name e = if switch then (map Elem $ newKids) : fun False conts else fun False conts fun switch (cont : conts) = [cont] : fun switch conts ---------------------------------------------------------------- registerLink :: PandocMonad m => LinkTarget -> P m Int registerLink link = do curSlideId <- asks envCurSlideId linkReg <- gets stLinkIds mediaReg <- gets stMediaIds hasSpeakerNotes <- curSlideHasSpeakerNotes let maxLinkId = case M.lookup curSlideId linkReg of Just mp -> case M.keys mp of [] -> if hasSpeakerNotes then 2 else 1 ks -> maximum ks Nothing -> if hasSpeakerNotes then 2 else 1 maxMediaId = case M.lookup curSlideId mediaReg of Just [] -> if hasSpeakerNotes then 2 else 1 Just mInfos -> maximum $ map mInfoLocalId mInfos Nothing -> if hasSpeakerNotes then 2 else 1 maxId = max maxLinkId maxMediaId slideLinks = case M.lookup curSlideId linkReg of Just mp -> M.insert (maxId + 1) link mp Nothing -> M.singleton (maxId + 1) link modify $ \st -> st{ stLinkIds = M.insert curSlideId slideLinks linkReg} return $ maxId + 1 registerMedia :: PandocMonad m => FilePath -> [ParaElem] -> P m MediaInfo registerMedia fp caption = do curSlideId <- asks envCurSlideId linkReg <- gets stLinkIds mediaReg <- gets stMediaIds globalIds <- gets stMediaGlobalIds hasSpeakerNotes <- curSlideHasSpeakerNotes let maxLinkId = case M.lookup curSlideId linkReg of Just mp -> case M.keys mp of [] -> if hasSpeakerNotes then 2 else 1 ks -> maximum ks Nothing -> if hasSpeakerNotes then 2 else 1 maxMediaId = case M.lookup curSlideId mediaReg of Just [] -> if hasSpeakerNotes then 2 else 1 Just mInfos -> maximum $ map mInfoLocalId mInfos Nothing -> if hasSpeakerNotes then 2 else 1 maxLocalId = max maxLinkId maxMediaId maxGlobalId = case M.elems globalIds of [] -> 0 ids -> maximum ids (imgBytes, mbMt) <- P.fetchItem fp let imgExt = (mbMt >>= extensionFromMimeType >>= (\x -> return $ '.':x)) <|> case imageType imgBytes of Just Png -> Just ".png" Just Jpeg -> Just ".jpeg" Just Gif -> Just ".gif" Just Pdf -> Just ".pdf" Just Eps -> Just ".eps" Just Svg -> Just ".svg" Just Emf -> Just ".emf" Nothing -> Nothing let newGlobalId = case M.lookup fp globalIds of Just ident -> ident Nothing -> maxGlobalId + 1 let newGlobalIds = M.insert fp newGlobalId globalIds let mediaInfo = MediaInfo { mInfoFilePath = fp , mInfoLocalId = maxLocalId + 1 , mInfoGlobalId = newGlobalId , mInfoMimeType = mbMt , mInfoExt = imgExt , mInfoCaption = (not . null) caption } let slideMediaInfos = case M.lookup curSlideId mediaReg of Just minfos -> mediaInfo : minfos Nothing -> [mediaInfo] modify $ \st -> st{ stMediaIds = M.insert curSlideId slideMediaInfos mediaReg , stMediaGlobalIds = newGlobalIds } return mediaInfo makeMediaEntry :: PandocMonad m => MediaInfo -> P m Entry makeMediaEntry mInfo = do epochtime <- (floor . utcTimeToPOSIXSeconds) <$> asks envUTCTime (imgBytes, _) <- P.fetchItem (mInfoFilePath mInfo) let ext = case mInfoExt mInfo of Just e -> e Nothing -> "" let fp = "ppt/media/image" ++ (show $ mInfoGlobalId mInfo) ++ ext return $ toEntry fp epochtime $ BL.fromStrict imgBytes makeMediaEntries :: PandocMonad m => P m [Entry] makeMediaEntries = do mediaInfos <- gets stMediaIds let allInfos = mconcat $ M.elems mediaInfos mapM makeMediaEntry allInfos -- -- | Scales the image to fit the page -- -- sizes are passed in emu -- fitToPage' :: (Double, Double) -- image size in emu -- -> Integer -- pageWidth -- -> Integer -- pageHeight -- -> (Integer, Integer) -- imagesize -- fitToPage' (x, y) pageWidth pageHeight -- -- Fixes width to the page width and scales the height -- | x <= fromIntegral pageWidth && y <= fromIntegral pageHeight = -- (floor x, floor y) -- | x / fromIntegral pageWidth > y / fromIntegral pageWidth = -- (pageWidth, floor $ ((fromIntegral pageWidth) / x) * y) -- | otherwise = -- (floor $ ((fromIntegral pageHeight) / y) * x, pageHeight) -- positionImage :: (Double, Double) -> Integer -> Integer -> (Integer, Integer) -- positionImage (x, y) pageWidth pageHeight = -- let (x', y') = fitToPage' (x, y) pageWidth pageHeight -- in -- ((pageWidth - x') `div` 2, (pageHeight - y') `div` 2) getMaster :: PandocMonad m => P m Element getMaster = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive parseXml refArchive distArchive "ppt/slideMasters/slideMaster1.xml" -- We want to get the header dimensions, so we can make sure that the -- image goes underneath it. We only use this in a content slide if it -- has a header. -- getHeaderSize :: PandocMonad m => P m ((Integer, Integer), (Integer, Integer)) -- getHeaderSize = do -- master <- getMaster -- let ns = elemToNameSpaces master -- sps = [master] >>= -- findChildren (elemName ns "p" "cSld") >>= -- findChildren (elemName ns "p" "spTree") >>= -- findChildren (elemName ns "p" "sp") -- mbXfrm = -- listToMaybe (filter (shapeHasName ns "Title Placeholder 1") sps) >>= -- findChild (elemName ns "p" "spPr") >>= -- findChild (elemName ns "a" "xfrm") -- xoff = mbXfrm >>= -- findChild (elemName ns "a" "off") >>= -- findAttr (QName "x" Nothing Nothing) >>= -- (listToMaybe . (\s -> reads s :: [(Integer, String)])) -- yoff = mbXfrm >>= -- findChild (elemName ns "a" "off") >>= -- findAttr (QName "y" Nothing Nothing) >>= -- (listToMaybe . (\s -> reads s :: [(Integer, String)])) -- xext = mbXfrm >>= -- findChild (elemName ns "a" "ext") >>= -- findAttr (QName "cx" Nothing Nothing) >>= -- (listToMaybe . (\s -> reads s :: [(Integer, String)])) -- yext = mbXfrm >>= -- findChild (elemName ns "a" "ext") >>= -- findAttr (QName "cy" Nothing Nothing) >>= -- (listToMaybe . (\s -> reads s :: [(Integer, String)])) -- off = case xoff of -- Just (xoff', _) | Just (yoff',_) <- yoff -> (xoff', yoff') -- _ -> (1043490, 1027664) -- ext = case xext of -- Just (xext', _) | Just (yext',_) <- yext -> (xext', yext') -- _ -> (7024744, 1143000) -- return $ (off, ext) -- Hard-coded for now -- captionPosition :: ((Integer, Integer), (Integer, Integer)) -- captionPosition = ((457200, 6061972), (8229600, 527087)) captionHeight :: Integer captionHeight = 40 createCaption :: PandocMonad m => ((Integer, Integer), (Integer, Integer)) -> [ParaElem] -> P m Element createCaption contentShapeDimensions paraElements = do let para = Paragraph def{pPropAlign = Just AlgnCenter} paraElements elements <- mapM paragraphToElement [para] let ((x, y), (cx, cy)) = contentShapeDimensions let txBody = mknode "p:txBody" [] $ [mknode "a:bodyPr" [] (), mknode "a:lstStyle" [] ()] ++ elements return $ mknode "p:sp" [] [ mknode "p:nvSpPr" [] [ mknode "p:cNvPr" [("id","1"), ("name","TextBox 3")] () , mknode "p:cNvSpPr" [("txBox", "1")] () , mknode "p:nvPr" [] () ] , mknode "p:spPr" [] [ mknode "a:xfrm" [] [ mknode "a:off" [("x", show $ 12700 * x), ("y", show $ 12700 * (y + cy - captionHeight))] () , mknode "a:ext" [("cx", show $ 12700 * cx), ("cy", show $ 12700 * captionHeight)] () ] , mknode "a:prstGeom" [("prst", "rect")] [ mknode "a:avLst" [] () ] , mknode "a:noFill" [] () ] , txBody ] makePicElements :: PandocMonad m => Element -> PicProps -> MediaInfo -> [ParaElem] -> P m [Element] makePicElements layout picProps mInfo alt = do opts <- asks envOpts (pageWidth, pageHeight) <- asks envPresentationSize -- hasHeader <- asks envSlideHasHeader let hasCaption = mInfoCaption mInfo (imgBytes, _) <- P.fetchItem (mInfoFilePath mInfo) let (pxX, pxY) = case imageSize opts imgBytes of Right sz -> sizeInPixels $ sz Left _ -> sizeInPixels $ def master <- getMaster let ns = elemToNameSpaces layout ((x, y), (cx, cytmp)) <- getContentShapeSize ns layout master `catchError` (\_ -> return ((0, 0), (pageWidth, pageHeight))) let cy = if hasCaption then cytmp - captionHeight else cytmp let imgRatio = fromIntegral pxX / fromIntegral pxY :: Double boxRatio = fromIntegral cx / fromIntegral cy :: Double (dimX, dimY) = if imgRatio > boxRatio then (fromIntegral cx, fromIntegral cx / imgRatio) else (fromIntegral cy * imgRatio, fromIntegral cy) (dimX', dimY') = (round dimX * 12700, round dimY * 12700) :: (Integer, Integer) (xoff, yoff) = (fromIntegral x + (fromIntegral cx - dimX) / 2, fromIntegral y + (fromIntegral cy - dimY) / 2) (xoff', yoff') = (round xoff * 12700, round yoff * 12700) :: (Integer, Integer) let cNvPicPr = mknode "p:cNvPicPr" [] $ mknode "a:picLocks" [("noGrp","1") ,("noChangeAspect","1")] () -- cNvPr will contain the link information so we do that separately, -- and register the link if necessary. let cNvPrAttr = [("descr", mInfoFilePath mInfo), ("id","0"),("name","Picture 1")] cNvPr <- case picPropLink picProps of Just link -> do idNum <- registerLink link return $ mknode "p:cNvPr" cNvPrAttr $ mknode "a:hlinkClick" [("r:id", "rId" ++ show idNum)] () Nothing -> return $ mknode "p:cNvPr" cNvPrAttr () let nvPicPr = mknode "p:nvPicPr" [] [ cNvPr , cNvPicPr , mknode "p:nvPr" [] ()] let blipFill = mknode "p:blipFill" [] [ mknode "a:blip" [("r:embed", "rId" ++ (show $ mInfoLocalId mInfo))] () , mknode "a:stretch" [] $ mknode "a:fillRect" [] () ] let xfrm = mknode "a:xfrm" [] [ mknode "a:off" [("x",show xoff'), ("y",show yoff')] () , mknode "a:ext" [("cx",show dimX') ,("cy",show dimY')] () ] let prstGeom = mknode "a:prstGeom" [("prst","rect")] $ mknode "a:avLst" [] () let ln = mknode "a:ln" [("w","9525")] [ mknode "a:noFill" [] () , mknode "a:headEnd" [] () , mknode "a:tailEnd" [] () ] let spPr = mknode "p:spPr" [("bwMode","auto")] [xfrm, prstGeom, mknode "a:noFill" [] (), ln] let picShape = mknode "p:pic" [] [ nvPicPr , blipFill , spPr ] -- And now, maybe create the caption: if hasCaption then do cap <- createCaption ((x, y), (cx, cytmp)) alt return [picShape, cap] else return [picShape] paraElemToElement :: PandocMonad m => ParaElem -> P m Element paraElemToElement Break = return $ mknode "a:br" [] () paraElemToElement (Run rpr s) = do let sizeAttrs = case rPropForceSize rpr of Just n -> [("sz", (show $ n * 100))] Nothing -> if rPropCode rpr -- hardcoded size for code for now then [("sz", "1800")] else [] attrs = sizeAttrs ++ (if rPropBold rpr then [("b", "1")] else []) ++ (if rPropItalics rpr then [("i", "1")] else []) ++ (if rPropUnderline rpr then [("u", "sng")] else []) ++ (case rStrikethrough rpr of Just NoStrike -> [("strike", "noStrike")] Just SingleStrike -> [("strike", "sngStrike")] Just DoubleStrike -> [("strike", "dblStrike")] Nothing -> []) ++ (case rBaseline rpr of Just n -> [("baseline", show n)] Nothing -> []) ++ (case rCap rpr of Just NoCapitals -> [("cap", "none")] Just SmallCapitals -> [("cap", "small")] Just AllCapitals -> [("cap", "all")] Nothing -> []) ++ [] linkProps <- case rLink rpr of Just link -> do idNum <- registerLink link -- first we have to make sure that if it's an -- anchor, it's in the anchor map. If not, there's -- no link. return $ case link of InternalTarget _ -> let linkAttrs = [ ("r:id", "rId" ++ show idNum) , ("action", "ppaction://hlinksldjump") ] in [mknode "a:hlinkClick" linkAttrs ()] -- external ExternalTarget _ -> let linkAttrs = [ ("r:id", "rId" ++ show idNum) ] in [mknode "a:hlinkClick" linkAttrs ()] Nothing -> return [] let colorContents = case rSolidFill rpr of Just color -> case fromColor color of '#':hx -> [mknode "a:solidFill" [] [mknode "a:srgbClr" [("val", map toUpper hx)] ()] ] _ -> [] Nothing -> [] let codeContents = if rPropCode rpr then [mknode "a:latin" [("typeface", "Courier")] ()] else [] let propContents = linkProps ++ colorContents ++ codeContents return $ mknode "a:r" [] [ mknode "a:rPr" attrs $ propContents , mknode "a:t" [] s ] paraElemToElement (MathElem mathType texStr) = do res <- convertMath writeOMML mathType (unTeXString texStr) case res of Right r -> return $ mknode "a14:m" [] $ addMathInfo r Left (Str s) -> paraElemToElement (Run def s) Left _ -> throwError $ PandocShouldNeverHappenError "non-string math fallback" -- This is a bit of a kludge -- really requires adding an option to -- TeXMath, but since that's a different package, we'll do this one -- step at a time. addMathInfo :: Element -> Element addMathInfo element = let mathspace = Attr { attrKey = (QName "m" Nothing (Just "xmlns")) , attrVal = "http://schemas.openxmlformats.org/officeDocument/2006/math" } in add_attr mathspace element -- We look through the element to see if it contains an a14:m -- element. If so, we surround it. This is a bit ugly, but it seems -- more dependable than looking through shapes for math. Plus this is -- an xml implementation detail, so it seems to make sense to do it at -- the xml level. surroundWithMathAlternate :: Element -> Element surroundWithMathAlternate element = case findElement (QName "m" Nothing (Just "a14")) element of Just _ -> mknode "mc:AlternateContent" [("xmlns:mc", "http://schemas.openxmlformats.org/markup-compatibility/2006") ] [ mknode "mc:Choice" [ ("xmlns:a14", "http://schemas.microsoft.com/office/drawing/2010/main") , ("Requires", "a14")] [ element ] ] Nothing -> element paragraphToElement :: PandocMonad m => Paragraph -> P m Element paragraphToElement par = do let attrs = [("lvl", show $ pPropLevel $ paraProps par)] ++ (case pPropMarginLeft (paraProps par) of Just px -> [("marL", show $ 12700 * px), ("indent", "0")] Nothing -> [] ) ++ (case pPropAlign (paraProps par) of Just AlgnLeft -> [("algn", "l")] Just AlgnRight -> [("algn", "r")] Just AlgnCenter -> [("algn", "ctr")] Nothing -> [] ) props = [] ++ (case pPropSpaceBefore $ paraProps par of Just px -> [mknode "a:spcBef" [] [ mknode "a:spcPts" [("val", show $ 100 * px)] () ] ] Nothing -> [] ) ++ (case pPropBullet $ paraProps par of Just Bullet -> [] Just (AutoNumbering attrs') -> [mknode "a:buAutoNum" [("type", autoNumberingToType attrs')] ()] Nothing -> [mknode "a:buNone" [] ()] ) paras <- mapM paraElemToElement (paraElems par) return $ mknode "a:p" [] $ [mknode "a:pPr" attrs props] ++ paras shapeToElement :: PandocMonad m => Element -> Shape -> P m Element shapeToElement layout (TextBox paras) | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do sp <- getContentShape ns spTree elements <- mapM paragraphToElement paras let txBody = mknode "p:txBody" [] $ [mknode "a:bodyPr" [] (), mknode "a:lstStyle" [] ()] ++ elements emptySpPr = mknode "p:spPr" [] () return $ surroundWithMathAlternate $ replaceNamedChildren ns "p" "txBody" [txBody] $ replaceNamedChildren ns "p" "spPr" [emptySpPr] $ sp -- GraphicFrame and Pic should never reach this. shapeToElement _ _ = return $ mknode "p:sp" [] () shapeToElements :: PandocMonad m => Element -> Shape -> P m [Element] shapeToElements layout (Pic picProps fp alt) = do mInfo <- registerMedia fp alt case mInfoExt mInfo of Just _ -> do makePicElements layout picProps mInfo alt Nothing -> shapeToElements layout $ TextBox [Paragraph def alt] shapeToElements layout (GraphicFrame tbls cptn) = graphicFrameToElements layout tbls cptn shapeToElements layout shp = do element <- shapeToElement layout shp return [element] shapesToElements :: PandocMonad m => Element -> [Shape] -> P m [Element] shapesToElements layout shps = do concat <$> mapM (shapeToElements layout) shps graphicFrameToElements :: PandocMonad m => Element -> [Graphic] -> [ParaElem] -> P m [Element] graphicFrameToElements layout tbls caption = do -- get the sizing master <- getMaster (pageWidth, pageHeight) <- asks envPresentationSize let ns = elemToNameSpaces layout ((x, y), (cx, cytmp)) <- getContentShapeSize ns layout master `catchError` (\_ -> return ((0, 0), (pageWidth, pageHeight))) let cy = if (not $ null caption) then cytmp - captionHeight else cytmp elements <- mapM (graphicToElement cx) tbls let graphicFrameElts = mknode "p:graphicFrame" [] $ [ mknode "p:nvGraphicFramePr" [] $ [ mknode "p:cNvPr" [("id", "6"), ("name", "Content Placeholder 5")] () , mknode "p:cNvGraphicFramePr" [] $ [mknode "a:graphicFrameLocks" [("noGrp", "1")] ()] , mknode "p:nvPr" [] $ [mknode "p:ph" [("idx", "1")] ()] ] , mknode "p:xfrm" [] $ [ mknode "a:off" [("x", show $ 12700 * x), ("y", show $ 12700 * y)] () , mknode "a:ext" [("cx", show $ 12700 * cx), ("cy", show $ 12700 * cy)] () ] ] ++ elements if (not $ null caption) then do capElt <- createCaption ((x, y), (cx, cytmp)) caption return [graphicFrameElts, capElt] else return [graphicFrameElts] getDefaultTableStyle :: PandocMonad m => P m (Maybe String) getDefaultTableStyle = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive tblStyleLst <- parseXml refArchive distArchive "ppt/tableStyles.xml" return $ findAttr (QName "def" Nothing Nothing) tblStyleLst graphicToElement :: PandocMonad m => Integer -> Graphic -> P m Element graphicToElement tableWidth (Tbl tblPr hdrCells rows) = do let colWidths = if null hdrCells then case rows of r : _ | not (null r) -> replicate (length r) $ (tableWidth `div` (toInteger $ length r)) -- satisfy the compiler. This is the same as -- saying that rows is empty, but the compiler -- won't understand that `[]` exhausts the -- alternatives. _ -> [] else replicate (length hdrCells) $ (tableWidth `div` (toInteger $ length hdrCells)) let cellToOpenXML paras = do elements <- mapM paragraphToElement paras let elements' = if null elements then [mknode "a:p" [] [mknode "a:endParaRPr" [] ()]] else elements return $ [mknode "a:txBody" [] $ ([ mknode "a:bodyPr" [] () , mknode "a:lstStyle" [] ()] ++ elements')] headers' <- mapM cellToOpenXML hdrCells rows' <- mapM (mapM cellToOpenXML) rows let borderProps = mknode "a:tcPr" [] () let emptyCell = [mknode "a:p" [] [mknode "a:pPr" [] ()]] let mkcell border contents = mknode "a:tc" [] $ (if null contents then emptyCell else contents) ++ [ borderProps | border ] let mkrow border cells = mknode "a:tr" [("h", "0")] $ map (mkcell border) cells let mkgridcol w = mknode "a:gridCol" [("w", show ((12700 * w) :: Integer))] () let hasHeader = not (all null hdrCells) mbDefTblStyle <- getDefaultTableStyle let tblPrElt = mknode "a:tblPr" [ ("firstRow", if tblPrFirstRow tblPr then "1" else "0") , ("bandRow", if tblPrBandRow tblPr then "1" else "0") ] (case mbDefTblStyle of Nothing -> [] Just sty -> [mknode "a:tableStyleId" [] sty]) return $ mknode "a:graphic" [] $ [mknode "a:graphicData" [("uri", "http://schemas.openxmlformats.org/drawingml/2006/table")] $ [mknode "a:tbl" [] $ [ tblPrElt , mknode "a:tblGrid" [] (if all (==0) colWidths then [] else map mkgridcol colWidths) ] ++ [ mkrow True headers' | hasHeader ] ++ map (mkrow False) rows' ] ] getShapeByPlaceHolderType :: NameSpaces -> Element -> String -> Maybe Element getShapeByPlaceHolderType ns spTreeElem phType | isElem ns "p" "spTree" spTreeElem = let findPhType element = isElem ns "p" "sp" element && Just phType == (Just element >>= findChild (elemName ns "p" "nvSpPr") >>= findChild (elemName ns "p" "nvPr") >>= findChild (elemName ns "p" "ph") >>= findAttr (QName "type" Nothing Nothing)) in filterChild findPhType spTreeElem | otherwise = Nothing getShapeByPlaceHolderIndex :: NameSpaces -> Element -> String -> Maybe Element getShapeByPlaceHolderIndex ns spTreeElem phIdx | isElem ns "p" "spTree" spTreeElem = let findPhType element = isElem ns "p" "sp" element && Just phIdx == (Just element >>= findChild (elemName ns "p" "nvSpPr") >>= findChild (elemName ns "p" "nvPr") >>= findChild (elemName ns "p" "ph") >>= findAttr (QName "idx" Nothing Nothing)) in filterChild findPhType spTreeElem | otherwise = Nothing nonBodyTextToElement :: PandocMonad m => Element -> String -> [ParaElem] -> P m Element nonBodyTextToElement layout phType paraElements | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld , Just sp <- getShapeByPlaceHolderType ns spTree phType = do let hdrPara = Paragraph def paraElements element <- paragraphToElement hdrPara let txBody = mknode "p:txBody" [] $ [mknode "a:bodyPr" [] (), mknode "a:lstStyle" [] ()] ++ [element] return $ replaceNamedChildren ns "p" "txBody" [txBody] sp -- XXX: TODO | otherwise = return $ mknode "p:sp" [] () contentToElement :: PandocMonad m => Element -> [ParaElem] -> [Shape] -> P m Element contentToElement layout hdrShape shapes | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do element <- nonBodyTextToElement layout "title" hdrShape let hdrShapeElements = if null hdrShape then [] else [element] contentElements <- local (\env -> env {envContentType = NormalContent}) (shapesToElements layout shapes) return $ replaceNamedChildren ns "p" "sp" (hdrShapeElements ++ contentElements) spTree contentToElement _ _ _ = return $ mknode "p:sp" [] () twoColumnToElement :: PandocMonad m => Element -> [ParaElem] -> [Shape] -> [Shape] -> P m Element twoColumnToElement layout hdrShape shapesL shapesR | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do element <- nonBodyTextToElement layout "title" hdrShape let hdrShapeElements = if null hdrShape then [] else [element] contentElementsL <- local (\env -> env {envContentType =TwoColumnLeftContent}) (shapesToElements layout shapesL) contentElementsR <- local (\env -> env {envContentType =TwoColumnRightContent}) (shapesToElements layout shapesR) -- let contentElementsL' = map (setIdx ns "1") contentElementsL -- contentElementsR' = map (setIdx ns "2") contentElementsR return $ replaceNamedChildren ns "p" "sp" (hdrShapeElements ++ contentElementsL ++ contentElementsR) spTree twoColumnToElement _ _ _ _= return $ mknode "p:sp" [] () titleToElement :: PandocMonad m => Element -> [ParaElem] -> P m Element titleToElement layout titleElems | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do element <- nonBodyTextToElement layout "title" titleElems let titleShapeElements = if null titleElems then [] else [element] return $ replaceNamedChildren ns "p" "sp" titleShapeElements spTree titleToElement _ _ = return $ mknode "p:sp" [] () metadataToElement :: PandocMonad m => Element -> [ParaElem] -> [ParaElem] -> [[ParaElem]] -> [ParaElem] -> P m Element metadataToElement layout titleElems subtitleElems authorsElems dateElems | ns <- elemToNameSpaces layout , Just cSld <- findChild (elemName ns "p" "cSld") layout , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do titleShapeElements <- if null titleElems then return [] else sequence [nonBodyTextToElement layout "ctrTitle" titleElems] let combinedAuthorElems = intercalate [Break] authorsElems subtitleAndAuthorElems = intercalate [Break, Break] [subtitleElems, combinedAuthorElems] subtitleShapeElements <- if null subtitleAndAuthorElems then return [] else sequence [nonBodyTextToElement layout "subTitle" subtitleAndAuthorElems] dateShapeElements <- if null dateElems then return [] else sequence [nonBodyTextToElement layout "dt" dateElems] return $ replaceNamedChildren ns "p" "sp" (titleShapeElements ++ subtitleShapeElements ++ dateShapeElements) spTree metadataToElement _ _ _ _ _ = return $ mknode "p:sp" [] () slideToElement :: PandocMonad m => Slide -> P m Element slideToElement (Slide _ l@(ContentSlide hdrElems shapes) _ )= do layout <- getLayout l spTree <- local (\env -> if null hdrElems then env else env{envSlideHasHeader=True}) $ contentToElement layout hdrElems shapes return $ mknode "p:sld" [ ("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main"), ("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"), ("xmlns:p", "http://schemas.openxmlformats.org/presentationml/2006/main") ] [mknode "p:cSld" [] [spTree]] slideToElement (Slide _ l@(TwoColumnSlide hdrElems shapesL shapesR) _) = do layout <- getLayout l spTree <- local (\env -> if null hdrElems then env else env{envSlideHasHeader=True}) $ twoColumnToElement layout hdrElems shapesL shapesR return $ mknode "p:sld" [ ("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main"), ("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"), ("xmlns:p", "http://schemas.openxmlformats.org/presentationml/2006/main") ] [mknode "p:cSld" [] [spTree]] slideToElement (Slide _ l@(TitleSlide hdrElems) _) = do layout <- getLayout l spTree <- titleToElement layout hdrElems return $ mknode "p:sld" [ ("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main"), ("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"), ("xmlns:p", "http://schemas.openxmlformats.org/presentationml/2006/main") ] [mknode "p:cSld" [] [spTree]] slideToElement (Slide _ l@(MetadataSlide titleElems subtitleElems authorElems dateElems) _) = do layout <- getLayout l spTree <- metadataToElement layout titleElems subtitleElems authorElems dateElems return $ mknode "p:sld" [ ("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main"), ("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"), ("xmlns:p", "http://schemas.openxmlformats.org/presentationml/2006/main") ] [mknode "p:cSld" [] [spTree]] -------------------------------------------------------------------- -- Notes: getNotesMaster :: PandocMonad m => P m Element getNotesMaster = do let notesMasterPath = "ppt/notesMasters/notesMaster1.xml" distArchive <- asks envDistArchive root <- case findEntryByPath notesMasterPath distArchive of Just e -> case parseXMLDoc $ UTF8.toStringLazy $ fromEntry e of Just element -> return $ element Nothing -> throwError $ PandocSomeError $ notesMasterPath ++ " corrupt in reference file" Nothing -> throwError $ PandocSomeError $ notesMasterPath ++ " missing in reference file" return root getSlideNumberFieldId :: PandocMonad m => Element -> P m String getSlideNumberFieldId notesMaster | ns <- elemToNameSpaces notesMaster , Just cSld <- findChild (elemName ns "p" "cSld") notesMaster , Just spTree <- findChild (elemName ns "p" "spTree") cSld , Just sp <- getShapeByPlaceHolderType ns spTree "sldNum" , Just txBody <- findChild (elemName ns "p" "txBody") sp , Just p <- findChild (elemName ns "a" "p") txBody , Just fld <- findChild (elemName ns "a" "fld") p , Just fldId <- findAttr (QName "id" Nothing Nothing) fld = return fldId | otherwise = throwError $ PandocSomeError $ "No field id for slide numbers in notesMaster.xml" speakerNotesSlideImage :: Element speakerNotesSlideImage = mknode "p:sp" [] $ [ mknode "p:nvSpPr" [] $ [ mknode "p:cNvPr" [ ("id", "2") , ("name", "Slide Image Placeholder 1") ] () , mknode "p:cNvSpPr" [] $ [ mknode "a:spLocks" [ ("noGrp", "1") , ("noRot", "1") , ("noChangeAspect", "1") ] () ] , mknode "p:nvPr" [] $ [ mknode "p:ph" [("type", "sldImg")] ()] ] , mknode "p:spPr" [] () ] -- we want to wipe links from the speaker notes in the -- paragraphs. Powerpoint doesn't allow you to input them, and it -- would provide extra complications. removeParaLinks :: Paragraph -> Paragraph removeParaLinks paragraph = paragraph{paraElems = map f (paraElems paragraph)} where f (Run rProps s) = Run rProps{rLink=Nothing} s f pe = pe -- put an empty paragraph between paragraphs for more expected spacing. spaceParas :: [Paragraph] -> [Paragraph] spaceParas = intersperse (Paragraph def []) speakerNotesBody :: PandocMonad m => [Paragraph] -> P m Element speakerNotesBody paras = do elements <- mapM paragraphToElement $ spaceParas $ map removeParaLinks paras let txBody = mknode "p:txBody" [] $ [mknode "a:bodyPr" [] (), mknode "a:lstStyle" [] ()] ++ elements return $ mknode "p:sp" [] $ [ mknode "p:nvSpPr" [] $ [ mknode "p:cNvPr" [ ("id", "3") , ("name", "Notes Placeholder 2") ] () , mknode "p:cNvSpPr" [] $ [ mknode "a:spLocks" [("noGrp", "1")] ()] , mknode "p:nvPr" [] $ [ mknode "p:ph" [("type", "body"), ("idx", "1")] ()] ] , mknode "p:spPr" [] () , txBody ] speakerNotesSlideNumber :: Int -> String -> Element speakerNotesSlideNumber pgNum fieldId = mknode "p:sp" [] $ [ mknode "p:nvSpPr" [] $ [ mknode "p:cNvPr" [ ("id", "4") , ("name", "Slide Number Placeholder 3") ] () , mknode "p:cNvSpPr" [] $ [ mknode "a:spLocks" [("noGrp", "1")] ()] , mknode "p:nvPr" [] $ [ mknode "p:ph" [ ("type", "sldNum") , ("sz", "quarter") , ("idx", "10") ] () ] ] , mknode "p:spPr" [] () , mknode "p:txBody" [] $ [ mknode "a:bodyPr" [] () , mknode "a:lstStyle" [] () , mknode "a:p" [] $ [ mknode "a:fld" [ ("id", fieldId) , ("type", "slidenum") ] [ mknode "a:rPr" [("lang", "en-US")] () , mknode "a:t" [] (show pgNum) ] , mknode "a:endParaRPr" [("lang", "en-US")] () ] ] ] slideToSpeakerNotesElement :: PandocMonad m => Slide -> P m (Maybe Element) slideToSpeakerNotesElement slide | Slide _ _ mbNotes <- slide , Just (SpeakerNotes paras) <- mbNotes = do master <- getNotesMaster fieldId <- getSlideNumberFieldId master num <- slideNum slide let imgShape = speakerNotesSlideImage sldNumShape = speakerNotesSlideNumber num fieldId bodyShape <- speakerNotesBody paras return $ Just $ mknode "p:notes" [ ("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main") , ("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships") , ("xmlns:p", "http://schemas.openxmlformats.org/presentationml/2006/main") ] [ mknode "p:cSld" [] [ mknode "p:spTree" [] [ mknode "p:nvGrpSpPr" [] [ mknode "p:cNvPr" [("id", "1"), ("name", "")] () , mknode "p:cNvGrpSpPr" [] () , mknode "p:nvPr" [] () ] , mknode "p:grpSpPr" [] [ mknode "a:xfrm" [] [ mknode "a:off" [("x", "0"), ("y", "0")] () , mknode "a:ext" [("cx", "0"), ("cy", "0")] () , mknode "a:chOff" [("x", "0"), ("y", "0")] () , mknode "a:chExt" [("cx", "0"), ("cy", "0")] () ] ] , imgShape , bodyShape , sldNumShape ] ] ] slideToSpeakerNotesElement _ = return Nothing ----------------------------------------------------------------------- getSlideIdNum :: PandocMonad m => SlideId -> P m Int getSlideIdNum sldId = do slideIdMap <- asks envSlideIdMap case M.lookup sldId slideIdMap of Just n -> return n Nothing -> throwError $ PandocShouldNeverHappenError $ "Slide Id " ++ (show sldId) ++ " not found." slideNum :: PandocMonad m => Slide -> P m Int slideNum slide = getSlideIdNum $ slideId slide idNumToFilePath :: Int -> FilePath idNumToFilePath idNum = "slide" ++ (show $ idNum) ++ ".xml" slideToFilePath :: PandocMonad m => Slide -> P m FilePath slideToFilePath slide = do idNum <- slideNum slide return $ "slide" ++ (show $ idNum) ++ ".xml" slideToRelId :: PandocMonad m => Slide -> P m String slideToRelId slide = do n <- slideNum slide offset <- asks envSlideIdOffset return $ "rId" ++ (show $ n + offset) data Relationship = Relationship { relId :: Int , relType :: MimeType , relTarget :: FilePath } deriving (Show, Eq) elementToRel :: Element -> Maybe Relationship elementToRel element | elName element == QName "Relationship" (Just "http://schemas.openxmlformats.org/package/2006/relationships") Nothing = do rId <- findAttr (QName "Id" Nothing Nothing) element numStr <- stripPrefix "rId" rId num <- case reads numStr :: [(Int, String)] of (n, _) : _ -> Just n [] -> Nothing type' <- findAttr (QName "Type" Nothing Nothing) element target <- findAttr (QName "Target" Nothing Nothing) element return $ Relationship num type' target | otherwise = Nothing slideToPresRel :: PandocMonad m => Slide -> P m Relationship slideToPresRel slide = do idNum <- slideNum slide n <- asks envSlideIdOffset let rId = idNum + n fp = "slides/" ++ idNumToFilePath idNum return $ Relationship { relId = rId , relType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" , relTarget = fp } getRels :: PandocMonad m => P m [Relationship] getRels = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive relsElem <- parseXml refArchive distArchive "ppt/_rels/presentation.xml.rels" let globalNS = "http://schemas.openxmlformats.org/package/2006/relationships" let relElems = findChildren (QName "Relationship" (Just globalNS) Nothing) relsElem return $ mapMaybe elementToRel relElems presentationToRels :: PandocMonad m => Presentation -> P m [Relationship] presentationToRels pres@(Presentation _ slides) = do mySlideRels <- mapM slideToPresRel slides let notesMasterRels = if presHasSpeakerNotes pres then [Relationship { relId = length mySlideRels + 2 , relType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" , relTarget = "notesMasters/notesMaster1.xml" }] else [] insertedRels = mySlideRels ++ notesMasterRels rels <- getRels -- we remove the slide rels and the notesmaster (if it's -- there). We'll put these back in ourselves, if necessary. let relsWeKeep = filter (\r -> relType r /= "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" && relType r /= "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster") rels -- We want to make room for the slides in the id space. The slides -- will start at Id2 (since Id1 is for the slide master). There are -- two slides in the data file, but that might change in the future, -- so we will do this: -- -- 1. We look to see what the minimum relWithoutSlide id (greater than 1) is. -- 2. We add the difference between this and the number of slides to -- all relWithoutSlide rels (unless they're 1) -- 3. If we have a notesmaster slide, we make space for that as well. let minRelNotOne = case filter (1<) $ map relId relsWeKeep of [] -> 0 -- doesn't matter in this case, since -- there will be nothing to map the -- function over l -> minimum l modifyRelNum :: Int -> Int modifyRelNum 1 = 1 modifyRelNum n = n - minRelNotOne + 2 + length insertedRels relsWeKeep' = map (\r -> r{relId = modifyRelNum $ relId r}) relsWeKeep return $ insertedRels ++ relsWeKeep' -- We make this ourselves, in case there's a thumbnail in the one from -- the template. topLevelRels :: [Relationship] topLevelRels = [ Relationship { relId = 1 , relType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" , relTarget = "ppt/presentation.xml" } , Relationship { relId = 2 , relType = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" , relTarget = "docProps/core.xml" } , Relationship { relId = 3 , relType = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/extended-properties" , relTarget = "docProps/app.xml" } ] topLevelRelsEntry :: PandocMonad m => P m Entry topLevelRelsEntry = elemToEntry "_rels/.rels" $ relsToElement topLevelRels relToElement :: Relationship -> Element relToElement rel = mknode "Relationship" [ ("Id", "rId" ++ (show $ relId rel)) , ("Type", relType rel) , ("Target", relTarget rel) ] () relsToElement :: [Relationship] -> Element relsToElement rels = mknode "Relationships" [("xmlns", "http://schemas.openxmlformats.org/package/2006/relationships")] (map relToElement rels) presentationToRelsEntry :: PandocMonad m => Presentation -> P m Entry presentationToRelsEntry pres = do rels <- presentationToRels pres elemToEntry "ppt/_rels/presentation.xml.rels" $ relsToElement rels elemToEntry :: PandocMonad m => FilePath -> Element -> P m Entry elemToEntry fp element = do epochtime <- (floor . utcTimeToPOSIXSeconds) <$> asks envUTCTime return $ toEntry fp epochtime $ renderXml element slideToEntry :: PandocMonad m => Slide -> P m Entry slideToEntry slide = do idNum <- slideNum slide local (\env -> env{envCurSlideId = idNum}) $ do element <- slideToElement slide elemToEntry ("ppt/slides/" ++ idNumToFilePath idNum) element slideToSpeakerNotesEntry :: PandocMonad m => Slide -> P m (Maybe Entry) slideToSpeakerNotesEntry slide = do idNum <- slideNum slide local (\env -> env{envCurSlideId = idNum}) $ do mbElement <- slideToSpeakerNotesElement slide mbNotesIdNum <- do mp <- asks envSpeakerNotesIdMap return $ M.lookup idNum mp case mbElement of Just element | Just notesIdNum <- mbNotesIdNum -> Just <$> elemToEntry ("ppt/notesSlides/notesSlide" ++ show notesIdNum ++ ".xml") element _ -> return Nothing slideToSpeakerNotesRelElement :: PandocMonad m => Slide -> P m (Maybe Element) slideToSpeakerNotesRelElement slide | Slide _ _ mbNotes <- slide , Just _ <- mbNotes = do idNum <- slideNum slide return $ Just $ mknode "Relationships" [("xmlns", "http://schemas.openxmlformats.org/package/2006/relationships")] [ mknode "Relationship" [ ("Id", "rId2") , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide") , ("Target", "../slides/slide" ++ show idNum ++ ".xml") ] () , mknode "Relationship" [ ("Id", "rId1") , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster") , ("Target", "../notesMasters/notesMaster1.xml") ] () ] slideToSpeakerNotesRelElement _ = return Nothing slideToSpeakerNotesRelEntry :: PandocMonad m => Slide -> P m (Maybe Entry) slideToSpeakerNotesRelEntry slide = do idNum <- slideNum slide mbElement <- slideToSpeakerNotesRelElement slide mp <- asks envSpeakerNotesIdMap let mbNotesIdNum = M.lookup idNum mp case mbElement of Just element | Just notesIdNum <- mbNotesIdNum -> Just <$> elemToEntry ("ppt/notesSlides/_rels/notesSlide" ++ show notesIdNum ++ ".xml.rels") element _ -> return Nothing slideToSlideRelEntry :: PandocMonad m => Slide -> P m Entry slideToSlideRelEntry slide = do idNum <- slideNum slide element <- slideToSlideRelElement slide elemToEntry ("ppt/slides/_rels/" ++ idNumToFilePath idNum ++ ".rels") element linkRelElement :: PandocMonad m => Int -> LinkTarget -> P m Element linkRelElement rIdNum (InternalTarget targetId) = do targetIdNum <- getSlideIdNum targetId return $ mknode "Relationship" [ ("Id", "rId" ++ show rIdNum) , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide") , ("Target", "slide" ++ show targetIdNum ++ ".xml") ] () linkRelElement rIdNum (ExternalTarget (url, _)) = do return $ mknode "Relationship" [ ("Id", "rId" ++ show rIdNum) , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink") , ("Target", url) , ("TargetMode", "External") ] () linkRelElements :: PandocMonad m => M.Map Int LinkTarget -> P m [Element] linkRelElements mp = mapM (\(n, lnk) -> linkRelElement n lnk) (M.toList mp) mediaRelElement :: MediaInfo -> Element mediaRelElement mInfo = let ext = case mInfoExt mInfo of Just e -> e Nothing -> "" in mknode "Relationship" [ ("Id", "rId" ++ (show $ mInfoLocalId mInfo)) , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") , ("Target", "../media/image" ++ (show $ mInfoGlobalId mInfo) ++ ext) ] () speakerNotesSlideRelElement :: PandocMonad m => Slide -> P m (Maybe Element) speakerNotesSlideRelElement slide = do idNum <- slideNum slide mp <- asks envSpeakerNotesIdMap return $ case M.lookup idNum mp of Nothing -> Nothing Just n -> let target = "../notesSlides/notesSlide" ++ show n ++ ".xml" in Just $ mknode "Relationship" [ ("Id", "rId2") , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide") , ("Target", target) ] () slideToSlideRelElement :: PandocMonad m => Slide -> P m Element slideToSlideRelElement slide = do idNum <- slideNum slide let target = case slide of (Slide _ (MetadataSlide _ _ _ _) _) -> "../slideLayouts/slideLayout1.xml" (Slide _ (TitleSlide _) _) -> "../slideLayouts/slideLayout3.xml" (Slide _ (ContentSlide _ _) _) -> "../slideLayouts/slideLayout2.xml" (Slide _ (TwoColumnSlide _ _ _) _) -> "../slideLayouts/slideLayout4.xml" speakerNotesRels <- maybeToList <$> speakerNotesSlideRelElement slide linkIds <- gets stLinkIds mediaIds <- gets stMediaIds linkRels <- case M.lookup idNum linkIds of Just mp -> linkRelElements mp Nothing -> return [] let mediaRels = case M.lookup idNum mediaIds of Just mInfos -> map mediaRelElement mInfos Nothing -> [] return $ mknode "Relationships" [("xmlns", "http://schemas.openxmlformats.org/package/2006/relationships")] ([mknode "Relationship" [ ("Id", "rId1") , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout") , ("Target", target)] () ] ++ speakerNotesRels ++ linkRels ++ mediaRels) slideToSldIdElement :: PandocMonad m => Slide -> P m Element slideToSldIdElement slide = do n <- slideNum slide let id' = show $ n + 255 rId <- slideToRelId slide return $ mknode "p:sldId" [("id", id'), ("r:id", rId)] () presentationToSldIdLst :: PandocMonad m => Presentation -> P m Element presentationToSldIdLst (Presentation _ slides) = do ids <- mapM slideToSldIdElement slides return $ mknode "p:sldIdLst" [] ids presentationToPresentationElement :: PandocMonad m => Presentation -> P m Element presentationToPresentationElement pres@(Presentation _ slds) = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive element <- parseXml refArchive distArchive "ppt/presentation.xml" sldIdLst <- presentationToSldIdLst pres let modifySldIdLst :: Content -> Content modifySldIdLst (Elem e) = case elName e of (QName "sldIdLst" _ _) -> Elem sldIdLst _ -> Elem e modifySldIdLst ct = ct notesMasterRId = length slds + 2 notesMasterElem = mknode "p:notesMasterIdLst" [] [ mknode "p:NotesMasterId" [("r:id", "rId" ++ show notesMasterRId)] () ] -- if there's a notesMasterIdLst in the presentation.xml file, -- we want to remove it. We then want to put our own, if -- necessary, after the slideMasterIdLst element. removeNotesMaster' :: Content -> [Content] removeNotesMaster' (Elem e) = case elName e of (QName "notesMasterIdLst" _ _) -> [] _ -> [Elem e] removeNotesMaster' ct = [ct] removeNotesMaster :: [Content] -> [Content] removeNotesMaster = concatMap removeNotesMaster' insertNotesMaster' :: Content -> [Content] insertNotesMaster' (Elem e) = case elName e of (QName "sldMasterIdLst" _ _) -> [Elem e, Elem notesMasterElem] _ -> [Elem e] insertNotesMaster' ct = [ct] insertNotesMaster :: [Content] -> [Content] insertNotesMaster = if presHasSpeakerNotes pres then concatMap insertNotesMaster' else id newContent = insertNotesMaster $ removeNotesMaster $ map modifySldIdLst $ elContent element return $ element{elContent = newContent} presentationToPresEntry :: PandocMonad m => Presentation -> P m Entry presentationToPresEntry pres = presentationToPresentationElement pres >>= elemToEntry "ppt/presentation.xml" -- adapted from the Docx writer docPropsElement :: PandocMonad m => DocProps -> P m Element docPropsElement docProps = do utctime <- asks envUTCTime let keywords = case dcKeywords docProps of Just xs -> intercalate "," xs Nothing -> "" return $ mknode "cp:coreProperties" [("xmlns:cp","http://schemas.openxmlformats.org/package/2006/metadata/core-properties") ,("xmlns:dc","http://purl.org/dc/elements/1.1/") ,("xmlns:dcterms","http://purl.org/dc/terms/") ,("xmlns:dcmitype","http://purl.org/dc/dcmitype/") ,("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance")] $ (mknode "dc:title" [] $ fromMaybe "" $ dcTitle docProps) : (mknode "dc:creator" [] $ fromMaybe "" $ dcCreator docProps) : (mknode "cp:keywords" [] keywords) : (\x -> [ mknode "dcterms:created" [("xsi:type","dcterms:W3CDTF")] x , mknode "dcterms:modified" [("xsi:type","dcterms:W3CDTF")] x ]) (formatTime defaultTimeLocale "%FT%XZ" utctime) docPropsToEntry :: PandocMonad m => DocProps -> P m Entry docPropsToEntry docProps = docPropsElement docProps >>= elemToEntry "docProps/core.xml" defaultContentTypeToElem :: DefaultContentType -> Element defaultContentTypeToElem dct = mknode "Default" [("Extension", defContentTypesExt dct), ("ContentType", defContentTypesType dct)] () overrideContentTypeToElem :: OverrideContentType -> Element overrideContentTypeToElem oct = mknode "Override" [("PartName", overrideContentTypesPart oct), ("ContentType", overrideContentTypesType oct)] () contentTypesToElement :: ContentTypes -> Element contentTypesToElement ct = let ns = "http://schemas.openxmlformats.org/package/2006/content-types" in mknode "Types" [("xmlns", ns)] $ (map defaultContentTypeToElem $ contentTypesDefaults ct) ++ (map overrideContentTypeToElem $ contentTypesOverrides ct) data DefaultContentType = DefaultContentType { defContentTypesExt :: String , defContentTypesType:: MimeType } deriving (Show, Eq) data OverrideContentType = OverrideContentType { overrideContentTypesPart :: FilePath , overrideContentTypesType :: MimeType } deriving (Show, Eq) data ContentTypes = ContentTypes { contentTypesDefaults :: [DefaultContentType] , contentTypesOverrides :: [OverrideContentType] } deriving (Show, Eq) contentTypesToEntry :: PandocMonad m => ContentTypes -> P m Entry contentTypesToEntry ct = elemToEntry "[Content_Types].xml" $ contentTypesToElement ct pathToOverride :: FilePath -> Maybe OverrideContentType pathToOverride fp = OverrideContentType ("/" ++ fp) <$> (getContentType fp) mediaFileContentType :: FilePath -> Maybe DefaultContentType mediaFileContentType fp = case takeExtension fp of '.' : ext -> Just $ DefaultContentType { defContentTypesExt = ext , defContentTypesType = case getMimeType fp of Just mt -> mt Nothing -> "application/octet-stream" } _ -> Nothing mediaContentType :: MediaInfo -> Maybe DefaultContentType mediaContentType mInfo | Just ('.' : ext) <- mInfoExt mInfo = Just $ DefaultContentType { defContentTypesExt = ext , defContentTypesType = case mInfoMimeType mInfo of Just mt -> mt Nothing -> "application/octet-stream" } | otherwise = Nothing getSpeakerNotesFilePaths :: PandocMonad m => P m [FilePath] getSpeakerNotesFilePaths = do mp <- asks envSpeakerNotesIdMap let notesIdNums = M.elems mp return $ map (\n -> "ppt/notesSlides/notesSlide" ++ show n ++ ".xml") notesIdNums presentationToContentTypes :: PandocMonad m => Presentation -> P m ContentTypes presentationToContentTypes p@(Presentation _ slides) = do mediaInfos <- (mconcat . M.elems) <$> gets stMediaIds filePaths <- patternsToFilePaths $ inheritedPatterns p let mediaFps = filter (match (compile "ppt/media/image*")) filePaths let defaults = [ DefaultContentType "xml" "application/xml" , DefaultContentType "rels" "application/vnd.openxmlformats-package.relationships+xml" ] mediaDefaults = nub $ (mapMaybe mediaContentType $ mediaInfos) ++ (mapMaybe mediaFileContentType $ mediaFps) inheritedOverrides = mapMaybe pathToOverride filePaths docPropsOverride = mapMaybe pathToOverride ["docProps/core.xml"] presOverride = mapMaybe pathToOverride ["ppt/presentation.xml"] relativePaths <- mapM slideToFilePath slides let slideOverrides = mapMaybe (\fp -> pathToOverride $ "ppt/slides/" ++ fp) relativePaths speakerNotesOverrides <- (mapMaybe pathToOverride) <$> getSpeakerNotesFilePaths return $ ContentTypes (defaults ++ mediaDefaults) (inheritedOverrides ++ docPropsOverride ++ presOverride ++ slideOverrides ++ speakerNotesOverrides) presML :: String presML = "application/vnd.openxmlformats-officedocument.presentationml" noPresML :: String noPresML = "application/vnd.openxmlformats-officedocument" getContentType :: FilePath -> Maybe MimeType getContentType fp | fp == "ppt/presentation.xml" = Just $ presML ++ ".presentation.main+xml" | fp == "ppt/presProps.xml" = Just $ presML ++ ".presProps+xml" | fp == "ppt/viewProps.xml" = Just $ presML ++ ".viewProps+xml" | fp == "ppt/tableStyles.xml" = Just $ presML ++ ".tableStyles+xml" | fp == "docProps/core.xml" = Just $ "application/vnd.openxmlformats-package.core-properties+xml" | fp == "docProps/app.xml" = Just $ noPresML ++ ".extended-properties+xml" | "ppt" : "slideMasters" : f : [] <- splitDirectories fp , (_, ".xml") <- splitExtension f = Just $ presML ++ ".slideMaster+xml" | "ppt" : "slides" : f : [] <- splitDirectories fp , (_, ".xml") <- splitExtension f = Just $ presML ++ ".slide+xml" | "ppt" : "notesMasters" : f : [] <- splitDirectories fp , (_, ".xml") <- splitExtension f = Just $ presML ++ ".notesMaster+xml" | "ppt" : "notesSlides" : f : [] <- splitDirectories fp , (_, ".xml") <- splitExtension f = Just $ presML ++ ".notesSlide+xml" | "ppt" : "theme" : f : [] <- splitDirectories fp , (_, ".xml") <- splitExtension f = Just $ noPresML ++ ".theme+xml" | "ppt" : "slideLayouts" : _ : [] <- splitDirectories fp= Just $ presML ++ ".slideLayout+xml" | otherwise = Nothing autoNumberingToType :: ListAttributes -> String autoNumberingToType (_, numStyle, numDelim) = typeString ++ delimString where typeString = case numStyle of Decimal -> "arabic" UpperAlpha -> "alphaUc" LowerAlpha -> "alphaLc" UpperRoman -> "romanUc" LowerRoman -> "romanLc" _ -> "arabic" delimString = case numDelim of Period -> "Period" OneParen -> "ParenR" TwoParens -> "ParenBoth" _ -> "Period"