Jan Peša
Jan Peša

Reputation: 6970

Converting and rendering web fonts to base64 - keep original look

I want to defer font loading on my site inspired by deferred font loading logic for Smashing Magazine.

Main part of this is converting fonts to base64 and preparing your CSS file. My steps so far:

  1. Pick fonts on Google Web Fonts and download them.
  2. Use Font Squirrel Webfont Generator to convert downloaded TTF files to CSS file with base64 embedded WOFF fonts (Expert options -> CSS -> Base64 Encode).
  3. Load CSS file async (not important here).

CSS snippet for Open Sans Bold:

@font-face {
  font-family: 'Open Sans';
  src: url(data:application/x-font-woff;charset=utf-8;base64,<base64_encoded>) format('woff');
  font-weight: 700;
  font-style: normal;
}

The problem is, that converted fonts look a lot different. Take a look at Open Sans Bold: GWF vs base64 rendering comparison

Especially notice accents being way off and absolutely horrible letter a. Other font families and variants look very noticeably different as well (size and shape distortions, etc.).


So the question is: How do you properly encode TTF files from Google Web Fonts (or other source) to base64 format and use it in a way that the result is identical to the original file?

Upvotes: 120

Views: 154830

Answers (5)

Noel Ruault
Noel Ruault

Reputation: 139

This bash script effortlessly converts .otf, .ttf, .woff, and .woff2 font files to its css-base64 @font-face version, ensuring they stay as gorgeous in your CSS as they do in the wild.

Why It's Awesome:

  • Supports Multiple Formats: .otf, .ttf, .woff and .woff2 (can be extended)
  • True to Original: Keeps your fonts looking exactly as intended.
  • Flexible Output: Generate CSS files or copy @font-face code directly to your clipboard.
  • Easy-Peasy: User-friendly prompts make it a cakewalk.

Quick Start:

Code:

#!/bin/bash

# Initialize a variable to hold all @font-face codes, used when choosing the clipboard
all_font_face_codes=""

FONT_DIR="."
OUTPUT_DIR="./css"
SUPPORTED_EXTENSIONS=("otf" "ttf" "woff" "woff2")

usage() {
    echo "This script is designed to generate CSS files or copy @font-face code to the clipboard for font files in a specified directory."
    echo "Usage: $0 -i path/to/fonts -o path/to/output/css"
    exit 1
}

# Parse command-line arguments
while getopts "i:o:h" opt; do
    case ${opt} in
        i)
            FONT_DIR="${OPTARG}"
            ;;
        o)
            OUTPUT_DIR="${OPTARG}"
            ;;
        h|*)
            usage
            ;;
    esac
done

# Prompt user for action: create CSS files or copy to clipboard
options=("Create CSS files" "Copy the @font-face code to the clipboard")
select opt in "${options[@]}"; do
    case $opt in
        "Create CSS files")
            user_choice=1
            break
            ;;
        "Copy the @font-face code to the clipboard")
            user_choice=2
            break
            ;;
        *)
            echo "Invalid choice. Please enter 1 or 2."
            ;;
    esac
done

mkdir -p $OUTPUT_DIR

# Function to generate @font-face code
generate_font_face_code() {
    local font_filename="$1"
    local font_name
    font_name="${font##*/}"      # Extract filename from path
    font_name="${font_name%.*}"  # Remove extension

    local font_format

    # Check if base64 supports the -w or -b options
    if base64 -w 0 < /dev/null &>/dev/null; then
        base64_string=$(base64 -w 0 < "$font_filename")
    elif base64 -b 0 < /dev/null &>/dev/null; then
        base64_string=$(base64 -b 0 < "$font_filename")
    else
        echo "The base64 command on your system does not support the required options."
        exit 1
    fi

    font_filename_lowercase=$(echo "$font_filename" | tr '[:upper:]' '[:lower:]')
    case "$font_filename_lowercase" in
        *.ttf)
            font_format='truetype'
            ;;
        *.otf)
            font_format='opentype'
            ;;
        *.woff)
            font_format='woff'
            ;;
        *.woff2)
            font_format='woff2'
            ;;
    esac

    echo "@font-face {
        font-family: '$font_name';
        /* Remember to add font-weight, font-style, etc. if needed */
        src: url(data:application/$font_format;charset=utf-8;base64,$base64_string) format('$font_format');
    }"
}

# Function to copy to clipboard
copy_to_clipboard() {
    local data=$1

    if command -v pbcopy &>/dev/null; then
        echo "$data" | pbcopy
    elif command -v xclip &>/dev/null; then
        echo "$data" | xclip -selection clipboard
    else
        echo "Clipboard support is not available on your OS."
        exit 1
    fi
}

# Loop through each file in the directory and target supported font extensions
for file in "$FONT_DIR"/*; do
    # Extract the file extension in lowercase
    ext=$(echo "${file##*.}" | tr '[:upper:]' '[:lower:]')

    # Check if the file extension is supported
    if [[ " ${SUPPORTED_EXTENSIONS[@]} " =~ " ${ext} " ]]; then
        font="$file"
        font_face_code=$(generate_font_face_code "$font")

        case $user_choice in
            1)
                # User chose to create CSS files
                # Remove extension from filename and append .css
                css_filename="${OUTPUT_DIR}/${file%.*}.css"
                echo "$font_face_code" >> "$css_filename"
                echo "Generated CSS for ${file%.*}"
                ;;
            2)
                # User chose to copy to clipboard
                all_font_face_codes+="$font_face_code"$'\n'
                echo "Generated CSS source for ${file%.*}"
                ;;
        esac
    fi
done

# Output message based on user choice
if [ $user_choice -eq 1 ]; then
    echo "All fonts processed."
    echo "CSS files generated in $OUTPUT_DIR"
elif [ $user_choice -eq 2 ]; then
    copy_to_clipboard "$all_font_face_codes"
    echo "All @font-face codes copied to clipboard."
fi

Example usage

$ ./font_encoder.sh # [-h -i -o]

Try it out and watch your fonts transform smoothly, happy coding! 🚀

Upvotes: 1

herrstrietzel
herrstrietzel

Reputation: 17115

In addition to the previous answers – some warnings:

Dedicated Font converters might drop features or change data

Generators/Converters like fontquirrel or transfonter will actually parse and rebuild your font file.

This process might also introduce changes due to optimization settings like hinting data that impacts font rendering.

2023: Many converters like fontsquirrel and transfonter don't support variable fonts

When using these tools to get a base64 data URL you might lose variable font features (design axes related data is stripped) when generating a data URL.

That doesn't mean, you shouldn't use these converters at all - they mostly work well if

  • you thoroughly check all conversion presets
  • you need static font support and need to subset the glyph range

A generic base64 converter won't manipulate any font data

As demonstrated by Ilyich: You can use any base64 encoder. (E.g browserlings converter.

Here's another JS helper example based on these steps:

  • fetch external CSS URL
  • find font URLs via regex
  • fetch all URLs as a blob()
  • convert blob to base64 data URL
  • replace external URLs with base64 data URLs

inputUrl.addEventListener("input", async(e) => {
  let url = e.currentTarget.value;
  let css = await getdataUrlsFromCss(url)
  // output and download button
  fontCss.value = css;
  btnDownload.href = URL.createObjectURL(new Blob([css]));
});

// init
inputUrl.dispatchEvent(new Event("input"));


async function getdataUrlsFromCss(url) {
  // fetch external css
  let css = await (await fetch(url)).text();

  // find external urls in css via regex
  let urls = css.match(/https:\/\/[^)]+/g);

  for (let i = 0; i < urls.length; i++) {
    let url = urls[i];

    // fetch font file
    let blob = await (await await fetch(url)).blob();

    // create base64 string
    let base64 = await blobToBase64(blob);

    //replace urls with data url
    css = css.replaceAll(url, base64);
  }

  return css;
}


/**
 * fetched blob to base64
 */
function blobToBase64(blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
}
body {
  font-family: sans-serif
}

legend {
  font-weight: bold;
}

fieldset {
  margin-bottom: 1em;
}

fieldset input,
fieldset textarea {
  border: none
}

input {
  width: 100%;
  display: block;
  margin-bottom: 1em;
}

textarea {
  width: 100%;
  min-height: 20em;
}

.btn-default {
  text-decoration: none;
  border: 1px solid #000;
  background: #ccc;
  color: #000;
  font-weight: bold;
  padding: 0.3em;
}
<h1>Fonts to base64</h1>
<fieldset>
  <legend>Enter CSS Url</legend>
  <input type="text" id="inputUrl" value="https://fonts.googleapis.com/css2?family=Open+Sans:ital@0;1&display=swap">
</fieldset>

<fieldset>
  <legend>New Css</legend>
  <textarea id="fontCss"></textarea>
  <p><a class="btn-default" id="btnDownload" href="#" download="fontface.css">Download css</a></p>
</fieldset>

For testing: Codepen example

Upvotes: 5

Mr Peter
Mr Peter

Reputation: 1

Simple Nodejs Script That works for .woff fonts. Just change the extension to your font files extension and it will work with other extensions as well

const { readdirSync, mkdir, existsSync, readFileSync, writeFileSync } = require("fs")
const { resolve } = require("path")

const woffDirPath=resolve(".", "public", "assets", "fonts", "Typold", "woff")
const files = readdirSync(woffDirPath)
const base64Path = resolve(".", "public", "assets", "fonts", "Typold", "base64")
if (!existsSync(base64Path)) mkdir(base64Path, (err) => {
    console.log("Error on dir creattion", err);
});

for (let file of files) {
    if (file.includes(".woff")) {
        const fileData = readFileSync(resolve(woffDirPath, file), { encoding: "base64" })
        writeFileSync(resolve(base64Path, file.replace(".woff", ".txt")), fileData)
    }
}
console.log("done");

Upvotes: 0

djangodude
djangodude

Reputation: 5680

In the Font Squirrel Expert options, make sure to set the 'TrueType Hinting' option to 'Keep Existing'. Either of the other options will cause the TrueType instructions (hints) to be modified, which will in turn affect the rendering of the font.

Alternatively, if you're happy with the rendering of the font directly from GWF, you can just take that file and do the base64 encoding yourself. In OS X or Linux, use the built-in base64 command in Terminal/shell:

$ base64 -i myfont.ttf -o fontbase64.txt

For Windows, you'll need to download a program to encode in base64 (there are several free/Open Source tools available). Copy the contents of that file, then use in your CSS as:

@font-face {
    font-family: 'myfont';
    src: url(data:font/truetype;charset=utf-8;base64,<<copied base64 string>>) format('truetype');
    font-weight: normal;
    font-style: normal;
}

(Note that you may need to make some adjustments to the various @font-face info to match your particular font data; this is just an example template)

Upvotes: 167

Ilyich
Ilyich

Reputation: 5776

Use this code snippet to base64 encode your font directly in the browser (OS independent, no need to install anything)

function base64convert (files) {
  console.clear()
  const reader = new FileReader()
  reader.onload = (e) => {
    console.log(e.target.result)
  }
  reader.readAsDataURL(files[0])
}
<input type="file" onchange="base64convert(this.files)">

Then copy the output and paste it into your CSS:

@font-face {
    font-family: 'myfont';
    src: url("<<copied base64 string>>");
}

Upvotes: 110

Related Questions