Для горизонтальной строки встроенных изображений, выровненных по базовой линии, базовая линия проходит по нижнему краю внешнего поля каждого изображения. В этом руководстве показано, как найти эту координату в Chrome на Mac, проверить её с помощью небольшого маркера и понять, когда этот метод неприменим.
Почему у строки, состоящей только из изображений, есть базовая линия?
Строка, состоящая только из изображений, всё равно участвует во встроенной раскладке CSS. Успешно отображённый img — это замещаемый элемент: браузер размещает его блок согласно CSS, а не рассматривает изображение как текст. WHATWG описывает это поведение при отрисовке в правилах отрисовки изображений стандарта HTML.
В обычной горизонтальной встроенной раскладке изображение без собственной текстовой базовой линии выравнивается по нижнему краю внешнего поля. Поэтому два выровненных по базовой линии изображения разной высоты могут иметь общую базовую линию, хотя их верхние края расположены на разной высоте. Это правило относится к их блокам и не учитывает изображение внутри них. См. CSS 2, вертикальное выравнивание.
Здесь важны три разных границы:
| Граница | Значение | Подходит как ориентир для базовой линии? |
|---|---|---|
| Край видимого изображения | Место, где заканчиваются видимые пиксели изображения | Нет; в файле могут быть прозрачные области |
| Нижний край рамки изображения | Нижняя граница, возвращаемая getBoundingClientRect() |
Да, с учётом нижнего внешнего поля и при подтверждённом выравнивании по базовой линии |
| Нижний край линейного блока | Нижняя граница всей строки CSS | Нет; она может находиться ниже базовой линии |
Шрифт родительского элемента и line-height по-прежнему важны, даже если видимых букв нет. CSS описывает невидимый вклад, основанный на метриках шрифта, который называется распоркой (strut) и помогает задать минимальную высоту и глубину строки. Поэтому изображение может стоять на базовой линии, а под ним при этом остаётся свободное место. В спецификации CSS Inline Layout описаны эти метрики шрифта и границы раскладки; полученная опубликованная версия — рабочий черновик, поэтому не следует считать, что её новые функции поддерживаются во всех браузерах.
Как найти базовую линию на существующей странице?
Откройте страницу, которую можно проверить в Chrome на Mac. Щёлкните правой кнопкой мыши по изображению и выберите Inspect. Chrome откроет панель Elements с выбранным элементом; в панели Styles показаны применённые к нему правила CSS. Google описывает этот процесс в руководстве Открытие Chrome DevTools.
Перед вычислением координаты проверьте следующие условия:
- Изображение участвует в обычном встроенном потоке целевой строки.
- Его фактическое выравнивание задано как
vertical-align: baseline. - Используется горизонтальный режим письма.
- Ни к изображению, ни к его предкам не применены трансформация или визуальное смещение, влияющие на измерение.
- Вы измеряете само изображение, а не окружающую обёртку с собственным поведением базовой линии.
Эти условия задают границы применимости вычисления. Например, vertical-align: bottom выравнивает элемент по нижнему краю строки, а middle использует базовую линию родителя со смещением на половину его высоты x. Ни одно из этих значений не означает, что нижний край изображения указывает на базовую линию родителя. MDN объясняет эти различия в справочнике по vertical-align.
Если все условия выполнены, используйте следующую зависимость, выражая расстояния в CSS-пикселях:
baselineY = imageBorderBoxBottom + imageMarginBottom
getBoundingClientRect() возвращает нижнюю границу рамки относительно области просмотра. Прямоугольник включает внутренние поля и границы, поэтому их не нужно прибавлять повторно. Внешние поля находятся за пределами этого прямоугольника. MDN описывает возвращаемую геометрию и систему координат в справочнике Element.getBoundingClientRect().
Для наглядного примера предположим, что нижняя граница рамки изображения равна 240.5, а его нижнее внешнее поле — 6. Следовательно, базовая линия находится на расстоянии 246.5 CSS-пикселей от верхнего края области просмотра. Если верхняя граница рамки контейнера равна 200, базовая линия расположена на расстоянии 46.5 CSS-пикселей от верхнего края рамки контейнера. Это расстояние отсчитывается от края рамки, а не автоматически от края содержимого.
Для перепроверки измерьте другое подходящее изображение в той же строке. Его нижний край внешнего поля должен указывать на ту же базовую линию, даже если высота изображения отличается. Если координаты не совпадают, прежде чем менять CSS, проверьте выравнивание, обёртки, позиционирование и переносы строк. При сравнении сохраняйте дробные значения.
Как проверить результат с помощью маркера базовой линии?
Удобный диагностический инструмент — пустой встроенно-блочный элемент с нулевой шириной, высотой, внешними и внутренними полями и границами, выровненный по базовой линии. Поскольку он не содержит линейных блоков в потоке, его базовая линия совпадает с нижним краем внешнего поля. При нулевых размерах этот край совпадает с измеренным вертикальным положением элемента. Это следует из правила базовой линии встроенно-блочного элемента в CSS 2.
Следующая полная программа для консоли создаёт временный пример с двумя нейтральными прямоугольными изображениями и маркером. Она использует теневой DOM, чтобы отделить внутренние стили примера от обычных селекторов страницы. Запустите её в документе со стандартным режимом отображения, без трансформаций документа и элемента body. Программа измеряет только созданную ею строку, а не содержимое существующей страницы.
Откройте Console сочетанием клавиш Command + Option + J, описанным в сочетаниях клавиш DevTools в Chrome, и запустите программу. Устанавливать библиотеки или загружать внешние файлы изображений не нужно. Пример создаёт прямоугольники с помощью стандартного Canvas API.
(async () => {
if (document.compatMode !== "CSS1Compat") {
throw new Error("Use a standards-mode document for this demonstration.");
}
const host = document.createElement("div");
host.style.cssText =
"all:initial;position:fixed;top:16px;left:16px;" +
"z-index:2147483647;background:white;padding:16px;" +
"color:#222;font:16px/1.5 sans-serif;";
const root = host.attachShadow({ mode: "open" });
const label = document.createElement("div");
label.textContent = "Sample A and Sample B";
const row = document.createElement("div");
row.style.cssText =
"display:block;position:relative;width:280px;" +
"margin:0;padding:0;border:0;white-space:nowrap;" +
"writing-mode:horizontal-tb;font:16px/24px sans-serif;" +
"background:#eee;";
const close = document.createElement("button");
close.textContent = "Remove demonstration";
close.addEventListener("click", () => host.remove());
root.append(label, row, close);
document.body.append(host);
try {
const images = [];
const samples = [
{ label: "Sample A", height: 32, color: "#777" },
{ label: "Sample B", height: 48, color: "#aaa" }
];
for (const sample of samples) {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = sample.height;
const context = canvas.getContext("2d");
if (!context) throw new Error("A 2D canvas context is required.");
context.fillStyle = sample.color;
context.fillRect(0, 0, canvas.width, canvas.height);
const image = document.createElement("img");
image.alt = sample.label;
image.width = 64;
image.height = sample.height;
image.style.cssText =
"display:inline;vertical-align:baseline;" +
"margin:0 12px 0 0;padding:0;border:0;";
image.src = canvas.toDataURL("image/png");
row.append(image);
images.push(image);
}
await Promise.all(images.map(image => image.decode()));
if (!host.isConnected) return;
const marker = document.createElement("span");
marker.style.cssText =
"display:inline-block;vertical-align:baseline;" +
"width:0;height:0;margin:0;padding:0;border:0;" +
"font-size:0;line-height:0;";
row.append(marker);
const baselineY = marker.getBoundingClientRect().bottom;
const rowTop = row.getBoundingClientRect().top;
console.table(images.map(image => ({
sample: image.alt,
imageBottom: image.getBoundingClientRect().bottom,
markerBaseline: baselineY,
baselineFromRowTop: baselineY - rowTop
})));
const guide = document.createElement("div");
guide.style.cssText =
"position:absolute;left:0;right:0;height:1px;" +
"background:#222;pointer-events:none;";
guide.style.top = `${baselineY - rowTop}px`;
row.append(guide);
} catch (error) {
host.remove();
throw error;
}
})();
Перед измерением программа дожидается декодирования обоих изображений. Документированный метод HTMLImageElement.decode() возвращает промис и может завершиться ошибкой при сбое декодирования; в этом случае программа удаляет временную панель и сообщает об ошибке.
У обоих изображений нижнее внешнее поле равно нулю, поэтому значения imageBottom должны совпадать с markerBaseline. Тонкая горизонтальная направляющая отмечает эту координату. Фон строки также позволяет отличить базовую линию от оставшегося под ней пространства. По завершении выберите Remove demonstration.
Для контролируемого эксперимента измените нижнее внешнее поле изображений в примере с 0 на 6px в стиле изображения. Нижние края их рамок должны оказаться на шесть CSS-пикселей выше базовой линии, отмеченной маркером. Используйте приведённую выше формулу, чтобы получить общую координату. Так в примере появятся два независимых ориентира: край внешнего поля изображения и пустой маркер.
Когда этому методу нужен другой подход?
Маркер — это диагностический элемент, поэтому он измеряет получившуюся раскладку. Его добавление в произвольный существующий абзац может создать возможность переноса или иначе повлиять на раскладку. Если предварительные условия соблюдены, лучше использовать вычисление по существующему изображению. Если вы добавили маркер в реальное содержимое, убедитесь, что те же изображения остались в той же строке и их положения не изменились.
| Ситуация | Следующий шаг |
|---|---|
Для изображений задано выравнивание top, bottom или middle |
Проверьте это правило выравнивания; не применяйте без изменений формулу для изображения, выровненного по базовой линии |
| Изображение находится внутри встроенно-блочной обёртки | Проверьте базовую линию обёртки: она может определяться последней строкой её содержимого |
| Изображения занимают несколько перенесённых строк | Определите базовую линию отдельно для каждой строки |
| Для родительского элемента используется раскладка flex или grid | Перед тем как считать строку встроенной, проверьте правила выравнивания этой раскладки |
| В документе используется вертикальный режим письма | Применяйте правила базовой линии с учётом режима письма, а не координату y нижнего края |
| Изображение не загружено успешно | Сначала выясните, в каком состоянии оно отображается, и лишь затем считайте его отображаемым блоком изображения |
Последнее условие важно, поскольку WHATWG описывает разные варианты отображения для загруженных изображений, изображений, загрузка которых ещё не завершена, и замещающего текста. Поэтому для сломанного изображения, которое показывает свою подпись, может потребоваться иная интерпретация, чем для успешно отрисованного изображения, которое вы хотели измерить. См. правила отрисовки изображений в HTML.
Не делайте вывод об общей базовой линии только потому, что верхние границы нескольких прямоугольников изображений расположены примерно на одной высоте. Изображения разной высоты могут иметь общую базовую линию, но разные верхние края. И наоборот, совпадение верхних краёв не подтверждает выравнивание по базовой линии. Проверьте, какие блоки участвуют в одной строке и какое правило выравнивания применяется.
Могут ли API для измерения текста или библиотеки формирования текста дать ответ?
Используемые здесь методы браузера для измерения геометрии возвращают прямоугольники, а не отдельное поле с координатой базовой линии строки. Range.getClientRects() помогает проверить области, занимаемые диапазоном, но возвращаемые им прямоугольники не показывают напрямую координату базовой линии. MDN описывает этот интерфейс в справочнике Range.getClientRects().
Метрики текста Canvas отвечают на другой вопрос: как фрагмент текста на холсте располагается относительно выбранной базовой линии текста в контексте Canvas. Например, actualBoundingBoxAscent и actualBoundingBoxDescent описывают границы видимого текста относительно этого ориентира. Они не позволяют напрямую определить положение существующей строки изображений в DOM. См. документацию MDN по TextMetrics.
У библиотеки формирования текста задачи уже, чем у браузерной раскладки строк. В руководстве HarfBuzz сказано, что библиотека формирует последовательности текста и предоставляет сведения о раскладке глифов, тогда как перенос строк абзаца и выравнивание по ширине не входят в её задачи. Поэтому установка HarfBuzz не позволит измерить строку браузера, состоящую только из изображений. В описании возможностей библиотеки указаны эти границы.
Для горизонтальной строки, состоящей только из изображений, сначала убедитесь, что используется обычный встроенный поток и выравнивание по базовой линии. Затем считайте нижнюю границу рамки подходящего изображения и прибавьте нижнее внешнее поле. Для наглядной перепроверки используйте маркер нулевого размера в контролируемой раскладке, а если эти два ориентира расходятся — проверьте контекст форматирования.
Источники
Стандарт HTML: отрисовка — WHATWG.
Стандарт HTML: элемент canvas — WHATWG.
Открытие Chrome DevTools — Google Chrome для разработчиков.
Свойство CSS vertical-align — MDN Web Docs.
Элемент: метод getBoundingClientRect() — MDN Web Docs.
HTMLImageElement: метод decode() — MDN Web Docs.
Range: метод getClientRects() — MDN Web Docs.
TextMetrics — MDN Web Docs.
Чего не делает HarfBuzz — HarfBuzz.