【问题标题】:Make Different Fonts Display at the Same Actual Size?使不同的字体以相同的实际大小显示?
【发布时间】:2021-04-21 15:01:20
【问题描述】:

当我在一个网页中以相同的字体大小使用两种不同的字体时,它们通常以不同的实际大小显示:

此示例使用两种 Google 字体,Gentium 和 Metamorphous,字体大小相同,指定为 20 像素。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <link id="Gentium Book Basic" rel="stylesheet" type="text/css" 
        href="http://fonts.googleapis.com/css?family=Gentium Book Basic" 
        media="all">
  <link id="Metamorphous" rel="stylesheet" type="text/css" 
        href="http://fonts.googleapis.com/css?family=Metamorphous" media="all">
  </head>
<body style="font-size: 20px">
  <span style="font-family: Gentium Book Basic">Test Text Length (Gentium)</span>
 <br>
 <span style="font-family: Metamorphous">Test Text Length (Metamorphous) </span>
</body>
</html>

可以在here 找到此示例的 JSBin。

我对在像 px 这样的绝对长度中指定字体大小的理解是,字体将被缩放以匹配该长度。我的期望是相同字体大小的两种不同字体将具有匹配的高度或匹配的长度(我知道字体的纵横比可能不同)。但这里的情况似乎并非如此。有没有什么方法可以让两种任意字体显示在相同的高度或相同的长度上,而无需手动计算和应用校正?

编辑:显示以相同字体大小显示的两种字体的下降到上升距离的示例。

显然,这两种字体的显示距离不同。

编辑:显示两种字体中带有和不带有重音符号的字母的示例:

同样,很明显,字母的大小不同。

编辑:继续this article 中描述的内容,问题是 font-size 控制字体 em 值的显示大小。但是 em 值是任意的(它不必对应于字体内的任何东西,特别是不一定是小写 'm' 的高度),并且不包括升序和降序,可以是任何大小(取自上述文章的示例):

所以结果是“100px”字体可以是任何有效大小。上述文章的作者计算出当时 Google 网络字体的有效大小范围为 0.618 到 3.378。

由于字体指标(例如 em 大小、大写字母高度、升序和降序值)未在 CSS 中公开,因此在 CSS 中似乎没有任何方法可以使两种任意字体具有相同的有效大小.对于任何特定字体,您可以使用字体编辑器查找字体度量值,并使用这些数字根据需要缩放字体。对于任意字体,一个选项是显示一些文本并使用测量的边界框来确定有效大小并计算适当的比例因子。

感谢所有为解释这一点做出贡献的人!

【问题讨论】:

  • 我认为除了手动调整之外您无能为力。我已经建立了一个小沙箱进行实验。我使用ch 单元绘制了两个div 元素。我们的想法是查看每种字体的 1 个字符将呈现多大。不同的font-size 单位似乎没有任何效果。 jsfiddle.net/t31779e1

标签: css font-size


【解决方案1】:

不要将font-size 视为单个字符本身的实际大小,而是将其视为包含每个字符的块的大小,就像排版字母一样:

块的大小在您的 CSS 中定义(使用 px、pts、ems 等),但这些块中字符的实际大小可能因使用的字体而异。

字体任何给定部分的实际物理高度取决于用户定义的 DPI 设置、当前元素的字体大小和所使用的特定字体。

https://en.wikipedia.org/wiki/Em_(typography)#CSS

您可以使用font-size-adjust 属性来帮助更改其中一种字体以使其更接近另一种字体:https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust 尽管目前它的支持仅限于 Firefox:http://caniuse.com/#feat=font-size-adjust

【讨论】:

  • 这似乎不是真的。请参阅上面评论中的 JSFiddle (jsfiddle.net/t31779e1),它表明“块”大小(如您所说)对于相同字体大小的不同字体是不同的。
  • @Dr.Pain 定义每个块的宽度和高度有点违背您的目的。 Here's an updated fiddle 移除了限制宽度和高度,设置为 inline-block 以便您可以并排查看它们,并使用轮廓而不是背景。我还为每个 div 添加了一个字母,并将 line-height 属性设置为匹配。您可以看到它们的高度匹配(它们已被调整为具有相同的基线)并且它们的高度完全等于设置的字体大小(20px = 20px,2em/2rem = 32px)。
  • @Dr.Pain 如果您想看一下,这里有一篇非常深入的文章:iamvdo.me/en/blog/…
  • 感谢您的那篇文章。我不完全确定我理解了其中的所有内容,但很明显问题出在字体本身中 em 正方形和上升/下降的定义。
【解决方案2】:

字号是字形从上行(如字母“h”的顶部)到下行(如字母“g”的底部)的大小。如果您将字体大小设置为 20 像素,则从字母“h”顶部到字母“g”底部的长度将为 20 像素。有些字母有终端或马刺,字母的末端可能会在某些字母上延伸一到两个像素。

在您的示例中,两种字体之间存在 px 差异。 Metamorphous 字体在一些 Gentium 没有的字母上方有一个标记,这就是高度差异的原因。

You can read more here.

编辑:参见此处,C 上方的“caron”与右侧的两个 Gentium 字母相比。

【讨论】:

  • 感谢您的解释。但是,对于许多字体组合来说显然不是这样:
  • 抱歉,Stackoverflow 不允许我继续之前的评论。我添加了一个示例,显示以相同字体大小显示的两种字体的上升到下降距离是如何完全不同的。发生这种情况有什么原因吗?
  • 我做到了。不同之处在于 Metamorphous 的字母包含那些上方也有标记的字母,该标记是字母的一部分。因为它是字母表的一部分,所以它是字体大小的一部分,因此是较大的大小。 Gentium 没有这些标记,所以 'g' 到 'h' 是字体大小,而在 Metamorphous 中,'g' 到 是字体大小而不是 'h'。跨度>
  • 我完全不明白你的推理。首先,Gentium 还包含带有重音符号的字符。如果字母上方的重​​音被算作字母的一部分,那么为了使字母适合 20px,字母肯定会变小,而不是变大。这是一个 bin,显示了两种字体中的重音和非重音字母:jsbin.com/vorahadede/edit?html,output 它们的大小显然不同。
  • @Dr.Pain 你注意到了在挑选出单个字母时存在的问题之一,我也不应该这样做,尤其是对于包含此类内容的字体。这可能会让人感到困惑。
【解决方案3】:

我花了很多时间在 StackOverflow 中寻找类似情况的答案,但最终没有找到完美的答案。我最终做的是测量两种字体,然后调整第二种字体的上边距和比例以匹配第一种。 (通过使用缩放而不是改变字体大小,它可以让我们在调整大小后不需要重新计算文本度量)

我为后代整理了几支笔。这是第二个,它处理字体大小的规范化和两种字体之间的对齐:https://codepen.io/zacholas/pen/oNBPWga

第一个,只处理测量,是:https://codepen.io/zacholas/pen/ExZwJjx


我想我必须粘贴一些代码才能链接到 codepen,所以这里是所有比较代码:

HTML:

<h1>Welcome</h1>
<p>
  <strong>What's all this about?</strong><br>
  I've been working on the new version of the <a href="https://app.mason.ai/" target="_blank">Mason image editor app</a>, and in it, I need to compare multiple fonts to replace them with each other to have the final layout not look crappy due to spacing differences between fonts. (To allow users to customize fonts in templates and have them still look nice)
</p>
<p>The <a href="https://codepen.io/zacholas/pen/ExZwJjx" target="_blank">first pen</a> focused on getting all the measurements and ratios necessary.</p>
<p>This pen encompasses the second part, which is comparison and normalization of different fonts against a default.</p>
<p>
  <strong>How it works</strong><br>
  First we get the metrics for the two fonts and compare their top/bottom spacing. We then normalize them to align in a purdy vertically-centered way. And then we scale down the second font's container so that it matches the size of the first.
</p>
<p>Enjoy!</p>
<p><em><a href="https://zachswinehart.com" target="_blank">- Zach</a></em></p>

<hr>
<h3>Demo</h3>
<p>If all of my code is working correctly, the text in the "new font adjusted" box should look all purdy and be vertically and horizontally centered.</p>
<p><strong>NOTE:</strong><em> You'll need to make a change in the dropdown before the text in the "new font adjusted" box actually gets adjusted.</em></p>
<label for="font-picker">Choose a font to swap:</label>
<select id="font-picker" disabled>
  <option value=""> — Template Default — </option>
</select>
<div >
  <div id="image-box" class="flex-row">
    <div>
      <h6>Original:</h6>
      <div class="reference-box">
        <div class="text-background">
          <div class="text-container text-utc-lander" id="original-text">
            Hxy
          </div>
        </div>
      </div>
    </div>
    
    <div>
      <h6>New font unadjusted:</h6>
      <div class="reference-box">
        <div class="text-background">
          <div class="text-container text-utc-lander" id="unadjusted-text">
            Hxy
          </div>
        </div>
      </div>
    </div>
    
    <div>
      <h6>New font adjusted:</h6>
      <div id="modified" class="reference-box">
        <div class="text-background">
          <div class="scaler" id="adjusted-text-scaler">
            <div class="text-container text-utc-lander" id="adjusted-text">
              Hxy
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<hr>

<h2>Canvases used for calculating</h2>

<div id="sample-output-container">
</div>

SCSS:

@import 'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css';

// The font size in the demo boxes
$test-font-size: 200px!default;
// $test-font-size: 100px;

body {
  background: #eee;
  padding: 10px;
  font-family: Arial;
}

hr {
  margin: 40px 0;
}

h6 {
  font-size: 17px;
  margin: 12px 0 5px 0;
}

//* In production, you should probably use code like this to position canvases off-screen:
// .test-canvas {
//   position: fixed;
//   top: -99999px;
//   left: -99999px;
//   display:none;
// }

.text-utc-lander { font-family: 'UTCLander-Regular'; }
.text-ar-bonnie { font-family: 'ARBONNIE'; }
.text-adam-cg { font-family: 'ADAMCGPRO'; }
.text-abolition { font-family: 'Abolition-Regular'; }
.text-avenir { font-family: 'AvenirNextLTPro-BoldItalic'; }
.text-agency { font-family: 'AgencyFB-Reg'; }

/* Testing a real life example with absolute CSS done to make a F-ed up font
   like UTC lander look good, which we'll then need to modify positioning and
   sizing for in order for it to look good with normal fonts */
.flex-row {
  display: flex;
  justify-content: space-between;
}

#image-box {
  .reference-box {
    background: url('https://mason-app-staging.herokuapp.com/images/sports_stadium_generic.jpg');
    background-size: cover;
    position: relative;
    width: $test-font-size * 2;
    height: $test-font-size * 1.2;
    
    &:before, &:after {
      content: '';
      left: $test-font-size * .1;
      right: $test-font-size * .1;
      position: absolute;
      height: 1px;
      background: rgba(0,0,0,0.1);
      z-index: 5;
    }
    
    &:before {
      top: $test-font-size * 0.245;
    }
    
    &:after {
      bottom: $test-font-size * 0.245;
    }
    
    .text-background {
      position: absolute;
      left: ($test-font-size * 0.1);
      top: ($test-font-size * 0.1);
      width: ($test-font-size * 1.8);
      height: ($test-font-size * 1);
      background:#39b510;
      color: #fff;
      text-transform: uppercase;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .text-container {
      margin-top: -10px; // Will be overwritten anyway
      text-align: center;
      font-size: $test-font-size;
      line-height: 1;
    }
  }
}

#comparison-output {
  background: #fff;
  padding: 20px;
  margin-top: 40px;
  flex: 1;
}


//* Debug output from the first example
#sample-output-container {
  // * {
  //   line-height: 1;
  // }
  
  > div {
    width: 700px;
    background: #CCC;
    margin-bottom: 20px;
    position: relative;
    height: 200px;
    
    > .text-container {
      background: #fff;
      position: absolute;
      display: flex;
      height: 150px;
      left: 25px;
      width: 300px;
      top: 25px;
      align-items: center;
      justify-content: center;
      
      > span {
        background: #edc79e;
      }
    }
    
    > .info-box {
      font-size: 12px;
      font-family: Arial;
      background: #fff;
      position: absolute;
      width: 300px;
      top: 25px;
      right: 25px;
      padding: 10px;
    }
  }
}


/*
Webfonts
- Code from here down is just base 64'd webfonts.
- All are in "normal" font weight
- Families available:
   - 'ARBONNIE';
   - 'ADAMCGPRO';
   - 'Abolition-Regular';
   - 'AgencyFB-Reg';
   - 'AvenirNextLTPro-BoldItalic';
   - 'UTCLander-Regular';
*/

/* ***** SKIPPING BASE-64'D FONTS FOR STACKOVERFLOW */

JS:

import FontFaceObserver from "https://cdn.skypack.dev/fontfaceobserver@2.1.0";
// var FontFaceObserver = require('fontfaceobserver');
const TYPE_DEFAULT_FONT = 'defaultFont';
const TYPE_CURRENT_FONT = 'currentFont';

// debug output canvases
const removeCalculationCanvases = false;

const allAvailableFonts = [
    { label: 'AR Bonnie', value: 'ARBONNIE' },
    { label: 'Adam CG Pro', value: 'ADAMCGPRO' },
    { label: 'Abolition Regular', value: 'Abolition-Regular' },
    { label: 'Avenir Next LT Pro Bold Italic', value: 'AvenirNextLTPro-BoldItalic' },
    { label: 'Agency FB', value: 'AgencyFB-Reg' },
    { label: 'UTC Lander', value: 'UTCLander-Regular' },
]

const INITIAL_STATE = {
    [TYPE_DEFAULT_FONT]: {
        label: null,
        fontFamily: null,
        fontSize: null,
        metrics: {},
    },
    [TYPE_CURRENT_FONT]: {
        label: null,
        fontFamily: null,
        fontSize: null,
        metrics: {},
        postAdjustmentMetrics: {}
    }
}

let state = {
    ...INITIAL_STATE
}

const _roundToTwo = num => {
    return +(Math.round(Number(num) + "e+2")  + "e-2");
}

const _roundToFive = num => {
    return +(Math.round(Number(num) + "e+5")  + "e-5");
}

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}


const getTextMetrics = async(fontFamily, fontSize, testtext = 'Sixty Handgloves ABC') => {
    //* For now we'll just keep the test text hard-coded but maybe we'll pass in the element value at some point. (However, being that the text will be editable I don't think that's wise)
    testtext = 'Hxy';
  
    const fontSizePx = fontSize.split('px')[0];

    //* Generate a hash from the font name for the canvas ID
    const canvasId = Math.abs(fontFamily.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));
    
    
    console.log('waiting for font to load')
    var font = new FontFaceObserver(fontFamily);
    await font.load();
    console.log('font loaded');

    //* Initialize the test canvas so that we can measure stuff
    const testCanvasWidth = 400;
    const testCanvasHeight = 200;
    const testCanvasPadding = 10;
    // const canvasDrawingTextFontSize = 1000;
    const canvasDrawingTextFontSize = fontSizePx;
    const testCanvas = document.createElement('canvas');
    testCanvas.id = (`cvs-${canvasId}-${Math.random().toString(36).substring(7)}`);
    testCanvas.className = `test-canvas ${canvasId}`;
    testCanvas.width = testCanvasWidth;
    testCanvas.height = testCanvasHeight;
    // document.body.appendChild(testCanvas);
    var testCanvasCtx = testCanvas.getContext("2d");
    testCanvas.style.font = `${canvasDrawingTextFontSize}px ${fontFamily}`;
    testCanvasCtx.font = [`${canvasDrawingTextFontSize}px`, fontFamily].join(' ');
    testCanvasCtx.clearRect(0, 0, testCanvasWidth, testCanvasHeight);
    testCanvasCtx.fontFamily = fontFamily;
    testCanvasCtx.fillStyle = "#fff";
    testCanvasCtx.fillRect(0,0,testCanvas.width, testCanvas.height);
    testCanvasCtx.fillStyle = "#333333";
    testCanvasCtx.fillText(testtext, testCanvasPadding, testCanvasHeight);
  
  // console.log('before timeout');
  
  
    
    // await timeout(3000);
  // console.log('timeout done');
    document.body.appendChild(testCanvas);
      

      
    //* Get Core Measurements
    var xHeight = testCanvasCtx.measureText("x").height;
    var capHeight = testCanvasCtx.measureText("H").height;
    // var measuredTextMetrics = testCanvasCtx.measureText("Hxy");
    var measuredTextMetrics = testCanvasCtx.measureText(testtext);

    //* Make the measurements usable (cast to numbers to allow for nulls)

    let metrics = {};
    metrics.measured = {
        actualBoundingBoxAscent: _roundToFive(measuredTextMetrics.actualBoundingBoxAscent),
        actualBoundingBoxDescent: _roundToFive(measuredTextMetrics.actualBoundingBoxDescent),
        actualBoundingBoxLeft: _roundToFive(measuredTextMetrics.actualBoundingBoxLeft),
        actualBoundingBoxRight: _roundToFive(measuredTextMetrics.actualBoundingBoxRight),
        fontBoundingBoxAscent: _roundToFive(measuredTextMetrics.fontBoundingBoxAscent),
        fontBoundingBoxDescent: _roundToFive(measuredTextMetrics.fontBoundingBoxDescent),
        width: _roundToFive(measuredTextMetrics.width)
    };
  
    
    const fontSizeMultiplicand = fontSizePx / canvasDrawingTextFontSize;
  
    const {
        actualBoundingBoxAscent,
        // actualBoundingBoxDescent,
        // actualBoundingBoxLeft,
        // actualBoundingBoxRight,
        fontBoundingBoxAscent,
        fontBoundingBoxDescent,
    } = metrics.measured;
    metrics.calculated = {
        gapAboveText: _roundToFive((fontBoundingBoxAscent - actualBoundingBoxAscent) * fontSizeMultiplicand),
        gapBelowText: _roundToFive(fontBoundingBoxDescent * fontSizeMultiplicand),
        textHeight: _roundToFive(actualBoundingBoxAscent * fontSizeMultiplicand),
        totalHeight: _roundToFive((fontBoundingBoxAscent + fontBoundingBoxDescent) * fontSizeMultiplicand),
    };
    const {
        gapBelowText, gapAboveText, textHeight, totalHeight
    } = metrics.calculated;

    metrics.calculated.gapBelowTextPercent = _roundToFive(gapBelowText / totalHeight);
    metrics.calculated.gapAboveTextPercent = _roundToFive(gapAboveText / totalHeight);
    metrics.calculated.gapTopBottomRatio = _roundToFive(gapAboveText / gapBelowText);
    metrics.calculated.textHeightPercent = _roundToFive(textHeight / totalHeight);
    metrics.calculated.baselineMarginTop = gapBelowText - gapAboveText;

    if(removeCalculationCanvases === true){
        testCanvas.remove(); // cleanup
    }

    return metrics;
  
  
    
};

const setFontState = async(fontFamily, fontSize, fontLabel, type = TYPE_CURRENT_FONT) => {
    if(fontFamily){
        console.log('about to get text metrics')
        const metrics = await getTextMetrics(fontFamily, fontSize);
      console.log('metrics received');
        state[type] = {
            label: fontLabel ? fontLabel : fontFamily,
            fontFamily,
            fontSize,
            metrics
        }
    }
    else {
        state[type] = {
            ...INITIAL_STATE[type]
        }
    }
  return true;
}


const watchForFontChange = async() => {
    document.addEventListener('input', async(event) => {
        if (event.target.id !== 'font-picker') return; // Only run on the font change menu

        let label = null;
        if(
            event.target.options.length &&
            typeof event.target.options[event.target.selectedIndex] !== 'undefined' &&
            event.target.options[event.target.selectedIndex].text
        ) {
            label = event.target.options[event.target.selectedIndex].text;
        }

        // For now just grab font size from the default font state, but probably will change later
        const fontFamily = event.target.value;
        const fontSize = state[TYPE_DEFAULT_FONT].fontSize;
        await setFontState(fontFamily, fontSize, label);
        console.log('font changed', state);

        //* Set the font families in the display
        if(fontFamily){
            document.getElementById(`unadjusted-text`).style.fontFamily = fontFamily;
            document.getElementById(`adjusted-text`).style.fontFamily = fontFamily;
        }
        else {
            document.getElementById(`unadjusted-text`).style.fontFamily = null;
            document.getElementById(`adjusted-text`).style.fontFamily = null;
        }


        //* Calculate the adjustments for the new font compared to the baseline
        // const currentFontSize = parseInt(state.currentFont.fontSize,10);
        const defaultFontMetrics = state.defaultFont.metrics;
        const currentFontMetrics = state.currentFont.metrics;
        // const fontSizeAdjustPx = defaultFontMetrics.calculated.textHeight - currentFontMetrics.calculated.textHeight;
        // const fontSizeAdjustPcnt = _roundToFive(fontSizeAdjustPx / currentFontMetrics.calculated.textHeight);


        //* Apply the adjustments
        // const newFontSize = currentFontSize + (currentFontSize * fontSizeAdjustPcnt);
        // console.log('newFontSize', newFontSize);
        const textToAdjust = document.getElementById(`adjusted-text`);
        // const fontSizeStr = `${newFontSize}px`;


        textToAdjust.style.marginTop = `${currentFontMetrics.calculated.baselineMarginTop}px`;

        const scaler = document.getElementById('adjusted-text-scaler');
        const scale = _roundToTwo(defaultFontMetrics.calculated.textHeight / currentFontMetrics.calculated.textHeight);
        scaler.style.transform = `scale(${scale})`;

    }, false);
}


const addFontOptionsToDropdown = () => {
    const parentSelect = document.getElementById(`font-picker`);
    for(let i=0; i < allAvailableFonts.length; i++){
        const thisOption = allAvailableFonts[i];
        if(thisOption.value){
            const label = thisOption.label ? thisOption.label : thisOption.value;
            const thisOptionTag = document.createElement("option");
            thisOptionTag.value = thisOption.value;
            const thisOptionText = document.createTextNode(label);
            thisOptionTag.appendChild(thisOptionText);
            parentSelect.appendChild(thisOptionTag);
        }
    }
}

const parseDefaultFont = async() => {
    const thisText = document.getElementById(`original-text`);

    // We might need to do some special stuff for uppercase vs non-uppercase text
    const thisTextStyle = window.getComputedStyle(thisText);
    const textTransform = thisTextStyle.getPropertyValue('text-transform');
    const marginTop = thisTextStyle.getPropertyValue('margin-top');
    console.log('marginTop', marginTop);
    const uppercase = textTransform === 'uppercase';

    const fontFamily = thisTextStyle.getPropertyValue('font-family');
    const fontSize = thisTextStyle.getPropertyValue('font-size');
    console.log('fontSize', fontSize);
    await setFontState(fontFamily, fontSize, null, TYPE_DEFAULT_FONT);

    document.getElementById(`original-text`).style.marginTop = `${state.defaultFont.metrics.calculated.baselineMarginTop}px`;

    return !! fontFamily;
}

const init = async() => {
  console.log(' ');
  console.log(' ');
  console.log(' ');
  console.log('initialized.');
    const defaultFont = await parseDefaultFont();
    if(defaultFont){
        addFontOptionsToDropdown(); // Parse JSON object into the select html tag
        await watchForFontChange();
    }
    else {
        // Handle Error -- for some reason there wasn't a font family for the default text.
    }
    document.getElementById('font-picker').disabled = false;
    
    console.log('state after init done', state);
}


//* Wait for all the base 64'd fonts to load before we run it
document.addEventListener("DOMContentLoaded", (ready => {
  init();
  // setTimeout(function(){ init(); }, 1000);
}));

【讨论】:

    【解决方案4】:

    你应该使用像 rem 然后 px :) 因为 rem 是一个相对的度量单位,而 px 是绝对的。但是字体总是有不同的大小,而且它不可能达到你想要的效果。

    【讨论】:

    • 这如何回答这个问题?
    猜你喜欢
    • 1970-01-01
    • 2020-03-11
    • 1970-01-01
    • 1970-01-01
    • 2017-04-14
    • 2014-05-05
    • 1970-01-01
    • 2017-05-31
    • 2015-10-29
    相关资源
    最近更新 更多