From 575a360c6c1c49f6ce04b6dbde0ed167d40b9f48 Mon Sep 17 00:00:00 2001 From: Jesse Rosenthal Date: Sat, 17 Feb 2018 15:57:40 -0500 Subject: Powerpoint writer: Output speaker notes. There are a number of interlocking parts here. The main thing to note is that, to match the MSPowerPoint-generated pptx files, we only include the notesMaster and notesSlide files if there are notes. This means we have to be careful with the rIds, and build a number of files conditionally. --- src/Text/Pandoc/Writers/Powerpoint/Output.hs | 312 ++++++++++++++++++++++++--- 1 file changed, 287 insertions(+), 25 deletions(-) (limited to 'src/Text/Pandoc/Writers/Powerpoint') diff --git a/src/Text/Pandoc/Writers/Powerpoint/Output.hs b/src/Text/Pandoc/Writers/Powerpoint/Output.hs index 1ed021086..801e0485e 100644 --- a/src/Text/Pandoc/Writers/Powerpoint/Output.hs +++ b/src/Text/Pandoc/Writers/Powerpoint/Output.hs @@ -56,7 +56,7 @@ 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) +import Data.Maybe (mapMaybe, listToMaybe, fromMaybe, isJust, maybeToList, catMaybes) import Text.Pandoc.ImageSize import Control.Applicative ((<|>)) import System.FilePath.Glob @@ -109,6 +109,11 @@ data WriterEnv = WriterEnv { envRefArchive :: Archive , 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) @@ -125,6 +130,7 @@ instance Default WriterEnv where , envSlideIdOffset = 1 , envContentType = NormalContent , envSlideIdMap = mempty + , envSpeakerNotesIdMap = mempty } data ContentType = NormalContent @@ -185,7 +191,7 @@ alwaysInheritedPatterns = -- We only look for these under special conditions contingentInheritedPatterns :: Presentation -> [Pattern] contingentInheritedPatterns pres = [] ++ - if hasSpeakerNotes pres + if presHasSpeakerNotes pres then map compile [ "ppt/notesMasters/notesMaster*.xml" , "ppt/notesMasters/_rels/notesMaster*.xml.rels" , "ppt/theme/theme2.xml" @@ -253,6 +259,8 @@ presentationToArchiveP p@(Presentation docProps slides) = do 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 @@ -261,6 +269,8 @@ presentationToArchiveP p@(Presentation docProps slides) = do return $ foldr addEntryToArchive newArch' $ slideEntries ++ slideRelEntries ++ + spkNotesEntries ++ + spkNotesRelEntries ++ mediaEntries ++ [contentTypesEntry, docPropsEntry, relsEntry, presEntry, presRelsEntry] @@ -268,6 +278,12 @@ 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) <$> @@ -291,6 +307,7 @@ presentationToArchive opts pres = do , envOpts = opts , envPresentationSize = presSize , envSlideIdMap = makeSlideIdMap pres + , envSpeakerNotesIdMap = makeSpeakerNotesMap pres } let st = def { stMediaGlobalIds = initialGlobalIds refArchive distArchive @@ -304,8 +321,14 @@ presentationToArchive opts pres = do -- Check to see if the presentation has speaker notes. This will -- influence whether we import the notesMaster template. -hasSpeakerNotes :: Presentation -> Bool -hasSpeakerNotes (Presentation _ slides) = any isJust $ map slideSpeakerNotes slides +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 -------------------------------------------------- @@ -448,15 +471,16 @@ 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 - [] -> 1 + [] -> if hasSpeakerNotes then 2 else 1 ks -> maximum ks - Nothing -> 1 + Nothing -> if hasSpeakerNotes then 2 else 1 maxMediaId = case M.lookup curSlideId mediaReg of - Just [] -> 1 + Just [] -> if hasSpeakerNotes then 2 else 1 Just mInfos -> maximum $ map mInfoLocalId mInfos - Nothing -> 1 + 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 @@ -470,15 +494,16 @@ registerMedia fp caption = do 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 - [] -> 1 + [] -> if hasSpeakerNotes then 2 else 1 ks -> maximum ks - Nothing -> 1 + Nothing -> if hasSpeakerNotes then 2 else 1 maxMediaId = case M.lookup curSlideId mediaReg of - Just [] -> 1 + Just [] -> if hasSpeakerNotes then 2 else 1 Just mInfos -> maximum $ map mInfoLocalId mInfos - Nothing -> 1 + Nothing -> if hasSpeakerNotes then 2 else 1 maxLocalId = max maxLinkId maxMediaId maxGlobalId = case M.elems globalIds of @@ -973,6 +998,21 @@ getShapeByName ns spTreeElem name filterChild (\e -> (isElem ns "p" "sp" e) && (shapeHasName ns name e)) spTreeElem | otherwise = Nothing + + +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 + -- getShapeById :: NameSpaces -> Element -> String -> Maybe Element -- getShapeById ns spTreeElem ident -- | isElem ns "p" "spTree" spTreeElem = @@ -1109,6 +1149,148 @@ slideToElement (Slide _ l@(MetadataSlide titleElems subtitleElems authorElems da ("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" [] () + ] + +speakerNotesBody :: PandocMonad m => [Paragraph] -> P m Element +speakerNotesBody paras = do + elements <- mapM paragraphToElement 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 sld@(Slide _ _ mbNotes) + | Nothing <- mbNotes = return Nothing + | Just (SpeakerNotes paras) <- mbNotes = do + master <- getNotesMaster + fieldId <- getSlideNumberFieldId master + num <- slideNum sld + 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 + ] + ] + ] + ----------------------------------------------------------------------- getSlideIdNum :: PandocMonad m => SlideId -> P m Int @@ -1252,6 +1434,53 @@ slideToEntry slide = 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) + | Nothing <- mbNotes = return Nothing + | 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") + ] () + ] + +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 @@ -1288,6 +1517,20 @@ mediaRelElement mInfo = , ("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 @@ -1297,6 +1540,8 @@ slideToSlideRelElement slide = do (Slide _ (ContentSlide _ _) _) -> "../slideLayouts/slideLayout2.xml" (Slide _ (TwoColumnSlide _ _ _) _) -> "../slideLayouts/slideLayout4.xml" + speakerNotesRels <- maybeToList <$> speakerNotesSlideRelElement slide + linkIds <- gets stLinkIds mediaIds <- gets stMediaIds @@ -1313,7 +1558,7 @@ slideToSlideRelElement slide = do ([mknode "Relationship" [ ("Id", "rId1") , ("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout") , ("Target", target)] () - ] ++ linkRels ++ mediaRels) + ] ++ speakerNotesRels ++ linkRels ++ mediaRels) slideToSldIdElement :: PandocMonad m => Slide -> P m Element slideToSldIdElement slide = do @@ -1328,7 +1573,7 @@ presentationToSldIdLst (Presentation _ slides) = do return $ mknode "p:sldIdLst" [] ids presentationToPresentationElement :: PandocMonad m => Presentation -> P m Element -presentationToPresentationElement pres = do +presentationToPresentationElement pres@(Presentation _ slds) = do refArchive <- asks envRefArchive distArchive <- asks envDistArchive element <- parseXml refArchive distArchive "ppt/presentation.xml" @@ -1340,18 +1585,28 @@ presentationToPresentationElement pres = do _ -> Elem e modifySldIdLst ct = ct - removeSpeakerNotes' :: Content -> [Content] - removeSpeakerNotes' (Elem e) = case elName e of - (QName "notesMasterIdLst" _ _) -> [] - _ -> [Elem e] - removeSpeakerNotes' ct = [ct] + notesMasterRId = length slds + 2 + + modifySpeakerNotes' :: Content -> [Content] + modifySpeakerNotes' (Elem e) = case elName e of + (QName "notesMasterIdLst" _ _) -> + if presHasSpeakerNotes pres + then [Elem $ + mknode "p:notesMasterIdLst" [] + [ mknode + "p:NotesMasterId" + [("r:id", "rId" ++ show notesMasterRId)] + () + ] + ] + else [] + _ -> [Elem e] + modifySpeakerNotes' ct = [ct] - removeSpeakerNotes :: [Content] -> [Content] - removeSpeakerNotes = if not (hasSpeakerNotes pres) - then concatMap removeSpeakerNotes' - else id + modifySpeakerNotes :: [Content] -> [Content] + modifySpeakerNotes = concatMap modifySpeakerNotes' - newContent = removeSpeakerNotes $ map modifySldIdLst $ elContent element + newContent = modifySpeakerNotes $ map modifySldIdLst $ elContent element return $ element{elContent = newContent} @@ -1452,6 +1707,12 @@ mediaContentType mInfo } | 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 @@ -1471,9 +1732,10 @@ presentationToContentTypes p@(Presentation _ slides) = do let slideOverrides = mapMaybe (\fp -> pathToOverride $ "ppt/slides/" ++ fp) relativePaths + speakerNotesOverrides <- (mapMaybe pathToOverride) <$> getSpeakerNotesFilePaths return $ ContentTypes (defaults ++ mediaDefaults) - (inheritedOverrides ++ docPropsOverride ++ presOverride ++ slideOverrides) + (inheritedOverrides ++ docPropsOverride ++ presOverride ++ slideOverrides ++ speakerNotesOverrides) presML :: String presML = "application/vnd.openxmlformats-officedocument.presentationml" -- cgit v1.2.3