How to convert SVG icons into an icon font

In this post we'll go over how to combine or convert SVG files into an icon font file. In short, we'll git clone a repository svgtofont, modify src/cli.js and then run a node script to generate the font.

svgtofont

We'll use the library svgtofont to combine all the SVGs into a font file. It supports generating eot, ttf, woff, woff2 and svg files. Svgtofont uses other projects such as svgicons2svgfont and svg2ttf.

Cloning the repository

Command to clone it and set it up locally in your terminal: git clone https://github.com/jaywcjlove/svgtofont.git

Generating the icon files

Add two folders in the newly cloned svgtofont folder: icons and output. "icons" is the folder where you will put all the svg files.

Modify existing src/cli.ts file by adding the website property roughly on line 46. website: { ... }, full example below: Change your-font-name to what you want to call the font.


svgtofont({
  src: sourcesPath, // svg path
  dist: outputPath, // output path
  // emptyDist: true, // Clear output directory contents
  fontName: (argv.fontName) || "svgfont", // font name
  css: true, // Create CSS files.
  website: {
    description: 'your-font-name',
    logo: 'no-logo',
    links: [],
  },
  outSVGReact: true,
  outSVGPath: true,
  svgicons2svgfont: {
    fontHeight: 1000,
    normalize: true,
  },
})

The output

Run the converter with a node script node src/cli.js --sources ./icons --output ./output --fontName your-font-name in your terminal. If it doesn't create the files in output folder then check that the SVG files are valid. I had invalid SVG files and scratched my head wondering why the library did not work.

The generated icon css classes follow the naming convention of your-font-name-{svg name}. For example for muumi.svg it would be your-font-name-muumi. You can check all the generated icons in output/index.html. That's it, now add the css and font files to you project and create elements like <i class="icon your-font-name-muumi"></i>

Easily download all DALL-E prompt images with a button

There is currently no way (September 2022) to download all images for DALL-E at https://labs.openai.com/. Which made me write a quick and dirty script to download all images for the current prompt or for all of the prompts. If you need such functionality then you can copy-paste the code and paste it into the browser's developer tools (F12 as default). After pasting the code it should add two buttons in the header that you can click.
download buttons in header

Some notes:

  • It helps a lot to disable "Ask where to save each file before downloading" as otherwise you'll be spammed with prompts to download the files. And to change the download folder as it's 200 images for all the items.
    In chrome: Settings -> Downloads -> Uncheck the toggle button.
  • Since the items in the list to the right are lazy loaded it's good to scroll all the way to the bottom if downloading all the images.
  • Because this script uses timings it might fail to find the download buttons, and it'll then log the item in the list to the right, so you can mouseover the item, and it should highlight the element. You can then click the current prompt button for those items.
  • This searches for the text "Download" to find the download button. If DALL-E has localization you will have to change the DOWNLOAD for this line of code to what it is for you.
    if (item.textContent.toUpperCase().includes('DOWNLOAD')) {

The code to copy paste into developer tools:


/**
 * Get the items in the right panel and download for each item.
 */
async function downloadAllImages() {
  var items = document.querySelectorAll('.hist-task-link');
  for (let i = 0; i < items.length; i++) {
    const item = items.item(i);
    item.click();
    await awaitTimeout(2000);
    await downloadImagesForCurrent(item);
    await awaitTimeout(10);
  }
}

/**
 * Download all the items for an item in the right side panel.
 * Since the items are lazy loaded it helps to scroll all the way to the bottom first so all the items are loaded.
 */
function downloadImagesForCurrent(listItem) {
  const promise = new Promise(async (res) => {
    const extraButtons = document.querySelectorAll('.task-page-quick-actions-button');
    for (let i = 0; i < extraButtons.length; i++) {
      await downloadImage(extraButtons.item(i), listItem);
      await awaitTimeout(100);
    }

    res();
  });

  return promise;
}

/**
 * Download an image by clicking the ... and then the download button that appears.
 * It helps to disable "Ask where to save each file before downloading", eg in chrome:  Settings -> Downloads -> Uncheck that toggle button.
 */
async function downloadImage(extraButton, listItem) {
  extraButton.click();
  await awaitTimeout(50);
  const downloadButton = getLastDownloadButton();
  if (!downloadButton) {
    console.log('Could not find download button for', listItem);
    return;
  }

  downloadButton.click();
}

/**
 * We need to get the last download button because there might be multiple open at once.
 */
function getLastDownloadButton() {
  const selectableItems = document.querySelectorAll('.menu-item-selectable');
  for (let i = selectableItems.length - 1; i >= 0; i--) {
    const item = selectableItems.item(i);
    if (item.textContent.toUpperCase().includes('DOWNLOAD')) {
      return item;
    }
  }
}

/**
 * Adds an awaitable setTimeout since we need to wait for buttons.
 */
function awaitTimeout(delay) {
  return new Promise(res => setTimeout(() => res(), delay));
}

!function() {
  const header = document.querySelector('.app-header-contents');
  if (!header) {
    console.log('Could not find header element to add buttons, you need to use code manually.')
    console.log('downloadAllImages() - to download all images');
    console.log('downloadImagesForCurrent() - to download current prompt');
    return;
  }

  const downloadAllButton = document.createElement('button');
  downloadAllButton.innerText = 'Download All';
  downloadAllButton.onclick = downloadAllImages;

  const downloadCurrent = document.createElement('button');
  downloadCurrent.innerText = 'Download Current Prompt';
  downloadCurrent.onclick = downloadImagesForCurrent;

  header.appendChild(downloadAllButton);
  header.appendChild(downloadCurrent);
}();

Making a FFXIV ACT Chat extractor

This is about the making of Nuusie's Final Fantasy XIV Chat Extractor for the ACT Plugin.. And some of the obstacles encountered along the way. Some of the obstacles were custom colours, manipulating text to look like an emote, *Luna is writing a blog post*, quotation marks.
The chat extractor can be found here: https://github.com/saaratrix/nuu-ffxiv-act-chat-extractor

Introduction

The idea came from a friend who wanted to extract their stored chat logs and the tools she found was not as trustworthy to her as something made by me. For example this tool was hosted through a web server and she didn't want to upload her chat logs there. https://github.com/ErstonGreatman/ffxiv-act-chat-extractor.

Which lead to the first obstacle that I wanted to design the tool so you can run it locally and I wanted it to be lightweight. So that if someone does look through the code they can hopefully easily see that it just does what's expected. You could add the script and css bundles inside the html page so that it's still 1 single page but the file wouldn't look as readable. Even something smaller like Svelte still has 23 kb of minified javascript.

Developing everything in 1 file meant that I started using comments to try and separate things into sections. For example like this:


// ******************
// A section!
// ******************

Angular E2E Protractor Introduction

This is an introduction to Angular E2E tests with two examples using Angular 10. The first example is the default test generated by the Angular 10 CLI and the second test does the following:

  1. Logs in
  2. Creates an item
  3. Modifies the item
  4. Deletes the item
  5. Logs out

Angular uses Protractor to do the e2e tests. If you generate a project with the Angular CLI it's very simple to run your e2e tests by simply running ng e2e.

Source code can be found at https://github.com/saaratrix/angular-e2e-example

Debugging a Blender Add-on in PyCharm

This is a guide on how to set up debugging in a Blender Add-on version 2.80 or higher using PyCharm. I used this guide: https://code.blender.org/2015/10/debugging-python-code-with-pycharm/ to set up debugging.
Important: You need Pycharm Professional for debugging Blender because it requires the remote debugging feature which isn't available in community edition in 2019 or lower.

Create a Blender Add-on project with PyCharm

We'll be setting up a fresh Blender Add-on project with PyCharm that works with source control. This guide was written for Blender 2.80 but it works the same in later versions as of May 2024.

I have made a boilerplate template for Blender 4.x, 3.x that you can copy and use as a base. https://github.com/saaratrix/empty-blender-add-on-template .

How to add user preferences for a Blender Add-on

When trying to implement user preferences for a 2.80 Blender add-on the official documentation was confusing for me and it didn't work with their example code. Their comment on what to set bl_idname as was the confusing part. Probably because of my Python inexperience.
https://docs.blender.org/api/current/bpy.types.AddonPreferences.html

After some searching I found this article that helped me understand what I had done wrong and make it work.
https://b3d.interplanety.org/en/add-on-preferences-panel/

Example code with preferences in own file

The code to get add-on preferences in Blender 2.80 in its own file was this code below. bl_idname should be __name__ if the class is inside __init__.py otherwise __package__.


import bpy


class EXAMPLE_addonPreferences(bpy.types.AddonPreferences):
    bl_idname = __package__

    # Code below copied from https://b3d.interplanety.org/en/add-on-preferences-panel/
    add_bevel: bpy.props.EnumProperty(
        items=[
            ('bevel', 'Add bevel', '', '', 0),
            ('no_bevel', 'No bevel', '', '', 1)
        ],
        default='no_bevel'
    )

    def draw(self, context):
        layout = self.layout
        layout.label(text='Add bevel modifier:')
        row = layout.row()
        row.prop(self, 'add_bevel', expand=True)


Here is an image of the add-on preferences UI from the code above.