Skip to content

Commit

Permalink
LT-21708 Add image and caption textframes to word export (#28)
Browse files Browse the repository at this point in the history
* LT-21708 Add image and caption textframes to word export

- Reconfigure images to be inline instead of anchored.
(Textframes require inline images; textboxes require anchored.)
- Clean up cloning of image runs.
- Create a global paragraph style to use for textframes.
- Link textframe style to images and their captions and ensure image
and associated caption are added to the same textframe.
- Ensure Word does not merge adjacent images into the same textframe.
This requires adding an empty paragraph between the image textframes,
otherwise Word will automatically merge them into a single textframe
that can't be split.
  • Loading branch information
aror92 authored Apr 24, 2024
1 parent f0b50f3 commit 81f697a
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 183 deletions.
329 changes: 149 additions & 180 deletions Src/xWorks/LcmWordGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,12 @@ internal void LinkParaStyle(string styleName)
if (par.ParagraphProperties.Descendants<ParagraphStyleId>().Any())
return;

par.ParagraphProperties.Append(new ParagraphStyleId() { Val = styleName });
par.ParagraphProperties.PrependChild(new ParagraphStyleId() { Val = styleName });
}
else
{
WP.ParagraphProperties paragraphProps = new WP.ParagraphProperties(new ParagraphStyleId() { Val = styleName });
par.Append(paragraphProps);
par.PrependChild(paragraphProps);
}
}

Expand Down Expand Up @@ -434,31 +434,22 @@ public void Append(WP.Table table)
/// </summary>
/// <param name="run">The run to append.</param>
/// <param name="forceNewParagraph">Even if a paragraph exists, force the creation of a new paragraph.</param>
public void AppendToParagraph(WP.Run run, bool forceNewParagraph)
public void AppendToParagraph(IFragment fragToCopy, OpenXmlElement run, bool forceNewParagraph)
{
// Deep clone the run b/c of its tree of properties and to maintain styles.
WP.Paragraph lastPar = forceNewParagraph ? GetNewParagraph() : GetLastParagraph();
lastPar.AppendChild(run.CloneNode(true));
lastPar.AppendChild(CloneRun(fragToCopy, run));
}

public void AppendCaptionToParagraph(WP.SimpleField caption)
public OpenXmlElement CloneRun(IFragment fragToCopy, OpenXmlElement run)
{
// Deep clone the run b/c of its tree of properties and to maintain styles.
WP.Paragraph lastPar = GetLastParagraph();
lastPar.AppendChild(caption.CloneNode(true));
}
if (run.Descendants<DocumentFormat.OpenXml.Drawing.Blip>().Any())
{
return CloneImageRun(fragToCopy, run);
}

return run.CloneNode(true);

/// <summary>
/// Appends a new run inside the last paragraph of the doc fragment--creates a new paragraph if none exists or if forceNewParagraph is true.
/// The run will be added to the end of the paragraph.
/// </summary>
/// <param name="run">The run to append.</param>
/// <param name="forceNewParagraph">Even if a paragraph exists, force the creation of a new paragraph.</param>
public void AppendImageToParagraph(IFragment fragToCopy, OpenXmlElement run, bool forceNewParagraph)
{
WP.Paragraph lastPar = forceNewParagraph ? GetNewParagraph() : GetLastParagraph();
var clonedRun = CloneImageRun(fragToCopy, run);
lastPar.AppendChild(clonedRun);
}

/// <summary>
Expand Down Expand Up @@ -983,50 +974,81 @@ public void AddEntryData(IFragmentWriter writer, List<ConfiguredLcmGenerator.Con
ConfigurableDictionaryNode config = piece.Config;

var elements = frag.DocBody.Elements().ToList();

// This variable will track whether or not we have already added an image from this piece to the Word doc.
// In the case that more than one image appears in the same piece
// (e.g. one entry with multiple senses and a picture for each sense),
// we need to add an empty paragraph between the images to prevent
// all the images and their captions from being merged into a single textframe by Word.
Boolean pieceHasImage = false;

foreach (OpenXmlElement elem in elements)
{
if (elem.Descendants<DocumentFormat.OpenXml.Drawing.Blip>().Any())
switch (elem)
{
// The image should begin its own paragraph, as should the next run,
// to maintain correct order of the image.
// If the image has a caption, it should remain in the same paragraph as the image.
wordWriter.ForceNewParagraph = true;
wordWriter.WordFragment.AppendImageToParagraph(frag, elem, wordWriter.ForceNewParagraph);
}
case WP.Run run:
// TODO: should no longer need to add spaces here after before/after text is handled.
// For spaces to show correctly, set preserve spaces on the text element
WP.Text txt = new WP.Text(" ");
txt.Space = SpaceProcessingModeValues.Preserve;
run.AppendChild(txt);

if (config.Label == "Pictures" || config.Parent?.Label == "Pictures")
{
// Runs containing pictures or captions need to be in separate paragraphs
// from whatever precedes and follows them because they will be added into textframes,
// while non-picture content should not be added to the textframes.
wordWriter.ForceNewParagraph = true;

else{
switch (elem)
{
//case WP.SimpleField caption:
// TODO: Captions should be created as runs inside simplefields--make sure to handle this case
// SimpleField stores an image caption.
// In this case, the caption is added to the last referenced paragraph,
// which should contain the associated image.
//wordWriter.WordFragment.AppendCaptionToParagraph(caption);
//break;
case WP.Run run:
// TODO: should no longer need to add spaces here after before/after text is handled.
// For spaces to show correctly, set preserve spaces on the text element
WP.Text txt = new WP.Text(" ");
txt.Space = SpaceProcessingModeValues.Preserve;
run.AppendChild(txt);
wordWriter.WordFragment.AppendToParagraph(run, wordWriter.ForceNewParagraph);
wordWriter.ForceNewParagraph = false;
// Word automatically merges adjacent textframes with the same size specifications.
// If the run we are adding is an image (i.e. a Drawing object),
// and it is being added after another image run was previously added from the same piece,
// we need to append an empty paragraph between to maintain separate textframes.
//
// Checking for adjacent images and adding an empty paragraph between won't work,
// because each image run is followed by runs containing its caption,
// copyright & license, etc.
//
// But, a lexical entry corresponds to a single piece and all the images it contains
// are added sequentially at the end of the piece, after all of the senses.
// This means the order of runs w/in a piece is: headword run, sense1 run, sense2 run, ... ,
// [image1 run, caption1 run, copyright&license1 run], [image2 run, caption2 run, copyright&license2 run], ...
// We need empty paragraphs between the [] textframe chunks, which corresponds to adding an empty paragraph
// immediately before any image run other than the first image run in a piece.
if (run.Descendants<Drawing>().Any())
{
if (pieceHasImage)
{
wordWriter.WordFragment.AppendToParagraph(frag, new Run(), true);
}

// We have now added at least one image from this piece.
pieceHasImage = true;
}

wordWriter.WordFragment.AppendToParagraph(frag, run, wordWriter.ForceNewParagraph);
wordWriter.WordFragment.LinkParaStyle(WordStylesGenerator.PictureAndCaptionTextframeStyle);
}

// Add the paragraph style.
else
{
wordWriter.WordFragment.AppendToParagraph(frag, run, wordWriter.ForceNewParagraph);
wordWriter.ForceNewParagraph = false;
wordWriter.WordFragment.LinkParaStyle(frag.ParagraphStyle);
}

break;
case WP.Table table:
wordWriter.WordFragment.Append(table);
break;

// Start a new paragraph with the next run to maintain the correct position of the table.
wordWriter.ForceNewParagraph = true;
break;
default:
throw new Exception("Unexpected element type on DocBody: " + elem.GetType().ToString());
case WP.Table table:
wordWriter.WordFragment.Append(table);

// Start a new paragraph with the next run to maintain the correct position of the table.
wordWriter.ForceNewParagraph = true;
break;

default:
throw new Exception("Unexpected element type on DocBody: " + elem.GetType().ToString());

}
}
}
}
Expand Down Expand Up @@ -1085,15 +1107,6 @@ public IFragment AddImage(string classAttribute, string srcAttribute, string pic
}
public IFragment AddImageCaption(string captionContent)
{
// TODO: captions need to be added into simplefields and linked to the relevant image/table to show as captions
/*SimpleField simpleField = new SimpleField(new Run(new RunProperties(new NoProof()), new WP.Text() { Text = " ", Space = SpaceProcessingModeValues.Preserve }));
simpleField.Instruction = @"SEQ " + "Figure 1";
Run runLabel = new Run(new WP.Text() { Text = " " + captionContent, Space = SpaceProcessingModeValues.Preserve });
simpleField.Append(runLabel);
DocFragment frag = new DocFragment();
frag.DocBody.Append(simpleField);
return frag;*/

return new DocFragment(captionContent);
}
public IFragment GenerateSenseNumber(string formattedSenseNumber, string senseNumberWs, ConfigurableDictionaryNode senseConfigNode)
Expand Down Expand Up @@ -1291,127 +1304,83 @@ public static Drawing CreateImage(WordprocessingDocument doc, string filepath, s
// Create and add a floating image with image wrap set to top/bottom
// Name for the image -- the name of the file after all containing folders and the file extension are removed.
string name = (filepath.Split('\\').Last()).Split('.').First();
string haPosition = "right";
// Define the reference of the image.
DrawingWP.Anchor anchor = new DrawingWP.Anchor();
anchor.Append(new DrawingWP.SimplePosition() { X = 0L, Y = 0L });
anchor.Append(
new DrawingWP.HorizontalPosition(
new DrawingWP.HorizontalAlignment(haPosition)
)
{
RelativeFrom =
DrawingWP.HorizontalRelativePositionValues.Margin
}
);
anchor.Append(
new DrawingWP.VerticalPosition(
new DrawingWP.PositionOffset("0")
)
{
RelativeFrom =
DrawingWP.VerticalRelativePositionValues.Paragraph
}
);
anchor.Append(
new DrawingWP.Extent()
{
Cx = widthEmus,
Cy = heightEmus
}
);
anchor.Append(
new DrawingWP.EffectExtent()
{
LeftEdge = 0L,
TopEdge = 0L,
RightEdge = 0L,
BottomEdge = 0L
}
);
anchor.Append(new DrawingWP.WrapTopBottom());
anchor.Append(
new DrawingWP.DocProperties()
{
Id = (UInt32Value)1U,
Name = name
}
);
anchor.Append(
new DrawingWP.NonVisualGraphicFrameDrawingProperties(
new XmlDrawing.GraphicFrameLocks() { NoChangeAspect = true })
);
anchor.Append(
new XmlDrawing.Graphic(
new XmlDrawing.GraphicData(
new Pictures.Picture(

new Pictures.NonVisualPictureProperties(
new Pictures.NonVisualDrawingProperties()
{
Id = (UInt32Value)0U,
Name = name
},
new Pictures.NonVisualPictureDrawingProperties()),

new Pictures.BlipFill(
new XmlDrawing.Blip(
new XmlDrawing.BlipExtensionList(
new XmlDrawing.BlipExtension()
var element = new Drawing(
new DrawingWP.Inline(
new DrawingWP.Extent()
{
Cx = widthEmus,
Cy = heightEmus
},
new DrawingWP.EffectExtent()
{
LeftEdge = 0L,
TopEdge = 0L,
RightEdge = 0L,
BottomEdge = 0L
},
new DrawingWP.DocProperties()
{
Id = (UInt32Value)1U,
Name = name
},
new DrawingWP.NonVisualGraphicFrameDrawingProperties(
new XmlDrawing.GraphicFrameLocks() { NoChangeAspect = true }),
new XmlDrawing.Graphic(
new XmlDrawing.GraphicData(
new Pictures.Picture(
new Pictures.NonVisualPictureProperties(
new Pictures.NonVisualDrawingProperties()
{
Id = (UInt32Value)0U,
Name = name
},
new Pictures.NonVisualPictureDrawingProperties(
new XmlDrawing.PictureLocks()
{NoChangeAspect = true, NoChangeArrowheads = true}
)
),
new Pictures.BlipFill(
new XmlDrawing.Blip(
new XmlDrawing.BlipExtensionList(
new XmlDrawing.BlipExtension(
new DocumentFormat.OpenXml.Office2010.Drawing.UseLocalDpi() {Val = false}
) { Uri = "{28A0092B-C50C-407E-A947-70E740481C1C}" }
)
)
{
Embed = partId,
CompressionState = XmlDrawing.BlipCompressionValues.Print
},
new XmlDrawing.SourceRectangle(),
new XmlDrawing.Stretch(new XmlDrawing.FillRectangle())
),
new Pictures.ShapeProperties(
new XmlDrawing.Transform2D(
new XmlDrawing.Offset() { X = 0L, Y = 0L },
new XmlDrawing.Extents()
{
Uri =
"{28A0092B-C50C-407E-A947-70E740481C1C}"
})
)
{
Embed = partId,
CompressionState =
XmlDrawing.BlipCompressionValues.Print
},
new XmlDrawing.Stretch(
new XmlDrawing.FillRectangle())),

new Pictures.ShapeProperties(

new XmlDrawing.Transform2D(
new XmlDrawing.Offset() { X = 0L, Y = 0L },

new XmlDrawing.Extents()
{
Cx = widthEmus,
Cy = heightEmus
}),

new XmlDrawing.PresetGeometry(
new XmlDrawing.AdjustValueList()
Cx = widthEmus,
Cy = heightEmus
}
),
new XmlDrawing.PresetGeometry(
new XmlDrawing.AdjustValueList()
) { Preset = XmlDrawing.ShapeTypeValues.Rectangle },
new XmlDrawing.NoFill()
) {BlackWhiteMode = XmlDrawing.BlackWhiteModeValues.Auto}
)
{ Preset = XmlDrawing.ShapeTypeValues.Rectangle }
)
)
)
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
) { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" }
)
)
{
DistanceFromTop = (UInt32Value)0U,
DistanceFromBottom = (UInt32Value)0U,
DistanceFromLeft = (UInt32Value)0U,
DistanceFromRight = (UInt32Value)0U
}
);

anchor.DistanceFromTop = (UInt32Value)0U;
anchor.DistanceFromBottom = (UInt32Value)0U;

// Want 4pt padding on right & left; calculate what this is in emus.
anchor.DistanceFromLeft = (UInt32Value)(rlMarginInches * emusPerInch);
anchor.DistanceFromRight = (UInt32Value)(rlMarginInches * emusPerInch);
anchor.SimplePos = false;

// RelativeHeight determines order of display when elements overlap
// (e.g. how far towards the front or back this element's priority should be).
// Since we choose not to allow overlap, we needn't worry about relative height.
anchor.RelativeHeight = (UInt32Value)0U;
anchor.BehindDoc = false;
anchor.Locked = false;
anchor.LayoutInCell = true;
anchor.AllowOverlap = false;

Drawing element = new Drawing();
element.Append(anchor);

return element;
}

Expand Down
Loading

0 comments on commit 81f697a

Please sign in to comment.