More from Alice GG
Neovim is by far my favorite text editor. The clutter-free interface and keyboard-only navigation are what keep me productive in my daily programming. In an earlier post, I explained how I configure it into a minimalist development environment. Today, I will show you how to use it with Godot and GDScript. Configure Godot First, we need to tell Godot to use nvim as a text editor instead of the built-in one. Open Godot, and head to Editor Settings > General > Text Editor > External. There, you will need to tick the box Use external editor, indicate your Neovim installation path, and use --server /tmp/godothost --remote-send "<C-\><C-N>:n {file}<CR>{line}G{col}|" as execution flags. While in the settings, head to Network > Language Server and note down the remote port Godot is using. By default, it should be 6005. We will need that value later. Connecting to Godot with vim-godot Neovim will be able to access Godot features by using a plugin called vim-godot. We will need to edit the nvim configuration file to install plugins and configure Neovim. On Mac and Linux, it is located at ~/.config/nvim/init.vim I use vim-plug to manage my plugins, so I can just add it to my configuration like this: call plug#begin('~/.vim/plugged') " ... Plug 'habamax/vim-godot' " ... call plug#end() Once the configuration file is modified and saved, use the :PlugInstall command to install it. You’ll also need to indicate Godot’s executable path. Add this line to your init.vim: let g:godot_executable = '/Applications/Godot.app/Contents/MacOS/Godot' For vim-godot to communicate with the Godot editor, it will need to listen to the /tmp/godothost file we configured in the editor previously. To do that, simply launch nvim with the flag --listen /tmp/godothost. To save you some precious keypress, I suggest creating a new alias in your bashrc/zshrc like this: alias gvim="nvim --listen /tmp/godothost" Getting autocompletion with coc.nvim Godot ships with a language server. It means the Godot editor can provide autocompletion, syntax highlighting, and advanced navigation to external editors like nvim. While Neovim now has built-in support for the language server protocol, I’ve used the plugin coc.nvim to obtain these functionalities for years and see no reason to change. You can also install it with vim-plug by adding the following line to your plugin list: Plug 'neoclide/coc.nvim', {'branch':'release'} Run :PlugInstall again to install it. You’ll need to indicate the Godot language server address and port using the command :CocConfig. It should open Coc’s configuration file, which is a JSON file normally located at ~/.config/nvim/coc-settings.json. In this file enter the following data, and make sure the port number matches the one located in your editor: { "languageserver": { "godot": { "host": "127.0.0.1", "filetypes": ["gdscript"], "port": 6005 } } } I recommend adding Coc’s example configuration to your init.vim file. You can find it on GitHub. It will provide you with a lot of useful shortcuts, such as using gd to go to a function definition and gr to list its references. Debugging using nvim-dap If you want to use the debugger from inside Neovim, you’ll need to install another plugin called nvim-dap. Add the following to your plugins list: Plug 'mfussenegger/nvim-dap' The plugin authors suggest configuring it using Lua, so let’s do that by adding the following in your init.vim: lua <<EOF local dap = require("dap") dap.adapters.godot = { type = "server", host = "127.0.0.1", port = 6006, } dap.configurations.gdscript = { { type = "godot", request = "launch", name = "Launch scene", project = "${workspaceFolder}", launch_scene = true, }, } vim.api.nvim_create_user_command("Breakpoint", "lua require'dap'.toggle_breakpoint()", {}) vim.api.nvim_create_user_command("Continue", "lua require'dap'.continue()", {}) vim.api.nvim_create_user_command("StepOver", "lua require'dap'.step_over()", {}) vim.api.nvim_create_user_command("StepInto", "lua require'dap'.step_into()", {}) vim.api.nvim_create_user_command("REPL", "lua require'dap'.repl.open()", {}) EOF This will connect to the language server (here on port 6005), and allow you to pilot the debugger using the following commands: :Breakpoint to create (or remove) a breakpoint :Continue to launch the game or run until the next breakpoint :StepOver to step over a line :StepInto to step inside a function definition :REPL to launch a REPL (useful if you want to examine values) Conclusion I hope you’ll have a great time developing Godot games with Neovim. If it helps you, you can check out my entire init.vim file on GitHub gist.
It’s been around 2 years that I’ve had to stop with my long-term addiction to stable jobs. Quite a few people who read this blog are wondering what the hell exactly I’ve been doing since then so I’m going to update all of you on the various projects I’ve been working on. Meme credit: Fabian Stadler Mikochi Last year, I created Mikochi, a minimalist remote file browser written in Go and Preact. It has slowly been getting more and more users, and it’s now sitting at more than 200 GitHub stars and more than 6000 Docker pulls. I personally use it almost every day and it fits my use case perfectly. It is basically feature-complete so I don’t do too much development on it. I’ve actually been hoping users help me solve the few remaining GitHub issues. So far it happened twice, a good start I guess. Itako You may have seen a couple of posts on this blog regarding finance. It’s a subject I’ve been trying to learn more about for a while now. This led me to read some excellent books including Nassim Taleb’s Fooled by Randomness, Robert Shiller’s Irrational Exuberance, and Robert Carver’s Smart Portfolios. Those books have pushed me toward a more systematic approach to investing, and I’ve built Itako to help me with that. I’ve not talked about it on this blog so far, but it’s a SaaS software that gives clear data visualizations of a stock portfolio performance, volatility, and diversification. It’s currently in beta and usable for free. I’m quite happy that there are actually people using it and that it seems to work without any major issues. However, I think making it easier to use and adding a couple more features would be necessary to make it into a commercially viable product. I try to work on it when I find the time, but for the next couple of months, I have to prioritize the next project. Dice’n Goblins I play RPGs too much and now I’m even working on making them. This project was actually not started by me but by Daphnée Portheault. In the past, we worked on a couple of game jams and produced Cosmic Delusion and Duat. Now we’re trying to make a real commercial game called Dice’n Goblins. The game is about a Goblin who tries to escape from a dungeon that seems to grow endlessly. It’s inspired by classic dungeon crawlers like Etrian Odyssey and Lands of Lore. The twist is that you have to use dice to fight monsters. Equipping items you find in the dungeon gives you new dice and using skills allows you to change the dice values during combat (and make combos). We managed to obtain a decent amount of traction on this project and now it’s being published by Rogue Duck Interactive. The full game should come out in Q1 2025, for PC, Mac, and Linux. You can already play the demo (and wishlist the game) on Steam. If you’re really enthusiastic about it, don’t hesitate to join the Discord community. Technically it’s quite a big change for me to work on game dev since I can’t use that many of the reflexes I’ve built while working on infra subjects. But I’m getting more and more comfortable with using Godot and figuring out all the new game development related lingo. It’s also been an occasion to do a bit of work with non-code topics, like press relations. Japanese Something totally not relevant to tech. Since I’ve managed to reach a ‘goed genoeg’ level of Dutch, I’ve also started to learn more Japanese. I’ve almost reached the N4 level. (By almost I mean I’ve failed but it was close.) A screenshot from the Kanji Study Android App I’ve managed to learn all the hiraganas, katakanas, basic vocabulary, and grammar. So now all I’ve left to do is a huge amount of immersion and grind more kanjis. This is tougher than I thought it would be but I guess it’s fun that I can pretend to be studying while playing Dragon Quest XI in Japanese.
Since 2019, Apple has required all MacOS software to be signed and notarized. This is meant to prevent naive users from installing malware while running software from unknown sources. Since this process is convoluted, it stops many indie game developers from releasing their Godot games on Mac. To solve this, this article will attempt to document each and every step of the signing and notarization process. Photo by Natasya Chen Step 0: Get a Mac While there tools exists to codesign/notarize Mac executables from other platforms, I think having access to a MacOS machine will remove quite a few headaches. A Mac VM, or even a cloud machine, might do the job. I have not personally tested those alternatives, so if you do, please tell me if it works well. Step 1: Get an Apple ID and the Developer App You can create an Apple ID through Apple’s website. While the process should be straightforward, it seems like Apple has trust issues when it comes to email from protonmail.com or custom domains. Do not hesitate to contact support in case you encounter issues creating or logging into your Apple ID. They are quite responsive. Once you have a working Apple ID, use it to log into the App Store on your Mac and install the Apple Developer application. Step 2: Enroll in the Apple Developer Program Next, open the Apple Developer app, log in, and ask to “Enroll” in the developer program. This will require you to scan your ID, fill in data about you and your company, and most likely confirm those data with a support agent by phone. The process costs ~99$ and should take between 24 and 48 hours. Step 3: Setup Xcode Xcode will be used to codesign and notarize your app through Godot. You should install the app through the App Store like you did for the Apple Developer application. Once the app is installed, we need to accept the license. First, launch Xcode and close it. Then open a terminal and run the following commands: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer sudo xcodebuild -license accept Step 4: Generate a certificate signing request To obtain a code signing certificate, we need to generate a certificate request. To do this, open Keychain, and from the top menu, select Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority. Fill in your email address and choose Request is: Saved to disk. Click on Continue and save the .certSigningRequest file somewhere. Step 5: Obtain a code signing certificate Now, head to the Apple Developer website. Log in and go to the certificate list. Click on the + button to create a new certificate. Check Developer ID Application when asked for the type of certificate and click on continue. On the next screen, upload the certificate signing request we generated in step 4. You’ll be prompted to download your certificate. Do it and add it to Keychain by double-clicking on the file. You can check that your certificate was properly added by running the following command: security find-identity -v -p codesigning It should return (at least) one identity. Step 6: Get an App Store Connect API Key Back to the Apple Developer website, go to Users and Access, and open the Integrations tab. From this page, you should request access to the App Store Connect API. This access should normally be granted immediately. From this page, create a new key by clicking on the + icon. Give your key a name you will remember and give it the Developer access. Click on Generate and the key will be created. You will then be prompted to download your key. Do it and store the file safely, as you will only be able to download it once. Step 7: Configure Godot Open your Godot project and head to the project settings using the top menu (Project > Project Settings). From there search for VRAM Compression and check Import ETC2 ASTC. Then make sure you have installed up-to-date export templates by going through the Editor > Manage Export Templates menu and clicking on Download and Install. To export your project, head to the Project > Export. Click on Add and select macOS to create new presets. In the presets form on the left, you’ll have to fill in a unique Bundle Identifier in the Application section, this can be com.yourcompany.yourgame. In the Codesign section, select Xcode codesign and fill in your Apple Team ID and Identity. Those can be found using the security find-identity -v -p codesigning command: the first (~40 characters) part of the output is your identity, and the last (~10 characters, between parentheses) is your Team ID. In the Notarization section, select Xcode notarytool and fill in your API UUID (found on the appstoreconnect page), API Key (the file you saved in Step 6), and API Key ID (also found on the appstoreconnect page). Click on Export Project… to start the export. Step 8: Checking the notarization status Godot will automatically send your exported file for notarization. You can check the notarization progress by running: xcrun notarytool history --key YOUR_AUTH_KEY_FILE.p8 --key-id YOUR_KEY_ID --issuer YOUR_ISSUER_ID According to Apple, the process should rarely take more than 15 minutes. Empirically, this is sometimes very false and the process can give you enough time to grab a coffee, bake a cake, and water your plants. Once the notarization appears completed (with the status Valid or Invalid), you can run this command to check the result (using the job ID found in the previous command output): xcrun notarytool log --key YOUR_AUTH_KEY_FILE.p8 --key-id YOUR_KEY_ID --issuer YOUR_ISSUER_ID YOUR_JOB_ID Step 9: Stapling your executable To make sure that your executable can work offline, you are supposed to ‘staple’ the notarization to it. This is done by running the following command: xcrun stapler staple MY_SOFTWARE.dmg Extra: Exporting the game as .app Godot can export your game as .dmg, .zip, or .app. For most users, it is more convenient to receive the game as .app, as those can be directly executed. However, the notarization process doesn’t support uploading .app files to Apple’s server. I think the proper way to obtain a notarized .app file is to: Export the project .dmg from Godot with code signing and notarization Mount the .dmg and extract the .app located inside of it Staple the .app bundle Extra: Code signing GodotSteam GodotSteam is a Godot add-on that wraps the Steam API inside GDscript. If you use it, you might encounter issues during notarization, because it adds a bunch of .dylib and .framework files. What I did to work around that was to codesign the framework folders: codesign --deep --force --verify --verbose --sign "Developer ID Application: My Company" libgodotsteam.macos.template_release.framework codesign --deep --force --verify --verbose --sign "Developer ID Application: My Company" libgodotsteam.macos.template_debug.framework I also checked the options Allow DyId environment variable and Disable Library Validation in the export settings (section Codesign > Entitlements). FAQ: Is this really necessary if I’m just going to publish my game on Steam? Actually, I’m not 100% sure, but I think it is only “recommended” and Steam can bypass the notarization. Steamworks does contain a checkbox asking if App Bundles Are Notarized, so I assume it might do something.
Talking to the press is an inevitable part of marketing a game or software. To make the journalist’s job easier, it’s a good idea to put together a press kit. The press kit should contain all the information someone could want to write an article about your product, as well as downloadable, high-resolution assets. Dice'n Goblins Introducing Milou Milou is a NodeJS software that generates press kits in the form of static websites. It aims at creating beautiful, fast, and responsive press kits, using only YAML configuration files. I built it on top of presskit.html, which solved the same problem but isn’t actively maintained at the moment. Milou improves on its foundation by using a more modern CSS, YAML instead of XML, and up-to-date Javascript code. Installation First, you will need to have NodeJS installed: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 22 Once Node is ready, you can use NPM to install Milou: npm install -g milou Running milou -V should display its version (currently 1.1.1). Let’s build a press kit Let’s create a new project: mkdir mypresskit cd mypresskit milou new The root directory of your project will be used for your company. In this directory, the file data.yml should contain data about your company, such as your company name, location, website, etc… You can find an example of a fully completed company data.yml file on GitHub. To validate that your file is a valid YAML file, you can use an online validator. Your company directory should contain a sub-folder called images, you should put illustrations you want to appear in your press kit inside it. Any file named header.*** will be used as the page header, favicon.ico will be used as the page favicon, and files prefixed by the word logo will appear in a dedicated logo section of the page (eg. logo01.png or logo.jpg). Other images you put in this folder will be included in your page, in the Images section. After completing your company page, we can create a product page. This will be done in a subfolder: mkdir myproduct cd myproduct milou new -t product Just like for a company, you should fill in the data.yml file with info about your product, like its title, features, and prices. You can find an example of a product file on GitHub. The product folder should also contain an images subfolder. It works the same way as for the company. When your product is ready, go back to the company folder and build the press kit: cd ../ milou build . This will generate the HTML and CSS files for your online presskit in the directory build. You can then use any web server to access them. For example, this will make them accessible from http://localhost:3000/ cd build npx serve To put your press kit online, you can upload this folder to any static site host, like CloudFlare Pages, Netlify, or your own server running Nginx. Conclusion Milou is still quite new, and if you encounter issues while using it, don’t hesitate to open an issue. And if it works perfectly for you, leave a star on GitHub.
In 1945, mathematician George Pólya released the book “How to solve it”. It aims at helping math teachers guide their students into solving abstract problems by asking the right questions. It has since had a large influence on math education and computer science, to the point of being mentioned by Marvin Minksy as a book that everyone should know. In this post, I will try to see how we can use Pólya’s methodology to solve a concrete software engineering problem: rendering 3D objects. Understanding the problem Before starting to solve the problem, we must make sure that we completely understand the problem. The first question to ask ourselves is What is the data? The data is a 3D object. The object is made out of triangles. Each triangle is made out of 3 vertices (and a normal vector). Those objects are stored in .STL files, but I will not cover the parsing of those files in this article, and will rely on the hschendel/stl lib instead. The second question, which is probably the most important is What is the unknown?. Or in programming terms, What should the program output? Our program should output an image. An image is a 2D matrix of pixels, each pixel representing a color. The most common way of representing color is the RGBA model, which stands for Red, Green, Blue, and Alpha. In Golang, images can be represented using the image.Image data structure from the standard library. The third question is What is the condition (linking the data to the output)? The data gives us information about the space occupied by our 3D object. If the 3D object is in front of our pixel, this pixel should be in a different color. We will use the method known as “raycasting” which consists of sending a ray from each pixel, and checking what the ray hits. Devise a plan Now that we have understood our problem a little bit better, we should try to plan what our solution will look like. The most helpful question to come up with a solution is Do you know a related problem? For raycasting, a related problem would be Does a vector intersect with a triangle? To solve this we can implement the Möller–Trumbore intersection algorithm. This algorithm transforms the above problem into two new questions Does the ray intersect with the triangle’s plane? and if yes, Does the ray-plane intersection lie outside the triangle? This first question is simple to solve, the only way a vector doesn’t intersect with a plane is if the vector and plane are parallel. In that case, the dot product of the ray and the triangle’s normal vector would be zero, since the dot product of two perpendicular vectors is 0 and the normal vector is itself perpendicular to the triangle’s plane. If the ray intersects with our triangle’s plane, then we can check if the intersection is inside the plane by switching to barycentric coordinates. Barycentric coordinates are a way to represent a point in a plane in relation to the vertices of the triangle. Each corner of the triangle will get the coordinates (0,0,1), (0,1,0) and (1,0,0). Any point outside of the triangle will get coordinates outside of the range [0,1]. Now that we know an algorithm that can solve our main issue, we can come up with the outline of our program: func MTintersect(ray, triangle) bool { if isParallel(ray, triangle) { return false } u , v := projectBaryocentric(vec3, triangle) return u > 0 && u < 1 && v > 0 && u + v < 1 } func main () { solid := readSTL() image := newImage(width, height) for i := range width { for j := range height { image.Set(i, j, white) ray := castRay(i, j) for triangle := range solid.Triangles { ok := MTintersect(ray, triangle) if ok { image.set(i, j, blue) } } } } writePNG(image) } Carrying out the plan This is the easy part. We just write the code. The main suggestion that Pólya makes, is to check that every step of the solution is correct. While programming, this can be achieved by writing unit tests to ensure the correctness of our code. Looking back Once we have something that seems to work it is tempting to just git push and call it a day. But there are a few more questions we should ask ourselves. First Can we check the result? A good way to answer that is to test our program ourselves, either by manually going through a checklist or by writing an integration test that covers our problem. Then we should ask ourselves Can we derive the result differently? This question is not only a good way to learn about other ways to solve our problem (like Scanline rendering in our case) but also a good opportunity to check if maybe the code we wrote was not the most intuitive solution and could be refactored. The last question is Can you use the result for another problem? We can answer this question by checking if our code is written in a way that is reusable enough if we ever want to. For example, the raycaster above could be used as the first step into the implementation of a more sophisticated ray tracing algorithm, if we wanted to handle reflections and lightning. Conclusion If you want to check the source code for the raycaster I made before writing this article, it is on my GitHub. You can find How to solve it by Pólya in any good library. To learn more about computer graphics check out Ray Tracing in a weekend. And for the details of the Möller-Trumbore algorithm, this video is the one that made the most sense to me.
More in programming
I was chatting with a friend recently, and she mentioned an annoyance when reading fanfiction on her iPad. She downloads fic from AO3 as EPUB files, and reads it in the Kindle app – but the files don’t have a cover image, and so the preview thumbnails aren’t very readable: She’s downloaded several hundred stories, and these thumbnails make it difficult to find things in the app’s “collections” view. This felt like a solvable problem. There are tools to add cover images to EPUB files, if you already have the image. The EPUB file embeds some key metadata, like the title and author. What if you had a tool that could extract that metadata, auto-generate an image, and use it as the cover? So I built that. It’s a small site where you upload EPUB files you’ve downloaded from AO3, the site generates a cover image based on the metadata, and it gives you an updated EPUB to download. The new covers show the title and author in large text on a coloured background, so they’re much easier to browse in the Kindle app: If you’d find this helpful, you can use it at alexwlchan.net/my-tools/add-cover-to-ao3-epubs/ Otherwise, I’m going to explain how it works, and what I learnt from building it. There are three steps to this process: Open the existing EPUB to get the title and author Generate an image based on that metadata Modify the EPUB to insert the new cover image Let’s go through them in turn. Open the existing EPUB I’ve not worked with EPUB before, and I don’t know much about it. My first instinct was to look for Python EPUB libraries on PyPI, but there was nothing appealing. The results were either very specific tools (convert EPUB to/from format X) or very unmaintained (the top result was last updated in April 2014). I decied to try writing my own code to manipulate EPUBs, rather than using somebody else’s library. I had a vague memory that EPUB files are zips, so I changed the extension from .epub to .zip and tried unzipping one – and it turns out that yes, it is a zip file, and the internal structure is fairly simple. I found a file called content.opf which contains metadata as XML, including the title and author I’m looking for: <?xml version='1.0' encoding='utf-8'?> <package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="uuid_id"> <metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"> <dc:title>Operation Cameo</dc:title> <meta name="calibre:timestamp" content="2025-01-25T18:01:43.253715+00:00"/> <dc:language>en</dc:language> <dc:creator opf:file-as="alexwlchan" opf:role="aut">alexwlchan</dc:creator> <dc:identifier id="uuid_id" opf:scheme="uuid">13385d97-35a1-4e72-830b-9757916d38a7</dc:identifier> <meta name="calibre:title_sort" content="operation cameo"/> <dc:description><p>Some unusual orders arrive at Operation Mincemeat HQ.</p></dc:description> <dc:publisher>Archive of Our Own</dc:publisher> <dc:subject>Fanworks</dc:subject> <dc:subject>General Audiences</dc:subject> <dc:subject>Operation Mincemeat: A New Musical - SpitLip</dc:subject> <dc:subject>No Archive Warnings Apply</dc:subject> <dc:date>2023-12-14T00:00:00+00:00</dc:date> </metadata> … That dc: prefix was instantly familiar from my time working at Wellcome Collection – this is Dublin Core, a standard set of metadata fields used to describe books and other objects. I’m unsurprised to see it in an EPUB; this is exactly how I’d expect it to be used. I found an article that explains the structure of an EPUB file, which told me that I can find the content.opf file by looking at the root-path element inside the mandatory META-INF/container.xml file which is every EPUB. I wrote some code to find the content.opf file, then a few XPath expressions to extract the key fields, and I had the metadata I needed. Generate a cover image I sketched a simple cover design which shows the title and author. I wrote the first version of the drawing code in Pillow, because that’s what I’m familiar with. It was fine, but the code was quite flimsy – it didn’t wrap properly for long titles, and I couldn’t get custom fonts to work. Later I rewrote the app in JavaScript, so I had access to the HTML canvas element. This is another tool that I haven’t worked with before, so a fun chance to learn something new. The API felt fairly familiar, similar to other APIs I’ve used to build HTML elements. This time I did implement some line wrapping – there’s a measureText() API for canvas, so you can see how much space text will take up before you draw it. I break the text into words, and keeping adding words to a line until measureText tells me the line is going to overflow the page. I have lots of ideas for how I could improve the line wrapping, but it’s good enough for now. I was also able to get fonts working, so I picked Georgia to match the font used for titles on AO3. Here are some examples: I had several ideas for choosing the background colour. I’m trying to help my friend browse her collection of fic, and colour would be a useful way to distinguish things – so how do I use it? I realised I could get the fandom from the EPUB file, so I decided to use that. I use the fandom name as a seed to a random number generator, then I pick a random colour. This means that all the fics in the same fandom will get the same colour – for example, all the Star Wars stories are a shade of red, while Star Trek are a bluey-green. This was a bit harder than I expected, because it turns out that JavaScript doesn’t have a built-in seeded random number generator – I ended up using some snippets from a Stack Overflow answer, where bryc has written several pseudorandom number generators in plain JavaScript. I didn’t realise until later, but I designed something similar to the placeholder book covers in the Apple Books app. I don’t use Apple Books that often so it wasn’t a deliberate choice to mimic this style, but clearly it was somewhere in my subconscious. One difference is that Apple’s app seems to be picking from a small selection of background colours, whereas my code can pick a much nicer variety of colours. Apple’s choices will have been pre-approved by a designer to look good, but I think mine is more fun. Add the cover image to the EPUB My first attempt to add a cover image used pandoc: pandoc input.epub --output output.epub --epub-cover-image cover.jpeg This approach was no good: although it added the cover image, it destroyed the formatting in the rest of the EPUB. This made it easier to find the fic, but harder to read once you’d found it. An EPUB file I downloaded from AO3, before/after it was processed by pandoc. So I tried to do it myself, and it turned out to be quite easy! I unzipped another EPUB which already had a cover image. I found the cover image in OPS/images/cover.jpg, and then I looked for references to it in content.opf. I found two elements that referred to cover images: <?xml version="1.0" encoding="UTF-8"?> <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="PrimaryID"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <meta name="cover" content="cover-image"/> … </metadata> <manifest> <item id="cover-image" href="images/cover.jpg" media-type="image/jpeg" properties="cover-image"/> … </manifest> </package> This gave me the steps for adding a cover image to an EPUB file: add the image file to the zipped bundle, then add these two elements to the content.opf. Where am I going to deploy this? I wrote the initial prototype of this in Python, because that’s the language I’m most familiar with. Python has all the libraries I need: The zipfile module can unpack and modify the EPUB/ZIP The xml.etree or lxml modules can manipulate XML The Pillow library can generate images I built a small Flask web app: you upload the EPUB to my server, my server does some processing, and sends the EPUB back to you. But for such a simple app, do I need a server? I tried rebuilding it as a static web page, doing all the processing in client-side JavaScript. That’s simpler for me to host, and it doesn’t involve a round-trip to my server. That has lots of other benefits – it’s faster, less of a privacy risk, and doesn’t require a persistent connection. I love static websites, so can they do this? Yes! I just had to find a different set of libraries: The JSZip library can unpack and modify the EPUB/ZIP, and is the only third-party code I’m using in the tool Browsers include DOMParser for manipulating XML I’ve already mentioned the HTML <canvas> element for rendering the image This took a bit longer because I’m not as familiar with JavaScript, but I got it working. As a bonus, this makes the tool very portable. Everything is bundled into a single HTML file, so if you download that file, you have the whole tool. If my friend finds this tool useful, she can save the file and keep a local copy of it – she doesn’t have to rely on my website to keep using it. What should it look like? My first design was very “engineer brain” – I just put the basic controls on the page. It was fine, but it wasn’t good. That might be okay, because the only person I need to be able to use this app is my friend – but wouldn’t it be nice if other people were able to use it? If they’re going to do that, they need to know what it is – most people aren’t going to read a 2,500 word blog post to understand a tool they’ve never heard of. (Although if you have read this far, I appreciate you!) I started designing a proper page, including some explanations and descriptions of what the tool is doing. I got something that felt pretty good, including FAQs and acknowledgements, and I added a grey area for the part where you actually upload and download your EPUBs, to draw the user’s eye and make it clear this is the important stuff. But even with that design, something was missing. I realised I was telling you I’d create covers, but not showing you what they’d look like. Aha! I sat down and made up a bunch of amusing titles for fanfic and fanfic authors, so now you see a sample of the covers before you upload your first EPUB: This makes it clearer what the app will do, and was a fun way to wrap up the project. What did I learn from this project? Don’t be scared of new file formats My first instinct was to look for a third-party library that could handle the “complexity” of EPUB files. In hindsight, I’m glad I didn’t find one – it forced me to learn more about how EPUBs work, and I realised I could write my own code using built-in libraries. EPUB files are essentially ZIP files, and I only had basic needs. I was able to write my own code. Because I didn’t rely on a library, now I know more about EPUBs, I have code that’s simpler and easier for me to understand, and I don’t have a dependency that may cause problems later. There are definitely some file formats where I need existing libraries (I’m not going to write my own JPEG parser, for example) – but I should be more open to writing my own code, and not jumping to add a dependency. Static websites can handle complex file manipulations I love static websites and I’ve used them for a lot of tasks, but mostly read-only display of information – not anything more complex or interactive. But modern JavaScript is very capable, and you can do a lot of things with it. Static pages aren’t just for static data. One of the first things I made that got popular was find untagged Tumblr posts, which was built as a static website because that’s all I knew how to build at the time. Somewhere in the intervening years, I forgot just how powerful static sites can be. I want to build more tools this way. Async JavaScript calls require careful handling The JSZip library I’m using has a lot of async functions, and this is my first time using async JavaScript. I got caught out several times, because I forgot to wait for async calls to finish properly. For example, I’m using canvas.toBlob to render the image, which is an async function. I wasn’t waiting for it to finish, and so the zip would be repackaged before the cover image was ready to add, and I got an EPUB with a missing image. Oops. I think I’ll always prefer the simplicity of synchronous code, but I’m sure I’ll get better at async JavaScript with practice. Final thoughts I know my friend will find this helpful, and that feels great. Writing software that’s designed for one person is my favourite software to write. It’s not hyper-scale, it won’t launch the next big startup, and it’s usually not breaking new technical ground – but it is useful. I can see how I’m making somebody’s life better, and isn’t that what computers are for? If other people like it, that’s a nice bonus, but I’m really thinking about that one person. Normally the one person I’m writing software for is me, so it’s extra nice when I can do it for somebody else. If you want to try this tool yourself, go to alexwlchan.net/my-tools/add-cover-to-ao3-epubs/ If you want to read the code, it’s all on GitHub. [If the formatting of this post looks odd in your feed reader, visit the original article]
I’ve been doing Dry January this year. One thing I missed was something for apéro hour, a beverage to mark the start of the evening. Something complex and maybe bitter, not like a drink you’d have with lunch. I found some good options. Ghia sodas are my favorite. Ghia is an NA apéritif based on grape juice but with enough bitterness (gentian) and sourness (yuzu) to be interesting. You can buy a bottle and mix it with soda yourself but I like the little cans with extra flavoring. The Ginger and the Sumac & Chili are both great. Another thing I like are low-sugar fancy soda pops. Not diet drinks, they still have a little sugar, but typically 50 calories a can. De La Calle Tepache is my favorite. Fermented pineapple is delicious and they have some fun flavors. Culture Pop is also good. A friend gave me the Zero book, a drinks cookbook from the fancy restaurant Alinea. This book is a little aspirational but the recipes are doable, it’s just a lot of labor. Very fancy high end drink mixing, really beautiful flavor ideas. The only thing I made was their gin substitute (mostly junipers extracted in glycerin) and it was too sweet for me. Need to find the right use for it, a martini definitely ain’t it. An easier homemade drink is this Nonalcoholic Dirty Lemon Tonic. It’s basically a lemonade heavily flavored with salted preserved lemons, then mixed with tonic. I love the complexity and freshness of this drink and enjoy it on its own merits. Finally, non-alcoholic beer has gotten a lot better in the last few years thanks to manufacturing innovations. I’ve been enjoying NA Black Butte Porter, Stella Artois 0.0, Heineken 0.0. They basically all taste just like their alcoholic uncles, no compromise. One thing to note about non-alcoholic substitutes is they are not cheap. They’ve become a big high end business. Expect to pay the same for an NA drink as one with alcohol even though they aren’t taxed nearly as much.
The first time we had to evacuate Malibu this season was during the Franklin fire in early December. We went to bed with our bags packed, thinking they'd probably get it under control. But by 2am, the roaring blades of fire choppers shaking the house got us up. As we sped down the canyon towards Pacific Coast Highway (PCH), the fire had reached the ridge across from ours, and flames were blazing large out the car windows. It felt like we had left the evacuation a little too late, but they eventually did get Franklin under control before it reached us. Humans have a strange relationship with risk and disasters. We're so prone to wishful thinking and bad pattern matching. I remember people being shocked when the flames jumped the PCH during the Woolsey fire in 2017. IT HAD NEVER DONE THAT! So several friends of ours had to suddenly escape a nightmare scenario, driving through burning streets, in heavy smoke, with literally their lives on the line. Because the past had failed to predict the future. I feel into that same trap for a moment with the dramatic proclamations of wind and fire weather in the days leading up to January 7. Warning after warning of "extremely dangerous, life-threatening wind" coming from the City of Malibu, and that overly-bureaucratic-but-still-ominous "Particularly Dangerous Situation" designation. Because, really, how much worse could it be? Turns out, a lot. It was a little before noon on the 7th when we first saw the big plumes of smoke rise from the Palisades fire. And immediately the pattern matching ran astray. Oh, it's probably just like Franklin. It's not big yet, they'll get it out. They usually do. Well, they didn't. By the late afternoon, we had once more packed our bags, and by then it was also clear that things actually were different this time. Different worse. Different enough that even Santa Monica didn't feel like it was assured to be safe. So we headed far North, to be sure that we wouldn't have to evacuate again. Turned out to be a good move. Because by now, into the evening, few people in the connected world hadn't started to see the catastrophic images emerging from the Palisades and Eaton fires. Well over 10,000 houses would ultimately burn. Entire neighborhoods leveled. Pictures that could be mistaken for World War II. Utter and complete destruction. By the night of the 7th, the fire reached our canyon, and it tore through the chaparral and brush that'd been building since the last big fire that area saw in 1993. Out of some 150 houses in our immediate vicinity, nearly a hundred burned to the ground. Including the first house we moved to in Malibu back in 2009. But thankfully not ours. That's of course a huge relief. This was and is our Malibu Dream House. The site of that gorgeous home office I'm so fond to share views from. Our home. But a house left standing in a disaster zone is still a disaster. The flames reached all the way up to the base of our construction, incinerated much of our landscaping, and devoured the power poles around it to dysfunction. We have burnt-out buildings every which way the eye looks. The national guard is still stationed at road blocks on the access roads. Utility workers are tearing down the entire power grid to rebuild it from scratch. It's going to be a long time before this is comfortably habitable again. So we left. That in itself feels like defeat. There's an urge to stay put, and to help, in whatever helpless ways you can. But with three school-age children who've already missed over a months worth of learning from power outages, fire threats, actual fires, and now mudslide dangers, it was time to go. None of this came as a surprise, mind you. After Woolsey in 2017, Malibu life always felt like living on borrowed time to us. We knew it, even accepted it. Beautiful enough to be worth the risk, we said. But even if it wasn't a surprise, it's still a shock. The sheer devastation, especially in the Palisades, went far beyond our normal range of comprehension. Bounded, as it always is, by past experiences. Thus, we find ourselves back in Copenhagen. A safe haven for calamities of all sorts. We lived here for three years during the pandemic, so it just made sense to use it for refuge once more. The kids' old international school accepted them right back in, and past friendships were quickly rebooted. I don't know how long it's going to be this time. And that's an odd feeling to have, just as America has been turning a corner, and just as the optimism is back in so many areas. Of the twenty years I've spent in America, this feels like the most exciting time to be part of the exceptionalism that the US of A offers. And of course we still are. I'll still be in the US all the time on both business, racing, and family trips. But it won't be exclusively so for a while, and it won't be from our Malibu Dream House. And that burns.
Thou shalt not suffer a flaky test to live, because it’s annoying, counterproductive, and dangerous: one day it might fail for real, and you won’t notice. Here’s what to do.
The ware for January 2025 is shown below. Thanks to brimdavis for contributing this ware! …back in the day when you would get wares that had “blue wires” in them… One thing I wonder about this ware is…where are the ROMs? Perhaps I’ll find out soon! Happy year of the snake!