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