1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Imaging; 5using System.IO; 6using System.Runtime.InteropServices; 7using System.Text.RegularExpressions; 8using WindowCapture.Helpers; 9 10namespace WindowCapture.Integration 11{ 12 /// <summary> 13 /// Info about an open Word document 14 /// </summary> 15 public class WordDocumentInfo 16 { 17 public string Name; 18 public string FullPath; 19 public object DocumentObject; // Word.Document COM object 20 21 public override string ToString() 22 { 23 return Name; 24 } 25 } 26 27 /// <summary> 28 /// Represents a text item (description or caption) found in a Word document 29 /// </summary> 30 public class DocumentTextItem 31 { 32 public int ParagraphIndex; // 1-based index in doc.Paragraphs 33 public string OriginalText; // Full original text 34 public string TextToRephrase; // Text sent to Gemini (for captions: without prefix) 35 public bool IsCaption; // true if "Рисунок N — ..." format 36 public string CaptionPrefix; // e.g. "Рисунок 1 — " (preserved, not sent to Gemini) 37 public string NewText; // Rephrased text from Gemini 38 public bool Accepted = true; // User accepted in preview dialog 39 } 40 41 /// <summary> 42 /// Integration with Microsoft Word via COM automation 43 /// </summary> 44 public static class WordIntegration 45 { 46 private static object wordApp; 47 48 private static void Log(string message) 49 { 50 Logger.Log("word", message); 51 } 52 private static WordDocumentInfo selectedDocument; 53 54 /// <summary> 55 /// Deterministically release a transient COM RCW. Late-bound automation spawns many 56 /// short-lived wrappers (Selection/Range/Font/Fields/...) that otherwise linger until GC, 57 /// keeping WINWORD.EXE alive (the GC.Collect() in Cleanup is the current band-aid). 58 /// NEVER pass wordApp or a stored DocumentObject here — their lifetime is intentional. 59 /// </summary> 60 private static void SafeRelease(ref object o) 61 { 62 if (o == null) return; 63 try { while (Marshal.ReleaseComObject(o) > 0) { } } 64 catch { } 65 o = null; 66 } 67 68 // Word constants 69 private const int wdCollapseEnd = 0; 70 private const int wdAlignParagraphLeft = 0; 71 private const int wdAlignParagraphCenter = 1; 72 private const int wdAlignParagraphJustify = 3; 73 private const int wdLineSpaceMultiple = 5; 74 private const int wdStory = 6; 75 private const int wdLine = 5; 76 private const int wdExtend = 1; 77 private const int wdCaptionPositionBelow = 1; 78 private const int wdCaptionPositionAbove = 0; 79 80 /// <summary> 81 /// Check if Word is available 82 /// </summary> 83 public static bool IsWordInstalled() 84 { 85 try 86 { 87 Type wordType = Type.GetTypeFromProgID("Word.Application"); 88 return wordType != null; 89 } 90 catch 91 { 92 return false; 93 } 94 } 95 96 /// <summary> 97 /// Get currently running Word application, or null if none 98 /// </summary> 99 public static object GetRunningWordApp() 100 { 101 try 102 { 103 // Try to get running instance 104 return Marshal.GetActiveObject("Word.Application"); 105 } 106 catch 107 { 108 return null; 109 } 110 } 111 112 /// <summary> 113 /// Get list of open Word documents 114 /// </summary> 115 public static List<WordDocumentInfo> GetOpenDocuments() 116 { 117 var docs = new List<WordDocumentInfo>(); 118 119 try 120 { 121 object app = GetRunningWordApp(); 122 if (app == null) return docs; 123 124 wordApp = app; 125 126 // Get Documents collection 127 object documents = app.GetType().InvokeMember("Documents", 128 System.Reflection.BindingFlags.GetProperty, null, app, null); 129 130 // Get count 131 int count = (int)documents.GetType().InvokeMember("Count", 132 System.Reflection.BindingFlags.GetProperty, null, documents, null); 133 134 for (int i = 1; i <= count; i++) 135 { 136 try 137 { 138 object doc = documents.GetType().InvokeMember("Item", 139 System.Reflection.BindingFlags.InvokeMethod, null, documents, new object[] { i }); 140 141 string name = (string)doc.GetType().InvokeMember("Name", 142 System.Reflection.BindingFlags.GetProperty, null, doc, null); 143 144 string fullPath = ""; 145 try 146 { 147 fullPath = (string)doc.GetType().InvokeMember("FullName", 148 System.Reflection.BindingFlags.GetProperty, null, doc, null); 149 } 150 catch { } 151 152 docs.Add(new WordDocumentInfo 153 { 154 Name = name, 155 FullPath = fullPath, 156 DocumentObject = doc 157 }); 158 } 159 catch { } 160 } 161 162 // Release the Documents collection RCW (the individual doc RCWs are kept in 'docs'). 163 try { Marshal.ReleaseComObject(documents); } catch { } 164 } 165 catch { } 166 167 return docs; 168 } 169 170 /// <summary> 171 /// Get or set the selected Word document 172 /// </summary> 173 public static WordDocumentInfo SelectedDocument 174 { 175 get { return selectedDocument; } 176 set { selectedDocument = value; } 177 } 178 179 /// <summary> 180 /// Insert image with description and caption into Word document 181 /// Formatting: 182 /// - Description: Times New Roman 14, 1.5 line spacing, 1.25cm indent, justified, black 183 /// - Image: centered 184 /// - Caption: Times New Roman 12, centered, black, format "Рисунок N — text" 185 /// </summary> 186 public static bool InsertImageWithCaption(Bitmap image, string description, string captionText) 187 { 188 Log("=== InsertImageWithCaption start ==="); 189 Log("Description: " + (description ?? "null")); 190 Log("Caption: " + (captionText ?? "null")); 191 192 if (selectedDocument == null || selectedDocument.DocumentObject == null) 193 { 194 Log("ERROR: selectedDocument is null"); 195 return false; 196 } 197 198 try 199 { 200 object doc = selectedDocument.DocumentObject; 201 object app = wordApp; 202 203 if (app == null) 204 { 205 Log("wordApp is null, getting running Word app..."); 206 app = GetRunningWordApp(); 207 if (app == null) 208 { 209 Log("ERROR: Could not get Word app"); 210 return false; 211 } 212 wordApp = app; 213 } 214 215 Log("Getting next figure number..."); 216 // Get next figure number (count existing "Рисунок" captions + 1) 217 int nextFigureNumber = GetNextFigureNumber(doc); 218 Log("Next figure number: " + nextFigureNumber); 219 220 // Replace %PICNUM% in description with actual figure number 221 if (!string.IsNullOrEmpty(description) && description.Contains("%PICNUM%")) 222 { 223 description = description.Replace("%PICNUM%", nextFigureNumber.ToString()); 224 } 225 226 // Get Selection 227 object selection = app.GetType().InvokeMember("Selection", 228 System.Reflection.BindingFlags.GetProperty, null, app, null); 229 230 // Move to end of document 231 object range = doc.GetType().InvokeMember("Content", 232 System.Reflection.BindingFlags.GetProperty, null, doc, null); 233 range.GetType().InvokeMember("Collapse", 234 System.Reflection.BindingFlags.InvokeMethod, null, range, new object[] { wdCollapseEnd }); 235 236 // Select at end 237 range.GetType().InvokeMember("Select", 238 System.Reflection.BindingFlags.InvokeMethod, null, range, null); 239 240 // Insert description if provided 241 if (!string.IsNullOrEmpty(description)) 242 { 243 // Set font for description: Times New Roman 14, black 244 object font = selection.GetType().InvokeMember("Font", 245 System.Reflection.BindingFlags.GetProperty, null, selection, null); 246 font.GetType().InvokeMember("Name", 247 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { "Times New Roman" }); 248 font.GetType().InvokeMember("Size", 249 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 14f }); 250 font.GetType().InvokeMember("Color", 251 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 0 }); // wdColorBlack = 0 252 253 // Set paragraph format: 1.5 line spacing, 1.25cm indent, justified 254 object paragraphFormat = selection.GetType().InvokeMember("ParagraphFormat", 255 System.Reflection.BindingFlags.GetProperty, null, selection, null); 256 257 // Line spacing: 1.5 (18 points for 12pt font base, but we use LineSpacing property) 258 paragraphFormat.GetType().InvokeMember("LineSpacingRule", 259 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, new object[] { wdLineSpaceMultiple }); 260 paragraphFormat.GetType().InvokeMember("LineSpacing", 261 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, new object[] { 18f }); // 1.5 lines 262 263 // First line indent: 1.25 cm = 35.4 points (1 cm = 28.35 points) 264 paragraphFormat.GetType().InvokeMember("FirstLineIndent", 265 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, new object[] { 35.4f }); 266 267 // Alignment: justified 268 paragraphFormat.GetType().InvokeMember("Alignment", 269 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, new object[] { wdAlignParagraphJustify }); 270 271 // Insert description text 272 selection.GetType().InvokeMember("TypeText", 273 System.Reflection.BindingFlags.InvokeMethod, null, selection, new object[] { description }); 274 275 // New paragraph after description 276 selection.GetType().InvokeMember("TypeParagraph", 277 System.Reflection.BindingFlags.InvokeMethod, null, selection, null); 278 } 279 280 // Save image to temp file 281 string tempImagePath = Path.Combine(Path.GetTempPath(), "wc_temp_" + DateTime.Now.Ticks + ".png"); 282 image.Save(tempImagePath, ImageFormat.Png); 283 284 try 285 { 286 // Reset first line indent before image 287 object paragraphFormat2 = selection.GetType().InvokeMember("ParagraphFormat", 288 System.Reflection.BindingFlags.GetProperty, null, selection, null); 289 paragraphFormat2.GetType().InvokeMember("FirstLineIndent", 290 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat2, new object[] { 0f }); 291 292 // Center alignment for image 293 paragraphFormat2.GetType().InvokeMember("Alignment", 294 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat2, new object[] { wdAlignParagraphCenter }); 295 296 // Single line spacing for image paragraph (interdocument spacing = 0) 297 paragraphFormat2.GetType().InvokeMember("LineSpacingRule", 298 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat2, new object[] { 0 }); // wdLineSpaceSingle 299 300 // Insert image as InlineShape 301 object inlineShapes = selection.GetType().InvokeMember("InlineShapes", 302 System.Reflection.BindingFlags.GetProperty, null, selection, null); 303 304 object inlineShape = inlineShapes.GetType().InvokeMember("AddPicture", 305 System.Reflection.BindingFlags.InvokeMethod, null, inlineShapes, 306 new object[] { tempImagePath, false, true }); // FileName, LinkToFile, SaveWithDocument 307 308 // Scale image to fit 2 images per page 309 // A4 page: ~842pt height, margins ~144pt, usable ~700pt 310 // Per block (description + image + caption): ~350pt 311 // Description ~60pt, caption ~18pt, spacing ~30pt = ~108pt for text 312 // Max image height: ~240pt (~8.5cm) to fit 2 per page 313 // Max width: 425pt (15cm) 314 // Minimum scale: 75% (don't shrink more than 25%) 315 316 float width = (float)inlineShape.GetType().InvokeMember("Width", 317 System.Reflection.BindingFlags.GetProperty, null, inlineShape, null); 318 float height = (float)inlineShape.GetType().InvokeMember("Height", 319 System.Reflection.BindingFlags.GetProperty, null, inlineShape, null); 320 321 float maxWidth = 425f; // 15cm 322 float maxHeight = 240f; // ~8.5cm to fit 2 per page 323 float minScale = 0.75f; // Don't shrink more than 25% 324 325 float scaleW = width > maxWidth ? maxWidth / width : 1f; 326 float scaleH = height > maxHeight ? maxHeight / height : 1f; 327 float scale = Math.Min(scaleW, scaleH); 328 329 // Apply minimum scale constraint 330 if (scale < minScale) 331 scale = minScale; 332 333 if (scale < 1f) 334 { 335 float newWidth = width * scale; 336 float newHeight = height * scale; 337 inlineShape.GetType().InvokeMember("Width", 338 System.Reflection.BindingFlags.SetProperty, null, inlineShape, new object[] { newWidth }); 339 inlineShape.GetType().InvokeMember("Height", 340 System.Reflection.BindingFlags.SetProperty, null, inlineShape, new object[] { newHeight }); 341 } 342 343 // Move to end of line after image 344 selection.GetType().InvokeMember("EndKey", 345 System.Reflection.BindingFlags.InvokeMethod, null, selection, new object[] { wdStory }); 346 347 // Insert caption if provided: "Рисунок N — text" 348 if (!string.IsNullOrEmpty(captionText)) 349 { 350 // New paragraph for caption 351 selection.GetType().InvokeMember("TypeParagraph", 352 System.Reflection.BindingFlags.InvokeMethod, null, selection, null); 353 354 // Ensure "Рисунок" caption label exists 355 EnsureFigureCaptionLabel(app); 356 357 // Insert caption using InsertCaption method 358 // Parameters: Label, Title, TitleAutoText, Position, ExcludeLabel 359 // Title includes the dash and description text after the number 360 selection.GetType().InvokeMember("InsertCaption", 361 System.Reflection.BindingFlags.InvokeMethod, null, selection, 362 new object[] { 363 "Рисунок", // Label 364 " — " + captionText, // Title (appears after number) 365 Type.Missing, // TitleAutoText 366 wdCaptionPositionBelow, // Position 367 false // ExcludeLabel 368 }); 369 370 // Select the entire caption line to format it 371 // Move to start of line 372 selection.GetType().InvokeMember("HomeKey", 373 System.Reflection.BindingFlags.InvokeMethod, null, selection, 374 new object[] { wdLine }); 375 // Extend selection to end of line 376 selection.GetType().InvokeMember("EndKey", 377 System.Reflection.BindingFlags.InvokeMethod, null, selection, 378 new object[] { wdLine, wdExtend }); 379 380 // Format the caption: Times New Roman 12, centered, black, not italic 381 object fontCaption = selection.GetType().InvokeMember("Font", 382 System.Reflection.BindingFlags.GetProperty, null, selection, null); 383 fontCaption.GetType().InvokeMember("Name", 384 System.Reflection.BindingFlags.SetProperty, null, fontCaption, new object[] { "Times New Roman" }); 385 fontCaption.GetType().InvokeMember("Size", 386 System.Reflection.BindingFlags.SetProperty, null, fontCaption, new object[] { 12f }); 387 fontCaption.GetType().InvokeMember("Color", 388 System.Reflection.BindingFlags.SetProperty, null, fontCaption, new object[] { 0 }); // wdColorBlack 389 fontCaption.GetType().InvokeMember("Italic", 390 System.Reflection.BindingFlags.SetProperty, null, fontCaption, new object[] { false }); // Remove italic 391 392 object paragraphCaption = selection.GetType().InvokeMember("ParagraphFormat", 393 System.Reflection.BindingFlags.GetProperty, null, selection, null); 394 paragraphCaption.GetType().InvokeMember("Alignment", 395 System.Reflection.BindingFlags.SetProperty, null, paragraphCaption, new object[] { wdAlignParagraphCenter }); 396 paragraphCaption.GetType().InvokeMember("FirstLineIndent", 397 System.Reflection.BindingFlags.SetProperty, null, paragraphCaption, new object[] { 0f }); 398 // Normal line spacing for caption 399 paragraphCaption.GetType().InvokeMember("LineSpacingRule", 400 System.Reflection.BindingFlags.SetProperty, null, paragraphCaption, new object[] { 0 }); // wdLineSpaceSingle 401 402 // Move to end of line (deselect) and add new paragraph 403 selection.GetType().InvokeMember("EndKey", 404 System.Reflection.BindingFlags.InvokeMethod, null, selection, 405 new object[] { wdLine }); 406 selection.GetType().InvokeMember("TypeParagraph", 407 System.Reflection.BindingFlags.InvokeMethod, null, selection, null); 408 } 409 else 410 { 411 // No caption - still need new line after image 412 selection.GetType().InvokeMember("TypeParagraph", 413 System.Reflection.BindingFlags.InvokeMethod, null, selection, null); 414 } 415 } 416 finally 417 { 418 // Clean up temp file 419 try { File.Delete(tempImagePath); } catch { } 420 } 421 422 return true; 423 } 424 catch (Exception ex) 425 { 426 Log("ERROR: " + ex.Message + "\n" + ex.StackTrace); 427 return false; 428 } 429 } 430 431 /// <summary> 432 /// Get the next figure number by counting existing "Рисунок" captions in document 433 /// </summary> 434 private static int GetNextFigureNumber(object doc) 435 { 436 object fields = null; 437 try 438 { 439 Log("GetNextFigureNumber: getting fields..."); 440 // Get all fields in the document 441 fields = doc.GetType().InvokeMember("Fields", 442 System.Reflection.BindingFlags.GetProperty, null, doc, null); 443 444 int count = (int)fields.GetType().InvokeMember("Count", 445 System.Reflection.BindingFlags.GetProperty, null, fields, null); 446 Log("GetNextFigureNumber: found " + count + " fields"); 447 448 int figureCount = 0; 449 450 // Iterate through fields to count SEQ Рисунок fields 451 for (int i = 1; i <= count; i++) 452 { 453 object field = null, fieldCode = null; 454 try 455 { 456 field = fields.GetType().InvokeMember("Item", 457 System.Reflection.BindingFlags.InvokeMethod, null, fields, new object[] { i }); 458 459 fieldCode = field.GetType().InvokeMember("Code", 460 System.Reflection.BindingFlags.GetProperty, null, field, null); 461 462 string codeText = (string)fieldCode.GetType().InvokeMember("Text", 463 System.Reflection.BindingFlags.GetProperty, null, fieldCode, null); 464 465 // Caption fields use SEQ identifier, e.g. "SEQ Рисунок" 466 if (codeText != null && codeText.Contains("SEQ") && codeText.Contains("Рисунок")) 467 { 468 figureCount++; 469 } 470 } 471 catch (Exception ex) { Log("GetNextFigureNumber field error: " + ex.Message); } 472 finally { SafeRelease(ref fieldCode); SafeRelease(ref field); } 473 } 474 475 return figureCount + 1; 476 } 477 catch 478 { 479 return 1; // Default to 1 if can't count 480 } 481 finally { SafeRelease(ref fields); } 482 } 483 484 /// <summary> 485 /// Ensure the "Рисунок" caption label exists in Word 486 /// </summary> 487 private static void EnsureFigureCaptionLabel(object app) 488 { 489 try 490 { 491 object captionLabels = app.GetType().InvokeMember("CaptionLabels", 492 System.Reflection.BindingFlags.GetProperty, null, app, null); 493 494 // Try to get existing "Рисунок" label 495 try 496 { 497 captionLabels.GetType().InvokeMember("Item", 498 System.Reflection.BindingFlags.InvokeMethod, null, captionLabels, 499 new object[] { "Рисунок" }); 500 // Label exists, do nothing 501 } 502 catch 503 { 504 // Label doesn't exist, add it 505 captionLabels.GetType().InvokeMember("Add", 506 System.Reflection.BindingFlags.InvokeMethod, null, captionLabels, 507 new object[] { "Рисунок" }); 508 } 509 } 510 catch { } 511 } 512 513 /// <summary> 514 /// Get the last description text from Word document (text before last image) 515 /// Returns null if document is empty or has no descriptions 516 /// </summary> 517 public static string GetLastDescription() 518 { 519 if (selectedDocument == null || selectedDocument.DocumentObject == null) 520 return null; 521 522 try 523 { 524 object doc = selectedDocument.DocumentObject; 525 526 // Get document content 527 object content = doc.GetType().InvokeMember("Content", 528 System.Reflection.BindingFlags.GetProperty, null, doc, null); 529 530 if (content == null) 531 return null; 532 533 string fullText = null; 534 try 535 { 536 fullText = (string)content.GetType().InvokeMember("Text", 537 System.Reflection.BindingFlags.GetProperty, null, content, null); 538 } 539 catch 540 { 541 return null; // Document might be empty or inaccessible 542 } 543 544 // If document is empty or only whitespace, return null (this is normal for new documents) 545 if (string.IsNullOrEmpty(fullText) || fullText.Trim().Length < 20) 546 return null; 547 548 // Get paragraphs 549 object paragraphs = null; 550 try 551 { 552 paragraphs = doc.GetType().InvokeMember("Paragraphs", 553 System.Reflection.BindingFlags.GetProperty, null, doc, null); 554 } 555 catch 556 { 557 return null; 558 } 559 560 if (paragraphs == null) 561 return null; 562 563 int count = 0; 564 try 565 { 566 count = (int)paragraphs.GetType().InvokeMember("Count", 567 System.Reflection.BindingFlags.GetProperty, null, paragraphs, null); 568 } 569 catch 570 { 571 return null; 572 } 573 574 if (count == 0) 575 return null; 576 577 // Find last meaningful paragraph (not caption, not empty) 578 for (int i = count; i >= 1; i--) 579 { 580 object para = null, range = null; 581 try 582 { 583 para = paragraphs.GetType().InvokeMember("Item", 584 System.Reflection.BindingFlags.InvokeMethod, null, paragraphs, new object[] { i }); 585 586 if (para == null) continue; 587 588 range = para.GetType().InvokeMember("Range", 589 System.Reflection.BindingFlags.GetProperty, null, para, null); 590 591 if (range == null) continue; 592 593 string text = (string)range.GetType().InvokeMember("Text", 594 System.Reflection.BindingFlags.GetProperty, null, range, null); 595 596 if (string.IsNullOrWhiteSpace(text)) 597 continue; 598 599 // Skip if it's a caption (starts with "Рисунок") 600 text = text.Trim(); 601 if (text.StartsWith("Рисунок")) 602 continue; 603 604 // Skip very short texts (likely formatting artifacts) 605 if (text.Length < 20) 606 continue; 607 608 Log("GetLastDescription: found: " + text.Substring(0, Math.Min(50, text.Length)) + "..."); 609 return text; 610 } 611 catch { } 612 finally { SafeRelease(ref range); SafeRelease(ref para); } 613 } 614 615 // No description found - this is normal for new documents 616 return null; 617 } 618 catch (Exception ex) 619 { 620 Log("GetLastDescription error: " + ex.Message); 621 return null; 622 } 623 } 624 625 /// <summary> 626 /// Bring Word window to foreground 627 /// </summary> 628 public static void ActivateWord() 629 { 630 try 631 { 632 if (wordApp != null) 633 { 634 wordApp.GetType().InvokeMember("Activate", 635 System.Reflection.BindingFlags.InvokeMethod, null, wordApp, null); 636 } 637 } 638 catch { } 639 } 640 641 /// <summary> 642 /// Get document page count for preview 643 /// </summary> 644 public static int GetPageCount() 645 { 646 if (selectedDocument == null || selectedDocument.DocumentObject == null) 647 return 0; 648 649 try 650 { 651 object doc = selectedDocument.DocumentObject; 652 object builtInProps = doc.GetType().InvokeMember("BuiltInDocumentProperties", 653 System.Reflection.BindingFlags.GetProperty, null, doc, null); 654 object pagesProp = builtInProps.GetType().InvokeMember("Item", 655 System.Reflection.BindingFlags.GetProperty, null, builtInProps, new object[] { 14 }); // wdPropertyPages 656 return (int)pagesProp.GetType().InvokeMember("Value", 657 System.Reflection.BindingFlags.GetProperty, null, pagesProp, null); 658 } 659 catch 660 { 661 return 0; 662 } 663 } 664 665 /// <summary> 666 /// Get document preview by copying last page content as picture 667 /// Uses Word's CopyAsPicture to get document content 668 /// </summary> 669 public static Bitmap GetDocumentPreview() 670 { 671 if (selectedDocument == null || selectedDocument.DocumentObject == null) 672 return null; 673 674 // Ensure we have Word app reference 675 if (wordApp == null) 676 { 677 wordApp = GetRunningWordApp(); 678 if (wordApp == null) return null; 679 } 680 681 try 682 { 683 object doc = selectedDocument.DocumentObject; 684 685 // Get document content range 686 object content = doc.GetType().InvokeMember("Content", 687 System.Reflection.BindingFlags.GetProperty, null, doc, null); 688 689 // Copy content as picture to clipboard 690 content.GetType().InvokeMember("CopyAsPicture", 691 System.Reflection.BindingFlags.InvokeMethod, null, content, null); 692 693 // Get image from clipboard 694 if (System.Windows.Forms.Clipboard.ContainsImage()) 695 { 696 Image img = System.Windows.Forms.Clipboard.GetImage(); 697 if (img != null) 698 { 699 var bmp = BitmapHelper.Clone32(img); 700 img.Dispose(); 701 return bmp; 702 } 703 } 704 705 return null; 706 } 707 catch 708 { 709 return null; 710 } 711 } 712 713 /// <summary> 714 /// Get screenshot of Word window for preview using PrintWindow API 715 /// Works even if window is behind other windows 716 /// </summary> 717 public static Bitmap GetWordWindowScreenshot() 718 { 719 // Ensure we have Word app reference 720 if (wordApp == null) 721 { 722 wordApp = GetRunningWordApp(); 723 if (wordApp == null) return null; 724 } 725 726 try 727 { 728 // Get Word window handle 729 int hwnd = (int)wordApp.GetType().InvokeMember("Hwnd", 730 System.Reflection.BindingFlags.GetProperty, null, wordApp, null); 731 732 if (hwnd == 0) return null; 733 734 IntPtr hWnd = new IntPtr(hwnd); 735 736 // Check if window is minimized 737 if (IsIconic(hWnd)) 738 { 739 return null; 740 } 741 742 // Get window rect 743 WindowCapture.Native.WinApi.RECT rect; 744 if (!GetWindowRect(hWnd, out rect)) return null; 745 746 int width = rect.Right - rect.Left; 747 int height = rect.Bottom - rect.Top; 748 749 if (width <= 0 || height <= 0) return null; 750 751 // Use PrintWindow API to capture even if window is behind others 752 Bitmap bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 753 using (Graphics g = Graphics.FromImage(bmp)) 754 { 755 IntPtr hdc = g.GetHdc(); 756 bool success = PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT); 757 g.ReleaseHdc(hdc); 758 759 if (!success) 760 { 761 g.CopyFromScreen(rect.Left, rect.Top, 0, 0, new Size(width, height)); 762 } 763 } 764 765 return bmp; 766 } 767 catch 768 { 769 return null; 770 } 771 } 772 773 private const uint PW_CLIENTONLY = 0x1; 774 private const uint PW_RENDERFULLCONTENT = 0x2; 775 776 [DllImport("user32.dll")] 777 private static extern bool GetWindowRect(IntPtr hWnd, out WindowCapture.Native.WinApi.RECT lpRect); 778 779 [DllImport("user32.dll")] 780 private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags); 781 782 [DllImport("user32.dll")] 783 private static extern bool IsIconic(IntPtr hWnd); 784 785 // Word constants for PasteAndFormat 786 private const int wdFormatOriginalFormatting = 16; // Preserves original formatting 787 788 /// <summary> 789 /// Insert title page from template at the beginning of document 790 /// Replaces placeholders: %WORKNUMBER%, %DISCIPLINE%, %TOPIC%, %STUDENT%, %GROUP%, %TEACHER% 791 /// Uses Copy + PasteAndFormat(wdFormatOriginalFormatting) to preserve exact source formatting 792 /// </summary> 793 public static bool InsertTitlePage(string templatePath, Dictionary<string, string> replacements) 794 { 795 if (selectedDocument == null || selectedDocument.DocumentObject == null) 796 return false; 797 798 if (string.IsNullOrEmpty(templatePath) || !File.Exists(templatePath)) 799 return false; 800 801 try 802 { 803 object doc = selectedDocument.DocumentObject; 804 object app = wordApp; 805 806 if (app == null) 807 { 808 app = GetRunningWordApp(); 809 if (app == null) return false; 810 wordApp = app; 811 } 812 813 // Open template document (ReadOnly) 814 object documents = app.GetType().InvokeMember("Documents", 815 System.Reflection.BindingFlags.GetProperty, null, app, null); 816 817 object templateDoc = documents.GetType().InvokeMember("Open", 818 System.Reflection.BindingFlags.InvokeMethod, null, documents, 819 new object[] { templatePath, false, true }); // FileName, ConfirmConversions, ReadOnly 820 821 try 822 { 823 // Copy entire content of template 824 object templateContent = templateDoc.GetType().InvokeMember("Content", 825 System.Reflection.BindingFlags.GetProperty, null, templateDoc, null); 826 templateContent.GetType().InvokeMember("Copy", 827 System.Reflection.BindingFlags.InvokeMethod, null, templateContent, null); 828 829 // Activate target document 830 doc.GetType().InvokeMember("Activate", 831 System.Reflection.BindingFlags.InvokeMethod, null, doc, null); 832 833 // Get selection and move to start of document 834 object selection = app.GetType().InvokeMember("Selection", 835 System.Reflection.BindingFlags.GetProperty, null, app, null); 836 selection.GetType().InvokeMember("HomeKey", 837 System.Reflection.BindingFlags.InvokeMethod, null, selection, 838 new object[] { wdStory }); // Move to beginning 839 840 // PasteAndFormat with wdFormatOriginalFormatting (16) - keeps source formatting exactly 841 selection.GetType().InvokeMember("PasteAndFormat", 842 System.Reflection.BindingFlags.InvokeMethod, null, selection, 843 new object[] { wdFormatOriginalFormatting }); 844 } 845 finally 846 { 847 // Close template without saving 848 templateDoc.GetType().InvokeMember("Close", 849 System.Reflection.BindingFlags.InvokeMethod, null, templateDoc, 850 new object[] { 0 }); // wdDoNotSaveChanges = 0 851 Marshal.ReleaseComObject(templateDoc); 852 } 853 854 // Perform replacements in the document 855 if (replacements != null) 856 { 857 object docContent = doc.GetType().InvokeMember("Content", 858 System.Reflection.BindingFlags.GetProperty, null, doc, null); 859 860 object find = docContent.GetType().InvokeMember("Find", 861 System.Reflection.BindingFlags.GetProperty, null, docContent, null); 862 863 foreach (var replacement in replacements) 864 { 865 // Clear previous find 866 find.GetType().InvokeMember("ClearFormatting", 867 System.Reflection.BindingFlags.InvokeMethod, null, find, null); 868 869 object replace = find.GetType().InvokeMember("Replacement", 870 System.Reflection.BindingFlags.GetProperty, null, find, null); 871 replace.GetType().InvokeMember("ClearFormatting", 872 System.Reflection.BindingFlags.InvokeMethod, null, replace, null); 873 874 // Set find text 875 find.GetType().InvokeMember("Text", 876 System.Reflection.BindingFlags.SetProperty, null, find, new object[] { replacement.Key }); 877 878 // Set replacement text 879 replace.GetType().InvokeMember("Text", 880 System.Reflection.BindingFlags.SetProperty, null, replace, new object[] { replacement.Value }); 881 882 // Execute replacement (wdReplaceAll = 2) 883 find.GetType().InvokeMember("Execute", 884 System.Reflection.BindingFlags.InvokeMethod, null, find, 885 new object[] { 886 Type.Missing, // FindText (already set) 887 false, // MatchCase 888 false, // MatchWholeWord 889 false, // MatchWildcards 890 false, // MatchSoundsLike 891 false, // MatchAllWordForms 892 true, // Forward 893 1, // Wrap (wdFindContinue) 894 false, // Format 895 Type.Missing, // ReplaceWith (already set) 896 2 // Replace (wdReplaceAll) 897 }); 898 899 // Reset range for next search 900 docContent = doc.GetType().InvokeMember("Content", 901 System.Reflection.BindingFlags.GetProperty, null, doc, null); 902 find = docContent.GetType().InvokeMember("Find", 903 System.Reflection.BindingFlags.GetProperty, null, docContent, null); 904 } 905 } 906 907 return true; 908 } 909 catch (Exception ex) 910 { 911 System.Diagnostics.Debug.WriteLine("Title page insert error: " + ex.Message); 912 return false; 913 } 914 } 915 916 /// <summary> 917 /// Add page numbering with special first page (no number on first page, starts from 2 on second page) 918 /// Footer: centered, TNR 14, black 919 /// </summary> 920 public static bool AddPageNumbering() 921 { 922 if (selectedDocument == null || selectedDocument.DocumentObject == null) 923 return false; 924 925 try 926 { 927 object doc = selectedDocument.DocumentObject; 928 object app = wordApp; 929 930 if (app == null) 931 { 932 app = GetRunningWordApp(); 933 if (app == null) return false; 934 wordApp = app; 935 } 936 937 // Get active window 938 object activeWindow = doc.GetType().InvokeMember("ActiveWindow", 939 System.Reflection.BindingFlags.GetProperty, null, doc, null); 940 941 // Get active pane 942 object activePane = activeWindow.GetType().InvokeMember("ActivePane", 943 System.Reflection.BindingFlags.GetProperty, null, activeWindow, null); 944 945 // Get view 946 object view = activePane.GetType().InvokeMember("View", 947 System.Reflection.BindingFlags.GetProperty, null, activePane, null); 948 949 // Set view to print layout (wdPrintView = 3) 950 view.GetType().InvokeMember("Type", 951 System.Reflection.BindingFlags.SetProperty, null, view, new object[] { 3 }); 952 953 // Get sections 954 object sections = doc.GetType().InvokeMember("Sections", 955 System.Reflection.BindingFlags.GetProperty, null, doc, null); 956 957 // Get first section 958 object section = sections.GetType().InvokeMember("Item", 959 System.Reflection.BindingFlags.InvokeMethod, null, sections, new object[] { 1 }); 960 961 // Get headers/footers 962 object footers = section.GetType().InvokeMember("Footers", 963 System.Reflection.BindingFlags.GetProperty, null, section, null); 964 965 // Get primary footer (wdHeaderFooterPrimary = 1) 966 object primaryFooter = footers.GetType().InvokeMember("Item", 967 System.Reflection.BindingFlags.InvokeMethod, null, footers, new object[] { 1 }); 968 969 // Get page setup 970 object pageSetup = section.GetType().InvokeMember("PageSetup", 971 System.Reflection.BindingFlags.GetProperty, null, section, null); 972 973 // Set different first page 974 pageSetup.GetType().InvokeMember("DifferentFirstPageHeaderFooter", 975 System.Reflection.BindingFlags.SetProperty, null, pageSetup, new object[] { true }); 976 977 // Get footer range 978 object footerRange = primaryFooter.GetType().InvokeMember("Range", 979 System.Reflection.BindingFlags.GetProperty, null, primaryFooter, null); 980 981 // Clear footer first 982 footerRange.GetType().InvokeMember("Delete", 983 System.Reflection.BindingFlags.InvokeMethod, null, footerRange, null); 984 985 // Get fresh range after delete 986 footerRange = primaryFooter.GetType().InvokeMember("Range", 987 System.Reflection.BindingFlags.GetProperty, null, primaryFooter, null); 988 989 // Get page numbers collection 990 object pageNumbers = primaryFooter.GetType().InvokeMember("PageNumbers", 991 System.Reflection.BindingFlags.GetProperty, null, primaryFooter, null); 992 993 // Add page number at center bottom (wdAlignPageNumberCenter = 1) 994 // FirstPage = false means no page number on first page (due to DifferentFirstPageHeaderFooter) 995 pageNumbers.GetType().InvokeMember("Add", 996 System.Reflection.BindingFlags.InvokeMethod, null, pageNumbers, 997 new object[] { 1, false }); // PageNumberAlignment = Center, FirstPage = false 998 999 // StartingNumber = 1 means first page is "1" (hidden), second page shows "2" 1000 pageNumbers.GetType().InvokeMember("StartingNumber", 1001 System.Reflection.BindingFlags.SetProperty, null, pageNumbers, new object[] { 1 }); 1002 1003 // Format the page number: TNR 14, black, centered 1004 footerRange = primaryFooter.GetType().InvokeMember("Range", 1005 System.Reflection.BindingFlags.GetProperty, null, primaryFooter, null); 1006 1007 object font = footerRange.GetType().InvokeMember("Font", 1008 System.Reflection.BindingFlags.GetProperty, null, footerRange, null); 1009 1010 font.GetType().InvokeMember("Name", 1011 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { "Times New Roman" }); 1012 font.GetType().InvokeMember("Size", 1013 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 14f }); 1014 font.GetType().InvokeMember("Color", 1015 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 0 }); // wdColorBlack 1016 1017 object paragraphFormat = footerRange.GetType().InvokeMember("ParagraphFormat", 1018 System.Reflection.BindingFlags.GetProperty, null, footerRange, null); 1019 paragraphFormat.GetType().InvokeMember("Alignment", 1020 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, new object[] { wdAlignParagraphCenter }); 1021 1022 return true; 1023 } 1024 catch (Exception ex) 1025 { 1026 System.Diagnostics.Debug.WriteLine("Page numbering error: " + ex.Message); 1027 return false; 1028 } 1029 } 1030 1031 /// <summary> 1032 /// Check if Word has any open documents 1033 /// </summary> 1034 public static bool HasOpenDocuments() 1035 { 1036 try 1037 { 1038 object app = GetRunningWordApp(); 1039 if (app == null) return false; 1040 1041 object documents = app.GetType().InvokeMember("Documents", 1042 System.Reflection.BindingFlags.GetProperty, null, app, null); 1043 1044 int count = (int)documents.GetType().InvokeMember("Count", 1045 System.Reflection.BindingFlags.GetProperty, null, documents, null); 1046 1047 return count > 0; 1048 } 1049 catch 1050 { 1051 return false; 1052 } 1053 } 1054 1055 // Word Information enum constants 1056 private const int wdActiveEndPageNumber = 3; 1057 1058 /// <summary> 1059 /// Extract all descriptions and captions from a Word document 1060 /// Skips page 1 (title page), returns list of DocumentTextItem 1061 /// </summary> 1062 public static List<DocumentTextItem> ExtractDescriptionsAndCaptions(object doc) 1063 { 1064 var items = new List<DocumentTextItem>(); 1065 Log("=== ExtractDescriptionsAndCaptions start ==="); 1066 1067 try 1068 { 1069 object paragraphs = doc.GetType().InvokeMember("Paragraphs", 1070 System.Reflection.BindingFlags.GetProperty, null, doc, null); 1071 1072 int count = (int)paragraphs.GetType().InvokeMember("Count", 1073 System.Reflection.BindingFlags.GetProperty, null, paragraphs, null); 1074 1075 Log("Total paragraphs: " + count); 1076 1077 // Caption regex — also match when text starts with hidden field chars before "Рисунок" 1078 var captionRegex = new Regex(@"Рисунок\s+\d+\s*[\-—–]\s*"); 1079 1080 for (int i = 1; i <= count; i++) 1081 { 1082 try 1083 { 1084 object para = paragraphs.GetType().InvokeMember("Item", 1085 System.Reflection.BindingFlags.InvokeMethod, null, paragraphs, new object[] { i }); 1086 1087 object range = para.GetType().InvokeMember("Range", 1088 System.Reflection.BindingFlags.GetProperty, null, para, null); 1089 1090 // Check page number — skip page 1 (title page) 1091 try 1092 { 1093 object pageNum = range.GetType().InvokeMember("Information", 1094 System.Reflection.BindingFlags.GetProperty, null, range, 1095 new object[] { wdActiveEndPageNumber }); 1096 int page = Convert.ToInt32(pageNum); 1097 if (page <= 1) 1098 { 1099 continue; 1100 } 1101 } 1102 catch (Exception pgEx) 1103 { 1104 Log("Page number check failed for para " + i + ": " + pgEx.Message + " — processing anyway"); 1105 } 1106 1107 string text = (string)range.GetType().InvokeMember("Text", 1108 System.Reflection.BindingFlags.GetProperty, null, range, null); 1109 1110 if (string.IsNullOrWhiteSpace(text)) 1111 continue; 1112 1113 text = text.Trim('\r', '\n', '\a'); 1114 1115 if (text.Length < 5) 1116 continue; 1117 1118 // Check if it's a caption: "Рисунок N — text" 1119 // Use Search instead of Match to handle hidden field characters before "Рисунок" 1120 var captionMatch = captionRegex.Match(text); 1121 if (captionMatch.Success) 1122 { 1123 string prefix = text.Substring(0, captionMatch.Index + captionMatch.Length); 1124 string captionText = text.Substring(captionMatch.Index + captionMatch.Length).Trim(); 1125 1126 if (captionText.Length > 2) 1127 { 1128 var item = new DocumentTextItem 1129 { 1130 ParagraphIndex = i, 1131 OriginalText = text, 1132 TextToRephrase = captionText, 1133 IsCaption = true, 1134 CaptionPrefix = prefix 1135 }; 1136 items.Add(item); 1137 Log("Found caption [" + i + "]: " + text.Substring(0, Math.Min(80, text.Length))); 1138 } 1139 continue; 1140 } 1141 1142 // Check if it's a description: long text (>20 chars), not a caption 1143 if (text.Length < 20) 1144 continue; 1145 1146 // Skip texts containing "Рисунок" (broken captions) 1147 if (text.Contains("Рисунок")) 1148 continue; 1149 1150 // Check font and alignment to confirm it's a description paragraph 1151 string fName = ""; 1152 float fSize = 0; 1153 int align = -1; 1154 try 1155 { 1156 object font = range.GetType().InvokeMember("Font", 1157 System.Reflection.BindingFlags.GetProperty, null, range, null); 1158 1159 object fontNameObj = font.GetType().InvokeMember("Name", 1160 System.Reflection.BindingFlags.GetProperty, null, font, null); 1161 1162 object fontSizeObj = font.GetType().InvokeMember("Size", 1163 System.Reflection.BindingFlags.GetProperty, null, font, null); 1164 1165 object paraFormat = para.GetType().InvokeMember("Format", 1166 System.Reflection.BindingFlags.GetProperty, null, para, null); 1167 1168 object alignObj = paraFormat.GetType().InvokeMember("Alignment", 1169 System.Reflection.BindingFlags.GetProperty, null, paraFormat, null); 1170 1171 fName = fontNameObj != null ? fontNameObj.ToString() : ""; 1172 fSize = Convert.ToSingle(fontSizeObj); 1173 align = Convert.ToInt32(alignObj); 1174 } 1175 catch (Exception fmtEx) 1176 { 1177 Log("Format check failed for para " + i + ": " + fmtEx.Message); 1178 } 1179 1180 Log("Para [" + i + "] font='" + fName + "' size=" + fSize + " align=" + align + " len=" + text.Length + " text='" + text.Substring(0, Math.Min(50, text.Length)) + "'"); 1181 1182 // Accept description if: 1183 // - It's long enough (>20 chars) AND 1184 // - Either has typical formatting (TNR 12-14pt, justified/left) OR formatting unknown 1185 bool isDescription = false; 1186 1187 if (text.Length >= 20) 1188 { 1189 // Strict match: TNR, ~14pt, justified 1190 if (fName.Contains("Times") && fSize >= 12f && fSize <= 15f && 1191 (align == wdAlignParagraphJustify || align == wdAlignParagraphLeft)) 1192 { 1193 isDescription = true; 1194 } 1195 // Fallback: if font info is weird (mixed formatting returns 9999999), 1196 // accept long paragraphs that aren't centered (centered = headings/titles) 1197 else if (fSize > 100f && align != wdAlignParagraphCenter && text.Length >= 40) 1198 { 1199 isDescription = true; 1200 Log("Para [" + i + "] accepted via fallback (mixed formatting)"); 1201 } 1202 // Also accept if format check failed entirely but text is substantial 1203 else if (fName == "" && text.Length >= 40) 1204 { 1205 isDescription = true; 1206 Log("Para [" + i + "] accepted via fallback (no format info)"); 1207 } 1208 } 1209 1210 if (isDescription) 1211 { 1212 var item = new DocumentTextItem 1213 { 1214 ParagraphIndex = i, 1215 OriginalText = text, 1216 TextToRephrase = text, 1217 IsCaption = false, 1218 CaptionPrefix = null 1219 }; 1220 items.Add(item); 1221 Log("Found description [" + i + "]: " + text.Substring(0, Math.Min(60, text.Length))); 1222 } 1223 } 1224 catch (Exception ex) 1225 { 1226 Log("Error processing paragraph " + i + ": " + ex.Message); 1227 } 1228 } 1229 1230 Log("Total items found: " + items.Count); 1231 } 1232 catch (Exception ex) 1233 { 1234 Log("ExtractDescriptionsAndCaptions error: " + ex.Message); 1235 } 1236 1237 return items; 1238 } 1239 1240 /// <summary> 1241 /// Apply rephrased text replacements to the document 1242 /// Processes in reverse order to preserve paragraph indices 1243 /// </summary> 1244 public static int ApplyReplacements(object doc, List<DocumentTextItem> items) 1245 { 1246 Log("=== ApplyReplacements start ==="); 1247 int replacedCount = 0; 1248 1249 // Sort by paragraph index descending (reverse order) 1250 var sorted = new List<DocumentTextItem>(items); 1251 sorted.Sort((a, b) => b.ParagraphIndex.CompareTo(a.ParagraphIndex)); 1252 1253 object paragraphs = doc.GetType().InvokeMember("Paragraphs", 1254 System.Reflection.BindingFlags.GetProperty, null, doc, null); 1255 1256 foreach (var item in sorted) 1257 { 1258 if (!item.Accepted || string.IsNullOrEmpty(item.NewText)) 1259 continue; 1260 1261 try 1262 { 1263 object para = paragraphs.GetType().InvokeMember("Item", 1264 System.Reflection.BindingFlags.InvokeMethod, null, paragraphs, 1265 new object[] { item.ParagraphIndex }); 1266 1267 object range = para.GetType().InvokeMember("Range", 1268 System.Reflection.BindingFlags.GetProperty, null, para, null); 1269 1270 if (item.IsCaption) 1271 { 1272 // For captions, only replace text after "Рисунок N — " prefix 1273 // Find the dash separator in the range text 1274 string rangeText = (string)range.GetType().InvokeMember("Text", 1275 System.Reflection.BindingFlags.GetProperty, null, range, null); 1276 1277 // Find position of " — " or " - " or " – " 1278 int dashPos = -1; 1279 string[] dashPatterns = { " — ", " – ", " - " }; 1280 string foundDash = null; 1281 foreach (var dp in dashPatterns) 1282 { 1283 dashPos = rangeText.IndexOf(dp); 1284 if (dashPos >= 0) { foundDash = dp; break; } 1285 } 1286 1287 if (dashPos >= 0 && foundDash != null) 1288 { 1289 int textStart = dashPos + foundDash.Length; 1290 // Get range start position 1291 int rangeStart = (int)range.GetType().InvokeMember("Start", 1292 System.Reflection.BindingFlags.GetProperty, null, range, null); 1293 1294 int rangeEnd = (int)range.GetType().InvokeMember("End", 1295 System.Reflection.BindingFlags.GetProperty, null, range, null); 1296 1297 // Create a new range for just the text portion 1298 object textRange = doc.GetType().InvokeMember("Range", 1299 System.Reflection.BindingFlags.InvokeMethod, null, doc, 1300 new object[] { rangeStart + textStart, rangeEnd - 1 }); // -1 to exclude paragraph mark 1301 1302 // Replace text 1303 textRange.GetType().InvokeMember("Text", 1304 System.Reflection.BindingFlags.SetProperty, null, textRange, 1305 new object[] { item.NewText }); 1306 1307 replacedCount++; 1308 Log("Replaced caption [" + item.ParagraphIndex + "]: " + item.NewText.Substring(0, Math.Min(40, item.NewText.Length))); 1309 } 1310 else 1311 { 1312 Log("Could not find dash separator in caption at paragraph " + item.ParagraphIndex); 1313 } 1314 } 1315 else 1316 { 1317 // For descriptions, replace entire text and reapply formatting 1318 int rangeStart = (int)range.GetType().InvokeMember("Start", 1319 System.Reflection.BindingFlags.GetProperty, null, range, null); 1320 int rangeEnd = (int)range.GetType().InvokeMember("End", 1321 System.Reflection.BindingFlags.GetProperty, null, range, null); 1322 1323 // Create range excluding paragraph mark 1324 object textRange = doc.GetType().InvokeMember("Range", 1325 System.Reflection.BindingFlags.InvokeMethod, null, doc, 1326 new object[] { rangeStart, rangeEnd - 1 }); 1327 1328 textRange.GetType().InvokeMember("Text", 1329 System.Reflection.BindingFlags.SetProperty, null, textRange, 1330 new object[] { item.NewText }); 1331 1332 // Reapply formatting: TNR 14, justified, 1.5 spacing, indent 1.25cm 1333 // Re-get the paragraph range after text change 1334 para = paragraphs.GetType().InvokeMember("Item", 1335 System.Reflection.BindingFlags.InvokeMethod, null, paragraphs, 1336 new object[] { item.ParagraphIndex }); 1337 range = para.GetType().InvokeMember("Range", 1338 System.Reflection.BindingFlags.GetProperty, null, para, null); 1339 1340 object font = range.GetType().InvokeMember("Font", 1341 System.Reflection.BindingFlags.GetProperty, null, range, null); 1342 font.GetType().InvokeMember("Name", 1343 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { "Times New Roman" }); 1344 font.GetType().InvokeMember("Size", 1345 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 14f }); 1346 font.GetType().InvokeMember("Color", 1347 System.Reflection.BindingFlags.SetProperty, null, font, new object[] { 0 }); 1348 1349 object paragraphFormat = para.GetType().InvokeMember("Format", 1350 System.Reflection.BindingFlags.GetProperty, null, para, null); 1351 paragraphFormat.GetType().InvokeMember("Alignment", 1352 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, 1353 new object[] { wdAlignParagraphJustify }); 1354 paragraphFormat.GetType().InvokeMember("LineSpacingRule", 1355 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, 1356 new object[] { wdLineSpaceMultiple }); 1357 paragraphFormat.GetType().InvokeMember("LineSpacing", 1358 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, 1359 new object[] { 18f }); // 1.5 lines 1360 paragraphFormat.GetType().InvokeMember("FirstLineIndent", 1361 System.Reflection.BindingFlags.SetProperty, null, paragraphFormat, 1362 new object[] { 35.4f }); // 1.25cm 1363 1364 replacedCount++; 1365 Log("Replaced description [" + item.ParagraphIndex + "]: " + item.NewText.Substring(0, Math.Min(40, item.NewText.Length))); 1366 } 1367 } 1368 catch (Exception ex) 1369 { 1370 Log("Error replacing paragraph " + item.ParagraphIndex + ": " + ex.Message); 1371 } 1372 } 1373 1374 Log("Total replaced: " + replacedCount); 1375 return replacedCount; 1376 } 1377 1378 /// <summary> 1379 /// Release COM objects 1380 /// </summary> 1381 public static void Cleanup() 1382 { 1383 try 1384 { 1385 if (selectedDocument != null && selectedDocument.DocumentObject != null) 1386 { 1387 try { Marshal.ReleaseComObject(selectedDocument.DocumentObject); } catch { } 1388 selectedDocument = null; 1389 } 1390 if (wordApp != null) 1391 { 1392 try { Marshal.ReleaseComObject(wordApp); } catch { } 1393 wordApp = null; 1394 } 1395 } 1396 catch { } 1397 1398 // The hot paths obtain many short-lived COM objects (Selection/Range/Font/Paragraphs…) 1399 // via late-binding InvokeMember. Those RCWs aren't released deterministically, so without 1400 // this Word.exe lingers in memory after the user closes it. Forcing the GC to finalize the 1401 // now-unreferenced RCWs releases their underlying COM references (standard Office-interop fix). 1402 try 1403 { 1404 GC.Collect(); 1405 GC.WaitForPendingFinalizers(); 1406 GC.Collect(); 1407 } 1408 catch { } 1409 } 1410 } 1411}