Skip to content

Commit eb41984

Browse files
authored
feat: Add Image constructor for byte array content (#22573)
Add a convenience constructor to Image class that accepts byte array content and an image name, simplifying the creation of images from in-memory data. The constructor automatically handles DownloadHandler creation, MIME type detection via URLConnection, and sets inline content disposition for proper browser display. Fixes #21967
1 parent 33d88ba commit eb41984

File tree

2 files changed

+184
-0
lines changed
  • flow-html-components/src

2 files changed

+184
-0
lines changed

flow-html-components/src/main/java/com/vaadin/flow/component/html/Image.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.vaadin.flow.component.html;
1717

18+
import java.io.ByteArrayInputStream;
19+
import java.net.URLConnection;
1820
import java.util.Optional;
1921

2022
import com.vaadin.flow.component.ClickNotifier;
@@ -27,6 +29,7 @@
2729
import com.vaadin.flow.server.StreamResource;
2830
import com.vaadin.flow.server.streams.AbstractDownloadHandler;
2931
import com.vaadin.flow.server.streams.DownloadHandler;
32+
import com.vaadin.flow.server.streams.DownloadResponse;
3033

3134
/**
3235
* Component representing a <code>&lt;img&gt;</code> element.
@@ -115,6 +118,73 @@ public Image(DownloadHandler downloadHandler, String alt) {
115118
setAlt(alt);
116119
}
117120

121+
/**
122+
* Creates an image from byte array content with the given image name.
123+
*
124+
* This convenience constructor simplifies the creation of images from
125+
* in-memory byte data by automatically handling the creation of a
126+
* {@link DownloadHandler} with a {@link DownloadResponse}.
127+
*
128+
* The MIME type is automatically determined from the file extension in the
129+
* image name using {@link URLConnection#guessContentTypeFromName(String)}.
130+
* If the image name does not have a recognizable extension, the content
131+
* type will be null and the browser will attempt to determine it.
132+
*
133+
* The alternative text is set to the provided image name.
134+
*
135+
* Sets the <code>Content-Disposition</code> header to <code>inline</code>
136+
* to ensure the image is displayed in the browser rather than downloaded.
137+
*
138+
* @param imageContent
139+
* the image data as a byte array, not null
140+
* @param imageName
141+
* the image name (including file extension for MIME type
142+
* detection), not null
143+
*
144+
* @see #setSrc(DownloadHandler)
145+
* @see #setAlt(String)
146+
*/
147+
public Image(byte[] imageContent, String imageName) {
148+
this(imageContent, imageName,
149+
URLConnection.guessContentTypeFromName(imageName));
150+
}
151+
152+
/**
153+
* Creates an image from byte array content with the given image name and
154+
* MIME type.
155+
*
156+
* This convenience constructor simplifies the creation of images from
157+
* in-memory byte data by automatically handling the creation of a
158+
* {@link DownloadHandler} with a {@link DownloadResponse}.
159+
*
160+
* Use this constructor when you need to explicitly specify the MIME type,
161+
* either because {@link URLConnection#guessContentTypeFromName(String)}
162+
* does not recognize the file extension or when you want explicit control
163+
* over the content type.
164+
*
165+
* The alternative text is set to the provided image name.
166+
*
167+
* Sets the <code>Content-Disposition</code> header to <code>inline</code>
168+
* to ensure the image is displayed in the browser rather than downloaded.
169+
*
170+
* @param imageContent
171+
* the image data as a byte array, not null
172+
* @param imageName
173+
* the image name, not null
174+
* @param mimeType
175+
* the MIME type of the image (e.g., "image/png", "image/webp"),
176+
* or null to let the browser determine it
177+
*
178+
* @see #setSrc(DownloadHandler)
179+
* @see #setAlt(String)
180+
*/
181+
public Image(byte[] imageContent, String imageName, String mimeType) {
182+
this(DownloadHandler.fromInputStream(event -> {
183+
return new DownloadResponse(new ByteArrayInputStream(imageContent),
184+
imageName, mimeType, imageContent.length);
185+
}).inline(), imageName);
186+
}
187+
118188
/**
119189
* Gets the image URL.
120190
*

flow-html-components/src/test/java/com/vaadin/flow/component/html/ImageTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@
1515
*/
1616
package com.vaadin.flow.component.html;
1717

18+
import java.io.ByteArrayOutputStream;
19+
import java.io.OutputStream;
1820
import java.util.Optional;
1921

2022
import org.junit.Assert;
2123
import org.junit.Test;
24+
import org.mockito.ArgumentCaptor;
2225
import org.mockito.Mockito;
2326

2427
import com.vaadin.flow.dom.Element;
28+
import com.vaadin.flow.server.VaadinRequest;
29+
import com.vaadin.flow.server.VaadinResponse;
30+
import com.vaadin.flow.server.VaadinService;
31+
import com.vaadin.flow.server.VaadinSession;
32+
import com.vaadin.flow.server.streams.DownloadEvent;
2533
import com.vaadin.flow.server.streams.DownloadHandler;
2634
import com.vaadin.flow.server.streams.DownloadResponse;
2735
import com.vaadin.flow.server.streams.InputStreamDownloadHandler;
@@ -71,4 +79,110 @@ public Element getElement() {
7179
new TestImage(handler, "test.png");
7280
Assert.assertTrue(handler.isInline());
7381
}
82+
83+
/**
84+
* Helper method to capture and invoke a DownloadHandler, returning the
85+
* captured content type.
86+
*/
87+
private String captureAndInvokeDownloadHandler(Element element)
88+
throws Exception {
89+
ArgumentCaptor<DownloadHandler> handlerCaptor = ArgumentCaptor
90+
.forClass(DownloadHandler.class);
91+
Mockito.verify(element).setAttribute(Mockito.eq("src"),
92+
handlerCaptor.capture());
93+
94+
DownloadHandler handler = handlerCaptor.getValue();
95+
Assert.assertTrue("Handler should be InputStreamDownloadHandler",
96+
handler instanceof InputStreamDownloadHandler);
97+
98+
// Create mock event and response to capture content type
99+
VaadinRequest request = Mockito.mock(VaadinRequest.class);
100+
VaadinResponse response = Mockito.mock(VaadinResponse.class);
101+
VaadinSession session = Mockito.mock(VaadinSession.class);
102+
VaadinService service = Mockito.mock(VaadinService.class);
103+
OutputStream outputStream = new ByteArrayOutputStream();
104+
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
105+
Mockito.when(response.getService()).thenReturn(service);
106+
Mockito.when(service.getMimeType(Mockito.anyString()))
107+
.thenReturn("application/octet-stream");
108+
109+
DownloadEvent event = new DownloadEvent(request, response, session,
110+
element);
111+
handler.handleDownloadRequest(event);
112+
113+
ArgumentCaptor<String> contentTypeCaptor = ArgumentCaptor
114+
.forClass(String.class);
115+
Mockito.verify(response).setContentType(contentTypeCaptor.capture());
116+
return contentTypeCaptor.getValue();
117+
}
118+
119+
@Test
120+
public void byteArrayConstructor_typicalUseCase() throws Exception {
121+
Element element = Mockito.mock(Element.class);
122+
byte[] imageData = new byte[] { 1, 2, 3, 4, 5 };
123+
124+
class TestImage extends Image {
125+
public TestImage(byte[] content, String name) {
126+
super(content, name);
127+
}
128+
129+
@Override
130+
public Element getElement() {
131+
return element;
132+
}
133+
}
134+
135+
new TestImage(imageData, "test.png");
136+
Mockito.verify(element).setAttribute("alt", "test.png");
137+
138+
String contentType = captureAndInvokeDownloadHandler(element);
139+
Assert.assertEquals("image/png", contentType);
140+
}
141+
142+
@Test
143+
public void byteArrayConstructor_withExplicitMimeType() throws Exception {
144+
Element element = Mockito.mock(Element.class);
145+
byte[] imageData = new byte[] { 1, 2, 3, 4, 5 };
146+
147+
class TestImage extends Image {
148+
public TestImage(byte[] content, String name, String mimeType) {
149+
super(content, name, mimeType);
150+
}
151+
152+
@Override
153+
public Element getElement() {
154+
return element;
155+
}
156+
}
157+
158+
new TestImage(imageData, "test.webp", "image/webp");
159+
Mockito.verify(element).setAttribute("alt", "test.webp");
160+
161+
String contentType = captureAndInvokeDownloadHandler(element);
162+
Assert.assertEquals("image/webp", contentType);
163+
}
164+
165+
@Test
166+
public void byteArrayConstructor_withNullMimeType() throws Exception {
167+
Element element = Mockito.mock(Element.class);
168+
byte[] imageData = new byte[] { 1, 2, 3, 4, 5 };
169+
170+
class TestImage extends Image {
171+
public TestImage(byte[] content, String name, String mimeType) {
172+
super(content, name, mimeType);
173+
}
174+
175+
@Override
176+
public Element getElement() {
177+
return element;
178+
}
179+
}
180+
181+
new TestImage(imageData, "test.img", null);
182+
Mockito.verify(element).setAttribute("alt", "test.img");
183+
184+
String contentType = captureAndInvokeDownloadHandler(element);
185+
// When MIME type is null, it falls back to the service's getMimeType
186+
Assert.assertEquals("application/octet-stream", contentType);
187+
}
74188
}

0 commit comments

Comments
 (0)