From 81f697a0be846b1cdd63cae4c0da849418ba0715 Mon Sep 17 00:00:00 2001 From: Ariel Ror Date: Wed, 24 Apr 2024 14:49:20 -0400 Subject: [PATCH] LT-21708 Add image and caption textframes to word export (#28) * 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. --- Src/xWorks/LcmWordGenerator.cs | 329 ++++++++++++++---------------- Src/xWorks/WordStylesGenerator.cs | 38 +++- 2 files changed, 184 insertions(+), 183 deletions(-) diff --git a/Src/xWorks/LcmWordGenerator.cs b/Src/xWorks/LcmWordGenerator.cs index 20b5dfe819..cb378779d8 100644 --- a/Src/xWorks/LcmWordGenerator.cs +++ b/Src/xWorks/LcmWordGenerator.cs @@ -301,12 +301,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 +434,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 +974,81 @@ 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 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().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 +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) @@ -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; } diff --git a/Src/xWorks/WordStylesGenerator.cs b/Src/xWorks/WordStylesGenerator.cs index 5db6db276c..11cb8c993e 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,11 @@ 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 GenerateWordStyleFromPictureOptions(configNode, pictureOptions, styleName, cache, propertyTable); default: { @@ -517,6 +519,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) {