Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more customization options for generating different types of im… #1305

Merged
merged 1 commit into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 131 additions & 35 deletions src/main/java/net/datafaker/providers/base/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,97 +3,193 @@

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

import static java.awt.Color.WHITE;
import static net.datafaker.providers.base.Image.ImageType.BMP;
import static net.datafaker.providers.base.Image.ImageType.GIF;
import static net.datafaker.providers.base.Image.ImageType.JPEG;
import static net.datafaker.providers.base.Image.ImageType.PNG;
import static net.datafaker.providers.base.Image.ImageType.SVG;
import static net.datafaker.providers.base.Image.ImageType.TIFF;

/**
* Generates base64 encoded PNG, GIF, JPG and SVG images.
* Generates base64 encoded raster and vector images.
*
* @since 2.3.0
*/
public class Image extends AbstractProvider<BaseProviders> {

private static final int WIDTH = 256;
private static final int HEIGHT = 256;
private static final int BOX_SIZE = WIDTH / 8;
private static final int DEFAULT_WIDTH = 256;
private static final int DEFAULT_HEIGHT = 256;

public enum ImageType {
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
BMP("image/bmp"),
GIF("image/gif"),
JPEG("image/jpeg"),
PNG("image/png"),
SVG("image/svg+xml"),
TIFF("image/tiff");
Comment on lines +32 to +37
Copy link
Collaborator

@snuyanzin snuyanzin Jul 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need image/ prefix for every item?
May be just add it once in get method or in constructor of enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no, we wouldn't really need it, but the field is called mimetype, which is why you have of these duplication, but without the image/ part it wouldn't be a mimetype, it would be..... ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if you replace constructor with

 ImageType(String type) {
      this.mimeType = "images/" + type;
 }     

the field would be still mimeType however less duplication


private final String mimeType;

ImageType(String mimeType) {
this.mimeType = mimeType;
kingthorin marked this conversation as resolved.
Show resolved Hide resolved
}

public String getMimeType() {
return mimeType;
}
}

protected Image(BaseProviders faker) {
super(faker);
}

public String base64PNG() {
return generateBase64Image("png");
public String base64BMP() {
return base64(ImageBuilder.builder().type(BMP).build());
}

public String base64GIF() {
return base64(ImageBuilder.builder().type(GIF).build());
}

public String base64JPG() {
return generateBase64Image("jpeg");
return base64JPEG();
}

public String base64GIF() {
return generateBase64Image("gif");
public String base64JPEG() {
return base64(ImageBuilder.builder().type(JPEG).build());
}

public String base64PNG() {
return base64(ImageBuilder.builder().type(PNG).build());
}

public String base64SVG() {
return generateBase64SVGImage();
return base64(ImageBuilder.builder().type(SVG).build());
}

public String base64TIFF() {
return generateBase64RasterImage(TIFF, DEFAULT_WIDTH, DEFAULT_HEIGHT);
}

public String base64(Base64ImageRuleConfig config) {
if (config.imageType == SVG) {
return generateBase64VectorImage(config.imageType(), config.width(), config.height());
} else {
return generateBase64RasterImage(config.imageType(), config.width(), config.height());
}
}

public record Base64ImageRuleConfig(ImageType imageType, int width, int height) { }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it continue working if we pass Integer.MAX_VALUE ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tried, but most likely it wouldn't. Then again how often would you create an image of 2147483647 x 2147483647? Seems quite the picture, but I don't see a really good reason to limit it to any arbitrary value, I'd call that user error.


public static class ImageBuilder {
private ImageType imageType = PNG;
private int width = DEFAULT_WIDTH;
private int height = DEFAULT_HEIGHT;

private ImageBuilder() {
}

public static ImageBuilder builder() {
return new ImageBuilder();
}

public Base64ImageRuleConfig build() {
return new Base64ImageRuleConfig(imageType, width, height);
}

public ImageBuilder type(ImageType imageType) {
if(imageType == null) {
throw new IllegalArgumentException("Type cannot be null");
}

this.imageType = imageType;
return this;
}

public ImageBuilder width(int width) {
if (width <= 0) {
throw new IllegalArgumentException("Width must be greater than 0");
}

this.width = width;
return this;
}

public ImageBuilder height(int height) {
if (height <= 0) {
throw new IllegalArgumentException("Height must be greater than 0");
}

this.height = height;
return this;
}
}

private String generateBase64Image(String format) {
BufferedImage bufferedImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
private String generateBase64RasterImage(ImageType imageType, int width, int height) {
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = bufferedImage.createGraphics();

int boxSize = Math.max(1, width / 8);

// Fill the image with white background
graphics.setColor(WHITE);
graphics.fillRect(0, 0, WIDTH, HEIGHT);
graphics.fillRect(0, 0, width, height);

// Draw random colored boxes
for (int y = 0; y < HEIGHT; y += BOX_SIZE) {
for (int x = 0; x < WIDTH; x += BOX_SIZE) {
graphics.setColor(randomColor());
graphics.fillRect(x, y, BOX_SIZE, BOX_SIZE);
for (int y = 0; y < height; y += boxSize) {
for (int x = 0; x < width; x += boxSize) {
Color randomColor = randomColor();
graphics.setColor(randomColor);
graphics.fillRect(x, y, boxSize, boxSize);
}
}
graphics.dispose();

try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(bufferedImage, format, baos);
ImageIO.write(bufferedImage, imageType.name(), baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
return "data:" + imageType.mimeType + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

private Color randomColor() {
// Convert the bytes to unsigned integers (0-255) for RGB
byte[] randomBytes = faker.random().nextRandomBytes(3);
int red = randomBytes[0] & 0xFF;
int green = randomBytes[1] & 0xFF;
int blue = randomBytes[2] & 0xFF;
return new Color(red, green, blue);
}

private String generateBase64SVGImage() {
private String generateBase64VectorImage(ImageType imageType, int width, int height) {
StringBuilder svg = new StringBuilder();
svg.append("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"").append(WIDTH).append("\" height=\"").append(HEIGHT).append("\">");
svg.append("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"").append(DEFAULT_WIDTH).append("\" height=\"").append(DEFAULT_HEIGHT).append("\">");

int boxSize = Math.max(1, width / 8);

for (int y = 0; y < HEIGHT; y += BOX_SIZE) {
for (int x = 0; x < WIDTH; x += BOX_SIZE) {
for (int y = 0; y < height; y += boxSize) {
for (int x = 0; x < width; x += boxSize) {
Color randomColor = randomColor();
String color = String.format("#%02x%02x%02x", randomColor.getRed(), randomColor.getGreen(), randomColor.getBlue());
svg.append("<rect x=\"").append(x).append("\" y=\"").append(y).append("\" width=\"").append(BOX_SIZE).append("\" height=\"").append(BOX_SIZE).append("\" fill=\"").append(color).append("\"/>");
svg.append("<rect x=\"").append(x).append("\" y=\"").append(y).append("\" width=\"").append(boxSize).append("\" height=\"").append(boxSize).append("\" fill=\"").append(color).append("\"/>");
}
}

svg.append("</svg>");

String svgString = svg.toString();
String base64Svg = Base64.getEncoder().encodeToString(svgString.getBytes());
return "data:image/svg+xml;base64," + base64Svg;
return "data:" + imageType.mimeType + ";base64," + base64Svg;
}

private Color randomColor() {
// Convert the bytes to unsigned integers (0-255) for RGB
byte[] randomBytes = faker.random().nextRandomBytes(3);
int red = randomBytes[0] & 0xFF;
int green = randomBytes[1] & 0xFF;
int blue = randomBytes[2] & 0xFF;
return new Color(red, green, blue);
}
}
96 changes: 94 additions & 2 deletions src/test/java/net/datafaker/providers/base/ImageTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
package net.datafaker.providers.base;

import net.datafaker.providers.base.Image.ImageType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

class ImageTest extends BaseFakerTest<BaseFaker> {

@Test
void bmp() {
assertThat(faker.image().base64BMP()).startsWith("data:image/bmp;base64,");
}

@Test
void gif() {
assertThat(faker.image().base64GIF()).startsWith("data:image/gif;base64,");
}

@Test
void png() {
assertThat(faker.image().base64PNG()).startsWith("data:image/png;base64,");
Expand All @@ -17,12 +31,90 @@ void jpg() {
}

@Test
void gif() {
assertThat(faker.image().base64GIF()).startsWith("data:image/gif;base64,");
void jpeg() {
assertThat(faker.image().base64JPEG()).startsWith("data:image/jpeg;base64,");
}

@Test
void svg() {
assertThat(faker.image().base64SVG()).startsWith("data:image/svg+xml;base64,");
}

@Test
void tiff() {
assertThat(faker.image().base64TIFF()).startsWith("data:image/tiff;base64,");
}

@ParameterizedTest
@EnumSource(ImageType.class)
void base64(ImageType imageType) {
String base64Image = faker.image().base64(new Image.Base64ImageRuleConfig(imageType, 1000, 1000));

assertThat(base64Image)
.startsWith("data:" + imageType.getMimeType() + ";base64,");
assertThat(base64Image.substring(base64Image.indexOf(",") + 1))
.isNotBlank()
.isBase64();
}

@Test
void defaultBuilder() {
String image = faker.image().base64(Image.ImageBuilder.builder()
.build());
assertThat(image).startsWith("data:image/");
}

@Test
void customBase64builder() {
String gif = faker.image().base64(Image.ImageBuilder.builder()
.type(ImageType.GIF)
.build());
assertThat(gif).startsWith("data:image/gif;base64,");
}

@Test
void tinyBase64builder() {
String tiny = faker.image().base64(Image.ImageBuilder.builder()
.height(1)
.width(1)
.type(ImageType.PNG)
.build());

assertThat(tiny).startsWith("data:image/png;base64,");
}

@Test
void largeBase64builder() {
String large = faker.image().base64(Image.ImageBuilder.builder()
.height(1000)
.width(2000)
.type(ImageType.BMP)
.build());
assertThat(large).startsWith("data:image/bmp;base64,");
}

@Test
void shouldErrorOnIllegalType() {
assertThatIllegalArgumentException().isThrownBy(() -> Image.ImageBuilder.builder().type(null).build());
}

@Test
void shouldErrorOnNegativeWidth() {
assertThatIllegalArgumentException().isThrownBy(() -> Image.ImageBuilder.builder().width(-1).build());
}

@Test
void shouldErrorOnZeroWidth() {
assertThatIllegalArgumentException().isThrownBy(() -> Image.ImageBuilder.builder().width(0).build());
}

@Test
void shouldErrorOnNegativeHeight() {
assertThatIllegalArgumentException().isThrownBy(() -> Image.ImageBuilder.builder().height(-1).build());
}

@Test
void shouldErrorOnZeroHeight() {
assertThatIllegalArgumentException().isThrownBy(() -> Image.ImageBuilder.builder().height(0).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static void main(String[] args) {
System.out.println("PNG Image:");
System.out.println(faker.image().base64PNG());
System.out.println("JPG Image:");
System.out.println(faker.image().base64JPG());
System.out.println(faker.image().base64JPEG());
System.out.println("GIF Image:");
System.out.println(faker.image().base64GIF());
System.out.println("SVG Image:");
Expand Down