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}