Rob van der Woude's Scripting Pages
Powered by GeSHi

Source code for word2txt.cs

(view source code of word2txt.cs as plain text)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using System.Windows.Forms;
  9. using Word = Microsoft.Office.Interop.Word;
  10.  
  11.  
  12. namespace RobvanderWoude
  13. {
  14. 	internal class Word2Txt
  15. 	{
  16. 		static string progver = "1.03";
  17.  
  18.  
  19. 		static string plaintext = string.Empty;
  20.  
  21.  
  22. 		static int Main( string[] args )
  23. 		{
  24. 			string document = string.Empty;
  25. 			bool success = false;
  26. 			bool usexmlencoding = false;
  27. 			string xmlencoding = string.Empty;
  28. 			Encoding encoding = null;
  29.  
  30.  
  31. 			#region Parse Command Line
  32.  
  33. 			if ( args.Length == 0 || args.Length > 2 )
  34. 			{
  35. 				return ShowHelp( );
  36. 			}
  37.  
  38. 			foreach ( string arg in args )
  39. 			{
  40. 				if ( arg[0] == '/' )
  41. 				{
  42. 					if ( arg == "/?" )
  43. 					{
  44. 						return ShowHelp( );
  45. 					}
  46. 					else if ( arg.StartsWith( "/D", StringComparison.OrdinalIgnoreCase ) )
  47. 					{
  48. 						usexmlencoding = true;
  49. 					}
  50. 					else if ( arg.ToUpper( ) == "/E" )
  51. 					{
  52. 						return ListEncodings( );
  53. 					}
  54. 					else
  55. 					{
  56. 						return ShowHelp( "Invalid command line switch {0}", arg );
  57. 					}
  58. 				}
  59. 				else
  60. 				{
  61. 					if ( string.IsNullOrWhiteSpace( document ) )
  62. 					{
  63. 						document = arg;
  64. 						if ( !File.Exists( document ) )
  65. 						{
  66. 							return ShowHelp( "File \"{0}\" not found", document );
  67. 						}
  68. 					}
  69. 					else if ( encoding == null )
  70. 					{
  71. 						encoding = GetEncoding( arg );
  72. 						if ( encoding == null )
  73. 						{
  74. 							return ShowHelp( "Invalid encoding \"{0}\"", args[1] );
  75. 						}
  76. 					}
  77. 					else
  78. 					{
  79. 						return ShowHelp( "Too many command line arguments" );
  80. 					}
  81. 				}
  82. 			}
  83.  
  84. 			if ( string.IsNullOrWhiteSpace( document ) )
  85. 			{
  86. 				return ShowHelp( );
  87. 			}
  88.  
  89. 			#endregion Parse Command Line
  90.  
  91.  
  92. 			#region Extract Text
  93.  
  94. 			// First try using Word if possible
  95. 			if ( IsWordInstalled( ) )
  96. 			{
  97. 				// If Word is installed, this program can handle ANY document format that is recognized by Word
  98. 				success = ReadWordFile( document );
  99. 			}
  100.  
  101. 			// if Word isn't available or could not extract any text, try plan B
  102. 			if ( !success || string.IsNullOrWhiteSpace( plaintext ) )
  103. 			{
  104. 				string ext = Path.GetExtension( document ).ToLower( );
  105. 				if ( ext == ".doc" )
  106. 				{
  107. 					success = ReadDocFile( document );
  108. 				}
  109. 				else if ( ext == ".docx" || ext == ".odt" )
  110. 				{
  111. 					success = ReadDocxOrOdtFile( document );
  112. 				}
  113. 				else if ( ext == ".rtf" )
  114. 				{
  115. 					success = ReadRTFFile( document );
  116. 				}
  117. 				else if ( ext == ".wpd" )
  118. 				{
  119. 					success = ReadWPDFile( document );
  120. 				}
  121. 				else
  122. 				{
  123. 					return ShowHelp( "If Word is not installed or fails to extract text, this program can only handle .DOC, .DOCX, .ODT and .WPD files" );
  124. 				}
  125. 			}
  126.  
  127. 			#endregion Extract Text
  128.  
  129.  
  130. 			#region Cleanup Text and Display Result
  131.  
  132. 			if ( success && !string.IsNullOrWhiteSpace( plaintext ) )
  133. 			{
  134. 				// convert stray carriage returns to carriage return/linefeed pairs
  135. 				plaintext = ConvertStrayCarriageReturns( plaintext ).Trim( "\n\r\t ".ToCharArray( ) );
  136.  
  137. 				if ( usexmlencoding )
  138. 				{
  139. 					encoding = GetEncoding( xmlencoding );
  140. 				}
  141.  
  142. 				if ( encoding == null )
  143. 				{
  144. 					// send text to console using default output encoding
  145. 					Console.WriteLine( plaintext );
  146. 				}
  147. 				else
  148. 				{
  149. 					// temporarily change output encoding and send text to console
  150. 					Encoding oldencoding = Console.OutputEncoding;
  151. 					Console.OutputEncoding = encoding;
  152. 					Console.WriteLine( plaintext );
  153. 					Console.OutputEncoding = oldencoding;
  154. 				}
  155.  
  156. 				return 0;
  157. 			}
  158. 			else
  159. 			{
  160. 				// failed to extract text from Word file
  161. 				return 1;
  162. 			}
  163.  
  164. 			#endregion Cleanup Text and Display Result
  165. 		}
  166.  
  167.  
  168. 		static string ConvertStrayCarriageReturns( string text )
  169. 		{
  170. 			// convert stray carriage returns to carriage return/linefeed pairs
  171. 			// search for stray carriage returns (\r), i.e. the ones NOT followed by linefeeds (\n)
  172. 			Regex regex = new Regex( "\r(?!\n)" );
  173. 			// replace each matching stray carriage return by a carriage return/linefeed pair
  174. 			text = regex.Replace( text, Environment.NewLine );
  175. 			return text;
  176. 		}
  177.  
  178.  
  179. 		static Encoding GetEncoding( string myencoding )
  180. 		{
  181. 			if ( string.IsNullOrEmpty( myencoding ) )
  182. 			{
  183. 				return null;
  184. 			}
  185. 			// Get a list of available encodings
  186. 			EncodingInfo[] encodings = Encoding.GetEncodings( );
  187. 			// Try correctly spelled encodings first
  188. 			foreach ( EncodingInfo encoding in encodings )
  189. 			{
  190. 				if ( encoding.Name.ToLower( ) == myencoding.ToLower( ) )
  191. 				{
  192. 					return Encoding.GetEncoding( encoding.CodePage );
  193. 				}
  194. 			}
  195. 			// No direct match found, try again, ignoring dashes
  196. 			foreach ( EncodingInfo encoding in encodings )
  197. 			{
  198. 				if ( encoding.Name.Replace( "-", "" ).ToLower( ) == myencoding.Replace( "-", "" ).ToLower( ) )
  199. 				{
  200. 					return Encoding.GetEncoding( encoding.CodePage );
  201. 				}
  202. 			}
  203. 			// Still no match, try codepages
  204. 			foreach ( EncodingInfo encoding in encodings )
  205. 			{
  206. 				if ( encoding.CodePage.ToString( ) == myencoding )
  207. 				{
  208. 					return Encoding.GetEncoding( encoding.CodePage );
  209. 				}
  210. 			}
  211. 			// Still no match, giving up
  212. 			return null;
  213. 		}
  214.  
  215.  
  216. 		static bool IsWordInstalled( )
  217. 		{
  218. 			// Source: "How to Check Whether Word is Installed in the System or Not" by Tadit Dash
  219. 			// https://www.codeproject.com/Tips/689968/How-to-Check-Whether-Word-is-Installed-in-the-Syst
  220. 			return ( Type.GetTypeFromProgID( "Word.Application" ) != null );
  221. 		}
  222.  
  223.  
  224. 		static int ListEncodings( )
  225. 		{
  226. 			try
  227. 			{
  228. 				Console.Clear( );
  229. 			}
  230. 			catch
  231. 			{
  232. 				// Console.Clear( ) throws an IO exception if the output is redirected
  233. 			}
  234. 			int columnwidth = 8;
  235. 			EncodingInfo[] allencodings = Encoding.GetEncodings( );
  236. 			List<string> allencodingnames = new List<string>( );
  237. 			foreach ( EncodingInfo enc in allencodings )
  238. 			{
  239. 				allencodingnames.Add( enc.Name );
  240. 			}
  241. 			allencodingnames.Sort( );
  242. 			foreach ( string enc in allencodingnames )
  243. 			{
  244. 				columnwidth = Math.Max( columnwidth, enc.Length );
  245. 			}
  246. 			Console.WriteLine( "{0,-" + columnwidth + "}   {1}", "Encoding", "CodePage" );
  247. 			Console.WriteLine( "{0,-" + columnwidth + "}   {1}", "========", "========" );
  248. 			foreach ( string enc in allencodingnames )
  249. 			{
  250. 				Console.WriteLine( "{0,-" + columnwidth + "}   {1}", enc, GetEncoding( enc ).CodePage );
  251. 			}
  252. 			return 0;
  253. 		}
  254.  
  255.  
  256. 		static bool ReadDocFile( string docfile )
  257. 		{
  258. 			string doccontent = string.Empty;
  259. 			try
  260. 			{
  261. 				StreamReader sr = new StreamReader( docfile, false );
  262. 				doccontent = sr.ReadToEnd( ).Trim( "\n\t ".ToCharArray( ) );
  263. 				sr.Close( );
  264. 			}
  265. 			catch ( IOException )
  266. 			{
  267. 				ShowHelp( "Access to file \"{0}\" denied", docfile );
  268. 				return false;
  269. 			}
  270. 			if ( doccontent.Length == 0 )
  271. 			{
  272. 				return false;
  273. 			}
  274. 			if ( doccontent.Contains( "[Content_Types]" ) )
  275. 			{
  276. 				doccontent = doccontent.Substring( 0, doccontent.IndexOf( "[Content_Types]" ) );
  277. 			}
  278. 			Regex regex = new Regex( "[^\\000\\015\\367\\377]{20,}" );
  279. 			MatchCollection matches = regex.Matches( doccontent );
  280. 			if ( matches.Count == 0 )
  281. 			{
  282. 				return false;
  283. 			}
  284. 			plaintext = string.Empty;
  285. 			foreach ( Match match in matches )
  286. 			{
  287. 				string matchingtext = match.Value.Trim( "\n\t ".ToCharArray( ) );
  288. 				if ( Encoding.UTF8.GetByteCount( matchingtext ) == matchingtext.Length && !matchingtext.Contains( (char)4 ) )
  289. 				{
  290. 					plaintext += matchingtext + "\n";
  291. 				}
  292. 			}
  293. 			return true;
  294. 		}
  295.  
  296.  
  297. 		static bool ReadDocxOrOdtFile( string docfile )
  298. 		{
  299. 			string contentfile;
  300. 			string ext = Path.GetExtension( docfile ).ToLower( );
  301. 			if ( ext == ".odt" ) // OpenOffice document
  302. 			{
  303. 				contentfile = "content.xml";
  304. 			}
  305. 			else if ( ext == ".docx" ) // MS Office document
  306. 			{
  307. 				contentfile = "document.xml";
  308. 			}
  309. 			else
  310. 			{
  311. 				return false;
  312. 			}
  313.  
  314. 			string tempfile = Path.GetTempFileName( );
  315. 			string content = string.Empty;
  316. 			bool success = false;
  317.  
  318. 			try
  319. 			{
  320. 				// Open document as ZIP file and extract the XML file containing the text content
  321. 				using ( ZipArchive archive = ZipFile.OpenRead( docfile ) )
  322. 				{
  323. 					foreach ( ZipArchiveEntry entry in archive.Entries )
  324. 					{
  325. 						if ( entry.Name.ToLower( ) == contentfile )
  326. 						{
  327. 							entry.ExtractToFile( tempfile, true );
  328. 							success = true;
  329. 						}
  330. 					}
  331. 				}
  332. 			}
  333. 			catch ( IOException )
  334. 			{
  335. 				ShowHelp( "Access to file \"{0}\" denied", docfile );
  336. 				return false;
  337. 			}
  338.  
  339. 			if ( success )
  340. 			{
  341. 				// Read the text content from the extracted file
  342. 				StreamReader sr = new StreamReader( tempfile );
  343. 				content = sr.ReadToEnd( ).Trim( "\n\r\t ".ToCharArray( ) );
  344. 				sr.Close( );
  345. 			}
  346.  
  347. 			// Delete the extracted file
  348. 			File.Delete( tempfile );
  349.  
  350. 			if ( success )
  351. 			{
  352. 				// The first 100 characters of the extracted XML usually contain its encoding;
  353. 				// this encoding will be used if the /D command line switch was used
  354. 				Regex regex = new Regex( " encoding=\"([^\"]+)\"" );
  355. 				string xmlencoding = regex.Match( content, 0, 100 ).Groups[1].Value;
  356. 				// insert newlines after headers, list items and paragraphs
  357. 				regex = new Regex( "</(text|w):(h|p)>" );
  358. 				string plaintext = regex.Replace( content, "\n\n" );
  359. 				regex = new Regex( "<w:br/>" );
  360. 				plaintext = regex.Replace( plaintext, "\n\n" );
  361. 				// remove all XML tags
  362. 				regex = new Regex( "<[^>]+>" );
  363. 				plaintext = regex.Replace( plaintext, "" );
  364.  
  365. 				return true;
  366. 			}
  367. 			return false;
  368. 		}
  369.  
  370.  
  371. 		static bool ReadRTFFile( string rtffile )
  372. 		{
  373. 			// Use a hidden RichTextBox to convert RTF to plain text, by Wendy Zang
  374. 			// https://social.msdn.microsoft.com/Forums/vstudio/en-US/6e56af9b-d7d3-49f3-9ec4-80edde3fe54b/reading-modifying-rtf-files?forum=csharpgeneral#a64345e9-cfcb-43be-ab18-c08fae02cb2a
  375. 			RichTextBox rtbox = new RichTextBox( );
  376. 			string rtftext = string.Empty;
  377. 			try
  378. 			{
  379. 				rtftext = File.ReadAllText( rtffile );
  380. 				rtbox.Rtf = rtftext;
  381. 				plaintext = rtbox.Text;
  382. 			}
  383. 			catch ( IOException )
  384. 			{
  385. 				return false;
  386. 			}
  387. 			return true;
  388. 		}
  389.  
  390.  
  391. 		static bool ReadWordFile( string wordfile )
  392. 		{
  393. 			try
  394. 			{
  395. 				Word.Application wordapp = new Word.Application( );
  396. 				wordapp.Visible = false;
  397. 				Word.Document worddoc = wordapp.Documents.Open( wordfile );
  398. 				wordapp.Selection.WholeStory( );
  399. 				plaintext = worddoc.Content.Text;
  400. 				object savechanges = Word.WdSaveOptions.wdDoNotSaveChanges;
  401. 				worddoc.Close( ref savechanges );
  402. 				wordapp.Quit( ref savechanges );
  403. 				return true;
  404. 			}
  405. 			catch ( Exception )
  406. 			{
  407. 				return false;
  408. 			}
  409. 		}
  410.  
  411.  
  412. 		static bool ReadWPDFile( string wpfile )
  413. 		{
  414. 			string wpcontent = File.ReadAllText( wpfile, Encoding.UTF8 );
  415.  
  416. 			// Remove (most of) the WPD file header - WARNING: regex pattern depends on Encoding used for StreamReader!
  417. 			Regex regex = new Regex( "^[\\w\\W]*\\000{8,}([^\\w]+[B-HJ-NP-TV-Z\\d])*[^\\w-]+", RegexOptions.IgnoreCase );
  418. 			wpcontent = regex.Replace( wpcontent, "" );
  419.  
  420. 			plaintext = string.Empty;
  421.  
  422. 			// WPD file format info based on http://justsolve.archiveteam.org/wiki/WordPerfect
  423. 			// Modified for spaces, linefeeds and e acute by yours truly
  424. 			// More modifications are required for accented characters
  425. 			bool skip = false;
  426. 			int resume = -1;
  427. 			foreach ( char c in wpcontent )
  428. 			{
  429. 				int i = (int)c;
  430. 				if ( !skip )
  431. 				{
  432. 					if ( i == 63 || i == 128 || i == 160 || i == 65533 )
  433. 					{
  434. 						plaintext += ' ';
  435. 					}
  436. 					else if ( i >= 169 && i != 172 && i <= 174 )
  437. 					{
  438. 						plaintext += '-';
  439. 					}
  440. 					else if ( i == 10 || i == 13 || i == 208 )
  441. 					{
  442. 						plaintext += Environment.NewLine;
  443. 					}
  444. 					else if ( i >= 192 && i <= 236 )
  445. 					{
  446. 						skip = true;
  447. 						resume = i;
  448. 					}
  449. 					else if ( i == 15 )
  450. 					{
  451. 						plaintext += (char)233;
  452. 					}
  453. 					else if ( i <= 31 || ( i >= 129 && i <= 159 ) || ( i >= 161 && i <= 168 ) || i == 172 || ( i >= 175 && i <= 191 ) || ( i >= 237 && i <= 255 ) )
  454. 					{
  455. 						// control characters, ignore
  456. 					}
  457. 					else
  458. 					{
  459. 						plaintext += c;
  460. 					}
  461. 				}
  462. 				else if ( skip && i == resume )
  463. 				{
  464. 					skip = false;
  465. 					resume = -1;
  466. 				}
  467. 			}
  468. 			return !string.IsNullOrWhiteSpace( plaintext );
  469. 		}
  470.  
  471.  
  472. 		static int ShowHelp( params string[] errmsg )
  473. 		{
  474. 			#region Help Text
  475.  
  476. 			/*
  477. 			Word2Txt,  Version 1.03
  478. 			Extract plain text from a Word document and send it to the screen
  479.  
  480. 			Usage:   Word2Txt    "wordfile"  [ encoding | /D ]
  481.  
  482. 			or:      Word2Txt    /E
  483.  
  484. 			Where:   wordfile    is the path of the Word document to be read
  485. 			                     (no wildcards allowed)
  486. 			         encoding    force use of alternative encoding for plain
  487. 			                     text, e.g. UTF-8 to preserve accented characters
  488. 			                     or IBM437 to convert unicode quotes to ASCII
  489. 			         /D          use the encoding specified in the document file
  490. 			                     (for .DOCX and .ODT only, if Word isn't available)
  491. 			         /E          list all available encodings
  492.  
  493. 			Notes:   If a "regular" (MSI based) Microsoft Word (2007 or later)
  494. 			         installation is detected, this program will use Word to read
  495. 			         the text from the Word file, which may be ANY file format
  496. 			         recognized by Word.
  497. 			         If Word was already active when this program is started, any
  498. 			         other opened document(s) will be left alone, and only the
  499. 			         document opened by this program will be closed.
  500. 			         If Word is not available, the text can still be extracted, but
  501. 			         only from .DOC, .DOCX, .ODT, .RTF and .WPD files.
  502. 			         If the specified encoding does not match any available encoding
  503. 			         name, the program will try again, ignoring dashes; if that does
  504. 			         not provide a match, the program will try matching the specified
  505. 			         encoding with the available encodings' codepages.
  506. 			         This program requires .NET 4.5.
  507. 			         Return code ("errorlevel") 0 means no errors were encounterd
  508. 			         and some text was extracted from the file; otherwise the
  509. 			         return code will be 1.
  510.  
  511. 			Written by Rob van der Woude
  512. 			https://www.robvanderwoude.com
  513. 			*/
  514.  
  515. 			#endregion Help Text
  516.  
  517.  
  518. 			#region Error Message
  519.  
  520. 			if ( errmsg.Length > 0 )
  521. 			{
  522. 				List<string> errargs = new List<string>( errmsg );
  523. 				errargs.RemoveAt( 0 );
  524. 				Console.Error.WriteLine( );
  525. 				Console.ForegroundColor = ConsoleColor.Red;
  526. 				Console.Error.Write( "ERROR:\t" );
  527. 				Console.ForegroundColor = ConsoleColor.White;
  528. 				Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) );
  529. 				Console.ResetColor( );
  530. 			}
  531.  
  532. 			#endregion Error Message
  533.  
  534.  
  535. 			#region Display Help Text
  536.  
  537. 			Console.Error.WriteLine( );
  538.  
  539. 			Console.Error.WriteLine( "Word2Txt,  Version {0}", progver );
  540.  
  541. 			Console.Error.WriteLine( "Extract plain text from a Word document and send it to the screen" );
  542.  
  543. 			Console.Error.WriteLine( );
  544.  
  545. 			Console.Error.Write( "Usage:   " );
  546. 			Console.ForegroundColor = ConsoleColor.White;
  547. 			Console.Error.WriteLine( "Word2Txt    \"wordfile\"  [ encoding | /D ]" );
  548. 			Console.ResetColor( );
  549.  
  550. 			Console.Error.WriteLine( );
  551.  
  552. 			Console.Error.Write( "or:      " );
  553. 			Console.ForegroundColor = ConsoleColor.White;
  554. 			Console.Error.WriteLine( "Word2Txt    /E" );
  555. 			Console.ResetColor( );
  556.  
  557. 			Console.Error.WriteLine( );
  558.  
  559. 			Console.Error.Write( "Where:   " );
  560. 			Console.ForegroundColor = ConsoleColor.White;
  561. 			Console.Error.Write( "wordfile" );
  562. 			Console.ResetColor( );
  563. 			Console.Error.WriteLine( "    is the path of the Word document to be read" );
  564.  
  565. 			Console.Error.WriteLine( "                     (no wildcards allowed)" );
  566.  
  567. 			Console.ForegroundColor = ConsoleColor.White;
  568. 			Console.Error.Write( "         encoding" );
  569. 			Console.ResetColor( );
  570. 			Console.Error.WriteLine( "    force use of alternative encoding for plain" );
  571.  
  572. 			Console.Error.Write( "                     text, e.g. " );
  573. 			Console.ForegroundColor = ConsoleColor.White;
  574. 			Console.Error.Write( "UTF-8" );
  575. 			Console.ResetColor( );
  576. 			Console.Error.WriteLine( " to preserve accented characters" );
  577.  
  578. 			Console.Error.Write( "                     or " );
  579. 			Console.ForegroundColor = ConsoleColor.White;
  580. 			Console.Error.Write( "IBM437" );
  581. 			Console.ResetColor( );
  582. 			Console.Error.WriteLine( " to convert unicode quotes to ASCII" );
  583.  
  584. 			Console.ForegroundColor = ConsoleColor.White;
  585. 			Console.Error.Write( "         /D" );
  586. 			Console.ResetColor( );
  587. 			Console.Error.WriteLine( "          use the encoding specified in the document file" );
  588.  
  589. 			Console.Error.WriteLine( "                     (for .DOCX and .ODT only, if Word isn't available)" );
  590.  
  591. 			Console.ForegroundColor = ConsoleColor.White;
  592. 			Console.Error.Write( "         /E" );
  593. 			Console.ResetColor( );
  594. 			Console.Error.WriteLine( "          list all available encodings" );
  595.  
  596. 			Console.Error.WriteLine( );
  597.  
  598. 			Console.Error.WriteLine( "Notes:   If a \"regular\" (MSI based) Microsoft Word (2007 or later)" );
  599.  
  600. 			Console.Error.WriteLine( "         installation is detected, this program will use Word to read" );
  601.  
  602. 			Console.Error.WriteLine( "         the text from the Word file, which may be ANY file format" );
  603.  
  604. 			Console.Error.WriteLine( "         recognized by Word." );
  605.  
  606. 			Console.Error.WriteLine( "         If Word was already active when this program is started, any" );
  607.  
  608. 			Console.Error.WriteLine( "         other opened document(s) will be left alone, and only the" );
  609.  
  610. 			Console.Error.WriteLine( "         document opened by this program will be closed." );
  611.  
  612. 			Console.Error.WriteLine( "         If Word is not available, the text can still be extracted, but" );
  613.  
  614. 			Console.Error.WriteLine( "         only from .DOC, .DOCX, .ODT, .RTF and .WPD files." );
  615.  
  616. 			Console.Error.WriteLine( "         This program requires .NET 4.5." );
  617.  
  618. 			Console.Error.WriteLine( "         Return code (\"errorlevel\") 0 means no errors were encounterd" );
  619.  
  620. 			Console.Error.WriteLine( "         and some text was extracted from the file; otherwise the" );
  621.  
  622. 			Console.Error.WriteLine( "         return code will be 1." );
  623.  
  624. 			Console.Error.WriteLine( );
  625.  
  626. 			Console.Error.WriteLine( "Written by Rob van der Woude" );
  627.  
  628. 			Console.Error.WriteLine( "https://www.robvanderwoude.com" );
  629.  
  630. 			#endregion Display Help Text
  631.  
  632.  
  633. 			return 1;
  634. 		}
  635. 	}
  636. }
  637.  

page last uploaded: 2021-01-27