Blog

Code Snippet: Integrate Custom Fonts into Oxygen

(2020)

Content

Introduction

Oxygen Builder allows you to select standard web-safe font combinations, Google 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.

Plugins

There are several plugins that allow integrating custom fonts into Oxygen:

Those plugins work perfectly. 
However, you'll have to upload each single font file and register with name, weight, and style.

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.

The Code Snippet can also automate the deployment of fonts as far as possible and, since version 3, also optimally integrate Google Fonts. For the easy download of Google Fonts I have developed the Web Font Loader.

Download

The Code Snippet is available for download here:

oxygen-custom-fonts.code-snippets.json
Version 3.1.1, 2021-03-21

For installation and use of the downloaded JSON file you will need the plugin Code Snippets or Advanced Scripts.
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 folder wp-content/uploads/fonts
  2. Upload your font files (eot, otf, svg, ttf, woff, woff2) into this fonts folder.
    Alternatively, you can download Google Fonts via my Web Font Loader to host them locally on your server.
    Please extract the ZIP file and upload the extracted folder, named like your font, and all of its contents to the folder on the server.

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

With Google Fonts, which you have downloaded via my Web Font Loader, all available font files, weights, styles and also the available character sets are automatically detected. The necessary information is read from the enclosed JSON file.

For your own fonts from other sources, these details are determined based on the file names.

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.woff2" 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, Book
500Medium
600Demi, DemiBold, 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 (100, 200, ...) in the file names are also recognized as font weights.

Furthermore, the keywords "italic" and "oblique" are 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.

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

Testing the fonts

The snippet implements a test function that outputs a list of all fonts, weights and styles recognized and registered by the snippet. Here you can check the integration and display of the fonts, also in different font sizes.

Test by Shortcode

The snippet provides a shortcode [maltmann_custom_font_test].
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.

Test with Admin Function

In the WordPress administration there is a new menu item "Custom Fonts" under Appearance
Here you get 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. Detecting existing fonts and generating the CSS code takes only milliseconds on an average server.

The CSS code for the fonts is linked as CSS file or 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

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

Oxygen offers the possibility to embed Google Fonts directly.
If you want to use this code snippet to host Google Fonts locally on your own server, I recommend disabling Google Fonts support in Oxygen: Oxygen > Settings > Bloat Eliminator > Disable Google Fonts

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 a bit of 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.

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.

I recommend to disable Google Fonts support in Oxygen: Oxygen > Settings > Bloat Eliminator > Disable Google Fonts

Disclaimer

I have developed and tested the code snippet to the best of my knowledge under WordPress 5.7 and Oxygen 3.7.1.
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

See Source Code.

Source Code

<?php 
/*
Plugin Name:  MA Custom Fonts
Description:  Load custom fonts and inject to Oxygen
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Project:      Code Snippet: Load custom fonts and inject to Oxygen
Version:      3.1.1
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-integrate-custom-fonts-into-oxygen-en/
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/
Copyright:    © 2020-2021, 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.
   OR:
   Download Google Fonts with Web Font Loader (https://webfontloader.altmann.de/), unzip and upload the complete folder
3) This plugin finds all font files, injects them to Oxygen and emits related CSS to frontend.

Font Names:
For Google Font packages downloaded with Web Font Loader:
- The font name and specifications are read from the included JSON file.
For other font files:
- 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 80 font files in subdirectories)
- Scanning for fonts: 	ø 0.0098 sec.
- Emitting CSS: 		ø 0.0003 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-content/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
						Fixes:
						- EOT: Typo in extension detection
						- EOT: Missing quote in style output
2020-10-03 	2.1.1		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		Fix:
						- Corrected typo in variable name (2 occurrences) that could cause 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
2021-01-23	2.5.0		New features:
						- WP Admin Menu: Appearance > Custom Fonts 
						  Shows a list of all registered custom fonts, including samples, weights, formats
						  with adaptable sample font size 
						- Detect font weight terms "Book" (400) and "Demi" (600) 
						Changes:
						- Redesign of classes (MA_CustomFonts, ECF_Plugin)
						- Font swap is now a configuration option
						- Cut "-webfont" from font name
2021-01-23	2.5.1		Fix:
						- Changed compatibility check process: 
						  Changed Hook for plugin compatibility check from plugins_loaded to init
						  Check only if admin and function is_plugin_active exists
						  (Thanks to Sebastian Albert for reporting and testing!)
2021-01-24 	2.5.2		New Features:
						- Custom Fonts test (via Admin panel and shortcode) now allows custom sample text
2021-02-18	3.0.0		New Features:
						- Support for font packages from Web Font Loader (https://webfontloader.altmann.de/)
						- New configuration option: CSS output as inline CSS or external CSS file (cacheable)
						- New configuration option: CSS minimize (was controlled by debug switch before)
						- Changed configuration option: font-display may now be specified as desired, 
						  default is now 'auto'
2021-02-23	3.0.1		Fixes:
						- Compatibility with WordPress 5.6.2 (doesn't set REQUEST::action anymore)
						- Compatibility check with Swiss Knife's Font Manager feature
						- Compatibility with Swiss Knife (font lists did not display custom fonts light blue)
2021-03-08	3.0.2		Fix:
						- Compatibility with Windows server and local dev environments.
						  (Thanks to Franz Müller for reporting and testing!)
2021-03-20	3.1.0		New Features:
						- "Oblique" in font file name is now detected as italic style
						- Custom Fonts test: Option to show font weights/styles without files as browser would 
						  simulate. 
						Changes:
						- Output Custom Font CSS in head instead of footer to prevent font swap
						- Custom Fonts test: Changed logic for output font samples and related file info
						Fixes:
						- Custom Fonts test: Fixed font file count for fonts provided by Web Font Loader
2021-03-21	3.1.1		Fixes:
						- Fixed font loading in Gutenberg editor (with Oxygen Gutenberg Integration)
--------------------------------------------------------------------------------------------------------------
*/

if (!class_exists('MA_CustomFonts')
&&	!wp_doing_ajax() 														// skip for ajax
&& 	!wp_doing_cron()														// skip for cron
&& 	(!is_admin() || ( 				 										// skip for admin
		(isset($_REQUEST['page']) && ($_REQUEST['page']=='ma_customfonts') )  // ... except for Custom Font Test
	|| 	(isset($_REQUEST['post']) && isset($_REQUEST['action']) && ($_REQUEST['action'] == 'edit')) // ... and Gutenberg Editor
	)
)	
&& 	(!array_key_exists('heartbeat',$_REQUEST))								// skip for heartbeat
&& 	(!isset($_REQUEST['action']) || (isset($_REQUEST['action']) && ($_REQUEST['action'] != 'set_oxygen_edit_post_lock_transient')))	// skip for Oxygen editor lock
&&  (@$_SERVER['REQUEST_URI'] != '/favicon.ico')							// skip for favicon
) :
class MA_CustomFonts {

	const TITLE     	= 'MA Custom Fonts';
	const SLUG	    	= 'ma_custom_fonts';
	const VERSION   	= '3.1.1';

	// ===== CONFIGURATION =====
	public static $recursive 	= true; 	// enables recursive file scan
	public static $parsename 	= true; 	// enables parsing font weight and style from file name
	public static $fontdisplay	= 'auto';	// set font-display to auto, block, swap, fallback, optional or '' (disable)
	public static $cssoutput	= 'file';	// set to 'html' to output CSS inline into page, 
											// set to 'file' to create and reference a CSS file (cacheable by browser)
	public static $cssminimize	= true; 	// minimize CSS (true) or pretty print (false)

	public static $timing		= false; 	// write timing info (a lot!) 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	
	public static $sample_text 	= 'The quick brown fox jumps over the lazy dog.';	

	// ===== INTERNAL =====
	public 	static $prioritized_formats	= ['eot','woff2','woff','ttf','otf','svg'];
	private static $fonts 				= null;	// will be populated with fonts and related files we found
	private static $fonts_details_cache	= [];	// cache for already parsed font details
	private static $font_files_cnt		= 0;	// number of font files parsed
	
	// -----------------------------------------------------------------------------------
	// parses weight from a font file name (not used for Web Font Loader packages)
	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);
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() parsing font file name: "%s"',__CLASS__,__FUNCTION__, $retval->name));}
		$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|book)/i',
			500 => '/\-?(500|medium)/i',
			700 => '/\-?(700|bold)/i',
			900 => '/\-?(900|black|heavy)/i',
			'var' => '/\-?(VariableFont)/i',
		];
		$count = 0;
		// detect & cut style
		$new_name = preg_replace('/\-?(italic|oblique)/i', '', $retval->name, -1, $count); 
		if ($new_name && $count) {
			$retval->name = $new_name;
			$retval->style = 'italic';
			if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() detected italic, new font family name: "%s"',__CLASS__,__FUNCTION__, $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('%s::%s() detected weight %s, new font family name: "%s"',__CLASS__,__FUNCTION__, $retval->weight, $retval->name));}
				break;
			}
		}
		// cut -webfont
		$retval->name = preg_replace('/\-webfont$/i', '', $retval->name); 
		// 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('%s::%s() retval: [name:"%s", weigh:%d, style:%s]',__CLASS__,__FUNCTION__, $retval->name, $retval->weight, $retval->style));}
		// store to cache
		self::$fonts_details_cache[$name] = $retval;
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// construct CSS block from CSS properties stored in JSON from Web Font Loader
	static 	function create_css_from_ruleset($css_ruleset) {
		$retval = '';
		if (isset($css_ruleset)) {
			if (isset($css_ruleset->{'comment'})) {$retval .= sprintf("/* %s */\n",$css_ruleset->{'comment'});}
			$retval .= "@font-face {\n";
			$retval .= sprintf("\tfont-family: '%s';\n",$css_ruleset->{'font-family'});
			$retval .= sprintf("\tfont-style: %s;\n",$css_ruleset->{'font-style'});
			$retval .= sprintf("\tfont-weight: %s;\n",$css_ruleset->{'font-weight'});
			$retval .= sprintf("\tsrc: url('%s') format('%s');\n",$css_ruleset->{'url'}, $css_ruleset->{'format'});
			if (isset($css_ruleset->{'unicode-range'})) {$retval .= sprintf("\tunicode-range: %s;\n", $css_ruleset->{'unicode-range'});}
			if (self::$fontdisplay) {
				$retval .= sprintf("\tfont-display: %s;\n",self::$fontdisplay);
			}			
			$retval .= '}';
		}
		return $retval;
	}
	// -----------------------------------------------------------------------------------
	// find font files in font folder
	static function find_fonts() {
		$st = microtime(true);
		if (isset(self::$fonts)) return;
		self::$fonts = [];
		// set up fonts dir and url
		$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,  RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS);
			$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 and JSON files
		$font_splfiles = [];
		$json_splfiles = [];
		foreach( $file_iterator as $file) {
			// V3: A JSON file might be available from Web Font Loader
			if ($file->getExtension() == 'json') {
				$json_splfiles[] = $file;
			}
			if (in_array(strtolower($file->getExtension()), self::$prioritized_formats)) {
				$font_splfiles[] = $file;
			}
		}
		
		// V3: check JSON files. If it defines "family" read the font name and CSS
		$json_font_families = [];
		foreach ($json_splfiles as $json_splfile) {
			if ($font_details = @json_decode(@file_get_contents($json_splfile->getPathname()))) {
				// It's a JSON from Web Font Loader?
				if (isset($font_details->creator) && (strpos($font_details->creator, 'Web Font Loader')=== 0)) {
					// store font family name 
					$json_font_families[$json_splfile->getBasename('.json')] = $font_details->family;
					// drop all collected font files for that font since they are listed in JSON file
					$font_path = $json_splfile->getPath().'/';
					foreach ($font_splfiles as $idx => $font_splfile) {
						if (strpos($font_splfile->getPath().'/',$font_path) === 0) {
							self::$font_files_cnt ++;
							unset($font_splfiles[$idx]);
						}
					}
					$font_path = str_replace($fonts_basedir,'',$font_path);
					// encode every single path element since we might have spaces or special chars 
					$font_path = implode('/',array_map('rawurlencode',explode('/',$font_path)));
					
					// add CSS blocks (could be multiple unicode ranges) to fonts list

					$font_baseurl = $fonts_baseurl . $font_path;
					foreach ($font_details->css as $css_ruleset) {
						self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}]['has_css'] = true;
						// only formats woff and woff2, so just use format as file extension slot
						if (!isset(self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}])) {
							self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}] = [];	
						}
						$css_ruleset->url = $font_baseurl . $css_ruleset->url;

						$css_block = self::create_css_from_ruleset($css_ruleset);
						self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}][] = $css_block;
					}
				}
			}
		}
		// collect font definitions
		foreach ($font_splfiles as $font_splfile) {
			self::$font_files_cnt ++;
			$font_ext = $font_splfile->getExtension();
			$font_details = self::parse_font_name($font_splfile->getbasename('.'.$font_ext));
			$font_name = $font_details->name;
			if (in_array($font_name,array_values($json_font_families))) {
				// already found this font from Web Font Loader. Skip.
				continue;
			}
			$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)));
			// 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('%s::%s() final fonts: %s]',__CLASS__,__FUNCTION__, print_r(self::$fonts,true)));}
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() %d font files, %d font families.',__CLASS__,__FUNCTION__, self::$font_files_cnt, count(self::$fonts)));}
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
	}
	// -----------------------------------------------------------------------------------
	// returns a list of font families
	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('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$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;
	}
	// -----------------------------------------------------------------------------------
	// creates and emits CSS for custom fonts
	static function get_font_css() {
		// emit CSS for fonts in footer
		$st = microtime(true);
		$style = '';
		$fonts_dir_info = wp_get_upload_dir();
		$fonts_basedir = $fonts_dir_info['basedir'].'/fonts';
		$fonts_baseurl = $fonts_dir_info['baseurl'].'/fonts';
		foreach (self::$fonts 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 (isset($file_list['has_css'])) {
					// V3: Google Font package CSS from Web Font Loader already has CSS
					foreach (array_reverse(self::$prioritized_formats) as $font_ext) {
						// we only have woff and woff2
						if (!isset($file_list[$font_ext])) { continue; }
						foreach ($file_list[$font_ext] as $css) {
							$style .= trim($css).PHP_EOL;
						}
					}
				} else {
					// V2: Only have font info and file names. Build CSS
					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 (self::$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;
					if (self::$fontdisplay) {
						$style .= sprintf('  font-display: %s;'.PHP_EOL,self::$fontdisplay);
					}
					$style .= '}'.PHP_EOL;
				}
			}
		}
		// if Oxygen Builder is active, emit CSS to show custom fonts in light blue.
		$builder_style = defined('SHOW_CT_BUILDER') ? 'div.oxygen-select-box-option.ng-binding.ng-scope[ng-repeat*="elegantCustomFonts"] {color:lightblue !important;}' : '';
		
		
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() style: %s]',__CLASS__,__FUNCTION__, $style));}
		// minimize string if configured
		if (self::$cssminimize) {
			$style = preg_replace('/\r?\n */','',$style); 
		}

		$retval = '';
		if (self::$cssoutput == 'file') {
			// option: write CSS to file
			$css_path = $fonts_basedir.'/ma_customfonts.css';
			file_put_contents($css_path, $style);
			$css_url = str_replace($fonts_basedir,$fonts_baseurl ,$css_path);
			$retval = sprintf('<link id="MA_CustomFonts" itemprop="stylesheet" href="%s?%s" rel="stylesheet" type="text/css" />%s',$css_url, hash_file('CRC32', $css_path, false), $builder_style?'<style>'.$builder_style.'</style>':'');
		}
		if (self::$cssoutput == 'html') {
			// option: write CSS to html
			$retval = '<style id="MA_CustomFonts">'.$style.PHP_EOL.$builder_style.'</style>';
		}


		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	static function get_font_file_info_from_css($css) {
		$retval = [];
		if (!is_array($css)) {$css = [$css];}
		foreach($css as $css_block) {
			if (preg_match('/url\(\'(.*?)\'\)/',$css_block,$matches)) {
				$retval[] = $matches[1];
			}
		}
		$retval = array_unique($retval); 
		return $retval;
	}
	// -----------------------------------------------------------------------------------
	// returns HTML code to display all registered custom fonts
	// $mode 'admin':		formatting to be displayed on WP Admin > Appearance
	// $mode 'shortcode':	formatting to be displayed as shortcode output
	static function get_font_samples($mode = null) {
		$st = microtime(true);
		$output = '';
		$script_version = sprintf('%s, %s', (basename(__FILE__)=='ma-custom-fonts.php') ? 'Plugin' : 'Code Snippet', self::VERSION);
		$output = 	'<style>'.
					'#ma_customfonts-input-font-size {width:60px;text-align:center;min-height:1em;line-height:1em;padding:0;}'.
					'#ma_customfonts-input-sample-text {width:400px;text-align:left;}'.
					'.ma_customfonts-label {display:inline-block;width:150px;line-height:2em;}'.
					'.ma_customfonts-font-row {display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:0;line-height:20px;border-bottom:1px solid #e0e0e0;margin:0 1em;}'.
					'.ma_customfonts-font-row:hover {background-color:lightgray;}'.
					'.ma_customfonts-font-info {font-size:10px;line-height:1em;width:100px;}'.
					'.ma_customfonts-font-sample {font-size:15px;line-height:1em;flex-grow:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}'.
					'.ma_customfonts-format-info {font-size:10px;cursor:help;margin-left:1em;}'.
					'.ma_customfonts-simulated {display: none;}'.
					'</style>'.
					'<div '.($mode=='shortcode'?'style="display:inline-block;border:1px dashed darkgray;padding:10px;"':'').'>'.
						($mode=='shortcode'?'<h2>MA Custom Fonts</h2>':'').
						'<div style="display:inline-block;border:1px solid darkgray;border-radius:10px;padding:10px;">'.
							'<span class="ma_customfonts-label">Version:</span> '.$script_version.'<br/>'.
							'<span class="ma_customfonts-label">Font Families:</span> '.count(self::$fonts).'<br/>'.
							'<span class="ma_customfonts-label">Font Files:</span> '.self::$font_files_cnt.'<br/>'.
							'<span class="ma_customfonts-label">Sample Font Size:</span> '.
								'<input id="ma_customfonts-input-font-size" type="number" value="15" onchange="ma_customfonts_change_font_size();"><br/>'.
							'<span class="ma_customfonts-label">Sample Text:</span> '.
								'<input id="ma_customfonts-input-sample-text" value="'.self::$sample_text.'" onkeyup="ma_customfonts_change_sample_text();"><br/>'.
							'<span class="ma_customfonts-label">Simulated:</span> '.
								'<input id="ma_customfonts-input-simulated" type="checkbox" value="simulated" onchange="ma_customfonts_toggle_simulated();"> Show font weights/styles without files as browser would simulate.<br/>'.
						'</div>';

		// controls
		$controls_script = <<<'END_OF_CONTROLS_SCRIPT'
		<script>
		function changeCss(className, classValue) {
			// we need invisible container to store additional css definitions
			var cssMainContainer = jQuery('#css-modifier-container');
			if (cssMainContainer.length == 0) {
				cssMainContainer = jQuery('<div id="css-modifier-container"></div>');
				cssMainContainer.hide();
				cssMainContainer.appendTo(jQuery('body'));
			}
			// and we need one div for each class
			var classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
			if (classContainer.length == 0) {
				classContainer = jQuery('<div data-class="' + className + '"></div>');
				classContainer.appendTo(cssMainContainer);
			}
			// append additional style
			classContainer.html('<style>' + className + ' {' + classValue + '}</style>');
		}
		function ma_customfonts_change_font_size() {
			var $val = jQuery('#ma_customfonts-input-font-size').val();
			changeCss('.ma_customfonts-font-sample','font-size: '+$val+'px;');
		}
		function ma_customfonts_change_sample_text() {
			var $val = jQuery('#ma_customfonts-input-sample-text').val();
			jQuery('.ma_customfonts-font-sample').text($val);
		}
		function ma_customfonts_toggle_simulated() {
			var $simulated = jQuery('#ma_customfonts-input-simulated').is(':checked');
			jQuery('.ma_customfonts-simulated').css('display',$simulated?'flex':'none');
		}
		</script>
END_OF_CONTROLS_SCRIPT;
		$output .=  $controls_script;
		// prepare tags for every weight/style combination
		$weights = [100,200,300,400,500,600,700,800,900];
		$styles = ['normal','italic'];
		$weights_styles = [];
		foreach ($weights as $weight) { foreach ($styles as $style) { $weights_styles[] = $weight.'/'.$style; } }
		// display fonts in each weight/style combination
		$sample_text = self::$sample_text;

		// build output
		foreach (self::$fonts as $font_name => $font_details) {
			$output .= sprintf('<h3 style="padding-top: 20px;">%1$s</h3>',$font_name);
			ksort($font_details);
			foreach ($weights_styles as $weight_style) {
				list ($weight,$style) = explode('/',$weight_style);;
				$font_file_info = '';
				$font_file_list = [];
				if (isset($font_details[$weight_style])) {
					// walk through possible file formats
					foreach (self::$prioritized_formats as $font_ext) {
						// details available for this specific file format?
						if (isset($font_details[$weight_style][$font_ext])) {
							// details content type
							if (isset($font_details[$weight_style]['has_css'])) {	
								// CSS
								$font_file_list[$font_ext] = self::get_font_file_info_from_css($font_details[$weight_style][$font_ext]);
							} else {
								// just file name
								$font_file_list[$font_ext] = [$font_details[$weight_style][$font_ext]];
							}
						}
					}
					// build font file info output
					foreach ($font_file_list as $format => $files) {
						// cut leading path/url from file info
						$files = str_replace(wp_get_upload_dir(),'',$files);
						// decode html entities (e.g. %20) in file path
						foreach ($files as &$file) {$file = implode('/',array_map('rawurldecode',explode('/',$file)));;}
						// convert array to html
						$font_file_list[$format] = sprintf('<span title="%2$s">%1$s</span>', strtoupper($format), implode("\n",$files));
					}
					$font_file_info = '<span class="ma_customfonts-format-info">(' . implode(', ',array_values($font_file_list)) . ')</span>';
				}

				$output .= sprintf(	'<div class="ma_customfonts-font-row '.($font_file_info?'':'ma_customfonts-simulated').'">'.
										'<span class="ma_customfonts-font-info">%2$s %3$s</span>'.
										'<span class="ma_customfonts-font-sample" style="font-family:\'%1$s\';font-weight:%2$d;font-style:%3$s">%4$s</span>%5$s'.
									'</div>',$font_name, $weight, $style, $sample_text, $font_file_info?$font_file_info:'<span class="ma_customfonts-format-info"><em>(simulated)</em></span>');

			}
		}
		$output .= '</div>';

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $output;

	}
} // end of class MA_CustomFonts

// output the request for optimization process
if (WP_DEBUG && (MA_CustomFonts::$debug || MA_CustomFonts::$timing)) {error_log(sprintf('MA_CustomFonts: Request action="%s", URI="%s"', @$_REQUEST['action'], @$_SERVER['REQUEST_URI']));}
// pre-fill font definitions
MA_CustomFonts::get_font_families();

// create a primitive ECF_Plugin class if plugin "Elegant Custom Fonts" is not installed
if (!class_exists('ECF_Plugin')) :
	class ECF_Plugin {
		// -----------------------------------------------------------------------------------
		static function get_font_families() {
			$st = microtime(true);
			$font_family_list = MA_CustomFonts::get_font_families();
			$et = microtime(true);
			if (WP_DEBUG && MA_CustomFonts::$timing) {error_log(sprintf('MA_CustomFonts/%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
			return $font_family_list;
		}
	}
endif;



// emit custom font css in head 
add_action( 'wp_head', function(){
	echo MA_CustomFonts::get_font_css();
});
add_action( 'wp_footer', function(){
	if ( isset($_REQUEST['post']) && isset($_REQUEST['action']) && ($_REQUEST['action']=='edit')) {
		// also load CSS in footer when we are in Gutenberg Editor
		echo MA_CustomFonts::get_font_css();
	}
});

// Shortcode for testing custom fonts (listing all fonts with their formats, weights, styles)
add_action( 'plugins_loaded', 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 = MA_CustomFonts::get_font_samples('shortcode');
			return $output;
		}
	}
});


//-------------------------------------------------------------------------------------------------------------------
// Admin function Appearance > Custom Fonts to display samples of all detected fonts
function ma_customfonts() {
	$output =	'<h1>' . esc_html(get_admin_page_title()) . '</h1>'.
				MA_CustomFonts::get_font_samples('admin');
	echo $output;
	echo MA_CustomFonts::get_font_css();
}




endif; // end of conditional implementations



//===================================================================================================================
// Admin
//-------------------------------------------------------------------------------------------------------------------
// Add submenu page to the Appearance menu.
add_action('admin_menu', function(){
	add_submenu_page(	'themes.php', 										// parent slug of "Appearance"
						_x('Custom Fonts','page title','ma_customfonts'), 	// page title
						_x('Custom Fonts','menu title','ma_customfonts'), 	// menu title
						'manage_options',									// capabilitiy
						'ma_customfonts',									// menu slug
						'ma_customfonts' 									// function
					);
});
//-------------------------------------------------------------------------------------------------------------------
// Warn if plugins "Elegant Custom Forms", "Use Any Font", "Swiss Knife / Font Manager" are active
add_action('init',function(){
	if (is_admin()) {
		$GLOBALS['ma_custom_fonts_incompatible_plugins'] = [];
		if (function_exists('is_plugin_active') && is_plugin_active('elegant-custom-fonts/elegant-custom-fonts.php'))
			{$GLOBALS['ma_custom_fonts_incompatible_plugins'][] = '"Elegant Custom Fonts"';}
		if (function_exists('is_plugin_active') && is_plugin_active('use-any-font/use-any-font.php'))
			{$GLOBALS['ma_custom_fonts_incompatible_plugins'][] = '"Use Any Font"';}
		if (function_exists('is_plugin_active') && is_plugin_active('swiss-knife/swiss-knife.php') && (get_option('swiss_font_manager')=='yes')) 														
			{$GLOBALS['ma_custom_fonts_incompatible_plugins'][] = '"Swiss Knife" with feature "Font Manager" enabled';}
		if (count($GLOBALS['ma_custom_fonts_incompatible_plugins'])) {
			add_action('admin_notices', function(){
				if (WP_DEBUG ) {error_log('$ma_custom_fonts_incompatible_plugins: '.print_r($GLOBALS['ma_custom_fonts_incompatible_plugins'],true));}
				echo '<div class="notice notice-warning is-dismissible">
						<p>The Code Snippet "Oxygen: Custom Fonts" is not compatible with the Plugin '.implode(' or ',$GLOBALS['ma_custom_fonts_incompatible_plugins']).'.<br/>
						Please deactivate either the Code Snippet or the Plugin (feature).</p>
					</div>';
			});
		}
	}
});
magnifier