ODT writer: image anchoring, percent dimensions, text width resolution (#11746)

Inline images now emit `text:anchor-type=as-char` on their `draw:frame`,
anchoring the frame as a character so it flows with the surrounding text.
Without this, consumers default to paragraph anchoring and float the frame
beside (or after) the following paragraph.

When both width and height are given and the width is a percentage,
the width is now resolved against the reference document text area width,
mirroring how the docx writer derives its print width from the reference docx
section properties. The height follows the aspect ratio.
This commit is contained in:
Can H. Tartanoglu
2026-07-04 18:52:14 +02:00
committed by GitHub
parent 4f5226df4f
commit 57639391fc
5 changed files with 54 additions and 14 deletions
+40 -6
View File
@@ -17,7 +17,7 @@ import Control.Monad.Except (catchError, throwError)
import Control.Monad.State.Strict (StateT, evalStateT, gets, modify, lift)
import Control.Monad (MonadPlus(mplus))
import qualified Data.ByteString.Lazy as B
import Data.Maybe (fromMaybe)
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Generics (everywhere', mkT)
import Data.List (isPrefixOf)
import qualified Data.Map as Map
@@ -102,7 +102,8 @@ pandocToODT opts doc@(Pandoc meta _) = do
readDataFile "reference.odt"
-- handle formulas and pictures
-- picEntriesRef <- P.newIORef ([] :: [Entry])
doc' <- walkM (transformPicMath opts) $ walk fixDisplayMath doc
let refTextWidth = referenceTextWidthPt opts refArchive
doc' <- walkM (transformPicMath opts refTextWidth) $ walk fixDisplayMath doc
newContents <- lift $ writeOpenDocument opts{writerWrapText = WrapNone} doc'
epochtime <- floor `fmap` lift P.getPOSIXTime
let contentEntry = toEntry "content.xml" epochtime
@@ -303,16 +304,49 @@ addLang lang = everywhere' (mkT updateLangAttr)
updateLangAttr x = x
-- | transform both Image and Math elements
transformPicMath :: PandocMonad m => WriterOptions ->Inline -> O m Inline
transformPicMath opts (Image attr@(id', cls, _) lab (src,t)) = catchError
-- | Width of the text area in points, from the reference document's
-- page layout (page width minus horizontal margins). This mirrors how
-- the docx writer derives its print width from the reference docx
-- section properties.
referenceTextWidthPt :: WriterOptions -> Archive -> Maybe Double
referenceTextWidthPt opts archive = do
entry <- findEntryByPath "styles.xml" archive
styles <- either (const Nothing) Just $
parseXMLElement $ toTextLazy $ fromEntry entry
layout <- listToMaybe $ filterElementsName
((== "page-layout-properties") . qName) styles
let dim attrName = do
v <- findAttrBy ((== attrName) . qName) layout
d <- lengthToDim v
Just $ 72 * inInch opts d
pageWidth <- dim "page-width"
let margin = fromMaybe 0 . dim
Just $ pageWidth - margin "margin-left" - margin "margin-right"
transformPicMath :: PandocMonad m
=> WriterOptions -> Maybe Double -> Inline -> O m Inline
transformPicMath opts mbTextWidthPt (Image attr@(id', cls, _) lab (src,t)) =
catchError
(do (img, mbMimeType) <- P.fetchItem src
(ptX, ptY) <- case imageSize opts img of
Right s -> return $ sizeInPoints s
Left msg -> do
report $ CouldNotDetermineImageSize src msg
return (100, 100)
let textWidthPt = fromMaybe 420 mbTextWidthPt
let dims =
case (getDim Width, getDim Height) of
-- Percent dimensions are not valid ODF lengths. When both
-- dimensions are given and the width is relative, resolve
-- it against the reference document's text width (as the
-- docx writer does) and let the height follow the image's
-- aspect ratio.
(Just (Percent wp), Just _) ->
let wIn = wp / 100 * textWidthPt / 72
in [ ("width", tshow (Inch wIn))
, ("height", tshow (Inch (wIn / ratio))) ]
(Just w@(Inch i), Just (Percent _)) ->
[("width", tshow w), ("height", tshow (Inch (i / ratio)))]
(Just w, Just h) -> [("width", tshow w), ("height", tshow h)]
(Just w@(Percent _), Nothing) -> [("rel-width", tshow w),("rel-height", "scale"),("width", tshow ptX <> "pt"),("height", tshow ptY <> "pt")]
(Nothing, Just h@(Percent _)) -> [("rel-width", "scale"),("rel-height", tshow h),("width", tshow ptX <> "pt"),("height", tshow ptY <> "pt")]
@@ -347,7 +381,7 @@ transformPicMath opts (Image attr@(id', cls, _) lab (src,t)) = catchError
report $ CouldNotFetchResource src $ T.pack (show e)
return $ Emph lab)
transformPicMath _ (Math t math) = do
transformPicMath _ _ (Math t math) = do
entries <- gets stEntries
let dt = if t == InlineMath then DisplayInline else DisplayBlock
case readTeX math of
@@ -381,7 +415,7 @@ transformPicMath _ (Math t math) = do
, ("xlink:show", "embed")
, ("xlink:actuate", "onLoad")]
transformPicMath _ x = return x
transformPicMath _ _ x = return x
starMathCommentFromTeX :: T.Text -> T.Text
starMathCommentFromTeX tex =
+7 -1
View File
@@ -747,8 +747,14 @@ inlineToOpenDocument o ils
getDims (("height", h):xs) = ("svg:height", h) : getDims xs
getDims (("rel-height", w):xs) = ("style:rel-height", w) : getDims xs
getDims (_:xs) = getDims xs
-- Image is an Inline: anchor the frame as a character so
-- it flows with the text. Without an explicit anchor,
-- consumers default to paragraph anchoring and float the
-- frame beside the following text.
return $ inTags False "draw:frame"
(("draw:name", "img" <> tshow id') : getDims kvs) $
(("draw:name", "img" <> tshow id')
: ("text:anchor-type", "as-char")
: getDims kvs) $
selfClosingTag "draw:image" [ ("xlink:href" , s )
, ("xlink:type" , "simple")
, ("xlink:show" , "embed" )
+2 -2
View File
@@ -6,10 +6,10 @@
![Second image](lalune.jpg)
^D
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">Figure <text:sequence text:ref-name="refIllustration0" text:name="Illustration" text:formula="ooow:Illustration+1" style:num-format="1">1</text:sequence>: First
image</text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img2"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img2" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">Figure <text:sequence text:ref-name="refIllustration1" text:name="Illustration" text:formula="ooow:Illustration+1" style:num-format="1">2</text:sequence>: Second
image</text:p>
```
+3 -3
View File
@@ -21,7 +21,7 @@ Chapter<text:bookmark-end text:name="chapter1" /></text:h>
<text:p text:style-name="First_20_paragraph">Chapter 1 references
<text:bookmark-ref text:reference-format="text" text:ref-name="chapter1">The
Chapter</text:bookmark-ref></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">Voyage dans la Lune</text:p>
<text:p text:style-name="Text_20_body">Image 1 references
<text:sequence-ref text:reference-format="caption" text:ref-name="lalune">La
@@ -38,7 +38,7 @@ Lune</text:sequence-ref></text:p>
Chapter<text:bookmark-end text:name="chapter1" /></text:h>
<text:p text:style-name="First_20_paragraph">Chapter 1 references
<text:bookmark-ref text:reference-format="number" text:ref-name="chapter1"></text:bookmark-ref></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">lalune</text:p>
<text:p text:style-name="Text_20_body">Image 1 references
<text:sequence-ref text:reference-format="value" text:ref-name="lalune"></text:sequence-ref></text:p>
@@ -55,7 +55,7 @@ Chapter<text:bookmark-end text:name="chapter1" /></text:h>
<text:p text:style-name="First_20_paragraph">Chapter 1 references
<text:bookmark-ref text:reference-format="number" text:ref-name="chapter1"></text:bookmark-ref><text:s /><text:bookmark-ref text:reference-format="text" text:ref-name="chapter1">The
Chapter</text:bookmark-ref></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">Voyage dans la Lune</text:p>
<text:p text:style-name="Text_20_body">Image 1 references
<text:sequence-ref text:reference-format="value" text:ref-name="lalune"></text:sequence-ref><text:s /><text:sequence-ref text:reference-format="caption" text:ref-name="lalune">La
+2 -2
View File
@@ -923,10 +923,10 @@ link in pointy braces</text:span></text:a>.</text:p>
<text:h text:style-name="Heading_20_1" text:outline-level="1"><text:bookmark-start text:name="images" />Images<text:bookmark-end text:name="images" /></text:h>
<text:p text:style-name="First_20_paragraph">From “Voyage dans la Lune” by
Georges Melies (1902):</text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureWithCaption"><draw:frame draw:name="img1" text:anchor-type="as-char"><draw:image xlink:href="lalune.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame></text:p>
<text:p text:style-name="FigureCaption">lalune</text:p>
<text:p text:style-name="Text_20_body">Here is a movie
<draw:frame draw:name="img2"><draw:image xlink:href="movie.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame>
<draw:frame draw:name="img2" text:anchor-type="as-char"><draw:image xlink:href="movie.jpg" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" /></draw:frame>
icon.</text:p>
<text:p text:style-name="Horizontal_20_Line" />
<text:h text:style-name="Heading_20_1" text:outline-level="1"><text:bookmark-start text:name="footnotes" />Footnotes<text:bookmark-end text:name="footnotes" /></text:h>