git.iko.soy/typeable/pandoc
on master help sign in
Basic support for images in ODT documents

Highly influenced by the docx support, refactored some code to avoid DRY.

commitc924611de526601f64154bef83035f75e8f4c334 authorHubert Plociniczak <hubert.plociniczak@gmail.com> dateWed 12 Oct 2016, 17:42 (10 years ago) parentcbeb72d
4 files changed+131−38
src/Text/Pandoc/Readers/Docx/Parse.hs+4−16 src/Text/Pandoc/Readers/Odt.hs+28−16 src/Text/Pandoc/Readers/Odt/ContentReader.hs+83−6 src/Text/Pandoc/Shared.hs+16
src/Text/Pandoc/Readers/Docx/Parse.hs+4−16
@@ -65,7 +65,7 @@ import Control.Monad.State
6565import Control.Applicative ((<|>))
6666import qualified Data.Map as M
6767import Control.Monad.Except
68−import Text.Pandoc.Shared (safeRead)
68+import Text.Pandoc.Shared (safeRead, filteredFilesFromArchive)
6969import Text.TeXMath.Readers.OMML (readOMML)
7070import Text.Pandoc.Readers.Docx.Fonts (getUnicode, Font(..))
7171import Text.TeXMath (Exp)
@@ -86,7 +86,6 @@ data ReaderEnv = ReaderEnv { envNotes :: Notes
8686
8787data ReaderState = ReaderState { stateWarnings :: [String] }
8888 deriving Show
89−
9089
9190data DocxError = DocxError | WrongElem
9291 deriving Show
@@ -276,7 +275,7 @@ archiveToDocxWithWarnings archive = do
276275 comments = archiveToComments archive
277276 numbering = archiveToNumbering archive
278277 rels = archiveToRelationships archive
279− media = archiveToMedia archive
278+ media = filteredFilesFromArchive archive filePathIsMedia
280279 (styles, parstyles) = archiveToStyles archive
281280 rEnv =
282281 ReaderEnv notes comments numbering rels media Nothing styles parstyles InDocument
@@ -402,7 +401,6 @@ archiveToComments zf =
402401 case cmts of
403402 Just c -> Comments cmts_namespaces c
404403 Nothing -> Comments cmts_namespaces M.empty
405−
406404
407405filePathToRelType :: FilePath -> Maybe DocumentLocation
408406filePathToRelType "word/_rels/document.xml.rels" = Just InDocument
@@ -424,7 +422,7 @@ filePathToRelationships ar fp | Just relType <- filePathToRelType fp
424422 , Just relElems <- (parseXMLDoc . UTF8.toStringLazy . fromEntry) entry =
425423 mapMaybe (relElemToRelationship relType) $ elChildren relElems
426424filePathToRelationships _ _ = []
427−
425+
428426archiveToRelationships :: Archive -> [Relationship]
429427archiveToRelationships archive =
430428 concatMap (filePathToRelationships archive) $ filesInArchive archive
@@ -435,16 +433,6 @@ filePathIsMedia fp =
435433 in
436434 (dir == "word/media/")
437435
438−getMediaPair :: Archive -> FilePath -> Maybe (FilePath, B.ByteString)
439−getMediaPair zf fp =
440− case findEntryByPath fp zf of
441− Just e -> Just (fp, fromEntry e)
442− Nothing -> Nothing
443−
444−archiveToMedia :: Archive -> Media
445−archiveToMedia zf =
446− mapMaybe (getMediaPair zf) (filter filePathIsMedia (filesInArchive zf))
447−
448436lookupLevel :: String -> String -> Numbering -> Maybe Level
449437lookupLevel numId ilvl (Numbering _ numbs absNumbs) = do
450438 absNumId <- lookup numId $ map (\(Numb nid absnumid) -> (nid, absnumid)) numbs
@@ -741,7 +729,7 @@ elemToCommentStart ns element
741729 , Just cmtDate <- findAttr (elemName ns "w" "date") element = do
742730 bps <- mapD (elemToBodyPart ns) (elChildren element)
743731 return $ CommentStart cmtId cmtAuthor cmtDate bps
744−elemToCommentStart _ _ = throwError WrongElem
732+elemToCommentStart _ _ = throwError WrongElem
745733
746734lookupFootnote :: String -> Notes -> Maybe Element
747735lookupFootnote s (Notes _ fns _) = fns >>= (M.lookup s)
src/Text/Pandoc/Readers/Odt.hs+28−16
@@ -37,6 +37,8 @@ import qualified Text.XML.Light as XML
3737
3838import qualified Data.ByteString.Lazy as B
3939
40+import System.FilePath
41+
4042import Text.Pandoc.Definition
4143import Text.Pandoc.Error
4244import Text.Pandoc.Options
@@ -48,39 +50,49 @@ import Text.Pandoc.Readers.Odt.StyleReader
4850
4951import Text.Pandoc.Readers.Odt.Generic.XMLConverter
5052import Text.Pandoc.Readers.Odt.Generic.Fallible
53+import Text.Pandoc.Shared (filteredFilesFromArchive)
5154
5255--
5356readOdt :: ReaderOptions
5457 -> B.ByteString
5558 -> Either PandocError (Pandoc, MediaBag)
56−readOdt _ bytes = case bytesToOdt bytes of
57− Right pandoc -> Right (pandoc , mempty)
58− Left err -> Left err
59+readOdt _ bytes = bytesToOdt bytes-- of
60+-- Right (pandoc, mediaBag) -> Right (pandoc , mediaBag)
61+-- Left err -> Left err
5962
6063--
61−bytesToOdt :: B.ByteString -> Either PandocError Pandoc
64+bytesToOdt :: B.ByteString -> Either PandocError (Pandoc, MediaBag)
6265bytesToOdt bytes = case toArchiveOrFail bytes of
6366 Right archive -> archiveToOdt archive
6467 Left _ -> Left $ ParseFailure "Couldn't parse odt file."
6568
6669--
67−archiveToOdt :: Archive -> Either PandocError Pandoc
70+archiveToOdt :: Archive -> Either PandocError (Pandoc, MediaBag)
6871archiveToOdt archive
69− | Just contentEntry <- findEntryByPath "content.xml" archive
70− , Just stylesEntry <- findEntryByPath "styles.xml" archive
71− , Just contentElem <- entryToXmlElem contentEntry
72− , Just stylesElem <- entryToXmlElem stylesEntry
73− , Right styles <- chooseMax (readStylesAt stylesElem )
74− (readStylesAt contentElem)
75− , startState <- readerState styles
76− , Right pandoc <- runConverter' read_body
77− startState
78− contentElem
79− = Right pandoc
72+ | Just contentEntry <- findEntryByPath "content.xml" archive
73+ , Just stylesEntry <- findEntryByPath "styles.xml" archive
74+ , Just contentElem <- entryToXmlElem contentEntry
75+ , Just stylesElem <- entryToXmlElem stylesEntry
76+ , Right styles <- chooseMax (readStylesAt stylesElem )
77+ (readStylesAt contentElem)
78+ , media <- filteredFilesFromArchive archive filePathIsOdtMedia
79+ , startState <- readerState styles media
80+ , Right pandocWithMedia <- runConverter' read_body
81+ startState
82+ contentElem
83+
84+ = Right pandocWithMedia
8085
8186 | otherwise
8287 -- Not very detailed, but I don't think more information would be helpful
8388 = Left $ ParseFailure "Couldn't parse odt file."
89+ where
90+ filePathIsOdtMedia :: FilePath -> Bool
91+ filePathIsOdtMedia fp =
92+ let (dir, _) = splitFileName fp
93+ in
94+ (dir == "Pictures/")
95+
8496
8597--
8698entryToXmlElem :: Entry -> Maybe XML.Element
src/Text/Pandoc/Readers/Odt/ContentReader.hs+83−6
@@ -42,6 +42,7 @@ module Text.Pandoc.Readers.Odt.ContentReader
4242import Control.Arrow
4343import Control.Applicative hiding ( liftA, liftA2, liftA3 )
4444
45+import qualified Data.ByteString.Lazy as B
4546import qualified Data.Map as M
4647import Data.List ( find )
4748import Data.Maybe
@@ -50,6 +51,7 @@ import qualified Text.XML.Light as XML
5051
5152import Text.Pandoc.Definition
5253import Text.Pandoc.Builder
54+import Text.Pandoc.MediaBag (insertMedia, MediaBag)
5355import Text.Pandoc.Shared
5456
5557import Text.Pandoc.Readers.Odt.Base
@@ -68,6 +70,7 @@ import qualified Data.Set as Set
6870--------------------------------------------------------------------------------
6971
7072type Anchor = String
73+type Media = [(FilePath, B.ByteString)]
7174
7275data ReaderState
7376 = ReaderState { -- | A collection of styles read somewhere else.
@@ -87,14 +90,17 @@ data ReaderState
8790 -- | A map from internal anchor names to "pretty" ones.
8891 -- The mapping is a purely cosmetic one.
8992 , bookmarkAnchors :: M.Map Anchor Anchor
90−
93+ -- | A map of files / binary data from the archive
94+ , envMedia :: Media
95+ -- | Hold binary resources used in the document
96+ , odtMediaBag :: MediaBag
9197-- , sequences
9298-- , trackedChangeIDs
9399 }
94100 deriving ( Show )
95101
96−readerState :: Styles -> ReaderState
97−readerState styles = ReaderState styles [] 0 Nothing M.empty
102+readerState :: Styles -> Media -> ReaderState
103+readerState styles media = ReaderState styles [] 0 Nothing M.empty media mempty
98104
99105--
100106pushStyle' :: Style -> ReaderState -> ReaderState
@@ -134,6 +140,16 @@ putPrettyAnchor ugly pretty state@ReaderState{..}
134140usedAnchors :: ReaderState -> [Anchor]
135141usedAnchors ReaderState{..} = M.elems bookmarkAnchors
136142
143+getMediaBag :: ReaderState -> MediaBag
144+getMediaBag ReaderState{..} = odtMediaBag
145+
146+getMediaEnv :: ReaderState -> Media
147+getMediaEnv ReaderState{..} = envMedia
148+
149+insertMedia' :: (FilePath, B.ByteString) -> ReaderState -> ReaderState
150+insertMedia' (fp, bs) state@ReaderState{..}
151+ = state { odtMediaBag = insertMedia fp Nothing bs odtMediaBag }
152+
137153--------------------------------------------------------------------------------
138154-- Reader type and associated tools
139155--------------------------------------------------------------------------------
@@ -190,6 +206,22 @@ popStyle = keepingTheValue (
190206getCurrentListLevel :: OdtReaderSafe _x ListLevel
191207getCurrentListLevel = getExtraState >>^ currentListLevel
192208
209+--
210+updateMediaWithResource :: OdtReaderSafe (FilePath, B.ByteString) (FilePath, B.ByteString)
211+updateMediaWithResource = keepingTheValue (
212+ (keepingTheValue getExtraState
213+ >>% insertMedia'
214+ )
215+ >>> setExtraState
216+ )
217+ >>^ fst
218+
219+lookupResource :: OdtReaderSafe String (FilePath, B.ByteString)
220+lookupResource = proc target -> do
221+ state <- getExtraState -< ()
222+ case lookup target (getMediaEnv state) of
223+ Just bs -> returnV (target, bs) -<< ()
224+ Nothing -> returnV ("", B.empty) -< ()
193225
194226type AnchorPrefix = String
195227
@@ -559,6 +591,7 @@ read_paragraph = matchingElement NsText "p"
559591 , read_reference_start
560592 , read_bookmark_ref
561593 , read_reference_ref
594+ , read_frame
562595 ] read_plain_text
563596
564597
@@ -583,6 +616,7 @@ read_header = matchingElement NsText "h"
583616 , read_reference_start
584617 , read_bookmark_ref
585618 , read_reference_ref
619+ , read_frame
586620 ] read_plain_text
587621 ) -< blocks
588622 anchor <- getHeaderAnchor -< children
@@ -687,6 +721,46 @@ read_table_cell = matchingElement NsTable "table-cell"
687721 $ matchChildContent' [ read_paragraph
688722 ]
689723
724+----------------------
725+-- Images
726+----------------------
727+
728+--
729+read_frame :: InlineMatcher
730+read_frame = matchingElement NsDraw "frame"
731+ $ proc blocks -> do
732+ w <- ( findAttr' NsSVG "width" ) -< ()
733+ h <- ( findAttr' NsSVG "height" ) -< ()
734+ attr <- arr (uncurry image_attributes) -< (w, h)
735+ titleNodes <- ( matchChildContent' [ read_frame_title ] ) -< blocks
736+ title <- arr inlineListToIdentifier -< (toList titleNodes)
737+ src <- matchChildContent' [ read_image_src ] -< blocks
738+ resource <- lookupResource -< src
739+ _ <- updateMediaWithResource -< resource
740+ arr (uncurry4 imageWith ) -< (attr, src, title, mempty)
741+
742+image_attributes :: Maybe String -> Maybe String -> Attr
743+image_attributes x y =
744+ ( "", [], (dim "width" x) ++ (dim "height" y))
745+ where
746+ dim _ (Just "") = []
747+ dim name (Just v) = [(name, v)]
748+ dim _ Nothing = []
749+
750+read_image_src :: (Namespace, ElementName, OdtReader Anchor Anchor)
751+read_image_src = matchingElement NsDraw "image"
752+ $ proc _ -> do
753+ imgSrc <- findAttr NsXLink "href" -< ()
754+ case imgSrc of
755+ Right src -> returnV src -<< ()
756+ Left _ -> returnV "" -< ()
757+
758+read_frame_title :: InlineMatcher
759+read_frame_title = matchingElement NsSVG "title"
760+ $ (matchChildContent [] read_plain_text)
761+
762+
763+
690764----------------------
691765-- Internal links
692766----------------------
@@ -783,8 +857,11 @@ read_text = matchChildContent' [ read_header
783857 ]
784858 >>^ doc
785859
786−read_body :: OdtReader _x Pandoc
860+read_body :: OdtReader _x (Pandoc, MediaBag)
787861read_body = executeIn NsOffice "body"
788862 $ executeIn NsOffice "text"
789− $ liftAsSuccess read_text
790−
863+ $ liftAsSuccess
864+ $ proc inlines -> do
865+ txt <- read_text -< inlines
866+ state <- getExtraState -< ()
867+ returnA -< (txt, getMediaBag state)
src/Text/Pandoc/Shared.hs+16
@@ -67,6 +67,7 @@ module Text.Pandoc.Shared (
6767 Element (..),
6868 hierarchicalize,
6969 uniqueIdent,
70+ inlineListToIdentifier,
7071 isHeaderBlock,
7172 headerShift,
7273 isTightList,
@@ -84,6 +85,7 @@ module Text.Pandoc.Shared (
8485 fetchItem',
8586 openURL,
8687 collapseFilePath,
88+ filteredFilesFromArchive,
8789 -- * Error handling
8890 err,
8991 warn,
@@ -110,6 +112,7 @@ import System.Exit (exitWith, ExitCode(..))
110112import Data.Char ( toLower, isLower, isUpper, isAlpha,
111113 isLetter, isDigit, isSpace )
112114import Data.List ( find, stripPrefix, intercalate )
115+import Data.Maybe (mapMaybe)
113116import Data.Version ( showVersion )
114117import qualified Data.Map as M
115118import Network.URI ( escapeURIString, nonStrictRelativeTo,
@@ -1028,6 +1031,19 @@ collapseFilePath = Posix.joinPath . reverse . foldl go [] . splitDirectories
10281031 isSingleton _ = Nothing
10291032 checkPathSeperator = fmap isPathSeparator . isSingleton
10301033
1034+--
1035+-- File selection from the archive
1036+--
1037+filteredFilesFromArchive :: Archive -> (FilePath -> Bool) -> [(FilePath, BL.ByteString)]
1038+filteredFilesFromArchive zf f =
1039+ mapMaybe (fileAndBinary zf) (filter f (filesInArchive zf))
1040+ where
1041+ fileAndBinary :: Archive -> FilePath -> Maybe (FilePath, BL.ByteString)
1042+ fileAndBinary a fp =
1043+ case findEntryByPath fp a of
1044+ Just e -> Just (fp, fromEntry e)
1045+ Nothing -> Nothing
1046+
10311047---
10321048--- Squash blocks into inlines
10331049---