Skip to main content

Get the basic flow working

ID
071da8b
date
2025-01-25 19:20:27+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
8294391
message
Get the basic flow working
changed files
2 files, 138 additions, 4 deletions

Changed files

index.html (3148 → 4900)

diff --git a/index.html b/index.html
index 8683a30..81dab6e 100644
--- a/index.html
+++ b/index.html
@@ -71,7 +71,7 @@
 
           // Get the epub file from the file input.
           const file = uploadForm.querySelector('input[type="file"]').files[0];
-          console.debug(`Got file ${file}`);
+          console.debug(`Got file ${file.name}`);
 
           // Find the `content
           JSZip.loadAsync(file)
@@ -87,10 +87,53 @@
 
               // Read the `content.opf` file, and get the key information
               // about this fic.
-              const contentOpfContent = await zip.file(contentOpfPath).async('string');
-              const ficInfo = getFicInfo(contentOpfContent);
+              const contentOpf = await zip.file(contentOpfPath).async('string');
+              const ficInfo = getFicInfo(contentOpf);
               console.debug(`Got fic info: ${JSON.stringify(ficInfo)}`);
 
+              // Create a cover image for this book which shows the title
+              // and author on a solid colour background.
+              //
+              // Then add this cover image to the zip file.
+              const canvas = createCoverImage(ficInfo);
+
+              const blob = await new Promise(
+                resolve => canvas.toBlob(resolve, 'image/png')
+              );
+
+              zip.file('media/cover.png', blob, { base64: true });
+
+              // Now modify the `content.opf` file to insert this image
+              // as the cover, and replace the file in the zip.
+              //
+              // This sort of text-based replacement is utterly cheating,
+              // but it also works.
+              const updatedContentOpf =
+                contentOpf
+                  .replace(
+                    "</manifest>",
+                    '<item id="cover-image" properties="cover-image" href="media/cover.png" media-type="image/png"/></manifest>'
+                  )
+                  .replace(
+                    "</metadata>",
+                    '<meta name="cover" content="cover-image"/></metadata>'
+                  );
+
+              zip.remove(contentOpfPath);
+              zip.file(contentOpfPath, updatedContentOpf);
+
+              // Create a download link for this zip file, and make
+              // the generated cover the link.
+              const zipContent = await zip.generateAsync({ type: 'blob' });
+
+              const downloadLink = document.createElement('a');
+              downloadLink.href = URL.createObjectURL(zipContent);
+              downloadLink.download = file.name;
+              downloadLink.appendChild(canvas);
+
+              document.querySelector('main').appendChild(downloadLink);
+
+
               reportSuccess(`
                 <span style="color: ${chooseColour(ficInfo.fandom)}">
                 Found fic info: ${JSON.stringify(ficInfo)}`)

static/app.js (4489 → 6831)

diff --git a/static/app.js b/static/app.js
index b180dac..aa82ae6 100644
--- a/static/app.js
+++ b/static/app.js
@@ -168,6 +168,97 @@ function numToHex(n) {
 
 
 
+/**
+ * Create a PNG cover image for a book.
+ *
+ * Returns a <canvas> element for this cover.
+ */
+function createCoverImage(ficInfo) {
+  // I want something with a rough 2:3 ratio that's big enough
+  // for the text to look sharp, but we don't need anything huge.
+  const width = 600;
+  const height = 900;
+  
+  const canvas = document.createElement("canvas");
+  canvas.setAttribute("width", 600);
+  canvas.setAttribute("height", 900);
+  
+  const ctx = canvas.getContext("2d");
+  ctx.fillStyle = chooseColour(ficInfo.fandom);
+  ctx.fillRect(0, 0, canvas.width, canvas.height);
+  
+  // Set text style.  We know white will look okay because we
+  // chose a dark background.
+  ctx.fillStyle = 'white';
+  ctx.textAlign = 'center';
+  
+  // Add the author name.
+  ctx.font = '60px Georgia';
+  ctx.fillText(ficInfo.author, width / 2, height * 0.85);
+  
+  // Add the title.
+  //
+  // This may be split across multiple lines if it's long; we only
+  // show part of the title if it's long to keep the text readable.
+  //
+  // If there are too many lines, we truncate it and add ellipsis
+  // to indicate it's been truncated.
+  ctx.font = '80px Georgia';
+  
+  var lines = getLines(ctx, [ficInfo.title, ficInfo.author, ficInfo.fandom].join(" "), width * 0.85);
+    
+  if (lines.length > 6) {
+    lines = lines.slice(0, 5);
+    lines[4] += '…'
+  }
+    
+  const lineHeight = 95;
+  const titleMidpoint = height * 0.425 - (lineHeight / 2 * (lines.length - 1));
+    
+  for (i = 0; i < lines.length; i++) {
+    ctx.fillText(
+      lines[i],
+      width / 2,
+      titleMidpoint + i * lineHeight
+    );
+  }
+  
+  return canvas;
+}
+
+/**
+ * Split a piece of text into lines to fit into a <canvas> without
+ * wrapping.
+ * 
+ * This function comes from Stack Overflow user crazy2be:
+ * https://stackoverflow.com/a/16599668/1558022
+ */
+function getLines(ctx, text, maxWidth) {
+    var words = text.split(" ");
+    var lines = [];
+    var currentLine = words[0];
+
+    for (var i = 1; i < words.length; i++) {
+        var word = words[i];
+        var width = ctx.measureText(currentLine + " " + word).width;
+        if (width < maxWidth) {
+            currentLine += " " + word;
+        } else {
+            lines.push(currentLine);
+            currentLine = word;
+        }
+    }
+    lines.push(currentLine);
+    return lines;
+}
+
+
+
 if (typeof module !== 'undefined') {
-  module.exports = { chooseColour, findContentOpfPath, getFicInfo };
+  module.exports = {
+    chooseColour,
+    createCoverImage,
+    findContentOpfPath,
+    getFicInfo,
+  };
 }