windowcapture
исходный код / Integration/DocxRenderer.cs

DocxRenderer.cs

461 строк · 18,685 байт · модуль Integration
  1using System;
  2using System.Collections.Generic;
  3using System.Drawing;
  4using System.Drawing.Drawing2D;
  5using System.IO;
  6using System.IO.Packaging;
  7using System.Text;
  8using System.Xml;
  9
 10namespace WindowCapture.Integration
 11{
 12    /// <summary>
 13    /// Simple renderer for .docx files - renders document content to bitmap
 14    /// </summary>
 15    public class DocxRenderer
 16    {
 17        private const int PageWidth = 280;
 18        private const int PageMargin = 10;
 19        private const int LineHeight = 14;
 20        private const int ParagraphSpacing = 8;
 21
 22        private static readonly Font NormalFont = new Font("Times New Roman", 9f);
 23        private static readonly Font BoldFont = new Font("Times New Roman", 9f, FontStyle.Bold);
 24        private static readonly Font ItalicFont = new Font("Times New Roman", 9f, FontStyle.Italic);
 25        private static readonly Font CaptionFont = new Font("Times New Roman", 8f);
 26        private static readonly Color TextColor = Color.Black;
 27        private static readonly Color PageColor = Color.White;
 28
 29        /// <summary>
 30        /// Try to extract embedded thumbnail from docx file
 31        /// Office documents contain a thumbnail in docProps/thumbnail.wmf or thumbnail.emf
 32        /// </summary>
 33        public static Bitmap ExtractThumbnail(string filePath)
 34        {
 35            if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
 36                return null;
 37
 38            try
 39            {
 40                using (var package = Package.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
 41                {
 42                    // Try different thumbnail locations
 43                    string[] thumbnailPaths = new string[]
 44                    {
 45                        "/docProps/thumbnail.emf",
 46                        "/docProps/thumbnail.wmf",
 47                        "/docProps/thumbnail.jpeg",
 48                        "/docProps/thumbnail.png"
 49                    };
 50
 51                    foreach (var path in thumbnailPaths)
 52                    {
 53                        Uri thumbUri = new Uri(path, UriKind.Relative);
 54                        if (package.PartExists(thumbUri))
 55                        {
 56                            var thumbPart = package.GetPart(thumbUri);
 57                            using (var stream = thumbPart.GetStream())
 58                            using (var ms = new MemoryStream())
 59                            {
 60                                byte[] buffer = new byte[4096];
 61                                int read;
 62                                while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
 63                                    ms.Write(buffer, 0, read);
 64
 65                                ms.Position = 0;
 66
 67                                // Try to create image from the data
 68                                try
 69                                {
 70                                    // For EMF/WMF files, use Image.FromStream which handles metafiles
 71                                    using (var img = Image.FromStream(ms))
 72                                    {
 73                                        // Convert to bitmap
 74                                        var bmp = new Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 75                                        using (var g = Graphics.FromImage(bmp))
 76                                        {
 77                                            g.Clear(Color.White);
 78                                            g.DrawImage(img, 0, 0, img.Width, img.Height);
 79                                        }
 80                                        return bmp;
 81                                    }
 82                                }
 83                                catch
 84                                {
 85                                    // If standard loading fails, try as raw bitmap
 86                                    ms.Position = 0;
 87                                    try
 88                                    {
 89                                        return new Bitmap(ms);
 90                                    }
 91                                    catch { }
 92                                }
 93                            }
 94                        }
 95                    }
 96                }
 97            }
 98            catch { }
 99
100            return null;
101        }
102
103        /// <summary>
104        /// Render a .docx file to bitmap preview
105        /// </summary>
106        public static Bitmap RenderDocx(string filePath, int maxHeight = 400)
107        {
108            if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
109                return null;
110
111            try
112            {
113                // First try to extract embedded thumbnail
114                var thumbnail = ExtractThumbnail(filePath);
115                if (thumbnail != null)
116                    return thumbnail;
117
118                // Fallback: Parse docx content and render
119                var elements = ParseDocx(filePath);
120                if (elements == null || elements.Count == 0)
121                    return null;
122
123                // Render to bitmap
124                return RenderElements(elements, maxHeight);
125            }
126            catch
127            {
128                return null;
129            }
130        }
131
132        /// <summary>
133        /// Parse docx file and extract document elements
134        /// </summary>
135        private static List<DocElement> ParseDocx(string filePath)
136        {
137            var elements = new List<DocElement>();
138
139            try
140            {
141                using (var package = Package.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
142                {
143                    // Find document.xml
144                    Uri docUri = new Uri("/word/document.xml", UriKind.Relative);
145                    if (!package.PartExists(docUri)) return elements;
146
147                    var docPart = package.GetPart(docUri);
148                    string xml;
149
150                    using (var stream = docPart.GetStream())
151                    using (var reader = new StreamReader(stream, Encoding.UTF8))
152                    {
153                        xml = reader.ReadToEnd();
154                    }
155
156                    // Parse relationships for images
157                    var imageMap = new Dictionary<string, byte[]>();
158                    Uri relsUri = new Uri("/word/_rels/document.xml.rels", UriKind.Relative);
159                    if (package.PartExists(relsUri))
160                    {
161                        var relsPart = package.GetPart(relsUri);
162                        var relMap = ParseRelationships(relsPart);
163
164                        // Load images
165                        foreach (var kv in relMap)
166                        {
167                            if (kv.Value.EndsWith(".png") || kv.Value.EndsWith(".jpg") ||
168                                kv.Value.EndsWith(".jpeg") || kv.Value.EndsWith(".gif"))
169                            {
170                                string imgPath = "/word/" + kv.Value.TrimStart('/');
171                                Uri imgUri = new Uri(imgPath, UriKind.Relative);
172                                if (package.PartExists(imgUri))
173                                {
174                                    var imgPart = package.GetPart(imgUri);
175                                    using (var imgStream = imgPart.GetStream())
176                                    using (var ms = new MemoryStream())
177                                    {
178                                        byte[] buffer = new byte[4096];
179                                        int read;
180                                        while ((read = imgStream.Read(buffer, 0, buffer.Length)) > 0)
181                                            ms.Write(buffer, 0, read);
182                                        imageMap[kv.Key] = ms.ToArray();
183                                    }
184                                }
185                            }
186                        }
187                    }
188
189                    ParseDocumentXml(xml, elements, imageMap);
190                }
191            }
192            catch
193            {
194                // Return what we have
195            }
196
197            return elements;
198        }
199
200        /// <summary>
201        /// Parse relationships file
202        /// </summary>
203        private static Dictionary<string, string> ParseRelationships(PackagePart relsPart)
204        {
205            var map = new Dictionary<string, string>();
206            try
207            {
208                using (var stream = relsPart.GetStream())
209                using (var reader = new StreamReader(stream))
210                {
211                    var doc = new XmlDocument();
212                    doc.LoadXml(reader.ReadToEnd());
213
214                    var relationships = doc.GetElementsByTagName("Relationship");
215                    foreach (XmlNode rel in relationships)
216                    {
217                        var idAttr = rel.Attributes["Id"];
218                        var targetAttr = rel.Attributes["Target"];
219                        if (idAttr != null && targetAttr != null)
220                        {
221                            map[idAttr.Value] = targetAttr.Value;
222                        }
223                    }
224                }
225            }
226            catch { }
227            return map;
228        }
229
230        /// <summary>
231        /// Parse Word document.xml and extract paragraphs
232        /// </summary>
233        private static void ParseDocumentXml(string xml, List<DocElement> elements, Dictionary<string, byte[]> imageMap)
234        {
235            try
236            {
237                var doc = new XmlDocument();
238                doc.LoadXml(xml);
239
240                var nsmgr = new XmlNamespaceManager(doc.NameTable);
241                nsmgr.AddNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
242                nsmgr.AddNamespace("a", "http://schemas.openxmlformats.org/drawingml/2006/main");
243                nsmgr.AddNamespace("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture");
244                nsmgr.AddNamespace("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
245
246                // Find all paragraphs
247                var paragraphs = doc.SelectNodes("//w:p", nsmgr);
248                if (paragraphs == null) return;
249
250                foreach (XmlNode para in paragraphs)
251                {
252                    var paraElement = new DocElement { Type = ElementType.Paragraph };
253                    var runs = new StringBuilder();
254                    bool isBold = false;
255                    bool isItalic = false;
256                    bool isCentered = false;
257                    bool hasImage = false;
258                    string imageRelId = null;
259
260                    // Check paragraph alignment
261                    var jcNode = para.SelectSingleNode(".//w:jc/@w:val", nsmgr);
262                    if (jcNode != null && jcNode.Value == "center")
263                        isCentered = true;
264
265                    // Check for images (drawings)
266                    var blipNode = para.SelectSingleNode(".//a:blip/@r:embed", nsmgr);
267                    if (blipNode != null)
268                    {
269                        hasImage = true;
270                        imageRelId = blipNode.Value;
271                    }
272
273                    // Get text runs
274                    var textRuns = para.SelectNodes(".//w:r", nsmgr);
275                    if (textRuns != null)
276                    {
277                        foreach (XmlNode run in textRuns)
278                        {
279                            // Check run properties
280                            var boldNode = run.SelectSingleNode(".//w:b", nsmgr);
281                            var italicNode = run.SelectSingleNode(".//w:i", nsmgr);
282                            if (boldNode != null) isBold = true;
283                            if (italicNode != null) isItalic = true;
284
285                            // Get text
286                            var textNodes = run.SelectNodes(".//w:t", nsmgr);
287                            if (textNodes != null)
288                            {
289                                foreach (XmlNode textNode in textNodes)
290                                {
291                                    runs.Append(textNode.InnerText);
292                                }
293                            }
294                        }
295                    }
296
297                    paraElement.Text = runs.ToString();
298                    paraElement.IsBold = isBold;
299                    paraElement.IsItalic = isItalic;
300                    paraElement.IsCentered = isCentered;
301                    paraElement.HasImage = hasImage;
302
303                    // Load image if present
304                    if (hasImage && imageRelId != null && imageMap.ContainsKey(imageRelId))
305                    {
306                        try
307                        {
308                            using (var ms = new MemoryStream(imageMap[imageRelId]))
309                            {
310                                paraElement.Image = new Bitmap(ms);
311                            }
312                        }
313                        catch { }
314                    }
315
316                    // Only add non-empty paragraphs or paragraphs with images
317                    if (!string.IsNullOrWhiteSpace(paraElement.Text) || paraElement.HasImage)
318                    {
319                        elements.Add(paraElement);
320                    }
321                }
322            }
323            catch
324            {
325                // Ignore parsing errors
326            }
327        }
328
329        /// <summary>
330        /// Render parsed elements to bitmap
331        /// </summary>
332        private static Bitmap RenderElements(List<DocElement> elements, int maxHeight)
333        {
334            // First pass - calculate total height
335            int totalHeight = PageMargin * 2;
336
337            using (var tempBmp = new Bitmap(1, 1))
338            using (var tempG = Graphics.FromImage(tempBmp))
339            {
340                foreach (var elem in elements)
341                {
342                    if (elem.HasImage && elem.Image != null)
343                    {
344                        // Scale image to fit width
345                        int imgWidth = Math.Min(elem.Image.Width, PageWidth - PageMargin * 2);
346                        int imgHeight = (int)((float)elem.Image.Height / elem.Image.Width * imgWidth);
347                        totalHeight += imgHeight + ParagraphSpacing;
348                    }
349
350                    if (!string.IsNullOrWhiteSpace(elem.Text))
351                    {
352                        Font font = elem.IsBold ? BoldFont : (elem.IsItalic ? ItalicFont : NormalFont);
353                        var textSize = tempG.MeasureString(elem.Text, font, PageWidth - PageMargin * 2);
354                        totalHeight += (int)textSize.Height + ParagraphSpacing;
355                    }
356                }
357            }
358
359            // Limit height
360            int bmpHeight = Math.Min(totalHeight, maxHeight);
361
362            // Create bitmap
363            var bmp = new Bitmap(PageWidth, bmpHeight);
364            using (var g = Graphics.FromImage(bmp))
365            {
366                g.SmoothingMode = SmoothingMode.AntiAlias;
367                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
368
369                // White background
370                g.Clear(PageColor);
371
372                // Draw border
373                using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
374                {
375                    g.DrawRectangle(pen, 0, 0, PageWidth - 1, bmpHeight - 1);
376                }
377
378                int y = PageMargin;
379
380                foreach (var elem in elements)
381                {
382                    if (y >= bmpHeight - PageMargin) break;
383
384                    // Draw image if present
385                    if (elem.HasImage && elem.Image != null)
386                    {
387                        int imgWidth = Math.Min(elem.Image.Width, PageWidth - PageMargin * 2);
388                        int imgHeight = (int)((float)elem.Image.Height / elem.Image.Width * imgWidth);
389
390                        int imgX = elem.IsCentered ? (PageWidth - imgWidth) / 2 : PageMargin;
391
392                        if (y + imgHeight <= bmpHeight)
393                        {
394                            g.DrawImage(elem.Image, imgX, y, imgWidth, imgHeight);
395                        }
396                        y += imgHeight + ParagraphSpacing / 2;
397                    }
398
399                    // Draw text
400                    if (!string.IsNullOrWhiteSpace(elem.Text) && y < bmpHeight - PageMargin)
401                    {
402                        Font font = elem.IsBold ? BoldFont : (elem.IsItalic ? ItalicFont : NormalFont);
403
404                        // Check if it's a figure caption
405                        if (elem.Text.StartsWith("Рисунок "))
406                            font = CaptionFont;
407
408                        var textSize = g.MeasureString(elem.Text, font, PageWidth - PageMargin * 2);
409                        int textX = elem.IsCentered ? (int)((PageWidth - textSize.Width) / 2) : PageMargin;
410
411                        using (var brush = new SolidBrush(TextColor))
412                        {
413                            var rect = new RectangleF(textX, y, PageWidth - PageMargin * 2, textSize.Height);
414                            g.DrawString(elem.Text, font, brush, rect);
415                        }
416
417                        y += (int)textSize.Height + ParagraphSpacing;
418                    }
419                }
420
421                // Draw fade at bottom if content is clipped
422                if (totalHeight > maxHeight)
423                {
424                    using (var fadeBrush = new LinearGradientBrush(
425                        new Rectangle(0, bmpHeight - 30, PageWidth, 30),
426                        Color.FromArgb(0, 255, 255, 255),
427                        Color.FromArgb(255, 255, 255, 255),
428                        90f))
429                    {
430                        g.FillRectangle(fadeBrush, 0, bmpHeight - 30, PageWidth, 30);
431                    }
432                }
433            }
434
435            return bmp;
436        }
437
438        /// <summary>
439        /// Document element types
440        /// </summary>
441        private enum ElementType
442        {
443            Paragraph,
444            Image
445        }
446
447        /// <summary>
448        /// Represents a document element (paragraph, image, etc.)
449        /// </summary>
450        private class DocElement
451        {
452            public ElementType Type;
453            public string Text;
454            public bool IsBold;
455            public bool IsItalic;
456            public bool IsCentered;
457            public bool HasImage;
458            public Bitmap Image;
459        }
460    }
461}