1+ using SixLabors . ImageSharp ;
2+ using SixLabors . ImageSharp . PixelFormats ;
3+ using SixLabors . ImageSharp . Processing ;
4+ using SixLabors . ImageSharp . Drawing . Processing ;
5+
6+ #nullable disable
7+
8+ namespace BookCoverCreator
9+ {
10+ /// <summary>
11+ /// Program
12+ /// </summary>
13+ public static class Program
14+ {
15+ private static double aspectRatioCover = 2.0 / 3.0 ; // 6x9 paperback
16+ private static double aspectRatioSpine = 0.13189 ; // 1.0 / 7.582 6x9 paperback spine
17+ private static float spineAlphaMultiplier = 2.0f ; // increase to reduce fade effect on the spine
18+ private static float spineAlphaPower = 1.0f ; // reduce to decrease fade effect on the spine
19+ private static int finalWidth = 4039 ;
20+ private static int finalHeight = 2775 ;
21+ private static int spineWidth = 366 ;
22+ private static string inputFolder ;
23+ private static string outputFolder ;
24+ private static string backCoverFileName = "BackCover.png" ;
25+ private static string spineFileName = "Spine.png" ;
26+ private static string frontCoverFileName = "FrontCover.png" ;
27+
28+ private static int frontWidth ;
29+ private static int backWidth ;
30+ private static Rectangle finalRect ;
31+ private static Rectangle spineRect ;
32+ private static Rectangle backCoverRect ;
33+ private static Rectangle backCoverMirrorRectDest ;
34+ private static Rectangle backCoverMirrorRectSource ;
35+ private static Rectangle frontCoverRect ;
36+ private static Rectangle frontCoverMirrorRectDest ;
37+ private static Rectangle frontCoverMirrorSource ;
38+
39+ /// <summary>
40+ /// Main
41+ /// </summary>
42+ /// <param name="args"></param>
43+ public static void Main ( string [ ] args )
44+ {
45+ if ( args . Length != 1 )
46+ {
47+ Console . WriteLine ( "Please pass one argument, the file name containing the metadata to process." ) ;
48+ Console . WriteLine ( "This file must contain the following parameters:" ) ;
49+ Console . WriteLine ( "InputFolder=value (the input folder containing the BackCover, Spine, and FrontCover files--default extension is .png)." ) ;
50+ Console . WriteLine ( "OutputFolder=value (the output folder)." ) ;
51+ Console . WriteLine ( ) ;
52+ Console . WriteLine ( "The following parameters are optional:" ) ;
53+ Console . WriteLine ( "BackCoverFile=value (the name of the back cover file in the input folder, default is BackCover.png)." ) ;
54+ Console . WriteLine ( "SpineFile=value (the name of the spine file in the input folder, default is Spine.png)." ) ;
55+ Console . WriteLine ( "FrontCoverFile=value (the name of the front cover file in the input folder, default is FrontCover.png)." ) ;
56+ Console . WriteLine ( "CoverAspectRatio=value (i.e. 0.6667 for 6x9 paperback)." ) ;
57+ Console . WriteLine ( "SpineAspectRatio=value (i.e. 0.13189 for 6x9 paperback)." ) ;
58+ Console . WriteLine ( "SpineAlphaMultiplier=value (i.e. 2.0, higher values reduce fade effect)." ) ;
59+ Console . WriteLine ( "SpineAlphaPower (i.e. 1.0, lower values reduce fade effect, set to 0 for no fade)." ) ;
60+ Console . WriteLine ( "TemplateWidth (i.e. 4039)" ) ;
61+ Console . WriteLine ( "TemplateHeight (i.e. 2775)" ) ;
62+ Console . WriteLine ( "TemplateSpineWidth (i.e. 366)" ) ;
63+ return ;
64+ }
65+ var dict = ParseKeyValueFile ( args [ 0 ] ) ;
66+ AssignVariables ( dict ) ;
67+
68+ string backCoverFile = Path . Combine ( inputFolder , backCoverFileName ) ;
69+ string spineFile = Path . Combine ( inputFolder , spineFileName ) ;
70+ string frontCoverFile = Path . Combine ( inputFolder , frontCoverFileName ) ;
71+ string backCoverFileResized = Path . Combine ( outputFolder , "BackCover.png" ) ;
72+ string backCoverFileMirrored = Path . Combine ( outputFolder , "BackCoverMirrored.png" ) ;
73+ string spineFileFaded = Path . Combine ( outputFolder , "SpineBlend.png" ) ;
74+ string frontCoverResized = Path . Combine ( outputFolder , "FrontCover.png" ) ;
75+ string frontCoverFileMirrored = Path . Combine ( outputFolder , "FrontCoverMirrored.png" ) ;
76+ Directory . CreateDirectory ( outputFolder ) ;
77+ ProcessSpine ( spineFile , spineFileFaded ) ;
78+ ProcessCover ( backCoverFile , backCoverFileResized , backCoverFileMirrored , true ) ;
79+ ProcessCover ( frontCoverFile , frontCoverResized , frontCoverFileMirrored , false ) ;
80+
81+ Console . WriteLine ( "Image processing complete. You can now take the files from the output folder at {0} and put them into your template image as individual layers." ) ;
82+ Console . WriteLine ( "Top down order is: " ) ;
83+ Console . WriteLine ( "1. SpineBlend.png" ) ;
84+ Console . WriteLine ( "2. BackCoverMirrored.png" ) ;
85+ Console . WriteLine ( "3. FrontCoverMirrored.png" ) ;
86+ Console . WriteLine ( "4. BackCover.png" ) ;
87+ Console . WriteLine ( "5. FrontCover.png" ) ;
88+ }
89+
90+ private static Dictionary < string , string > ParseKeyValueFile ( string fileName )
91+ {
92+ // Dictionary to store the key-value pairs
93+ Dictionary < string , string > keyValuePairs = new ( StringComparer . OrdinalIgnoreCase ) ;
94+
95+ // Read all lines from the file
96+ string [ ] lines = File . ReadAllLines ( fileName ) ;
97+
98+ // Iterate through each line
99+ foreach ( string line in lines )
100+ {
101+ // Skip empty lines or lines that do not contain '='
102+ if ( string . IsNullOrWhiteSpace ( line ) || ! line . Contains ( '=' ) )
103+ {
104+ continue ;
105+ }
106+
107+ // Split the line into key and value
108+ string [ ] parts = line . Split ( '=' , 2 ) ; // Limit the split to 2 parts
109+ if ( parts . Length == 2 )
110+ {
111+ string key = parts [ 0 ] . Trim ( ) ;
112+ string value = parts [ 1 ] . Trim ( ) ;
113+
114+ // Add the key-value pair to the dictionary
115+ keyValuePairs [ key ] = value ;
116+ }
117+ }
118+
119+ return keyValuePairs ;
120+ }
121+
122+ private static void AssignVariables ( Dictionary < string , string > dict )
123+ {
124+ foreach ( var kv in dict )
125+ {
126+ switch ( kv . Key . ToLowerInvariant ( ) )
127+ {
128+ case "inputfolder" :
129+ inputFolder = kv . Value ;
130+ break ;
131+ case "outputfolder" :
132+ outputFolder = kv . Value ;
133+ break ;
134+ case "backcoverfile" :
135+ backCoverFileName = kv . Value ;
136+ break ;
137+ case "spinefile" :
138+ spineFileName = kv . Value ;
139+ break ;
140+ case "frontcoverfile" :
141+ frontCoverFileName = kv . Value ;
142+ break ;
143+ case "coveraspectratio" :
144+ aspectRatioCover = double . Parse ( kv . Value ) ;
145+ break ;
146+ case "spineaspectratio" :
147+ aspectRatioSpine = double . Parse ( kv . Value ) ;
148+ break ;
149+ case "spinealphamultiplier" :
150+ spineAlphaMultiplier = float . Parse ( kv . Value ) ;
151+ break ;
152+ case "spinealphapower" :
153+ spineAlphaPower = float . Parse ( kv . Value ) ;
154+ break ;
155+ case "templatewidth" :
156+ finalWidth = int . Parse ( kv . Value ) ;
157+ break ;
158+ case "templateheight" :
159+ finalHeight = int . Parse ( kv . Value ) ;
160+ break ;
161+ case "templatespinewidth" :
162+ spineWidth = int . Parse ( kv . Value ) ;
163+ break ;
164+ }
165+ }
166+
167+ frontWidth = ( finalWidth - spineWidth ) / 2 ;
168+ backWidth = finalWidth - frontWidth - spineWidth ;
169+ finalRect = new ( 0 , 0 , finalWidth , finalHeight ) ;
170+ spineRect = new ( backWidth , 0 , spineWidth , finalHeight ) ;
171+ backCoverRect = new ( 0 , 0 , backWidth , finalHeight ) ;
172+ backCoverMirrorRectDest = new ( spineRect . X , 0 , spineRect . Width / 2 , spineRect . Height ) ;
173+ backCoverMirrorRectSource = new ( 0 , 0 , spineRect . Width / 2 , spineRect . Height ) ;
174+ frontCoverRect = new ( backWidth + spineWidth , 0 , frontWidth , finalHeight ) ;
175+ frontCoverMirrorRectDest = new ( spineRect . Left + ( spineRect . Width / 2 ) , 0 , spineRect . Width / 2 , 0 ) ;
176+ frontCoverMirrorSource = new ( frontCoverRect . Width - ( spineRect . Width / 2 ) , 0 , ( spineRect . Width / 2 ) , spineRect . Height ) ;
177+ }
178+
179+ private static void ProcessCover ( string inputFilePath , string outputFileResized , string outputFileMirrored , bool isBackCover )
180+ {
181+ using Image < Rgba32 > coverImage = Image . Load < Rgba32 > ( inputFilePath ) ;
182+ Crop ( coverImage , aspectRatioCover ) ;
183+
184+ // Create the final image
185+ using Image < Rgba32 > finalImage = new ( finalRect . Width , finalRect . Height , Color . Transparent ) ;
186+
187+ // Calculate positions to position the resized image
188+ Rectangle coverRectDest = isBackCover ? backCoverRect : frontCoverRect ;
189+ Rectangle mirrorRectDest = isBackCover ? backCoverMirrorRectDest : frontCoverMirrorRectDest ;
190+ Rectangle mirrorRectSource = isBackCover ? backCoverMirrorRectSource : frontCoverMirrorSource ;
191+
192+
193+ // Draw the resized image onto the final image
194+ coverImage . Mutate ( ctx => ctx . Resize ( new Size ( coverRectDest . Width , coverRectDest . Height ) ) ) ;
195+ finalImage . Mutate ( ctx => ctx . DrawImage ( coverImage , new Point ( coverRectDest . X , coverRectDest . Y ) , 1.0f ) ) ;
196+
197+ // Save the final image
198+ finalImage . Save ( outputFileResized ) ;
199+
200+ // Create the mirrored image
201+ using Image < Rgba32 > mirroredImage = new ( coverRectDest . Width , coverRectDest . Height ) ;
202+ mirroredImage . Mutate ( ctx => ctx . DrawImage ( coverImage , new Point ( 0 , 0 ) , 1.0f ) ) ;
203+ mirroredImage . Mutate ( ctx => ctx . Flip ( FlipMode . Horizontal ) ) ;
204+
205+ // extract the mirror rect source from the mirrored image
206+ using Image < Rgba32 > mirroredImageSource = mirroredImage . Clone ( ctx => ctx . Crop ( mirrorRectSource ) ) ;
207+
208+ // Draw the mirrored image onto the final image
209+ finalImage . Mutate ( ctx => ctx . Clear ( Color . Transparent ) ) ;
210+ finalImage . Mutate ( ctx => ctx . DrawImage ( mirroredImageSource , new Point ( mirrorRectDest . X , mirrorRectDest . Y ) , 1.0f ) ) ;
211+
212+ // Save the mirrored image
213+ finalImage . Save ( outputFileMirrored ) ;
214+ }
215+
216+ private static void ProcessSpine ( string inputFilePath , string outputFilePath )
217+ {
218+ using Image < Rgba32 > image = Image . Load < Rgba32 > ( inputFilePath ) ;
219+ Crop ( image , aspectRatioSpine ) ;
220+
221+ // Remove black rows from top and bottom
222+ image . Mutate ( ctx => RemoveBlackRows ( image ) ) ;
223+
224+ // Resize image to spine dimensions
225+ image . Mutate ( ctx => ctx . Resize ( new Size ( spineRect . Width , spineRect . Height ) ) ) ;
226+
227+ // Apply gradient fade from each edge to the center
228+ ApplyAlphaGradient ( image ) ;
229+
230+ // Create the final image
231+ using Image < Rgba32 > finalImage = new ( finalRect . Width , finalRect . Height , Color . Transparent ) ;
232+
233+ // Draw the resized and faded image onto the final image
234+ finalImage . Mutate ( ctx => ctx . DrawImage ( image , new Point ( spineRect . X , spineRect . Y ) , 1.0f ) ) ;
235+
236+ // Save the final image as a PNG
237+ finalImage . Save ( outputFilePath ) ;
238+ }
239+
240+ private static void Crop ( Image < Rgba32 > image , double aspectRatio )
241+ {
242+ // Calculate the desired dimensions of the cropped image
243+ var imageAspectRatio = ( double ) image . Width / ( double ) image . Height ;
244+ var cropWidth = aspectRatio > imageAspectRatio ? image . Width : ( int ) ( image . Height * aspectRatio ) ;
245+ var cropHeight = aspectRatio < imageAspectRatio ? image . Height : ( int ) ( image . Width / aspectRatio ) ;
246+
247+ // Calculate the crop rectangle, centered in the image
248+ var x = ( image . Width - cropWidth ) / 2 ;
249+ var y = ( image . Height - cropHeight ) / 2 ;
250+
251+ // Crop the image
252+ image . Mutate ( ctx => ctx . Crop ( new Rectangle ( x , y , cropWidth , cropHeight ) ) ) ;
253+ }
254+
255+ private static void RemoveBlackRows ( Image < Rgba32 > image )
256+ {
257+ if ( image . Width < 16 && image . Height < 16 )
258+ {
259+ return ;
260+ }
261+
262+ const byte threshold = 10 ;
263+
264+ // Remove black rows from the top
265+ int topNonBlackRow = 0 ;
266+ for ( int y = 0 ; y < image . Height ; y ++ )
267+ {
268+ bool isBlackRow = true ;
269+ for ( int x = 0 ; x < image . Width ; x ++ )
270+ {
271+ if ( image [ x , y ] . R > threshold || image [ x , y ] . G > threshold || image [ x , y ] . B > threshold )
272+ {
273+ isBlackRow = false ;
274+ break ;
275+ }
276+ }
277+
278+ if ( ! isBlackRow )
279+ {
280+ topNonBlackRow = y ;
281+ break ;
282+ }
283+ }
284+
285+ // Remove black rows from the bottom
286+ int bottomNonBlackRow = image . Height - 1 ;
287+ for ( int y = image . Height - 1 ; y >= 0 ; y -- )
288+ {
289+ bool isBlackRow = true ;
290+ for ( int x = 0 ; x < image . Width ; x ++ )
291+ {
292+ if ( image [ x , y ] . R > threshold || image [ x , y ] . G > threshold || image [ x , y ] . B > threshold )
293+ {
294+ isBlackRow = false ;
295+ break ;
296+ }
297+ }
298+
299+ if ( ! isBlackRow )
300+ {
301+ bottomNonBlackRow = y ;
302+ break ;
303+ }
304+ }
305+
306+ // Crop the image to remove black rows from top and bottom
307+ int newHeight = bottomNonBlackRow - topNonBlackRow + 1 ;
308+ if ( newHeight > 0 )
309+ {
310+ image . Mutate ( ctx => ctx . Crop ( new Rectangle ( 0 , topNonBlackRow , image . Width , newHeight ) ) ) ;
311+ }
312+ }
313+
314+ private static void ApplyAlphaGradient ( Image < Rgba32 > image )
315+ {
316+ int width = image . Width ;
317+ int halfWidth = width / 2 ;
318+
319+ static float GetAlphaFactor ( int x , int width , int halfWidth )
320+ {
321+ float distanceFromEdge = Math . Min ( x , width - x - 1 ) ;
322+ return ( float ) Math . Clamp ( Math . Pow ( ( distanceFromEdge / halfWidth ) * spineAlphaMultiplier , spineAlphaPower ) , 0.0 , 1.0 ) ;
323+ }
324+
325+ image . Mutate ( ctx =>
326+ {
327+ for ( int y = 0 ; y < image . Height ; y ++ )
328+ {
329+ for ( int x = 0 ; x < width ; x ++ )
330+ {
331+ float alphaFactor = GetAlphaFactor ( x , width , halfWidth ) ;
332+ Rgba32 pixel = image [ x , y ] ;
333+ pixel . A = ( byte ) ( pixel . A * alphaFactor ) ;
334+ image [ x , y ] = pixel ;
335+ }
336+ }
337+ } ) ;
338+ }
339+ }
340+ }
341+
342+ #nullable restore
0 commit comments