Skip to content

Commit 17a7a66

Browse files
committed
#1784 add random fingerprint generator
Usage: * `faker.fingerprint().png()` * `faker.fingerprint().png(300, 200, ARCH)` * `faker.fingerprint().base64()` * `faker.fingerprint().base64(800, 800, LOOP)`
1 parent 47f4512 commit 17a7a66

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

src/main/java/net/datafaker/providers/base/BaseProviders.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ default FinancialTerms financialTerms() {
233233
return getProvider(FinancialTerms.class, FinancialTerms::new);
234234
}
235235

236+
default Fingerprint fingerprint() {
237+
return getProvider(Fingerprint.class, Fingerprint::new);
238+
}
239+
236240
default FunnyName funnyName() {
237241
return getProvider(FunnyName.class, FunnyName::new);
238242
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package net.datafaker.providers.base;
2+
3+
import javax.imageio.ImageIO;
4+
import java.awt.image.BufferedImage;
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
import java.util.Base64;
8+
9+
/**
10+
* Generates synthetic fingerprint images.
11+
* <p>
12+
* The generated images mimic the three standard fingerprint pattern types:
13+
* whorl (spiral ridges), loop (ridges that loop around a core), and arch
14+
* (ridges that arch over the centre). A random pattern is chosen unless one
15+
* is supplied explicitly.
16+
*
17+
* @since 2.6.0
18+
*/
19+
public class Fingerprint extends AbstractProvider<BaseProviders> {
20+
21+
private static final int DEFAULT_WIDTH = 200;
22+
private static final int DEFAULT_HEIGHT = 250;
23+
private static final double TWO_PI = 2 * Math.PI;
24+
25+
/**
26+
* The three standard fingerprint ridge-flow patterns.
27+
*/
28+
public enum PatternType {WHORL, LOOP, ARCH}
29+
30+
protected Fingerprint(BaseProviders faker) {
31+
super(faker);
32+
}
33+
34+
/**
35+
* Returns raw PNG bytes of a random-pattern fingerprint image.
36+
*/
37+
public byte[] png() {
38+
return png(DEFAULT_WIDTH, DEFAULT_HEIGHT, randomType());
39+
}
40+
41+
/**
42+
* Returns raw PNG bytes of a fingerprint image with the specified size and pattern.
43+
*/
44+
public byte[] png(int width, int height, PatternType pattern) {
45+
return encodePng(render(width, height, pattern));
46+
}
47+
48+
/**
49+
* Returns a base64 PNG data URL of a random-pattern fingerprint.
50+
*/
51+
public String base64() {
52+
return base64(DEFAULT_WIDTH, DEFAULT_HEIGHT, randomType());
53+
}
54+
55+
/**
56+
* Returns a base64 PNG data URL of a random-pattern fingerprint image with the specified size and pattern.
57+
*/
58+
public String base64(int width, int height, PatternType pattern) {
59+
byte[] png = png(width, height, pattern);
60+
return "data:image/png;base64," + Base64.getEncoder().encodeToString(png);
61+
}
62+
63+
private PatternType randomType() {
64+
return faker.random().nextEnum(PatternType.class);
65+
}
66+
67+
private byte[] encodePng(BufferedImage image) {
68+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
69+
ImageIO.write(image, "PNG", baos);
70+
return baos.toByteArray();
71+
} catch (IOException e) {
72+
throw new RuntimeException("Failed to encode fingerprint as PNG", e);
73+
}
74+
}
75+
76+
private BufferedImage render(int width, int height, PatternType patternType) {
77+
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
78+
79+
// Core position – slightly randomised around the centre
80+
double cx = width * (0.47 + faker.random().nextDouble(-0.04, 0.04));
81+
double cy = height * (0.47 + faker.random().nextDouble(-0.04, 0.04));
82+
83+
// Ridge parameters
84+
double ridgePeriod = 11.0 + faker.random().nextDouble(-2.0, 3.0);
85+
double ridgeFraction = 0.42 + faker.random().nextDouble(-0.04, 0.06);
86+
87+
// Pattern-specific parameters
88+
double spiralFactor = 0.5 + faker.random().nextDouble(0.0, 0.5);
89+
double loopAmplitude = height * (0.18 + faker.random().nextDouble(0.0, 0.10));
90+
double loopSigma = Math.min(width, height) * (0.28 + faker.random().nextDouble(0.0, 0.12));
91+
double archAmplitude = height * (0.12 + faker.random().nextDouble(0.0, 0.08));
92+
double archSigma = width * (0.30 + faker.random().nextDouble(0.0, 0.15));
93+
94+
// Organic displacement noise: three sine-wave components
95+
int numNoise = 3;
96+
double[] noiseAmp = {3.2, 1.6, 0.7};
97+
double[] noiseFreq = {0.014, 0.032, 0.068};
98+
double[] phasesX = new double[numNoise];
99+
double[] phasesY = new double[numNoise];
100+
for (int i = 0; i < numNoise; i++) {
101+
phasesX[i] = faker.random().nextDouble(0.0, TWO_PI);
102+
phasesY[i] = faker.random().nextDouble(0.0, TWO_PI);
103+
}
104+
105+
// Oval mask (fingerprints are oval)
106+
double maskA = width * 0.44;
107+
double maskB = height * 0.46;
108+
double fadeZone = 0.13; // fraction of mask radius used for edge fade
109+
110+
// Ink tones
111+
int ridgeGray = faker.random().nextInt(25, 65);
112+
int valleyGray = faker.random().nextInt(195, 235);
113+
114+
for (int y = 0; y < height; y++) {
115+
for (int x = 0; x < width; x++) {
116+
117+
// Sine-wave displacement gives ridges an organic, non-straight look
118+
double noiseX = 0, noiseY = 0;
119+
for (int i = 0; i < numNoise; i++) {
120+
double f = noiseFreq[i];
121+
noiseX += noiseAmp[i] * Math.sin(x * f + y * f * 0.6 + phasesX[i]);
122+
noiseY += noiseAmp[i] * Math.sin(y * f + x * f * 0.6 + phasesY[i]);
123+
}
124+
125+
double dx = x + noiseX - cx;
126+
double dy = y + noiseY - cy;
127+
double r = Math.sqrt(dx * dx + dy * dy);
128+
129+
// Ridge value – periodic over ridgePeriod
130+
double ridgeVal = switch (patternType) {
131+
case WHORL -> {
132+
// Spiral: angle controls how much the ridges rotate
133+
double theta = Math.atan2(dy, dx); // −PI … PI
134+
yield r + spiralFactor * ridgePeriod * theta / Math.PI;
135+
}
136+
case LOOP -> {
137+
// Pull the effective centre upward near the core to form a loop
138+
double pull = loopAmplitude * Math.exp(-r * r / (2 * loopSigma * loopSigma));
139+
double loopDy = dy - pull;
140+
yield Math.sqrt(dx * dx + loopDy * loopDy);
141+
}
142+
case ARCH -> {
143+
// Ridges arch upward over the core
144+
double arch = archAmplitude * Math.exp(-dx * dx / (2 * archSigma * archSigma));
145+
yield dy + arch;
146+
}
147+
};
148+
149+
double mod = ((ridgeVal % ridgePeriod) + ridgePeriod) % ridgePeriod;
150+
boolean isRidge = mod < ridgePeriod * ridgeFraction;
151+
152+
// Oval mask with smooth edge fade toward white
153+
double mx = (x - width * 0.5) / maskA;
154+
double my = (y - height * 0.5) / maskB;
155+
double maskDist = mx * mx + my * my;
156+
157+
int pixel;
158+
if (maskDist >= 1.0) {
159+
pixel = 255;
160+
} else {
161+
double fade = Math.min(1.0, (1.0 - maskDist) / fadeZone);
162+
int base = isRidge ? ridgeGray : valleyGray;
163+
pixel = (int) Math.round(base * fade + 255.0 * (1.0 - fade));
164+
}
165+
166+
image.getRaster().setSample(x, y, 0, pixel);
167+
}
168+
}
169+
170+
return image;
171+
}
172+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package net.datafaker.providers.base;
2+
3+
import net.datafaker.Faker;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.params.ParameterizedTest;
6+
import org.junit.jupiter.params.provider.EnumSource;
7+
8+
import javax.imageio.ImageIO;
9+
import java.awt.image.BufferedImage;
10+
import java.io.ByteArrayInputStream;
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.util.Base64;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
class FingerprintTest {
18+
19+
private final Faker faker = new Faker();
20+
21+
@Test
22+
void pngReturnsPngBytes() {
23+
byte[] bytes = faker.fingerprint().png();
24+
25+
assertThat(bytes).isNotEmpty();
26+
// PNG magic: 0x89 P N G
27+
assertThat(bytes[0]).isEqualTo((byte) 0x89);
28+
assertThat(bytes[1]).isEqualTo((byte) 'P');
29+
assertThat(bytes[2]).isEqualTo((byte) 'N');
30+
assertThat(bytes[3]).isEqualTo((byte) 'G');
31+
}
32+
33+
@Test
34+
void pngHasExpectedDimensions() throws IOException {
35+
byte[] bytes = faker.fingerprint().png();
36+
37+
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
38+
assertThat(image.getWidth()).isEqualTo(200);
39+
assertThat(image.getHeight()).isEqualTo(250);
40+
}
41+
42+
@Test
43+
void base64ReturnsDataUrl() {
44+
String dataUrl = faker.fingerprint().base64();
45+
46+
assertThat(dataUrl).startsWith("data:image/png;base64,");
47+
String encoded = dataUrl.substring("data:image/png;base64,".length());
48+
assertThat(encoded).isNotBlank().isBase64();
49+
}
50+
51+
@Test
52+
void base64DecodesBackToPng() throws IOException {
53+
String dataUrl = faker.fingerprint().base64();
54+
55+
String encoded = dataUrl.substring("data:image/png;base64,".length());
56+
byte[] bytes = Base64.getDecoder().decode(encoded);
57+
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
58+
assertThat(image).isNotNull();
59+
assertThat(image.getWidth()).isEqualTo(200);
60+
assertThat(image.getHeight()).isEqualTo(250);
61+
}
62+
63+
@ParameterizedTest
64+
@EnumSource(Fingerprint.PatternType.class)
65+
void eachPatternTypeProducesValidPng(Fingerprint.PatternType pattern) throws IOException {
66+
byte[] bytes = faker.fingerprint().png(400, 500, pattern);
67+
68+
assertThat(bytes).isNotEmpty();
69+
assertThat(bytes[0]).isEqualTo((byte) 0x89);
70+
assertThat(bytes[1]).isEqualTo((byte) 'P');
71+
72+
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
73+
assertThat(image.getWidth()).isEqualTo(400);
74+
assertThat(image.getHeight()).isEqualTo(500);
75+
76+
File folder = new File("target/surefire-reports");
77+
folder.mkdirs();
78+
ImageIO.write(image, "png", new File(folder, "fingerprint-" + pattern + ".png"));
79+
}
80+
81+
@Test
82+
void twoPngCallsProduceDifferentImages() {
83+
byte[] first = faker.fingerprint().png();
84+
byte[] second = faker.fingerprint().png();
85+
86+
// Extremely unlikely to be identical given randomised parameters
87+
assertThat(first).isNotEqualTo(second);
88+
}
89+
}

0 commit comments

Comments
 (0)