imagevaadinretina-displayvaadin-flowpixel-density

Size an `Image` widget for pixel-dense ("Retina") monitors in a Vaadin 14 app


I know how to render an JPEG image on the screen in a Vaadin 14.1 app.

In particular, the 3 lines:

imageReader.setInput( imageInputStream ); // Render the image from the data provided by the `ImageInputStream`.
imageWidget.setWidth( imageReader.getWidth( 0 ) + "px" ); // Get the pixel size of the rendered image.
imageWidget.setHeight( imageReader.getHeight( 0 ) + "px" );

…of this:

private Image createImageWidget ( String mimeType , String fileName , InputStream inputStreamOfImageData )
{
    if ( mimeType.startsWith( "image" ) )
    {
        Image imageWidget = new Image();
        try (
                inputStreamOfImageData ;
                // Auto-close.
        )
        {
            // Get the octets of the image data.
            final byte[] bytes = IOUtils.toByteArray( inputStreamOfImageData );

            // Make the image widget for Vaadin.
            imageWidget.getElement().setAttribute( "src" , new StreamResource( fileName , ( ) -> new ByteArrayInputStream( bytes ) ) );
            imageWidget.setAlt( "File name: " + fileName );

            // Size the image for display in the HTML/CSS for faster rendering without the page layout jumping around in front of user.
            try (
                    ImageInputStream imageInputStream = ImageIO.createImageInputStream( new ByteArrayInputStream( bytes ) ) ;
                    // Auto-close.
            )
            {
                // Apparently, image readers (parsers/decoders) are detected at runtime and loaded via the "Java Service Provider Interface (SPI)" facility.
                final Iterator < ImageReader > imageReaders = ImageIO.getImageReadersByMIMEType( UploadView.MIME_TYPE_JPEG );  // OR, for multiple image types, call `ImageIO.getImageReaders( in )`.
                if ( imageReaders.hasNext() )
                {
                    ImageReader imageReader = imageReaders.next();
                    try
                    {
                        imageReader.setInput( imageInputStream ); // Render the image from the data provided by the `ImageInputStream`.
                        imageWidget.setWidth( imageReader.getWidth( 0 ) + "px" ); // Get the pixel size of the rendered image.
                        imageWidget.setHeight( imageReader.getHeight( 0 ) + "px" );
                    }
                    finally
                    {
                        imageReader.dispose();
                    }
                } else
                {
                    throw new IllegalStateException( "Failed to find any image readers for JPEG. " + "Message # e91ce8e4-0bd3-424d-8f7c-b3f51c7ef827." );
                }
            }
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }
        return imageWidget;
    }
    // TODO: Log error. Should not reach this point.
    System.out.println( "BASIL - ERROR - Ended up with a null `imageWidget`. " + "Message # 415bd917-67e2-49c3-b39a-164b0f900a3a." );
    return null;
}

I understand that in those 3 lines, we are informing the web page of the width and hight of the image rather than letting the web page discover the images’ size as they load. This makes layout faster, and avoids the annoyance of a web page jumping around in front of the user.

➥ My question is: What about modern monitors with a high density of pixels, Retina displays as Apple calls them?

How can I get an image to effectively display all its pixels, but taking up the least space on the screen?

If the monitor has old-fashioned low pixel density, the image should display with more centimeters of physical screen space than on a high-density Retina display where the image can take fewer centimeters of screen while still technically displaying all the image's pixels.


Solution

  • A quick search online didn't find any native browser feature for giving different dimensions to an <img> element depending on the native pixel density. What we do have is the window.devicePixelRatio property that can be read using JavaScript. See guide by Mozilla.

    You can use this together with executeJs in two different ways - either to set the size of each element dynamically or to fetch the value to the server and then do the calculation on the server for each Image component.

    The first alternative would look something like this:

    imageWidget.getElement().executeJs("this.style.width = ($0 / window.devicePixelRatio) + 'px';" +
        "this.style.height = ($1 / window.devicePixelRatio) + 'px';",
      imageWidth, imageHeight);
    

    The second alternative to fetch the ratio and use it would be like this:

    ui.getPage().executeJs("return window.devicePixelRatio;")
      .then(Double.class, ratio -> this.devicePixelRatio = ratio.doubleValue());
    
    // Later, in some other location
    imageWidget.setWidth((imageWidth / this.devicePixelRatio) + "px");
    imageWidget.setHeight((imageHeight / this.devicePixelRatio) + "px");
    

    EDIT: A variant of the second approach that uses retrieveExtendedClientDetails instead of executeJs:

    ui.getPage().retrieveExtendedClientDetails(details -> this.devicePixelRatio = details.getDevicePixelRatio());
    
    // Later, in some other location
    imageWidget.setWidth((imageWidth / this.devicePixelRatio) + "px");
    imageWidget.setHeight((imageHeight / this.devicePixelRatio) + "px");