1

I have uploaded google fonts on the local server to a custom directory and it seems to work well so far except that the Ultimo theme I am using is picking the base URL to generate font URL path. Ideally, the theme should pick the static content URL when CDN is enabled. I believe this requires few changes in the below file where I could either hardcode the CDN URL or make changes so that the theme uses CDN static content URL to generate a font URL path.

<?php

namespace Infortis\Base\Helper;

use Infortis\Base\Helper\Data as HelperData;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\View\Asset\Repository;
use Psr\Log\LoggerInterface;

class Fonts extends AbstractHelper
{
    /**
     * Custom font directory path, relative to: /app/design/frontend/Infortis/<theme_name>/web/
     */
    const CUSTOM_FONTS_DIR = 'fonts/custom/';

    /**
     * Quotation mark for complex font family names (e.g. containing whitespace)
     */
    const QUOTE = "'";

    /**
     * Main helper of the theme
     *
     * @var HelperData
     */
    protected $theme;

    /**
     * Asset repository
     *
     * @var \Magento\Framework\View\Asset\Repository
     */
    protected $assetRepo;

    /**
     * Logger
     *
     * @var LoggerInterface
     */
    protected $_logger;

    /**
     * Supported file extensions
     *
     * @var array
     */
    protected $supportedExtensions = ['eot', 'woff2', 'woff', 'ttf', 'otf', 'svg'];

    /**
     * @var string
     */
    protected $valueGoogleFontsString;

    /**
     * @var string
     */
    protected $valueLocalFontsString;

    /**
     * @var string
     */
    protected $valueFontStacksString;

    public function __construct(
        Context $context,
        Repository $assetRepo,
        HelperData $helper
    ) {
        $this->assetRepo = $assetRepo;
        $this->theme = $helper;
        $this->_logger = $context->getLogger();

        parent::__construct($context);
    }

    /**
     * Initialize font strings
     *
     * @param string
     * @param string
     * @param string
     */
    public function initFontStrings($valueGoogleFontsString, $valueLocalFontsString, $valueFontStacksString)
    {
        $this->valueGoogleFontsString = $valueGoogleFontsString;
        $this->valueLocalFontsString = $valueLocalFontsString;
        $this->valueFontStacksString = $valueFontStacksString;
    }

    /**
     * Get raw URL string with google fonts defined by user
     *
     * @param string $storeCode
     * @return string
     */
    protected function getGoogleFontsString($storeCode = null)
    {
        if ($this->valueGoogleFontsString === null)
        {
            $this->valueGoogleFontsString = $this->theme->getCfgDesign('defined_fonts/google_fonts_string', $storeCode);
        }

        // Clear the string: remove whitespaces and unneeded quote characters
        $this->valueGoogleFontsString = trim($this->valueGoogleFontsString);
        $this->valueGoogleFontsString = trim($this->valueGoogleFontsString, "'\"");

        return $this->valueGoogleFontsString;
    }

    /**
     * Get raw string with names of custom local fonts defined by user
     *
     * @param string $storeCode
     * @return string
     */
    protected function getLocalFontsString($storeCode = null)
    {
        if ($this->valueLocalFontsString === null)
        {
            $this->valueLocalFontsString = $this->theme->getCfgDesign('defined_fonts/local_fonts_string', $storeCode);
        }

        // Clear the string: remove whitespaces
        $this->valueLocalFontsString = trim($this->valueLocalFontsString);

        return $this->valueLocalFontsString;
    }

    /**
     * Get raw string with font stacks defined by user
     *
     * @param string $storeCode
     * @return string
     */
    protected function getFontStacksString($storeCode = null)
    {
        if ($this->valueFontStacksString === null)
        {
            $this->valueFontStacksString = $this->theme->getCfgDesign('defined_fonts/font_stacks_string', $storeCode);
        }

        // Clear the string: remove whitespaces
        $this->valueFontStacksString = trim($this->valueFontStacksString);

        return $this->valueFontStacksString;
    }

    /**
     * Get fallback font stack for body font
     *
     * @return string
     */
    public function getFallbackFontStack()
    {
        // Clear the string: remove whitespaces
        return trim($this->theme->getCfgDesign('font/fallback_body_font_stack'));
    }

    /**
     * Get final URL string with google fonts to load
     *
     * @return string
     */
    public function getGoogleFontsFinalUrl()
    {
        $url = $this->getGoogleFontsString();

        // Remove https protocol name
        $count = 0;
        $pattern = "https:";
        $url = preg_replace('/^' . preg_quote($pattern) . '/', '', $url, -1, $count);

        // If protocol name not found, search for http
        if ($count === 0)
        {
            $pattern2 = "http:";
            $url = preg_replace('/^' . preg_quote($pattern2) . '/', '', $url, -1);
        }

        return $url;
    }

    /**
     * Get array with custom local fonts defined by user.
     * Array contains values for the "font-face" CSS at-rule:
     * - 'font-family': font family name
     * - 'src': resource containing the font data
     *
     * @param string $storeCode
     * @return array
     */
    public function getLocalFontsFontface($storeCode = null)
    {
        $fontfaceProperties = [];

        $fontNames = $this->getLocalFontsFamilies(false, $storeCode); // Get font names (don't remove file extensions)
        if (empty($fontNames))
        {
            return $fontfaceProperties;
        }

        foreach ($fontNames as $name)
        {
            // Important: don't remove file extensions before calling generateFontfaceSource(...)
            $src = $this->generateFontfaceSource($name);

            // Prepare font name: trim quotes (before removing extension), remove extension, add quotes if needed
            $name = trim($name, "'\"");
            $name = $this->removeFileExtension($name);
            $name = $this->quoteIfNeeded($name);

            if ($src)
            {
                $fontfaceProperties[] = ['font-family' => $name, 'src' => $src];
            }
        }

        return $fontfaceProperties;
    }

    /**
     * Generate "src" property for "font-face" CSS at-rule which specifies
     * the resource containing the font data.
     *
     * @param string $fontName
     * @return string
     */
    protected function generateFontfaceSource($fontName)
    {
        // Trim quotes
        $fontName = trim($fontName, "'\"");

        // Check if the font name contains a file extension
        $origExt = pathinfo($fontName, PATHINFO_EXTENSION);
        if ($origExt)
        {
            $fontName = pathinfo($fontName, PATHINFO_FILENAME);
            $extensions = [strtolower($origExt)];
        }
        else
        {
            $extensions = $this->supportedExtensions;
        }

        // Loop through all extensions and build "src" property
        $result = "src:";
        $resultForEotType = '';
        $i = 0;
        foreach ($extensions as $ext)
        {
            // Asset path
            $fontPath = self::CUSTOM_FONTS_DIR . $this->convertFontNameToFileName($fontName) . '.' . $ext;

            // Try to create asset
            try
            {
                $asset = $this->assetRepo->createAsset($fontPath);
                $fontUrl = $asset->getUrl();
                $sourceAsset = $asset->getSourceFile();
            }
            catch(\Exception $e)
            {
                // The source file does not exist so the asset also does not exist
                $fontUrl = null;
            }

            // Check if asset exists
            if ($fontUrl)
            {
                if ($ext === 'eot')
                {
                    $resultForEotType = "src: url('{$fontUrl}');";
                    $result .= " url('{$fontUrl}') format('embedded-opentype'),";
                }
                elseif ($ext === 'woff2')
                {
                    $result .= " url('{$fontUrl}') format('woff2'),";
                }
                elseif ($ext === 'woff')
                {
                    $result .= " url('{$fontUrl}') format('woff'),";
                }
                elseif ($ext === 'ttf')
                {
                    $result .= " url('{$fontUrl}') format('truetype'),";
                }
                elseif ($ext === 'otf')
                {
                    $result .= " url('{$fontUrl}') format('opentype'),";
                }
                elseif ($ext === 'svg')
                {
                    $result .= " url('{$fontUrl}') format('svg'),";
                }

                $i++;
            }
        }

        // If at least one asset exists
        if ($i)
        {
            $result = rtrim($result, ', '); // Remove the last occurrence of the comma
            $result .= ";\n"; // Add final semicolon

            if ($resultForEotType)
            {
                $result = $resultForEotType . "\n" . $result;
            }
        }
        else
        {
            // If no asset exists, erase the initial value of $result
            $result = '';

            $this->_logger->info(
                "No font file available for font {$fontName}."
                . "Make sure to upload font file to the proper folder, set correct file permissions, deploy static files and flush the cache."
            );
        }

        return $result;
    }

    /**
     * Get names of Google fonts defined by user
     *
     * @param string $storeCode
     * @return array
     */
    protected function getGoogleFontsFamilies($storeCode = null)
    {
        $finalFontNames = [];

        // Get URL string with defined fonts. The string can look like in this example:
        // https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700|PT+Sans+Narrow|Roboto:300,300i,400&amp;subset=greek,latin-ext
        $fontStringRaw = $this->getGoogleFontsString($storeCode);
        if (empty($fontStringRaw))
        {
            return $finalFontNames;
        }

        // Get string starting from 'family=' and remove that start
        $fontStringRaw = strstr($fontStringRaw, 'family=');
        $fontStringRaw = str_replace('family=', '', $fontStringRaw);

        // If string contains '&'
        if (strpos($fontStringRaw, '&') !== false)
        {
            // Get definition of font families, omit definition of script subsets.
            // Retrieve the part of the string before the first occurrence of '&'.
            $fontStringRaw = strstr($fontStringRaw, '&', true);
        }

        // Get array of font definitions (strings) from the string
        // At this stage, the string looks like this: Open+Sans:400,400i,600,700|PT+Sans+Narrow|Roboto:300,300i,400
        $fontStringsArray = explode('|', $fontStringRaw);

        // Create list of font names
        foreach ($fontStringsArray as $fontString)
        {
            $exploded = explode(':', $fontString);

            // After the explode, $exploded[0] contains font name, $exploded[1] contains font styles and weights.
            // Convert font name to readable version, for example: Open+Sans => Open Sans.
            $rawFontName = $exploded[0];
            $count = 0;
            $fontName = str_replace('+', ' ', $rawFontName, $count);

            // If the number of replacements performed is greater than zero, font name will be quoted
            if ($count > 0)
            {
                $fontName = self::QUOTE . $fontName . self::QUOTE;
            }

            // Font string not always has definition of styles and weights, so the array not always has element [1].
            // If it has, list the styles.
            if (isset($exploded[1]))
            {
                $fontName = $fontName . '  (available: ';
                foreach (explode(',', $exploded[1]) as $style)
                {
                    // If style is italic
                    $end = strstr($style, 'i');
                    if ($end === 'i' || $end === 'italic')
                    {
                        $style = str_replace($end, '', $style);
                        $fontName .= $style . ' italic' . ', ';
                    }
                    else
                    {
                        $fontName .= $style . ', ';
                    }
                }
                $fontName = rtrim($fontName, ', '); // Remove the last occurrence of the comma
                $fontName .= ')';
            }
            // else
            // {
            //     $fontName = $fontName . '  (available: 400)';
            // }

            $finalFontNames[] = $fontName;
        }

        return $finalFontNames;
    }

    /**
     * Get names of custom local fonts defined by user
     *
     * @param bool $removeExtension
     * @param string $storeCode
     * @return array
     */
    protected function getLocalFontsFamilies($removeExtension = false, $storeCode = null)
    {
        $finalFontNames = [];

        // Get string with names defined by user. The string can look like in this example:
        // Ubuntu-Medium; Montserrat; Sun Valley; My Font.svg; 'Weston Free'
        $fontStringRaw = $this->getLocalFontsString($storeCode);
        if (empty($fontStringRaw))
        {
            return $finalFontNames;
        }

        // Create list of font names
        foreach (explode(';', $fontStringRaw) as $fontName)
        {
            // Trim whitespaces
            $fontName = trim($fontName);

            // Trim quotes added by user
            $fontName = trim($fontName, "'\"");

            // Check if font name contains a file extension and remove it
            if ($removeExtension)
            {
                $fontName = $this->removeFileExtension($fontName);
            }

            // Add quotes if needed
            $fontName = $this->quoteIfNeeded($fontName);

            $finalFontNames[] = $fontName;
        }

        return $finalFontNames;
    }

    /**
     * Get font stacks defined by user
     *
     * @param string $storeCode
     * @return array
     */
    protected function getCustomFontStacks($storeCode = null)
    {
        $finalFontNames = [];

        // Get string with font stacks defined by user
        $fontStringRaw = $this->getFontStacksString($storeCode);
        if (empty($fontStringRaw))
        {
            return $finalFontNames;
        }

        // Create list of font names
        foreach (explode(';', $fontStringRaw) as $fontName)
        {
            // Trim whitespaces and quotes
            $fontName = trim($fontName);
            $finalFontNames[] = $fontName;
        }

        return $finalFontNames;
    }

    /**
     * Remove file extension from font name
     *
     * @param string $fontName
     * @return string
     */
    protected function removeFileExtension($fontName)
    {
        // Remove extension
        $origExt = pathinfo($fontName, PATHINFO_EXTENSION);
        if ($origExt)
        {
            $fontName = pathinfo($fontName, PATHINFO_FILENAME);
        }

        return $fontName;
    }

    /**
     * Add quotes to font name
     *
     * @param string $fontName
     * @return string
     */
    protected function quoteIfNeeded($fontName)
    {
        // If font name contains space, it will be quoted
        if (strpos($fontName, ' ') !== false)
        {
            $fontName = self::QUOTE . $fontName . self::QUOTE;
        }

        // If font name contains hyphen, it will be quoted
        if (strpos($fontName, '-') !== false)
        {
            $fontName = self::QUOTE . $fontName . self::QUOTE;
        }

        return $fontName;
    }

    /**
     * Convert font name to file name
     *
     * @param string $fontName
     * @return string
     */
    protected function convertFontNameToFileName($fontName)
    {
        // Trim whitespaces and quotes, replace spaces with hyphen
        $fontName = trim($fontName);
        $fontName = trim($fontName, "'\"");
        $fontName = str_replace(' ', '-', $fontName);

        return $fontName;
    }

    /**
     * Remove additional info in parentheses
     *
     * @return string
     */
    public function removeAdditionalFontInfo($name)
    {
        // Remove text in parentheses
        $name = preg_replace('/\(.*\)/', '', $name);
        return trim($name);
    }

    /**
     * Get names of fonts defined by user
     *
     * @param string $storeCode
     * @return array
     */
    public function getDefinedFontFamilies($storeCode = null)
    {
        $finalFontNames = [];

        $googleFontsNames = $this->getGoogleFontsFamilies($storeCode);
        $localFontsNames = $this->getLocalFontsFamilies(true, $storeCode); // Remove file extensions from local font names
        $fontStacks = $this->getCustomFontStacks($storeCode);
        $finalFontNames = array_merge($googleFontsNames, $localFontsNames, $fontStacks);

        return $finalFontNames;
    }
}
5
  • Question is not clear, please explain
    – temper
    Oct 24, 2019 at 9:50
  • what part of the question is not clear?
    – Zoya
    Oct 24, 2019 at 13:35
  • What you exactly want? You can change fount url from header XML file or less file. Why you are doing code for that.
    – temper
    Oct 24, 2019 at 13:40
  • I want to serve google fonts via CDN.
    – Zoya
    Oct 24, 2019 at 14:46
  • You can simply add them in your default_head_view.xml file
    – temper
    Oct 24, 2019 at 15:12

1 Answer 1

0

You can inject the font file from the CDN through XML, but there's a bug in Magento that makes you escape the ampersand three times before it works.

Here's the XML code you for instance put in Magento_Theme/layout/default_head_blocks.xml;

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <css src="https://fonts.googleapis.com/css?family=Montserrat:200,300,400,500,600&amp;amp;amp;subset=latin-ext" src_type="url" />
    </head>
</page>

This bug is fixed in the upcoming release 2.3.4 so when you run >=2.3.4 you can do this;

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <css src="https://fonts.googleapis.com/css?family=Montserrat:200,300,400,500,600&amp;subset=latin-ext" src_type="url" />
    </head>
</page>

2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.