Esempio n. 1
0
const devideTTC = async (_ttcFontPath) => {
    const buf = fs.readFileSync(_ttcFontPath);
    const ttf_count = unpack('!L', buf, 0x08)[0];
    const ttf_offset_array = unpack('!' + ttf_count + 'L', buf, 0x0C);
    for (let i = 0; i < ttf_count; i++) {

        const table_header_offset = ttf_offset_array[i];

        const table_count = unpack('!H', buf, table_header_offset + 0x04)[0];
        const header_length = 0x0C + table_count * 0x10;

        let table_length = 0;
        for (let j = 0; j < table_count; j++) {
            const length = unpack('!L', buf, table_header_offset + 0x0C + 0x0C + j * 0x10)[0];
            table_length += ceil4(length);
        }
        const total_length = header_length + table_length;

        const new_buf = new Buffer(total_length);
        const header = unpack(header_length + 'c', buf, table_header_offset);
        packTo(header_length + 'c', new_buf, 0, header);
        let current_offset = header_length;

        for (let j = 0; j < table_count; j++) {
            const offset = unpack('!L', buf, table_header_offset + 0x0C + 0x08 + j * 0x10)[0];
            const length = unpack('!L', buf, table_header_offset + 0x0C + 0x0C + j * 0x10)[0];
            packTo('!L', new_buf, 0x0C + 0x08 + j * 0x10, [current_offset]);
            const current_table = unpack(length + 'c', buf, offset);
            packTo(length + 'c', new_buf, current_offset, current_table);

            current_offset += ceil4(length);
        }

        const ttfFont = fontkit.create(new_buf);
        const postscriptName = ttfFont.postscriptName.toString();
        const ttfPath = path.join(tmpFontCachePath, `${getHashCode(_ttcFontPath, postscriptName)}.ttf`);

        console.log('fullName: ', ttfFont.fullName.toString());
        console.log('\tpostscriptName: ', postscriptName);
        console.log('\tttfPath: ', ttfPath);

        fs.writeFileSync(ttfPath, new_buf);
    }
};
Esempio n. 2
0
    function getMetricsFromFont(entry, charsListOnPage) {
        var deferred = Q.defer();
        
        try {
            var startTime = Date.now();
            var font = fontkit.create(entry.weightCheck.bodyBuffer);

            var result = {
                name: font.fullName || font.postscriptName || font.familyName,
                numGlyphs: font.numGlyphs,
                averageGlyphComplexity: getAverageGlyphComplexity(font),
                compressedWeight: entry.weightCheck.afterCompression || entry.weightCheck.bodySize,
                unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
                numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
            };

            var endTime = Date.now();
            debug('Font analysis took %dms', endTime - startTime);

            // Mark fonts that are not used on the page (#224)
            var fontIsUsed = false;
            for (var range in result.unicodeRanges) {
                if (result.unicodeRanges[range].numGlyphsInCommonWithPageContent > 0) {
                    fontIsUsed = true;
                    break;
                }
            }
            result.isUsed = fontIsUsed;

            deferred.resolve(result);
        } catch(error) {
            deferred.reject(error);
        }

        return deferred.promise;
    }
Esempio n. 3
0
  return async function subsetFonts(assetGraph) {
    const htmlAssetTextsWithProps = [];
    const subsetUrl = urltools.ensureTrailingSlash(
      assetGraph.root + subsetPath
    );

    await assetGraph.populate({
      followRelations: {
        $or: [
          {
            to: {
              url: googleFontsCssUrlRegex
            }
          },
          {
            type: 'CssFontFaceSrc',
            from: {
              url: googleFontsCssUrlRegex
            }
          }
        ]
      }
    });

    // Collect texts by page

    const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
    const htmlAssets = assetGraph.findAssets({ type: 'Html', isInline: false });
    const traversalRelationQuery = {
      $or: [
        {
          type: { $in: ['HtmlStyle', 'CssImport'] }
        },
        {
          to: {
            type: 'Html',
            isInline: true
          }
        }
      ]
    };

    // Keep track of the injected CSS assets that should eventually be minified
    // Minifying them along the way currently doesn't work because some of the
    // manipulation is sensitive to the exact text contents. We should fix that.
    const subsetFontsToBeMinified = new Set();

    for (const htmlAsset of htmlAssets) {
      const accumulatedFontFaceDeclarations = [];

      assetGraph.eachAssetPreOrder(htmlAsset, traversalRelationQuery, asset => {
        if (asset.type === 'Css' && asset.isLoaded) {
          const seenNodes = new Set();

          const fontRelations = asset.outgoingRelations.filter(
            relation => relation.type === 'CssFontFaceSrc'
          );

          for (const fontRelation of fontRelations) {
            const node = fontRelation.node;

            if (!seenNodes.has(node)) {
              seenNodes.add(node);

              const fontFaceDeclaration = {
                relations: fontRelations.filter(r => r.node === node),
                ...initialValueByProp
              };

              node.walkDecls(declaration => {
                const propName = declaration.prop.toLowerCase();
                if (propName === 'font-family') {
                  fontFaceDeclaration[propName] = unquote(declaration.value);
                } else {
                  fontFaceDeclaration[propName] = declaration.value;
                }
              });
              accumulatedFontFaceDeclarations.push(fontFaceDeclaration);
            }
          }
        }
      });

      if (accumulatedFontFaceDeclarations.length > 0) {
        const seenFontFaceCombos = new Set();
        for (const fontFace of accumulatedFontFaceDeclarations) {
          const key = `${fontFace['font-family']}/${fontFace['font-style']}/${
            fontFace['font-weight']
          }`;
          if (seenFontFaceCombos.has(key)) {
            throw new Error(
              `Multiple @font-face with the same font-family/font-style/font-weight (maybe with different unicode-range?) is not supported yet: ${key}`
            );
          }
          seenFontFaceCombos.add(key);
        }

        const textByProps = fontTracer(
          htmlAsset.parseTree,
          gatherStylesheetsWithPredicates(htmlAsset.assetGraph, htmlAsset),
          memoizedGetCssRulesByProperty,
          htmlAsset
        );

        htmlAssetTextsWithProps.push({
          htmlAsset,
          fontUsages: groupTextsByFontFamilyProps(
            textByProps,
            accumulatedFontFaceDeclarations
          ),
          accumulatedFontFaceDeclarations
        });
      }
    }

    if (htmlAssetTextsWithProps.length <= 1) {
      subsetPerPage = false;
    }

    if (!subsetPerPage) {
      const globalFontUsage = {};

      // Gather all texts
      for (const htmlAssetTextWithProps of htmlAssetTextsWithProps) {
        for (const fontUsage of htmlAssetTextWithProps.fontUsages) {
          if (!globalFontUsage[fontUsage.fontUrl]) {
            globalFontUsage[fontUsage.fontUrl] = [];
          }

          globalFontUsage[fontUsage.fontUrl].push(fontUsage.text);
        }
      }

      // Merge subset values, unique glyphs, sort
      for (const htmlAssetTextWithProps of htmlAssetTextsWithProps) {
        for (const fontUsage of htmlAssetTextWithProps.fontUsages) {
          fontUsage.text = _.uniq(globalFontUsage[fontUsage.fontUrl].join(''))
            .sort()
            .join('');
        }
      }
    }

    if (fontDisplay) {
      for (const htmlAssetTextWithProps of htmlAssetTextsWithProps) {
        for (const fontUsage of htmlAssetTextWithProps.fontUsages) {
          fontUsage.props['font-display'] = fontDisplay;
        }
      }
    }

    // Generate codepoint sets for original font, the used subset and the unused subset
    for (const htmlAssetTextWithProps of htmlAssetTextsWithProps) {
      for (const fontUsage of htmlAssetTextWithProps.fontUsages) {
        const originalFont = assetGraph.findAssets({
          url: fontUsage.fontUrl
        })[0];
        if (originalFont.isLoaded) {
          let originalCodepoints;
          try {
            // Guard against 'Unknown font format' errors
            originalCodepoints = fontkit.create(originalFont.rawSrc)
              .characterSet;
          } catch (err) {}
          if (originalCodepoints) {
            const usedCodepoints = fontUsage.text
              .split('')
              .map(c => c.codePointAt(0));
            const unusedCodepoints = originalCodepoints.filter(
              n => !usedCodepoints.includes(n)
            );

            fontUsage.codepoints = {
              original: originalCodepoints,
              used: usedCodepoints,
              unused: unusedCodepoints
            };
          }
        }
      }
    }

    // Generate subsets:
    await getSubsetsForFontUsage(assetGraph, htmlAssetTextsWithProps, formats);

    // Warn about missing glyphs
    const missingGlyphsErrors = [];

    for (const { htmlAsset, fontUsages } of htmlAssetTextsWithProps) {
      for (const fontUsage of fontUsages) {
        if (fontUsage.subsets) {
          const characterSet = fontkit.create(
            Object.values(fontUsage.subsets)[0]
          ).characterSet;

          for (const char of [...fontUsage.pageText]) {
            // Turns out that browsers don't mind that these are missing:
            if (char === '\t' || char === '\n') {
              continue;
            }

            const codePoint = char.codePointAt(0);

            const isMissing = !characterSet.includes(codePoint);

            if (isMissing) {
              let location;
              const charIdx = htmlAsset.text.indexOf(char);

              if (charIdx === -1) {
                location = `${htmlAsset.urlOrDescription} (generated content)`;
              } else {
                const position = new LinesAndColumns(
                  htmlAsset.text
                ).locationForIndex(charIdx);
                location = `${htmlAsset.urlOrDescription}:${position.line +
                  1}:${position.column + 1}`;
              }

              missingGlyphsErrors.push({
                codePoint,
                char,
                htmlAsset,
                fontUsage,
                location
              });
            }
          }
        }
      }
    }

    if (missingGlyphsErrors.length) {
      const errorLog = missingGlyphsErrors.map(
        ({ char, fontUsage, location }) =>
          `- \\u{${char
            .charCodeAt(0)
            .toString(16)}} (${char}) in font-family '${
            fontUsage.props['font-family']
          }' (${fontUsage.props['font-weight']}/${
            fontUsage.props['font-style']
          }) at ${location}`
      );

      const message = `Missing glyph fallback detected.
When your primary webfont doesn't contain the glyphs you use, your browser will load your fallback fonts, which will be a potential waste of bandwidth.
These glyphs are used on your site, but they don't exist in the font you applied to them:`;

      assetGraph.warn(new Error(`${message}\n${errorLog.join('\n')}`));
    }

    // Insert subsets:

    for (const {
      htmlAsset,
      fontUsages,
      accumulatedFontFaceDeclarations
    } of htmlAssetTextsWithProps) {
      const insertionPoint = assetGraph.findRelations({
        type: 'HtmlStyle',
        from: htmlAsset
      })[0];
      const subsetFontUsages = fontUsages.filter(
        fontUsage => fontUsage.subsets
      );
      const unsubsettedFontUsages = fontUsages.filter(
        fontUsage => !subsetFontUsages.includes(fontUsage)
      );

      // Remove all existing preload hints to fonts that might have new subsets
      for (const fontUsage of fontUsages) {
        for (const relation of assetGraph.findRelations({
          type: { $in: ['HtmlPrefetchLink', 'HtmlPreloadLink'] },
          from: htmlAsset,
          to: {
            url: fontUsage.fontUrl
          }
        })) {
          if (relation.type === 'HtmlPrefetchLink') {
            const err = new Error(
              `Detached ${
                relation.node.outerHTML
              }. Will be replaced with preload with JS fallback.\nIf you feel this is wrong, open an issue at https://github.com/assetgraph/assetgraph/issues`
            );
            err.asset = relation.from;
            err.relation = relation;

            assetGraph.info(err);
          }

          relation.detach();
        }
      }

      if (unsubsettedFontUsages.length > 0) {
        // Insert <link rel="preload">
        const preloadRelations = unsubsettedFontUsages.map(fontUsage => {
          // Always preload unsubsetted font files, they might be any format, so can't be clever here
          return htmlAsset.addRelation(
            {
              type: 'HtmlPreloadLink',
              hrefType: 'rootRelative',
              to: fontUsage.fontUrl,
              as: 'font'
            },
            'before',
            insertionPoint
          );
        });

        // Generate JS fallback for browser that don't support <link rel="preload">
        const preloadData = unsubsettedFontUsages.map((fontUsage, idx) => {
          const preloadRelation = preloadRelations[idx];

          const formatMap = {
            '.woff': 'woff',
            '.woff2': 'woff2',
            '.ttf': 'truetype',
            '.svg': 'svg',
            '.eot': 'embedded-opentype'
          };
          const name = fontUsage.props['font-family'];
          const props = Object.keys(initialValueByProp).reduce(
            (result, prop) => {
              if (
                fontUsage.props[prop] !==
                normalizeFontPropertyValue(prop, initialValueByProp[prop])
              ) {
                result[prop] = fontUsage.props[prop];
              }
              return result;
            },
            {}
          );

          return `new FontFace(
            "${name}",
            "url('" + "${preloadRelation.href}".toString('url') + "') format('${
            formatMap[preloadRelation.to.extension]
          }')",
            ${JSON.stringify(props)}
          ).load();`;
        });

        const originalFontJsPreloadAsset = htmlAsset.addRelation(
          {
            type: 'HtmlScript',
            hrefType: 'inline',
            to: {
              type: 'JavaScript',
              text: `try{${preloadData.join('')}}catch(e){}`
            }
          },
          'before',
          insertionPoint
        ).to;

        for (const [
          idx,
          relation
        ] of originalFontJsPreloadAsset.outgoingRelations.entries()) {
          relation.hrefType = 'rootRelative';
          relation.to = preloadRelations[idx].to;
          relation.refreshHref();
        }

        originalFontJsPreloadAsset.minify();
      }
      if (subsetFontUsages.length < 1) {
        return { fontInfo: [] };
      }

      const subsetCssText = getFontUsageStylesheet(
        subsetFontUsages,
        accumulatedFontFaceDeclarations
      );
      let cssAsset = assetGraph.addAsset({
        type: 'Css',
        url: `${subsetUrl}subfontTemp.css`,
        text: subsetCssText
      });

      subsetFontsToBeMinified.add(cssAsset);

      if (!inlineSubsets) {
        for (const fontRelation of cssAsset.outgoingRelations) {
          const fontAsset = fontRelation.to;
          const extension = fontAsset.contentType.split('/').pop();

          const nameProps = ['font-family', 'font-weight', 'font-style']
            .map(prop =>
              fontRelation.node.nodes.find(decl => decl.prop === prop)
            )
            .map(decl => decl.value);

          const fontWeightRangeStr = nameProps[1]
            .split(/\s+/)
            .map(token => normalizeFontPropertyValue('font-weight', token))
            .join('_');
          const fileNamePrefix = `${unquote(nameProps[0])
            .replace(/__subset$/, '')
            .replace(/ /g, '_')}-${fontWeightRangeStr}${
            nameProps[2] === 'italic' ? 'i' : ''
          }`;

          const fontFileName = `${fileNamePrefix}-${fontAsset.md5Hex.slice(
            0,
            10
          )}.${extension}`;

          // If it's not inline, it's one of the unused variants that gets a mirrored declaration added
          // for the __subset @font-face. Do not move it to /subfont/
          if (fontAsset.isInline) {
            const fontAssetUrl = subsetUrl + fontFileName;
            const existingFontAsset = assetGraph.findAssets({
              url: fontAssetUrl
            })[0];
            if (existingFontAsset && fontAsset.isInline) {
              fontRelation.to = existingFontAsset;
              assetGraph.removeAsset(fontAsset);
            } else {
              fontAsset.url = subsetUrl + fontFileName;
            }
          }

          if (inlineCss) {
            fontRelation.hrefType = 'rootRelative';
          } else {
            fontRelation.hrefType = 'relative';
          }
        }
      }

      const cssAssetUrl = `${subsetUrl}fonts-${cssAsset.md5Hex.slice(
        0,
        10
      )}.css`;
      const existingCssAsset = assetGraph.findAssets({ url: cssAssetUrl })[0];
      if (existingCssAsset) {
        assetGraph.removeAsset(cssAsset);
        subsetFontsToBeMinified.delete(cssAsset);
        cssAsset = existingCssAsset;
      } else {
        cssAsset.url = cssAssetUrl;
      }

      if (!inlineSubsets) {
        for (const fontRelation of cssAsset.outgoingRelations) {
          const fontAsset = fontRelation.to;

          if (fontAsset.contentType === 'font/woff2') {
            // Only <link rel="preload"> for woff2 files
            // Preload support is a subset of woff2 support:
            // - https://caniuse.com/#search=woff2
            // - https://caniuse.com/#search=preload

            htmlAsset.addRelation(
              {
                type: 'HtmlPreloadLink',
                hrefType: 'rootRelative',
                to: fontAsset,
                as: 'font'
              },
              'before',
              insertionPoint
            );
          }
        }
      }
      const cssRelation = htmlAsset.addRelation(
        {
          type: 'HtmlStyle',
          hrefType: inlineCss ? 'inline' : 'rootRelative',
          to: cssAsset
        },
        'before',
        insertionPoint
      );

      // JS-based font preloading for browsers without <link rel="preload"> support
      if (inlineCss) {
        // If the CSS is inlined we can use the font declarations directly to load the fonts
        htmlAsset
          .addRelation(
            {
              type: 'HtmlScript',
              hrefType: 'inline',
              to: {
                type: 'JavaScript',
                text:
                  'try { document.fonts.forEach(function (f) { f.family.indexOf("__subset") !== -1 && f.load(); }); } catch (e) {}'
              }
            },
            'after',
            cssRelation
          )
          .to.minify();
      } else {
        // The CSS is external, can't use the font face declarations without waiting for a blocking load.
        // Go for direct FontFace construction instead
        const fontFaceContructorCalls = [];

        cssAsset.parseTree.walkAtRules('font-face', rule => {
          let name;
          let url;
          const props = {};

          rule.walkDecls(({ prop, value }) => {
            const propName = prop.toLowerCase();
            if (propName === 'font-weight') {
              value = value
                .split(/\s+/)
                .map(token => normalizeFontPropertyValue('font-weight', token))
                .join(' ');
              if (/^\d+$/.test(value)) {
                value = parseInt(value, 10);
              }
            }

            if (propName in initialValueByProp) {
              if (
                normalizeFontPropertyValue(propName, value) !==
                normalizeFontPropertyValue(
                  propName,
                  initialValueByProp[propName]
                )
              ) {
                props[propName] = value;
              }
            }

            if (propName === 'font-family') {
              name = unquote(value);
            } else if (propName === 'src') {
              const fontRelations = cssAsset.outgoingRelations.filter(
                relation => relation.node === rule
              );
              const urlStrings = value
                .split(/,\s*/)
                .filter(entry => entry.startsWith('url('));
              const urlValues = urlStrings.map((urlString, idx) =>
                urlString.replace(
                  fontRelations[idx].href,
                  '" + "/__subfont__".toString("url") + "'
                )
              );
              url = `"${urlValues.join(', ')}"`;
            }
          });

          fontFaceContructorCalls.push(
            `new FontFace("${name}", ${url}, ${JSON.stringify(props)}).load();`
          );
        });

        const jsPreloadAsset = htmlAsset
          .addRelation(
            {
              type: 'HtmlScript',
              hrefType: 'inline',
              to: {
                type: 'JavaScript',
                text: `try {${fontFaceContructorCalls.join('')}} catch (e) {}`
              }
            },
            'before',
            cssRelation
          )
          .to.minify();

        for (const [
          idx,
          relation
        ] of jsPreloadAsset.outgoingRelations.entries()) {
          relation.to = cssAsset.outgoingRelations[idx].to;
          relation.hrefType = 'rootRelative';
          relation.refreshHref();
        }
      }
    }

    // Async load Google Web Fonts CSS

    const googleFontStylesheets = assetGraph.findAssets({
      type: 'Css',
      url: { $regex: googleFontsCssUrlRegex }
    });
    for (const googleFontStylesheet of googleFontStylesheets) {
      const seenPages = new Set(); // Only do the work once for each font on each page
      for (const googleFontStylesheetRelation of googleFontStylesheet.incomingRelations) {
        let htmlParents;

        if (googleFontStylesheetRelation.type === 'CssImport') {
          // Gather Html parents. Relevant if we are dealing with CSS @import relations
          htmlParents = getParents(
            assetGraph,
            googleFontStylesheetRelation.to,
            {
              type: 'Html',
              isInline: false,
              isLoaded: true
            }
          );
        } else if (googleFontStylesheetRelation.from.type === 'Html') {
          htmlParents = [googleFontStylesheetRelation.from];
        } else {
          htmlParents = [];
        }
        for (const htmlParent of htmlParents) {
          if (seenPages.has(htmlParent)) {
            continue;
          }
          seenPages.add(htmlParent);

          let insertPoint = htmlParent.outgoingRelations.find(
            relation => relation.type === 'HtmlStyle'
          );

          // Resource hint: preconnect to the Google font stylesheet hostname
          insertPoint = insertPreconnect(
            htmlParent,
            googleFontStylesheetRelation.to.hostname,
            insertPoint
          );

          // Resource hint: preconnect to the Google font hostname
          insertPreconnect(
            htmlParent,
            googleFontStylesheetRelation.to.outgoingRelations[0].to.hostname,
            insertPoint
          );

          asyncLoadStyleRelationWithFallback(
            htmlParent,
            googleFontStylesheetRelation
          );
        }
      }
      googleFontStylesheet.unload();
    }

    // Use subsets in font-family:

    const webfontNameMap = {};

    for (const { fontUsages } of htmlAssetTextsWithProps) {
      for (const { subsets, fontFamilies, props } of fontUsages) {
        if (subsets) {
          for (const fontFamily of fontFamilies) {
            webfontNameMap[fontFamily.toLowerCase()] = `${
              props['font-family']
            }__subset`;
          }
        }
      }
    }

    let customPropertyDefinitions; // Avoid computing this unless necessary
    // Inject subset font name before original webfont
    const cssAssets = assetGraph.findAssets({
      type: 'Css',
      isLoaded: true
    });
    let changesMadeToCustomPropertyDefinitions = false;
    for (const cssAsset of cssAssets) {
      let changesMade = false;
      cssAsset.eachRuleInParseTree(cssRule => {
        if (cssRule.parent.type === 'rule' && cssRule.type === 'decl') {
          const propName = cssRule.prop.toLowerCase();
          if (
            (propName === 'font' || propName === 'font-family') &&
            cssRule.value.includes('var(')
          ) {
            if (!customPropertyDefinitions) {
              customPropertyDefinitions = findCustomPropertyDefinitions(
                cssAssets
              );
            }
            for (const customPropertyName of extractReferencedCustomPropertyNames(
              cssRule.value
            )) {
              for (const relatedCssRule of [
                cssRule,
                ...(customPropertyDefinitions[customPropertyName] || [])
              ]) {
                const modifiedValue = injectSubsetDefinitions(
                  relatedCssRule.value,
                  webfontNameMap
                );
                if (modifiedValue !== relatedCssRule.value) {
                  relatedCssRule.value = modifiedValue;
                  changesMadeToCustomPropertyDefinitions = true;
                }
              }
            }
          } else if (propName === 'font-family') {
            const fontFamilies = cssListHelpers.splitByCommas(cssRule.value);
            for (let i = 0; i < fontFamilies.length; i += 1) {
              const subsetFontFamily =
                webfontNameMap[unquote(fontFamilies[i]).toLowerCase()];
              if (
                subsetFontFamily &&
                !fontFamilies.includes(subsetFontFamily)
              ) {
                fontFamilies.splice(
                  i,
                  0,
                  cssQuoteIfNecessary(subsetFontFamily)
                );
                i += 1;
                cssRule.value = fontFamilies.join(', ');
                changesMade = true;
              }
            }
          } else if (propName === 'font') {
            const fontProperties = cssFontParser(cssRule.value);
            const fontFamilies =
              fontProperties && fontProperties['font-family'].map(unquote);
            if (fontFamilies) {
              const subsetFontFamily =
                webfontNameMap[fontFamilies[0].toLowerCase()];
              if (
                subsetFontFamily &&
                !fontFamilies.includes(subsetFontFamily)
              ) {
                // FIXME: Clean up and move elsewhere
                fontFamilies.unshift(subsetFontFamily);
                const stylePrefix = fontProperties['font-style']
                  ? `${fontProperties['font-style']} `
                  : '';
                const weightPrefix = fontProperties['font-weight']
                  ? `${fontProperties['font-weight']} `
                  : '';
                const lineHeightSuffix = fontProperties['line-height']
                  ? `/${fontProperties['line-height']}`
                  : '';
                cssRule.value = `${stylePrefix}${weightPrefix}${
                  fontProperties['font-size']
                }${lineHeightSuffix} ${fontFamilies
                  .map(cssQuoteIfNecessary)
                  .join(', ')}`;
                changesMade = true;
              }
            }
          }
        }
      });
      if (changesMade) {
        cssAsset.markDirty();
      }
    }

    // This is a bit crude, could be more efficient if we tracked the containing asset in findCustomPropertyDefinitions
    if (changesMadeToCustomPropertyDefinitions) {
      for (const cssAsset of cssAssets) {
        cssAsset.markDirty();
      }
    }

    // This is a bit awkward now, but if it's done sooner, it breaks the CSS source regexping:
    for (const cssAsset of subsetFontsToBeMinified) {
      await cssAsset.minify();
    }

    // Hand out some useful info about the detected subsets:
    return {
      fontInfo: htmlAssetTextsWithProps.map(({ fontUsages, htmlAsset }) => ({
        htmlAsset: htmlAsset.urlOrDescription,
        fontUsages: fontUsages.map(fontUsage => _.omit(fontUsage, 'subsets'))
      }))
    };
  };