Skip to main content

Command Palette

Search for a command to run...

Font APEX Changes in APEX 26.1

Updated
6 min read
Font APEX Changes in APEX 26.1
M

With around 20 years on the job, Matt is one of the most experienced software developers at Pretius. He likes meeting new people, traveling to conferences, and working on different projects.

He’s also a big sports fan (regularly watches Leeds United, Formula 1, and boxing), and not just as a spectator – he often starts his days on a mountain bike, to tune his mind.

As we know APEX 26.1 was released to a huge reception & Font APEX 2.5 was announced as part of a line of of new features.

However, the first thing to be aware about is that APEX 26.1 wasn't actually launched with Font APEX 2.5, it was launched with version 2.5.1!

So, aside from not following standard Oracle versioning conversions, it was actually a higher release version number than announced.

But what is actually different between Font APEX 2.4 (released with APEX 24.2) and Font APEX 2.5.1 released with APEX 26.1?

To complicate matters further, there's actually a double jump here as Font APEX 2.5 was also included in the APEX 26.1 download. So lets investigate the changes from 2.4 ▶️ 2.5 ▶️ 2.5.1

Here to help is my little Python script to detect changes

Detect Font APEX Changes Python script
import sys
import re
import csv
from collections import defaultdict
from fontTools.ttLib import TTFont

def normalize_hex_codepoint(value): value = value.strip().lower() value = value.replace("\", "").replace("u+", "").replace("0x", "") return int(value, 16)

def parse_css_icon_map(css_path): with open(css_path, "r", encoding="utf-8") as f: css = f.read()

codepoint_to_classes = defaultdict(list)
class_to_codepoint = {}

rule_pattern = re.compile( r'([^{}]+){[^{}]content\s:\s*["']\([0-9a-fA-F]+)["']', re.IGNORECASE )

class_pattern = re.compile(r'.(fa-[-a-z0-9]+):(before|after)', re.IGNORECASE)

for selector_block, hex_value in rule_pattern.findall(css): cp = normalize_hex_codepoint(hex_value) classes = class_pattern.findall(selector_block)

for class_name, _ in classes:
    if class_name not in class_to_codepoint:
        class_to_codepoint[class_name] = cp
    if class_name not in codepoint_to_classes[cp]:
        codepoint_to_classes[cp].append(class_name)

return codepoint_to_classes, class_to_codepoint

def load_font(path): font = TTFont(path) cmap = font.getBestCmap() glyf = font["glyf"] return font, cmap, glyf

def glyph_signature(font, glyph_name): glyf_table = font["glyf"] glyph = glyf_table[glyph_name]

if glyph.isComposite():
    components = []
    for comp in glyph.components:
        components.append((
            comp.glyphName,
            tuple(comp.transform) if hasattr(comp, "transform") else None
        ))
    return ("composite", tuple(components))

end_pts = tuple(getattr(glyph, "endPtsOfContours", [])) flags = tuple(getattr(glyph, "flags", []))

coords = () if glyph.numberOfContours > 0: result = glyph.getCoordinates(glyf_table) coordinates = result[0] coords = tuple((int(x), int(y)) for x, y in coordinates)

return ("simple", glyph.numberOfContours, end_pts, flags, coords)

def compare_fonts(old_font_path, new_font_path, old_css_path, new_css_path, output_csv_path=None): old_cp_to_classes, old_class_to_cp = parse_css_icon_map(old_css_path) new_cp_to_classes, new_class_to_cp = parse_css_icon_map(new_css_path)

old_font, old_cmap, old_glyf = load_font(old_font_path)
new_font, new_cmap, new_glyf = load_font(new_font_path)

old_codepoints = set(old_cmap.keys()) new_codepoints = set(new_cmap.keys())

Build class-level sets for added/removed detection

old_classes = set(old_class_to_cp.keys()) new_classes = set(new_class_to_cp.keys())

results = []

--- ADDED: classes in new CSS but not in old CSS ---

for class_name in sorted(new_classes - old_classes): cp = new_class_to_cp[class_name] results.append({ "status": "NEW", "codepoint": f"U+{cp:04X}", "decimal": cp, "old_classes": "", "new_classes": class_name, })

--- REMOVED: classes in old CSS but not in new CSS ---

for class_name in sorted(old_classes - new_classes): cp = old_class_to_cp[class_name] results.append({ "status": "REMOVED", "codepoint": f"U+{cp:04X}", "decimal": cp, "old_classes": class_name, "new_classes": "", })

--- CHANGED: same codepoint in both fonts but different glyph shape ---

common_codepoints = sorted(old_codepoints & new_codepoints) for cp in common_codepoints: old_glyph_name = old_cmap[cp] new_glyph_name = new_cmap[cp]

if old_glyph_name not in old_glyf or new_glyph_name not in new_glyf:
    results.append({
        "status": "MISSING GLYPH",
        "codepoint": f"U+{cp:04X}",
        "decimal": cp,
        "old_classes": ", ".join(old_cp_to_classes.get(cp, [])),
        "new_classes": ", ".join(new_cp_to_classes.get(cp, [])),
    })
    continue

old_sig = glyph_signature(old_font, old_glyph_name) new_sig = glyph_signature(new_font, new_glyph_name)

if old_sig != new_sig: results.append({ "status": "SHAPE CHANGED", "codepoint": f"U+{cp:04X}", "decimal": cp, "old_classes": ", ".join(old_cp_to_classes.get(cp, [])), "new_classes": ", ".join(new_cp_to_classes.get(cp, [])), })

Sort: NEW first, then REMOVED, then SHAPE CHANGED

order = {"NEW": 0, "REMOVED": 1, "SHAPE CHANGED": 2, "MISSING GLYPH": 3} results.sort(key=lambda r: (order.get(r["status"], 99), r["codepoint"]))

--- Print summary ---

new_count = sum(1 for r in results if r["status"] == "NEW") removed_count = sum(1 for r in results if r["status"] == "REMOVED") changed_count = sum(1 for r in results if r["status"] == "SHAPE CHANGED")

print(f"NEW icons: {new_count}") print(f"REMOVED icons: {removed_count}") print(f"SHAPE CHANGED icons:{changed_count}") print()

current_status = None for row in results: if row["status"] != current_status: current_status = row["status"] print(f"--- {current_status} ---") classes = row["new_classes"] or row["old_classes"] or "(no CSS class)" print(f' {classes} [{row["codepoint"]}]')

--- CSV output ---

if output_csv_path: with open(output_csv_path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter( f, fieldnames=["status", "codepoint", "decimal", "old_classes", "new_classes"] ) writer.writeheader() writer.writerows(results) print(f"\nResults saved to: {output_csv_path}")

return results

def main(): if len(sys.argv) < 5 or len(sys.argv) > 6: print("Usage:") print(" python diff_font_apex_icons.py old.ttf new.ttf old_css new_css [output.csv]") sys.exit(1)

old_font_path  = sys.argv[1]
new_font_path  = sys.argv[2]
old_css_path   = sys.argv[3]
new_css_path   = sys.argv[4]
output_csv_path = sys.argv[5] if len(sys.argv) == 6 else None

compare_fonts(old_font_path, new_font_path, old_css_path, new_css_path, output_csv_path)

if name == "main": main()

and also my runner script

Windows Batch File
@echo off

set OLD=C:\software\apex\images\libraries\font-apex\2.4 set NEW=C:\software\apex\images\libraries\font-apex\2.5

python diff_font_apex_icons.py ^ "%OLD%\fonts\Font-APEX-Large.ttf" ^ "%NEW%\fonts\Font-APEX-Large.ttf" ^ "%OLD%\css\font-apex.css" ^ "%NEW%\css\font-apex.css" ^ changed-large.csv;

Font APEX 2.4 ▶️ 2.5

Here we can see 50 new icons added and no changes to any of the glyphs

NEW icons:          50
REMOVED icons:      0
SHAPE CHANGED icons:0

--- NEW ---
  fa-axe [U+ECB5]
  fa-blueprint-construction [U+ECB6]
  fa-bricks [U+ECB7]
  fa-brush [U+ECB8]
  fa-bucket [U+ECB9]
  fa-bulldozer [U+ECCA]
  fa-compass-drafting [U+ECCB]
  fa-crane [U+ECCD]
  fa-crane-hook [U+ECCC]
  fa-drill [U+ECCE]
  fa-dump-truck [U+ECCF]
  fa-excavator [U+ECC0]
  fa-faucet [U+ECC1]
  fa-forklift [U+ECC2]
  fa-hammer [U+ECC4]
  fa-hammer-brush [U+ECC3]
  fa-hard-hat [U+ECC5]
  fa-ladder [U+ECC6]
  fa-land-surveying [U+ECC7]
  fa-mine-cart [U+ECC8]
  fa-mound [U+ECC9]
  fa-paint-can [U+ECDA]
  fa-paint-roller [U+ECDB]
  fa-paver [U+ECDC]
  fa-person-digging [U+ECDD]
  fa-pipe [U+ECDE]
  fa-plexiglass [U+ECDF]
  fa-portable-toilet [U+ECD0]
  fa-reel [U+ECD1]
  fa-roadblock [U+ECD2]
  fa-ruler [U+ECD4]
  fa-ruler-combination [U+ECD3]
  fa-safety-vest [U+ECD5]
  fa-screwdriver [U+ECD6]
  fa-sheet-metal [U+ECD7]
  fa-shovel [U+ECD8]
  fa-siding [U+ECD9]
  fa-toolbox [U+ECEA]
  fa-tower-crane [U+ECEB]
  fa-traffic-cone [U+ECEC]
  fa-triangle-person-digging [U+ECED]
  fa-trowel [U+ECE0]
  fa-trowel-bricks [U+ECEE]
  fa-trowel-stucco [U+ECEF]
  fa-truck-container [U+ECE1]
  fa-truck-ladder [U+ECE2]
  fa-truck-pickup [U+ECE3]
  fa-user-worker [U+ECE4]
  fa-welding-mask [U+ECE5]
  fa-wheelbarrow [U+ECE6]

These 50 correlate to the 50 new Construction icons in the UT application

These 50 also appear in the New icons in the UT application

The inclusion of the highlighted calendar icons are interesting. In APEX 24.2, they were available to use in your applications but not to pick. See APEX 24.2 icon picker here.

So you'll have to make up your own mind whether you consider them new or not.

Font APEX 2.5 ▶️ 2.5.1

Lets give our runner a try:

NEW icons:          0
REMOVED icons:      0
SHAPE CHANGED icons:4

--- SHAPE CHANGED ---
  fa-thumbs-down [U+F165]
  fa-thumbs-o-down [U+F088]
  fa-thumbs-o-up [U+F087]
  fa-thumbs-up [U+F164]

Interesting; the differences are only to the glyph of the thumbs icons. Lets take a look.

  • On the left we have APEX 24.2 icons from Font APEX 2.4

  • On the right we have APEX 26.1 icons from Font APEX 2.5.1

What do you think? We've lost all the definition in the fingers. The left one does look a bit old school though. But the right one looks like a hand from a Teletubbie.

I'm not sure about this though; because all the other hands include fingers.

Even fa-hand-grab-o shows knuckles!

Are these icons in keeping with the other icons? What you you think?

Conclusion

Is Font APEX 2.5 a fundamental jump from 2.4? Well, it is if you have a Construction based application or if you just like wearing mittens.

ENJOY!

What's the picture? Cherry Blossoms in Harrogate - right here. Visit Yorkshire!

M

'Significant if you enjoy wearing mittens'. You always manage to make me smile :-)

M

Ha Ha thanks - I try 🧤

J

Maybe it is offensive now to show 4 fingers in a Thumbs Up icon for all people who lost a finger, you can't keep up with wokeness these days 😅