@@ -36,6 +36,11 @@ const VALID_WIDTH_DESCRIPTOR_SRCSET = /^((\s*\d+w\s*(,|$)){1,})$/;
3636 */
3737const VALID_DENSITY_DESCRIPTOR_SRCSET = / ^ ( ( \s * \d ( \. \d ) ? x \s * ( , | $ ) ) { 1 , } ) $ / ;
3838
39+ /**
40+ * Used to determine whether two aspect ratios are similar in value.
41+ */
42+ const ASPECT_RATIO_TOLERANCE = .1 ;
43+
3944/**
4045 * ** EXPERIMENTAL **
4146 *
@@ -86,7 +91,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
8691 */
8792 @Input ( )
8893 set width ( value : string | number | undefined ) {
89- ngDevMode && assertValidNumberInput ( value , 'width' ) ;
94+ ngDevMode && assertGreaterThanZeroNumber ( value , 'width' ) ;
9095 this . _width = inputToInteger ( value ) ;
9196 }
9297 get width ( ) : number | undefined {
@@ -98,7 +103,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
98103 */
99104 @Input ( )
100105 set height ( value : string | number | undefined ) {
101- ngDevMode && assertValidNumberInput ( value , 'height' ) ;
106+ ngDevMode && assertGreaterThanZeroNumber ( value , 'height' ) ;
102107 this . _height = inputToInteger ( value ) ;
103108 }
104109 get height ( ) : number | undefined {
@@ -146,6 +151,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
146151 assertRequiredNumberInput ( this , this . width , 'width' ) ;
147152 assertRequiredNumberInput ( this , this . height , 'height' ) ;
148153 assertValidLoadingInput ( this ) ;
154+ assertNoImageDistortion ( this , this . imgElement , this . renderer ) ;
149155 if ( this . priority ) {
150156 const checker = this . injector . get ( PreconnectLinkChecker ) ;
151157 checker . check ( this . getRewrittenSrc ( ) , this . rawSrc ) ;
@@ -400,11 +406,12 @@ function assertNoPostInitInputChange(
400406 } ) ;
401407}
402408
403- // Verifies that a specified input has a correct type (number).
404- function assertValidNumberInput ( inputValue : unknown , inputName : string ) {
405- const isValid = typeof inputValue === 'number' ||
406- ( typeof inputValue === 'string' && / ^ \d + $ / . test ( inputValue . trim ( ) ) ) ;
407- if ( ! isValid ) {
409+ // Verifies that a specified input is a number greater than 0.
410+ function assertGreaterThanZeroNumber ( inputValue : unknown , inputName : string ) {
411+ const validNumber = typeof inputValue === 'number' && inputValue > 0 ;
412+ const validString =
413+ typeof inputValue === 'string' && / ^ \d + $ / . test ( inputValue . trim ( ) ) && parseInt ( inputValue ) > 0 ;
414+ if ( ! validNumber && ! validString ) {
408415 throw new RuntimeError (
409416 RuntimeErrorCode . INVALID_INPUT ,
410417 `The NgOptimizedImage directive has detected that the \`${ inputName } \` has an invalid ` +
@@ -413,6 +420,64 @@ function assertValidNumberInput(inputValue: unknown, inputName: string) {
413420 }
414421}
415422
423+ // Verifies that the rendered image is not visually distorted. Effectively this is checking:
424+ // - Whether the "width" and "height" attributes reflect the actual dimensions of the image.
425+ // - Whether image styling is "correct" (see below for a longer explanation).
426+ function assertNoImageDistortion (
427+ dir : NgOptimizedImage , element : ElementRef < any > , renderer : Renderer2 ) {
428+ const img = element . nativeElement ;
429+ const removeListenerFn = renderer . listen ( img , 'load' , ( ) => {
430+ removeListenerFn ( ) ;
431+ const renderedWidth = parseFloat ( img . clientWidth ) ;
432+ const renderedHeight = parseFloat ( img . clientHeight ) ;
433+ const renderedAspectRatio = renderedWidth / renderedHeight ;
434+ const nonZeroRenderedDimensions = renderedWidth !== 0 && renderedHeight !== 0 ;
435+
436+ const intrinsicWidth = parseFloat ( img . naturalWidth ) ;
437+ const intrinsicHeight = parseFloat ( img . naturalHeight ) ;
438+ const intrinsicAspectRatio = intrinsicWidth / intrinsicHeight ;
439+
440+ const suppliedWidth = dir . width ! ;
441+ const suppliedHeight = dir . height ! ;
442+ const suppliedAspectRatio = suppliedWidth / suppliedHeight ;
443+
444+ // Tolerance is used to account for the impact of subpixel rendering.
445+ // Due to subpixel rendering, the rendered, intrinsic, and supplied
446+ // aspect ratios of a correctly configured image may not exactly match.
447+ // For example, a `width=4030 height=3020` image might have a rendered
448+ // size of "1062w, 796.48h". (An aspect ratio of 1.334... vs. 1.333...)
449+ const inaccurateDimensions =
450+ Math . abs ( suppliedAspectRatio - intrinsicAspectRatio ) > ASPECT_RATIO_TOLERANCE ;
451+ const stylingDistortion = nonZeroRenderedDimensions &&
452+ Math . abs ( intrinsicAspectRatio - renderedAspectRatio ) > ASPECT_RATIO_TOLERANCE ;
453+ if ( inaccurateDimensions ) {
454+ console . warn (
455+ RuntimeErrorCode . INVALID_INPUT ,
456+ `${ imgDirectiveDetails ( dir . rawSrc ) } has detected that the aspect ratio of the ` +
457+ `image does not match the aspect ratio indicated by the width and height attributes. ` +
458+ `Intrinsic image size: ${ intrinsicWidth } w x ${ intrinsicHeight } h (aspect-ratio: ${
459+ intrinsicAspectRatio } ). ` +
460+ `Supplied width and height attributes: ${ suppliedWidth } w x ${
461+ suppliedHeight } h (aspect-ratio: ${ suppliedAspectRatio } ). ` +
462+ `To fix this, update the width and height attributes.` ) ;
463+ } else {
464+ if ( stylingDistortion ) {
465+ console . warn (
466+ RuntimeErrorCode . INVALID_INPUT ,
467+ `${ imgDirectiveDetails ( dir . rawSrc ) } has detected that the aspect ratio of the ` +
468+ `rendered image does not match the image's intrinsic aspect ratio. ` +
469+ `Intrinsic image size: ${ intrinsicWidth } w x ${ intrinsicHeight } h (aspect-ratio: ${
470+ intrinsicAspectRatio } ). ` +
471+ `Rendered image size: ${ renderedWidth } w x ${ renderedHeight } h (aspect-ratio: ${
472+ renderedAspectRatio } ). ` +
473+ `This issue can occur if "width" and "height" attributes are added to an image ` +
474+ `without updating the corresponding image styling. In most cases, ` +
475+ `adding "height: auto" or "width: auto" to the image styling will fix this issue.` ) ;
476+ }
477+ }
478+ } ) ;
479+ }
480+
416481// Verifies that a specified input is set.
417482function assertRequiredNumberInput ( dir : NgOptimizedImage , inputValue : unknown , inputName : string ) {
418483 if ( typeof inputValue === 'undefined' ) {
0 commit comments