Blog

Code Snippet: Integrate Custom Fonts into Oxygen

(2020)

Content

Task

Oxygen Builder allows you to select standard web-safe font combinations, Google Web Fonts and even TypeKit fonts.
Custom fonts, such as fonts licensed by the customer, are not available here.

Of course you can upload your own fonts to the server and then embed them by CSS yourself. But in Oxygen such a font is still not available for selection.

Official Solution - Plugin "Elegant Custom Fonts"

With the plugin "Elegant Custom Fonts" (author Louis Reingold, founder of Soflyy, the company behind Oxygen Builder) you can upload your own fonts and they will be available for selection in Oxygen. Perfect.

Alternative Solution - Code Snippet

For a customer project, I had to use a font licensed by the customer to comply with the CI, but I didn't want to install another plugin for just this one font.

Since I already had installed the plugin "Code Snippets" anyway in order to implement several other features in this project, I developed a solution, which can be integrated as code snippet.

For the Oxygen community I present a highly improved version of this code snippet below.

Download

The Code Snippet is available for download here:

oxygen-custom-fonts.code-snippets.json
Version 2.2.5, 2020-12-06

For installation and use of the downloaded JSON file you will need the plugin Code Snippets.
You can install the JSON file using the "Import" function of the plugin. 
Don't forget to activate the snippet after import.

Alternative: At the end of this page you can view and copy the complete source code of the snippet.

New functionalities and bug fixes are documented in the change log.
A few small things about compatibility should be noted.

Description

Instructions

  1. Create a new directory wp-content/uploads/fonts
  2. Upload your font files (eot, otf, svg, ttf, woff, woff2) into this fonts directory.

The script will find all font files, make them available for selection in Oxygen and generate CSS code in the frontend.

Font Files, Names, Weights and Styles

The snippet supports the font formats EOT, OTF, SVG, TTF, WOFF, WOFF2.

The filename of the font file will be used as font name.
So for the font file "Matthias Altmann.woff" Oxygen will display the font name "Matthias Altmann".

If the file name contains keywords for font weights, these are recognized and not included in the font name, but noted in the generated CSS code accordingly.
The following weights and styles are currently recognized:

Font WeightKeywords
100Thin
200ExtraLight, UltraLight
300Light
400Normal, Regular
500Medium
600DemiBold, SemiBold
700Bold
800ExtraBold, UltraBold
900Heavy

The keywords are not case sensitive and may contain hyphens between the keyword parts.
ExtraLight is therefore recognized in the same way as extra-light.

Numeric values in the file names are also recognized as font weights.

Furthermore, the keyword "italic" is recognized for the italic font style. Also here, the capitalization and a leading hyphen are irrelevant.

If there are multiple font files with the same name but different font formats (eot, otf, svg, ttf, woff, woff2), Oxygen will display the font name only once. However, the generated CSS will contain all available font formats. The browser will choose the preferred one by itself.

Since version 2.2.2 of the snippet, fonts with variable weights are now also supported: If the keyword VariableFont (case-insensitive) is recognized in the file name, the CSS is automatically adjusted for fonts with variable weights.

Test

A test function is implemented in the snippet: With the shortcode [maltmann_custom_font_test] a list of all fonts, weights and styles recognized and registered by the snippet is displayed.

This shortcode can be easily entered on a new page in the Gutenberg Editor. The page does not even need to be made public. A preview gives you a quick overview of the fonts registered by the snippet.

Performance

While developing the script I paid a lot of attention to efficiency and performance.

  • Search for font files: ø 0.0015 sec.
  • Output of CSS code: ø 0.0002 sec.

(measurements on iMac i9 3.6 GHz, MAMP, PHP 7.4.2, about 30 font files in subdirectories)

The CSS code for the fonts is emitted at the end of each page, no matter if one of the fonts is used on that page or not.
However, the browser loads a font only when it is really needed, and then only in the one preferred format.

Compatibility

I have developed and tested the code snippet to the best of my knowledge under WordPress 5.5.3 and Oxygen 3.6.1.

So far, I know of two WordPress plugins that integrate custom fonts into Oxygen in a similar way: "Elegant Custom Fonts" and "Use Any Font".
Since the way of implementation or the technical approach to integrate fonts into Oxygen are the same in these plugins and my code snippet, these solutions are not compatible. You have to choose one of these solutions. So please deactivate these two plugins before using my Code Snippet.

FAQ - Frequently Asked Questions

Why are there no font weights available for my font in Oxygen Global Settings?

In Oxygen Global Settings you can define the font weights for Google Fonts to be embedded. This way you can limit which font weights are loaded from Google Fonts Server and thus reduce the file size if only a few font weights are used.

For our own fonts, which we embed using this Code Snippet, we decide ourselves which font files we place in the "fonts" folder. Usually there is a separate font file for each font weight. All these font files provided by us and thus the different font weights are offered to the browser. However, the browser only loads the font files with the font weights that are really needed to display the page.

So there is no reason to limit the font sizes for our own fonts in the Oxygen Global Settings.

Can I host Google Fonts locally with this Code Snippet?

Yes. With caution.

My Code Snippet will search for font files and add them to the Oxygen font list. However, Oxygen offers some Google Fonts in the font list by itself.

For example, if we download the Google Font "Roboto" and host it locally, my Code Snippet will add it to the font list. Oxygen adds the Google Font "Roboto" itself. So "Roboto" is now included twice in the list: Once as local font and once as Google Font.

Even more interesting are Google Fonts with spaces in their names. The Google font "Open Sans" is given file names with "OpenSans" (without space) when downloaded. My Code Snippet adds this font as "OpenSans" to the Oxygen font list. But Oxygen itself adds the Google Font "Open Sans" (with space) to the font list.

To make matters worse, even with the option "Disable Google Fonts" enabled, Oxygen still displays 20 Google fonts in the font selector, and offers Google Fonts when searching for fonts. This is known to the Oxygen team, but there is currently no solution for this.

So I do not recommend to use this Code Snippet as a solution for local hosting of Google Fonts.
On my own websites I use the fantastic plugin "Self-Hosted Google Fonts". With this I am – without the slightest effort – GDPR compliant.

Disclaimer

I provide the code snippet for free use.
I cannot give a guarantee for the functionality in all conceivable WordPress environments.
Download and use of this code snippet is at your own risk and responsibility.

Change Log

Version 1 (2020-04-10)

  • Initial release for customer project

Version 2.0.0 (2020-09-15) 

  • Improved version 
  • Finds all font files (eot, otf, svg, ttf, woff, woff2) in directory wp-uploads/fonts/, optionally recursive 
  • Takes font name from file name 
  • Emits optimized CSS with alternative font formats 
  • Special handling for EOT for Internet Explorer

Version 2.1.0 (2020-09-16)

  • New features:
    • Detection of font weight and style from file name 
  • Bug fixes: 
    • EOT: Typo in extension detection 
    • EOT: Missing quote in style output

Version 2.1.1 (2020-10-03)

  • Bug fixes:
    • Handle empty fonts folder correctly.
      (Thanks to Mario Peischl for reporting!)

Version 2.2.0 (2020-11-23)

  • New features: 
    • Detection of font weight from number values 
    • CSS now contains font-display:swap;

Version 2.2.1 (2020-11-24) 

  • New features: 
    • Shortcode [maltmann_custom_font_test] for listing all custom fonts with their weights and styles 
  • Changes: 
    • Fonts are now sorted alphabetically for e.g. CSS output 
    • Added more request rules for skipping code execution when not needed

Version 2.2.2 (2020-11-25)

  • New features: 
    • Partial support for fonts with variable weights, detected by VariableFont in filename. CSS output as font-weight:100 900;

Version 2.2.3 (2020-11-25)

  • Changes:
    • In Oxygen font selectors the custom fonts are now displayed in lightblue to distinguish from default, websafe and Google Fonts

Version 2.2.4 (2020-11-27) 

  • Bug fix: 
    • Corrected typo in variable name (2 occurrences) that could lead to repeated search for font files. 
      (Thanks to Viorel Cosmin Miron for reporting!)

Version 2.2.5 (2020-12-06)

  • Changes:
    • In CSS, font sources are now listed in a prioritized order (eot,woff2,woff,ttf,otf,svg)
      (Thanks to Viorel Cosmin Miron for reporting!)
    • Test shortcode now also displays available font formats

Source Code

<?php
/* CUSTOM FONTS */

/*
Project: 		Code Snippet: Load custom fonts and inject to Oxygen
Version:		2.2.5
Description: 	en: https://www.altmann.de/en/blog-en/code-snippet-integrate-custom-fonts-into-oxygen-en/
				de: https://www.altmann.de/blog/code-snippet-eigene-schriftarten-in-oxygen-integrieren/
Author:			Matthias Altmann (https://www.altmann.de/)
Copyright:		© 2020, Matthias Altmann




Get Started:
1) Create a new directory wp-content/uploads/fonts
2) Upload your custom fonts (eot, otf, svg, ttf, woff, woff2) to this fonts directory. Sub-directories allowed.
3) This Code Snippet finds all font files, injects them to Oxygen and emits related CSS to frontend.

Font Names:
- The file name of the font files will be used as font name in Oxygen.
- If the file name of the font file contains a numeric weight, common tags for font weight (light, regular, bold, ...) 
  or style (italic) those tags will not be used for the font name but instead translated to proper CSS properties.
- If multiple font file formats for one font are found, they will all be offered to the browser. 

Configuration:
Find the comment ===== CONFIG ===== in the code to read about a few configuration variables.
Please only change if you understand the meaning.

Performance:
(measurements on iMac i9 3.6 GHz, MAMP, PHP 7.4.2, about 30 font files in subdirectories)
- Scanning for fonts: 	ø 0.0015 sec.
- Emitting CSS: 		ø 0.0002 sec.


Version History:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
2020-04-10	1.0.0		Initial Release for customer project
2020-09-15	2.0.0		Improved version
						- Finds all font files (eot, otf, svg, ttf, woff, woff2) in directory wp-uploads/fonts/
						- Optionally recursive
						- Takes font name from file name
						- Emits optimized CSS with alternative font formats
						- Special handling for EOT for Internet Explorer
2020-09-16	2.1.0		New features:
						- Detection of font weight and style from file name
						Bug fixes:
						- EOT: Typo in extension detection
						- EOT: Missing quote in style output
2020-10-03 	2.1.1		Bug fix:
						- Handle empty fonts folder correctly. (Thanks to Mario Peischl for reporting!)
						- Corrected title and file name (typo "cutsom") of Code Snippet
2020-11-23	2.2.0		New features:
						- Detection of font weight from number values 
						- CSS now contains font-display:swap;
2020-11-24	2.2.1		New features:
						- Shortcode [maltmann_custom_font_test] for listing all custom fonts with their weights 
						  and styles
						Changes:
						- Fonts are now sorted alphabetically for e.g. CSS output
						- Added more request rules to skipping code execution when not needed
2020-11-25	2.2.2		New features:
						- Partial support for fonts with variable weights, detected by "VariableFont" in 
						  filename. CSS output as font-weight:100 900;
2020-11-25	2.2.3		Changes:
						- In Oxygen font selectors the custom fonts are now displayed in lightblue 
						  to distinguish from default, websafe and Google Fonts 
2020-11-27	2.2.4		Bug fix:
						- Corrected typo in variable name (2 occurrences) that could lead to repeated search 
						  for font files. (Thanks to Viorel Cosmin Miron for reporting!)
2020-12-08	2.2.5		Changes:
						- In CSS, font sources are now listed in a prioritized order (eot,woff2,woff,ttf,otf,svg)
						  (Thanks to Viorel Cosmin Miron for reporting!)
						- Test shortcode now also displays available font formats
--------------------------------------------------------------------------------------------------------------
*/


if (	!class_exists('ECF_Plugin')
	&&	!wp_doing_ajax() 														// skip for ajax
	&& 	!wp_doing_cron()														// skip for cron
	&& 	(!array_key_exists('heartbeat',$_REQUEST))								// skip for hearbeat
	&& 	(@$_REQUEST['action'] != 'set_oxygen_edit_post_lock_transient')			// skip for Oxygen editor lock
	&&  (@$_SERVER['REQUEST_URI'] != '/favicon.ico')							// skip for favicon
	) :
	// create a primitive ECF_Plugin class if plugin "Elegant Custom Fonts" is not installed
	class ECF_Plugin {
		// ===== CONFIG =====
		public static $recursive 	= true; 	// set this to false for flat or true for recursive file scan
		public static $parsename 	= true; 	// set this to true for parsing font weight and style from file name
		public static $timing		= false; 	// write timing to wordpress debug.log if WP_DEBUG enabled		
		public static $debug		= false; 	// write debug info (a lot!) to wordpress debug.log if WP_DEBUG enabled		

		// ===== INTENAL =====
		public 	static $version				= '2.2.5';
		public 	static $prioritized_formats	= ['eot','woff2','woff','ttf','otf','svg'];
		private static $fonts 				= null;	// Will be populated with the found fonts and related files
		private static $fonts_details_cache	= [];	// cache for already parsed font details
		
		// -----------------------------------------------------------------------------------
		static function parse_font_name($name) {
			// already in cache?
			if (array_key_exists($name,self::$fonts_details_cache)) {return self::$fonts_details_cache[$name];}
			
			$retval = (object)['name'=>$name, 'weight'=>400, 'style'=>'normal'];
			if (!self::$parsename) {return $retval;}
			$st = microtime(true);
			$weights = (object)[ // must match from more to less specific !!
				// more specific
				200 => '/\-?(200|((extra|ultra)\-?light))/i',
				800 => '/\-?(800|((extra|ultra)\-?bold))/i',
				600 => '/\-?(600|([ds]emi\-?bold))/i',
				// less specific
				100 => '/\-?(100|thin)/i',
				300 => '/\-?(300|light)/i',
				400 => '/\-?(400|normal|regular)/i',
				500 => '/\-?(500|medium)/i',
				700 => '/\-?(700|bold)/i',
				900 => '/\-?(900|black|heavy)/i',
				'var' => '/\-?(VariableFont)/i',
			];
			if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts parse_font_name("%s")', $retval->name));}
			$count = 0;
			// detect & cut style
			$new_name = preg_replace('/\-?italic/i', '', $retval->name, -1, $count); 
			if ($new_name && $count) {
				$retval->name = $new_name;
				$retval->style = 'italic';
				if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts parse_font_name() detected italic, new name: "%s"', $retval->name));}
			}
			// detect & cut weight
			foreach ($weights as $weight => $pattern) {
				$new_name = preg_replace($pattern, '', $retval->name, -1, $count);
				if ($new_name && $count) {
					$retval->name = $new_name;
					$retval->weight = $weight;
					if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts parse_font_name() detected weight %d, new name: "%s"', $weight, $retval->name));}
					break;
				}
			}
			// variable font: detect & cut specifica
			if ($retval->weight == 'var') {
				$retval->name = preg_replace('/_(opsz,wght|opsz|wght)$/i', '', $retval->name); 
			}

			if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts parse_font_name() retval: [name:"%s", weigh:%d, style:%s]', $retval->name, $retval->weight, $retval->style));}
			// store to cache
			self::$fonts_details_cache[$name] = $retval;
			$et = microtime(true);
			if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts parse_font_name() Timing: %.5f sec.',$et-$st));}
			return $retval;
		}
		// -----------------------------------------------------------------------------------
		static function find_fonts() {
			$st = microtime(true);
			if (isset(self::$fonts)) return;
			self::$fonts = [];
			$fonts_dir_info = wp_get_upload_dir();
			$fonts_basedir = $fonts_dir_info['basedir'].'/fonts';
			$fonts_baseurl = $fonts_dir_info['baseurl'].'/fonts';
			if (!file_exists($fonts_basedir)) return;
			// property $recursive either recursive or flat file scan
			if (self::$recursive) {
				// recursive scan for font files (including subdirectories)
				$directory_iterator = new RecursiveDirectoryIterator($fonts_basedir);
				$file_iterator = new RecursiveIteratorIterator($directory_iterator);
			} else {
				// flat scan for font files (no subdirectories)
				$file_iterator = new FilesystemIterator($fonts_basedir);
			}
			// loop through files and collect font files
			$font_splfiles = [];
			foreach( $file_iterator as $file) {
    			if (in_array(strtolower($file->getExtension()), self::$prioritized_formats)) {
					$font_splfiles[] = $file;
    			}
			}
			// collect font definitions
			foreach ($font_splfiles as $font_splfile) {
				$font_ext = $font_splfile->getExtension();
				$font_details = self::parse_font_name($font_splfile->getbasename('.'.$font_ext));
				$font_name = $font_details->name;
				$font_weight = $font_details->weight;
				$font_style = $font_details->style;
				$font_path = str_replace($fonts_basedir,'',$font_splfile->getPath());
				// encode every single path element since we might have spaces or special chars 
				$font_path = implode('/',array_map('rawurlencode',explode('/',$font_path)));
				$font_baseurl = str_replace($fonts_basedir, $fonts_baseurl, $font_splfile->getFilename());
				// create entry for this font name
				if (!array_key_exists($font_name,self::$fonts)) {self::$fonts[$font_name] = [];}
				// create entry for this font weight/style 
				if (!array_key_exists($font_weight.'/'.$font_style,self::$fonts[$font_name])) {self::$fonts[$font_name][$font_weight.'/'.$font_style] = [];}
				// store font details for this file
				self::$fonts[$font_name][$font_weight.'/'.$font_style][$font_ext] = $fonts_baseurl . $font_path . '/' . rawurlencode($font_splfile->getBasename());
			}
			ksort(self::$fonts, SORT_NATURAL | SORT_FLAG_CASE);
			if (WP_DEBUG && self::$debug) {error_log(sprintf('Custom Fonts find_fonts() font: %s',print_r(self::$fonts,true)));}
			$et = microtime(true);
			if (WP_DEBUG && self::$timing) {error_log(sprintf('Custom Fonts find_fonts() Timing: %.5f sec.',$et-$st));}
		}
		// -----------------------------------------------------------------------------------
		static function get_font_families() {
			if (!isset(self::$fonts)) self::find_fonts();
			$st = microtime(true);
			$font_family_list = [];
			foreach (array_keys(self::$fonts) as $font_name) {
				$font_family_list[] = $font_name;
			}
			$et = microtime(true);
			if (WP_DEBUG && self::$timing) {error_log(sprintf('Custom Fonts get_font_families() Timing: %.5f sec.',$et-$st));}
			return $font_family_list;
		}
		// -----------------------------------------------------------------------------------
		// we call this function from footer emitter to get font definitions for emitting required files
		static function get_font_definitions() {
			return self::$fonts;
		}
	}		
	// pre-fill font definitions
	ECF_Plugin::get_font_families();



	add_action( 'wp_footer', function () { 
		// emit CSS for fonts in footer
		$st = microtime(true);
		$style = '';
		$font_defs = ECF_Plugin::get_font_definitions();
		ksort($font_defs, SORT_NATURAL | SORT_FLAG_CASE);
		foreach ($font_defs as $font_name => $font_details) {
			ksort($font_details);
			foreach ($font_details as $weight_style => $file_list) {
				list ($font_weight,$font_style) = explode('/',$weight_style);

				if ($font_weight == 'var') {
					$font_weight_output = '100 900';
				} else {
					$font_weight_output = $font_weight;
				}
				$style .= 	'@font-face{'.PHP_EOL.
							'  font-family:"'.$font_name.'";'.PHP_EOL.
							'  font-weight:'.$font_weight_output.';'.PHP_EOL.
							'  font-style:'.$font_style.';'.PHP_EOL;
							// .eot needs special handling for IE9 Compat Mode
				if (array_key_exists('eot',$file_list)) {$style .= '  src:url("'.$file_list['eot'].'");'.PHP_EOL;}
				$urls = [];

				// output font sources in prioritized order
				foreach (ECF_Plugin::$prioritized_formats as $font_ext) {
					if (array_key_exists($font_ext,$file_list)) {
						$font_url = $file_list[$font_ext];
						$format = '';
						switch ($font_ext) {
							case 'eot': $format = 'embedded-opentype'; break;
							case 'otf': $format = 'opentype'; break;
							case 'ttf': $format = 'truetype'; break;
							// others have same format as extension (svg, woff, woff2)
							default:	$format = strtolower($font_ext);
						}
						if ($font_ext == 'eot') {
							// IE6-IE8
							$urls[] = 'url("'.$font_url.'?#iefix") format("'.$format.'")';
						} else {
							$urls[] = 'url("'.$font_url.'") format("'.$format.'")';
						}
					}
				}
				$style .= '  src:' . join(','.PHP_EOL.'      ',$urls) . ';'.PHP_EOL;
				$style .= '  font-display: swap;'.PHP_EOL;
				$style .= '}'.PHP_EOL;
			}
		}
		// if Oxygen Builder is active, emit CSS to show custom fonts in light blue.
		if (defined( 'SHOW_CT_BUILDER' ) ) {
			$style .= 'div.oxygen-select-box-option.ng-binding.ng-scope[ng-repeat*="elegantCustomFonts"] {color:lightblue;}';
		}
		
		if (WP_DEBUG && ECF_Plugin::$debug) {error_log(sprintf('Custom Fonts style: %s', $style));}
		// miminize string if debug mode is disabled (= production system).
		if (!WP_DEBUG && !ECF_Plugin::$debug) {
			$style = preg_replace('/\r?\n */','',$style); 
		}
		$et = microtime(true);
		if (WP_DEBUG && ECF_Plugin::$timing) {error_log(sprintf('Custom Fonts style emitter Timing: %.5f sec.',$et-$st));}
		echo '<style id="maltmann-custom-font">'.$style.'</style>';
	} );


	// Shortcode for testing custom fonts (listing all fonts with their formats, weights, styles)
	add_action( 'init', function(){
		add_shortcode('maltmann_custom_font_test', 'maltmann_custom_font_test'); 
		if (!function_exists('maltmann_custom_font_test')) {
			function maltmann_custom_font_test($atts){
				$output = 	'<style>'.
							'.macf-font-row {display:flex;flex-directiomn:row;justify-content:space-between;width:500px;padding:0 20px}'.
							'.macf-font-row:hover {background-color:lightgray;}'.
							'.macf-format-info {padding-top:5px;font-size:10px;}'.
							'</style>'.
							'<div style="border: 1px dashed darkgray; padding: 10px;">'.
								'<h2>Custom Font Test</h2>'.
								'<div><i>(Script Version: '.ECF_Plugin::$version.')</i></div>';
				foreach ( ECF_Plugin::get_font_definitions() as $font_name => $font_details) {
					$output .= sprintf('<h3 style="padding-top: 20px;font-family:\'%1$s\';">%1$s</h3>',$font_name);
					ksort($font_details);
					foreach ($font_details as $weight_style => $file_list) {
						list ($font_weight,$font_style) = explode('/',$weight_style);
						// collect formats in prioritised order
						$formats = [];
						foreach (ECF_Plugin::$prioritized_formats as $font_ext) {if (array_key_exists($font_ext,$file_list)) {$formats[]=strtoupper($font_ext);}} 
						$format_info = '<span class="macf-format-info">('.implode(', ',$formats).')</span>';
						if ($font_weight == 'var') {
							$output .= '<div style="font-size:8pt;">(Variable Font, '.implode(', ',$formats).')</div>';
							foreach ([100,200,300,400,500,600,700,800,900] as $font_weight) {
								$output .= sprintf('<div class="macf-font-row"><span style="font-family:\'%1$s\';font-weight:%2$d;font-style:%3$s">%1$s %2$d %3$s</span></div>',$font_name, $font_weight, $font_style);
							}
						} else {
							$output .= sprintf('<div class="macf-font-row"><span style="font-family:\'%1$s\';font-weight:%2$d;font-style:%3$s">%1$s %2$d %3$s</span>%4$s</div>',$font_name, $font_weight, $font_style, $format_info);
						}
					}
				}
				$output .= '</div>';
				return $output;
			}
		}
	});


endif;
magnifier