11using System ;
2+ using System . Collections . Generic ;
3+ using System . Diagnostics ;
4+ using System . Diagnostics . CodeAnalysis ;
5+ using System . Globalization ;
26using System . IO ;
7+ using System . Linq ;
38using System . Text ;
49using Console = System . Console ;
510
611namespace UglyToad . PdfPig . ConsoleRunner
712{
813 public static class Program
914 {
15+ private class OptionalArg
16+ {
17+ public required string ShortSymbol { get ; init ; }
18+
19+ public required string Symbol { get ; init ; }
20+
21+ public required bool SupportsValue { get ; init ; }
22+
23+ public string ? Value { get ; set ; }
24+ }
25+
26+ private class ParsedArgs
27+ {
28+ public required IReadOnlyList < OptionalArg > SuppliedArgs { get ; init ; }
29+
30+ public required string SuppliedDirectoryPath { get ; init ; }
31+ }
32+
33+ private static IReadOnlyList < OptionalArg > GetSupportedArgs ( ) =>
34+ [
35+ new OptionalArg
36+ {
37+ SupportsValue = false ,
38+ ShortSymbol = "nr" ,
39+ Symbol = "no-recursion"
40+ } ,
41+ new OptionalArg
42+ {
43+ SupportsValue = true ,
44+ ShortSymbol = "o" ,
45+ Symbol = "output"
46+ } ,
47+ new OptionalArg
48+ {
49+ SupportsValue = true ,
50+ ShortSymbol = "l" ,
51+ Symbol = "limit"
52+ }
53+ ] ;
54+
55+ private static bool TryParseArgs (
56+ string [ ] args ,
57+ [ NotNullWhen ( true ) ] out ParsedArgs ? parsed )
58+ {
59+ parsed = null ;
60+ string ? path = null ;
61+ var suppliedOpts = new List < OptionalArg > ( ) ;
62+
63+ var opts = GetSupportedArgs ( ) ;
64+
65+ for ( var i = 0 ; i < args . Length ; i ++ )
66+ {
67+ var str = args [ i ] ;
68+
69+ var isOptFlag = str . StartsWith ( '-' ) ;
70+
71+ if ( ! isOptFlag )
72+ {
73+ if ( path == null )
74+ {
75+ path = str ;
76+ }
77+ else
78+ {
79+ return false ;
80+ }
81+ }
82+ else
83+ {
84+ var item = opts . SingleOrDefault ( x =>
85+ string . Equals ( "-" + x . ShortSymbol , str , StringComparison . OrdinalIgnoreCase )
86+ || string . Equals ( "--" + x . Symbol , str , StringComparison . OrdinalIgnoreCase ) ) ;
87+
88+ if ( item == null )
89+ {
90+ return false ;
91+ }
92+
93+ if ( item . SupportsValue )
94+ {
95+ if ( i == args . Length - 1 )
96+ {
97+ return false ;
98+ }
99+
100+ i ++ ;
101+ item . Value = args [ i ] ;
102+ }
103+
104+ suppliedOpts . Add ( item ) ;
105+ }
106+ }
107+
108+ if ( path == null )
109+ {
110+ return false ;
111+ }
112+
113+ parsed = new ParsedArgs
114+ {
115+ SuppliedArgs = suppliedOpts ,
116+ SuppliedDirectoryPath = path
117+ } ;
118+
119+ return true ;
120+ }
121+
10122 public static int Main ( string [ ] args )
11123 {
12124 if ( args . Length == 0 )
@@ -15,30 +127,47 @@ public static int Main(string[] args)
15127 return 7 ;
16128 }
17129
18- var path = args [ 0 ] ;
19-
20- if ( ! Directory . Exists ( path ) )
130+ if ( ! TryParseArgs ( args , out var parsed ) )
21131 {
22- Console . WriteLine ( $ "The provided path is not a valid directory: { path } .") ;
132+ var strJoined = string . Join ( " " , args ) ;
133+ Console . WriteLine ( $ "Unrecognized arguments passed: { strJoined } ") ;
23134 return 7 ;
24135 }
25136
26- var maxCount = default ( int ? ) ;
137+ if ( ! Directory . Exists ( parsed . SuppliedDirectoryPath ) )
138+ {
139+ Console . WriteLine ( $ "The provided path is not a valid directory: { parsed . SuppliedDirectoryPath } .") ;
140+ return 7 ;
141+ }
27142
28- if ( args . Length > 1 && int . TryParse ( args [ 1 ] , out var countIn ) )
143+ int ? maxCount = null ;
144+ var limit = parsed . SuppliedArgs . SingleOrDefault ( x => x . ShortSymbol == "l" ) ;
145+ if ( limit ? . Value != null && int . TryParse ( limit . Value , CultureInfo . InvariantCulture , out var maxCountArg ) )
29146 {
30- maxCount = countIn ;
147+ Console . WriteLine ( $ "Limiting input files to first: { maxCountArg } ") ;
148+ maxCount = maxCountArg ;
31149 }
150+
151+ var noRecursionMode = parsed . SuppliedArgs . Any ( x => x . ShortSymbol == "nr" ) ;
152+ var outputOpt = parsed . SuppliedArgs . SingleOrDefault ( x => x . ShortSymbol == "o" && x . Value != null ) ;
32153
33154 var hasError = false ;
34155 var errorBuilder = new StringBuilder ( ) ;
35- var fileList = Directory . GetFiles ( path , "*.pdf" , SearchOption . AllDirectories ) ;
156+ var fileList = Directory . GetFiles (
157+ parsed . SuppliedDirectoryPath ,
158+ "*.pdf" ,
159+ noRecursionMode ? SearchOption . TopDirectoryOnly : SearchOption . AllDirectories )
160+ . OrderBy ( x => x ) . ToList ( ) ;
36161 var runningCount = 0 ;
37162
38- Console . WriteLine ( $ "Found { fileList . Length } files.") ;
163+ Console . WriteLine ( $ "Found { fileList . Count } files.") ;
164+ Console . WriteLine ( ) ;
165+
166+ PrintTableColumns ( "File" , "Size" , "Words" , "Pages" , "Open cost (μs)" , "Total cost (μs)" , "Page cost (μs)" ) ;
39167
40- Console . WriteLine ( $ " { GetCleanFilename ( "File" ) } | Size \t | Words \t | Pages" ) ;
168+ var dataList = new List < DataRecord > ( ) ;
41169
170+ var sw = new Stopwatch ( ) ;
42171 foreach ( var file in fileList )
43172 {
44173 if ( maxCount . HasValue && runningCount >= maxCount )
@@ -50,8 +179,20 @@ public static int Main(string[] args)
50179 {
51180 var numWords = 0 ;
52181 var numPages = 0 ;
182+ long openMicros ;
183+ long totalPageMicros ;
184+
185+ sw . Reset ( ) ;
186+ sw . Start ( ) ;
187+
53188 using ( var pdfDocument = PdfDocument . Open ( file ) )
54189 {
190+ sw . Stop ( ) ;
191+
192+ openMicros = sw . Elapsed . Microseconds ;
193+
194+ sw . Start ( ) ;
195+
55196 foreach ( var page in pdfDocument . GetPages ( ) )
56197 {
57198 numPages ++ ;
@@ -63,13 +204,36 @@ public static int Main(string[] args)
63204 }
64205 }
65206 }
207+
208+ sw . Stop ( ) ;
209+ totalPageMicros = sw . Elapsed . Microseconds ;
66210 }
67211
68212 var filename = Path . GetFileName ( file ) ;
69213
70214 var size = new FileInfo ( file ) ;
71215
72- Console . WriteLine ( $ "{ GetCleanFilename ( filename ) } | { size . Length } \t | { numWords } \t | { numPages } ") ;
216+ var item = new DataRecord
217+ {
218+ FileName = filename ,
219+ OpenCostMicros = openMicros ,
220+ Pages = numPages ,
221+ Size = size . Length ,
222+ Words = numWords ,
223+ TotalCostMicros = totalPageMicros + openMicros ,
224+ PerPageMicros = Math . Round ( totalPageMicros / ( double ) Math . Max ( numPages , 1 ) , 2 )
225+ } ;
226+
227+ dataList . Add ( item ) ;
228+
229+ PrintTableColumns (
230+ item . FileName ,
231+ item . Size ,
232+ item . Words ,
233+ item . Pages ,
234+ item . OpenCostMicros ,
235+ item . TotalCostMicros ,
236+ item . PerPageMicros ) ;
73237 }
74238 catch ( Exception ex )
75239 {
@@ -88,12 +252,71 @@ public static int Main(string[] args)
88252 return 5 ;
89253 }
90254
255+ if ( outputOpt != null && outputOpt . Value != null )
256+ {
257+ WriteOutput ( outputOpt . Value , dataList ) ;
258+ }
259+
91260 Console . WriteLine ( "Complete! :)" ) ;
92261
93262 return 0 ;
94263 }
95264
96- private static string GetCleanFilename ( string name , int maxLength = 30 )
265+ private static void WriteOutput ( string outPath , IReadOnlyList < DataRecord > records )
266+ {
267+ using var fs = File . OpenWrite ( outPath ) ;
268+ using var sw = new StreamWriter ( fs ) ;
269+
270+ sw . WriteLine ( "File,Size,Words,Pages,Open Cost,Total Cost,Per Page" ) ;
271+ foreach ( var record in records )
272+ {
273+ var sizeStr = record . Size . ToString ( "D" , CultureInfo . InvariantCulture ) ;
274+ var wordsStr = record . Words . ToString ( "D" , CultureInfo . InvariantCulture ) ;
275+ var pagesStr = record . Pages . ToString ( "D" , CultureInfo . InvariantCulture ) ;
276+ var openCostStr = record . OpenCostMicros . ToString ( "D" , CultureInfo . InvariantCulture ) ;
277+ var totalCostStr = record . TotalCostMicros . ToString ( "D" , CultureInfo . InvariantCulture ) ;
278+ var ppcStr = record . PerPageMicros . ToString ( "F2" , CultureInfo . InvariantCulture ) ;
279+
280+ var numericPartsStr = string . Join ( "," ,
281+ [
282+ sizeStr ,
283+ wordsStr ,
284+ pagesStr ,
285+ openCostStr ,
286+ totalCostStr ,
287+ ppcStr
288+ ] ) ;
289+
290+ sw . WriteLine ( $ "\" { record . FileName } \" ,{ numericPartsStr } ") ;
291+ }
292+
293+ sw . Flush ( ) ;
294+ }
295+
296+ private static void PrintTableColumns ( params object [ ] values )
297+ {
298+ for ( var i = 0 ; i < values . Length ; i ++ )
299+ {
300+ var value = values [ i ] ;
301+ var valueStr = value . ToString ( ) ;
302+
303+ var cleaned = GetCleanStr ( valueStr ?? string . Empty ) ;
304+
305+ var padChars = 16 - cleaned . Length ;
306+
307+ var padding = padChars > 0 ? new string ( ' ' , padChars ) : string . Empty ;
308+
309+ var padded = cleaned + padding ;
310+
311+ Console . Write ( "| " ) ;
312+
313+ Console . Write ( padded ) ;
314+ }
315+
316+ Console . WriteLine ( ) ;
317+ }
318+
319+ private static string GetCleanStr ( string name , int maxLength = 16 )
97320 {
98321 if ( name . Length <= maxLength )
99322 {
@@ -105,4 +328,21 @@ private static string GetCleanFilename(string name, int maxLength = 30)
105328 return name . Substring ( 0 , maxLength ) ;
106329 }
107330 }
331+
332+ internal class DataRecord
333+ {
334+ public required string FileName { get ; init ; }
335+
336+ public required long Size { get ; init ; }
337+
338+ public required int Words { get ; init ; }
339+
340+ public required int Pages { get ; init ; }
341+
342+ public required long OpenCostMicros { get ; init ; }
343+
344+ public required long TotalCostMicros { get ; init ; }
345+
346+ public required double PerPageMicros { get ; init ; }
347+ }
108348}
0 commit comments