/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Checks to see if the aspect ratio of the images used on * the page are equal to the aspect ratio of their display sizes. The * audit will list all images that don't match with their display size * aspect ratio. */ import {Audit} from './audit.js'; import UrlUtils from '../lib/url-utils.js'; import * as i18n from '../lib/i18n/i18n.js'; const UIStrings = { /** Title of a Lighthouse audit that provides detail on the aspect ratios of all images on the page. This descriptive title is shown to users when all images use correct aspect ratios. */ title: 'Displays images with correct aspect ratio', /** Title of a Lighthouse audit that provides detail on the aspect ratios of all images on the page. This descriptive title is shown to users when not all images use correct aspect ratios. */ failureTitle: 'Displays images with incorrect aspect ratio', /** Description of a Lighthouse audit that tells the user why they should maintain the correct aspect ratios for all images. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'Image display dimensions should match natural aspect ratio. ' + '[Learn more about image aspect ratio](https://developer.chrome.com/docs/lighthouse/best-practices/image-aspect-ratio/).', /** Label for a column in a data table; entries in the column will be the numeric aspect ratio of an image as displayed in a web page. */ columnDisplayed: 'Aspect Ratio (Displayed)', /** Label for a column in a data table; entries in the column will be the numeric aspect ratio of the raw (actual) image. */ columnActual: 'Aspect Ratio (Actual)', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); const THRESHOLD_PX = 2; /** @typedef {Required} WellDefinedImage */ class ImageAspectRatio extends Audit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'image-aspect-ratio', title: str_(UIStrings.title), failureTitle: str_(UIStrings.failureTitle), description: str_(UIStrings.description), requiredArtifacts: ['ImageElements'], }; } /** * @param {WellDefinedImage} image * @return {{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}} */ static computeAspectRatios(image) { const url = UrlUtils.elideDataURI(image.src); const actualAspectRatio = image.naturalDimensions.width / image.naturalDimensions.height; const displayedAspectRatio = image.displayedWidth / image.displayedHeight; const targetDisplayHeight = image.displayedWidth / actualAspectRatio; const targetDisplayWidth = image.displayedHeight * actualAspectRatio; // Small rounding errors in aspect ratio can lead to large differences in target width/height // if the aspect ratio is close to 0. // // In these cases, we should compare the smaller dimension because any rounding errors will // affect that dimension less. const doRatiosMatch = targetDisplayHeight < targetDisplayWidth ? Math.abs(targetDisplayHeight - image.displayedHeight) < THRESHOLD_PX : Math.abs(targetDisplayWidth - image.displayedWidth) < THRESHOLD_PX; return { url, node: Audit.makeNodeItem(image.node), displayedAspectRatio: `${image.displayedWidth} x ${image.displayedHeight} (${displayedAspectRatio.toFixed(2)})`, actualAspectRatio: `${image.naturalDimensions.width} x ${image.naturalDimensions.height} (${actualAspectRatio.toFixed(2)})`, doRatiosMatch, }; } /** * @param {LH.Artifacts} artifacts * @return {LH.Audit.Product} */ static audit(artifacts) { const images = artifacts.ImageElements; /** @type {Array<{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}>} */ const results = []; images.filter(image => { // - filter out css background images since we don't have a reliable way to tell if it's a // sprite sheet, repeated for effect, etc // - filter out images that don't have following properties: // networkRecord, width, height, `object-fit` property // - filter all svgs as they have no natural dimensions to audit // - filter out images that have falsy naturalWidth or naturalHeight return !image.isCss && UrlUtils.guessMimeType(image.src) !== 'image/svg+xml' && image.naturalDimensions && image.naturalDimensions.height > 5 && image.naturalDimensions.width > 5 && image.displayedWidth && image.displayedHeight && image.computedStyles.objectFit === 'fill'; }).forEach(image => { const wellDefinedImage = /** @type {WellDefinedImage} */ (image); const processed = ImageAspectRatio.computeAspectRatios(wellDefinedImage); if (!processed.doRatiosMatch) results.push(processed); }); /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ {key: 'node', valueType: 'node', label: ''}, {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)}, {key: 'displayedAspectRatio', valueType: 'text', label: str_(UIStrings.columnDisplayed)}, {key: 'actualAspectRatio', valueType: 'text', label: str_(UIStrings.columnActual)}, ]; return { score: Number(results.length === 0), details: Audit.makeTableDetails(headings, results), }; } } export default ImageAspectRatio; export {UIStrings};