From 6f0b479bc22f0c4e301673322dae4663dce7a380 Mon Sep 17 00:00:00 2001 From: Sergey Rymsha Date: Thu, 16 Nov 2023 12:38:15 +0100 Subject: [PATCH] imagePlaceholder() creates objects that remain in memory #10332 (#10333) --- .../java/com/enonic/xp/image/ImageHelper.java | 15 +-- .../xp/image/ImagePlaceholderFactory.java | 105 ++++++++++++++++++ .../com/enonic/xp/image/ImageHelperTest.java | 6 +- .../xp/examples/portal/imagePlaceholder.js | 2 +- .../src/test/resources/test/url-test.js | 2 +- .../view/ImagePlaceholderFunctionTest.java | 2 +- 6 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 modules/core/core-api/src/main/java/com/enonic/xp/image/ImagePlaceholderFactory.java diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/image/ImageHelper.java b/modules/core/core-api/src/main/java/com/enonic/xp/image/ImageHelper.java index e3fef54fe39..820f7c1882c 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/image/ImageHelper.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/image/ImageHelper.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Base64; import java.util.Iterator; import javax.imageio.IIOImage; @@ -31,19 +30,7 @@ private ImageHelper() public static String createImagePlaceholder( final int width, final int height ) { - try - { - final BufferedImage image = createImage( width, height, true ); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - writeImage( out, image, "png", 0 ); - final byte[] bytes = out.toByteArray(); - - return "data:image/png;base64," + Base64.getEncoder().encodeToString( bytes ); - } - catch ( final IOException e ) - { - throw Exceptions.newRuntime( "Failed to create image placeholder" ).withCause( e ); - } + return new ImagePlaceholderFactory( width, height ).create(); } @Deprecated diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/image/ImagePlaceholderFactory.java b/modules/core/core-api/src/main/java/com/enonic/xp/image/ImagePlaceholderFactory.java new file mode 100644 index 00000000000..7f30bae4d36 --- /dev/null +++ b/modules/core/core-api/src/main/java/com/enonic/xp/image/ImagePlaceholderFactory.java @@ -0,0 +1,105 @@ +package com.enonic.xp.image; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; +import java.util.zip.DeflaterOutputStream; + +final class ImagePlaceholderFactory +{ + private static final byte[] PNG_MAGIC = {(byte) 137, 80, 78, 71, 13, 10, 26, 10}; + + private static final byte[] IDAT_TYPE_BYTES = "IDAT".getBytes( StandardCharsets.ISO_8859_1 ); + + private static final byte[] IHDR_TYPE_BYTES = "IHDR".getBytes( StandardCharsets.ISO_8859_1 ); + + private static final byte[] IHDR_CONST_PART = {(byte) 8, 6, 0, 0, 0}; + + private static final byte[] IEND = {(byte) 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; + + private static final byte[] PREFIX = "data:image/png;base64,".getBytes( StandardCharsets.ISO_8859_1 ); + + private final int width; + + private final int height; + + ImagePlaceholderFactory( final int width, final int height ) + { + this.width = width; + this.height = height; + } + + public String create() + { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try + { + output.write( PREFIX ); + try (var base64Stream = Base64.getEncoder().wrap( output )) + { + base64Stream.write( PNG_MAGIC ); + writeChunk( base64Stream, IHDR_TYPE_BYTES, createIhdr() ); + writeChunk( base64Stream, IDAT_TYPE_BYTES, createIdat() ); + base64Stream.write( IEND ); + } + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + + return output.toString( StandardCharsets.ISO_8859_1 ); + } + + private byte[] createIhdr() + throws IOException + { + ByteArrayOutputStream ihdr = new ByteArrayOutputStream(); + writeBigEndianInt( ihdr, width ); + writeBigEndianInt( ihdr, height ); + ihdr.write( IHDR_CONST_PART ); + return ihdr.toByteArray(); + } + + private byte[] createIdat() + throws IOException + { + final ByteArrayOutputStream idat = new ByteArrayOutputStream(); + + final int bufferSize = 512; // DeflaterOutputStream uses this size as a default buffer size + byte[] buffer = new byte[bufferSize]; + int totalBytes = width * height * 5; // 5 bytes per pixel (RGBA) + try (DeflaterOutputStream defStream = new DeflaterOutputStream( idat )) + { + for ( int i = 0; i < totalBytes; i += bufferSize ) + { + defStream.write( buffer, 0, Math.min( bufferSize, totalBytes - i ) ); + } + } + return idat.toByteArray(); + } + + private void writeChunk( final OutputStream outputStream, final byte[] typeBytes, final byte[] data ) + throws IOException + { + CheckedOutputStream crcStream = new CheckedOutputStream( outputStream, new CRC32() ); + writeBigEndianInt( outputStream, data.length ); + crcStream.write( typeBytes ); + crcStream.write( data ); + writeBigEndianInt( outputStream, (int) crcStream.getChecksum().getValue() ); + } + + private static void writeBigEndianInt( final OutputStream stream, final int value ) + throws IOException + { + stream.write( ( value >>> 24 ) & 0xFF ); + stream.write( ( value >>> 16 ) & 0xFF ); + stream.write( ( value >>> 8 ) & 0xFF ); + stream.write( value & 0xFF ); + } +} diff --git a/modules/core/core-api/src/test/java/com/enonic/xp/image/ImageHelperTest.java b/modules/core/core-api/src/test/java/com/enonic/xp/image/ImageHelperTest.java index 11e03a20526..470c26050bb 100644 --- a/modules/core/core-api/src/test/java/com/enonic/xp/image/ImageHelperTest.java +++ b/modules/core/core-api/src/test/java/com/enonic/xp/image/ImageHelperTest.java @@ -3,16 +3,14 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; class ImageHelperTest { @Test void createImagePlaceholder() { - final String str = ImageHelper.createImagePlaceholder( 2, 2 ); - assertNotNull( str ); - assertEquals( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAC0lEQVR42mNgQAcAABIAAeRVjecAAAAASUVORK5CYII=", + final String str = ImageHelper.createImagePlaceholder( 2, 3 ); + assertEquals( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAYAAAC56t6BAAAAC0lEQVR4nGNgwAcAAB4AAfb96ZYAAAAASUVORK5CYII=", str ); } diff --git a/modules/lib/lib-portal/src/main/resources/lib/xp/examples/portal/imagePlaceholder.js b/modules/lib/lib-portal/src/main/resources/lib/xp/examples/portal/imagePlaceholder.js index ee8c3c2adad..623b9442655 100644 --- a/modules/lib/lib-portal/src/main/resources/lib/xp/examples/portal/imagePlaceholder.js +++ b/modules/lib/lib-portal/src/main/resources/lib/xp/examples/portal/imagePlaceholder.js @@ -10,7 +10,7 @@ var url = portalLib.imagePlaceholder({ // BEGIN // URL returned. -var expected = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGUlEQVR42u3BAQEAAACCIP+vbkhAAQAA7wYMGAAB93LuRQAAAABJRU5ErkJggg=='; +var expected = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGklEQVR4nO3BAQ0AAADCoPdPbQ8HFAAAAPwbDwAAAQUUSzEAAAAASUVORK5CYII='; // END assert.assertEquals(expected, url); diff --git a/modules/lib/lib-portal/src/test/resources/test/url-test.js b/modules/lib/lib-portal/src/test/resources/test/url-test.js index ef20f87927f..ff7697e18ff 100644 --- a/modules/lib/lib-portal/src/test/resources/test/url-test.js +++ b/modules/lib/lib-portal/src/test/resources/test/url-test.js @@ -248,7 +248,7 @@ exports.imagePlaceholderTest = function () { }); assert.assertEquals( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAYAAADVTVykAAAAEklEQVR42mNgGAWjYBSMgpEOAAUKAAEVKR6qAAAAAElFTkSuQmCC', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAYAAADVTVykAAAAFUlEQVR4nGNgGAWjYBSMglEwCkgHAAZAAAEAAhBgAAAAAElFTkSuQmCC', result); return true; }; diff --git a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/view/ImagePlaceholderFunctionTest.java b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/view/ImagePlaceholderFunctionTest.java index fab95bda8fe..7a4669155ed 100644 --- a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/view/ImagePlaceholderFunctionTest.java +++ b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/view/ImagePlaceholderFunctionTest.java @@ -19,7 +19,7 @@ protected void setupFunction() public void testExecute() { final Object result = execute( "imagePlaceholder", "width=2", "height=2" ); - assertEquals( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAC0lEQVR42mNgQAcAABIAAeRVjecAAAAASUVORK5CYII=", + assertEquals( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAC0lEQVR4nGNgwAQAABQAAX3+Hu4AAAAASUVORK5CYII=", result ); }