Skip to content

Commit 47c7df2

Browse files
committed
Initial commit
1 parent 9142806 commit 47c7df2

6 files changed

+391
-0
lines changed

BookCoverCreator.csproj

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
13+
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
14+
</ItemGroup>
15+
16+
</Project>

BookCoverCreator.sln

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.1.32328.378
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BookCoverCreator", "BookCoverCreator.csproj", "{9B893D49-4F75-481E-A918-EFBCCF094F76}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{9B893D49-4F75-481E-A918-EFBCCF094F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{9B893D49-4F75-481E-A918-EFBCCF094F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{9B893D49-4F75-481E-A918-EFBCCF094F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{9B893D49-4F75-481E-A918-EFBCCF094F76}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {574A8529-C1F4-4DCB-A8F5-4534F2248D1D}
24+
EndGlobalSection
25+
EndGlobal

BookCoverCreatorApp.cs

+342
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
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

Properties/launchSettings.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profiles": {
3+
"ConsoleApp1": {
4+
"commandName": "Project",
5+
"commandLineArgs": "\"D:/backup/Files/BaneWar/Assets/Images/Book1/Cover/Publish/Working/args.txt\""
6+
}
7+
}
8+
}

templates/PaperbackCover.pdn

1 MB
Binary file not shown.

templates/PaperbackTemplate6x9.webp

81.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)