From 98443331363e35d07a09738253de4c71486c2640 Mon Sep 17 00:00:00 2001 From: Ariel Rorabaugh Date: Tue, 23 Apr 2024 20:32:17 -0400 Subject: [PATCH] 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. Change-Id: I331b0d0294c137ea685a42ddac41282c4f7bb146 --- Src/xWorks/LcmWordGenerator.cs | 320 +++++++++++++----------------- Src/xWorks/WordStylesGenerator.cs | 39 +++- 2 files changed, 176 insertions(+), 183 deletions(-) diff --git a/Src/xWorks/LcmWordGenerator.cs b/Src/xWorks/LcmWordGenerator.cs index 20b5dfe819..6ac384cdc3 100644 --- a/Src/xWorks/LcmWordGenerator.cs +++ b/Src/xWorks/LcmWordGenerator.cs @@ -3,6 +3,7 @@ // (http://www.gnu.org/licenses/lgpl-2.1.html) using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Office2010.Drawing; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using Icu.Collation; @@ -301,12 +302,12 @@ internal void LinkParaStyle(string styleName) if (par.ParagraphProperties.Descendants().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); } } @@ -434,31 +435,22 @@ public void Append(WP.Table table) /// /// The run to append. /// Even if a paragraph exists, force the creation of a new paragraph. - 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().Any()) + { + return CloneImageRun(fragToCopy, run); + } + + return run.CloneNode(true); - /// - /// 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. - /// - /// The run to append. - /// Even if a paragraph exists, force the creation of a new paragraph. - public void AppendImageToParagraph(IFragment fragToCopy, OpenXmlElement run, bool forceNewParagraph) - { - WP.Paragraph lastPar = forceNewParagraph ? GetNewParagraph() : GetLastParagraph(); - var clonedRun = CloneImageRun(fragToCopy, run); - lastPar.AppendChild(clonedRun); } /// @@ -983,50 +975,70 @@ public void AddEntryData(IFragmentWriter writer, List().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 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. + if (run.Descendants().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()); - } } } } @@ -1085,15 +1097,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) @@ -1291,127 +1294,84 @@ 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 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, + EditId = "50D07946" + } ); - 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; } diff --git a/Src/xWorks/WordStylesGenerator.cs b/Src/xWorks/WordStylesGenerator.cs index 5db6db276c..41d3cac544 100644 --- a/Src/xWorks/WordStylesGenerator.cs +++ b/Src/xWorks/WordStylesGenerator.cs @@ -31,6 +31,7 @@ public class WordStylesGenerator internal const string DictionaryMinor = "Dictionary-Minor"; internal const string WritingSystemPrefix = "writingsystemprefix"; internal const string WritingSystemStyleName = "Writing System Abbreviation"; + internal const string PictureAndCaptionTextframeStyle = "Image-Textframe-Style"; public static Style GenerateLetterHeaderStyle( ReadOnlyPropertyTable propertyTable, LcmStyleSheet mediatorStyleSheet) @@ -327,10 +328,12 @@ public static Styles GenerateWordStylesFromConfigurationNode( // children of collections. return GenerateWordStyleForSenses(configNode, senseOptions, ref styleName, propertyTable); - // TODO: handle listAndPara and pictureOptions cases + // TODO: handle listAndPara case and character portion of pictureOptions // case IParaOption listAndParaOpts: - //case DictionaryNodePictureOptions pictureOptions: - //return GenerateWordStyleFromPictureOptions(configNode, pictureOptions, styleName, cache, propertyTable); + + case DictionaryNodePictureOptions pictureOptions: + //return new Styles(); + return GenerateWordStyleFromPictureOptions(configNode, pictureOptions, styleName, cache, propertyTable); default: { @@ -517,6 +520,36 @@ private static Styles GenerateWordStyleForSenses(ConfigurableDictionaryNode conf return styleRules; } + private static Styles GenerateWordStyleFromPictureOptions(ConfigurableDictionaryNode configNode, DictionaryNodePictureOptions pictureOptions, + string baseSelection, LcmCache cache, ReadOnlyPropertyTable propertyTable) + { + var styles = new Styles(); + + var frameStyle = new Style(); + + // A textframe for holding an image/caption has to be a paragraph + frameStyle.Type = StyleValues.Paragraph; + + // We use FLEX's max image width as the width for the textframe. + // Note: 1 inch is equivalent to 72 points, and width is specified in twentieths of a point. + // Thus, we calculate textframe width by multiplying max image width in inches by 72*30 = 1440 + var textFrameWidth = LcmWordGenerator.maxImageWidthInches * 1440; + + // A paragraph is turned into a textframe simply by adding a frameproperties object inside the paragraph properties. + // We leave a 4-pt border around the textframe--80 twentieths of a point. + var textFrameBorder = "80"; + var textFrameProps = new FrameProperties() { Width = textFrameWidth.ToString(), HeightType = HeightRuleValues.Auto, HorizontalSpace = textFrameBorder, VerticalSpace = textFrameBorder, Wrap = TextWrappingValues.NotBeside, VerticalPosition = VerticalAnchorValues.Text, HorizontalPosition = HorizontalAnchorValues.Margin, XAlign = HorizontalAlignmentValues.Right }; + var parProps = new ParagraphProperties(); + frameStyle.StyleId = PictureAndCaptionTextframeStyle; + frameStyle.StyleName = new StyleName(){Val = PictureAndCaptionTextframeStyle}; + parProps.Append(textFrameProps); + frameStyle.Append(parProps); + styles.Append(frameStyle); + + //TODO: define picture/caption character styles based on user specifications in FLEx + return styles; + } + private static Styles GenerateWordStylesFromListAndParaOptions(ConfigurableDictionaryNode configNode, IParaOption listAndParaOpts, ref string baseSelection, LcmCache cache, ReadOnlyPropertyTable propertyTable) {