Skip to content

Commit bfdf826

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 bfdf826

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ The list below is not complete and shows only a part of available providers. To
345345
* File
346346
* Final Space
347347
* Finance
348+
* Fingerprint
348349
* Food
349350
* Formula 1 (:racing_car:)
350351
* Friends

docs/documentation/providers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Datafaker comes with a total of 256 data providers:
135135
| [Final Space](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/entertainment/FinalSpace.html) | Final Space is an adult animated space opera comedy drama television series. | Entertainment | 1.6.0 |
136136
| [Finance](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/base/Finance.html) | | Base | 0.8.0 |
137137
| [Financial Terms](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/base/FinancialTerms.html) | Provides financial terms. | Base | 2.4.0 |
138+
| [Fingerprint](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/base/Fingerprint.html) | Provides random fingerprint generation. | Base | 2.6.0 |
138139
| [Food](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/food/Food.html) | | Food | 0.8.0 |
139140
| [Football](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/sport/Football.html) | | Sport | 1.5.0 |
140141
| [Formula1](https://javadoc.io/doc/net.datafaker/datafaker/latest/net/datafaker/providers/sport/Formula1.html) | | Sport | 1.2.0 |

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+
}

src/main/resources/META-INF/native-image/reachability-metadata.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3700,6 +3700,10 @@
37003700
"name": "financialTerms",
37013701
"parameterTypes": []
37023702
},
3703+
{
3704+
"name": "fingerprint",
3705+
"parameterTypes": []
3706+
},
37033707
{
37043708
"name": "funnyName",
37053709
"parameterTypes": []
@@ -6475,6 +6479,41 @@
64756479
}
64766480
]
64776481
},
6482+
{
6483+
"type": "net.datafaker.providers.base.Fingerprint",
6484+
"methods": [
6485+
{
6486+
"name": "<init>",
6487+
"parameterTypes": [
6488+
"net.datafaker.providers.base.BaseProviders"
6489+
]
6490+
},
6491+
{
6492+
"name": "png",
6493+
"parameterTypes": []
6494+
},
6495+
{
6496+
"name": "png",
6497+
"parameterTypes": [
6498+
"int",
6499+
"int",
6500+
"net.datafaker.providers.base.Fingerprint$PatternType"
6501+
]
6502+
},
6503+
{
6504+
"name": "base64",
6505+
"parameterTypes": []
6506+
},
6507+
{
6508+
"name": "base64",
6509+
"parameterTypes": [
6510+
"int",
6511+
"int",
6512+
"net.datafaker.providers.base.Fingerprint$PatternType"
6513+
]
6514+
}
6515+
]
6516+
},
64786517
{
64796518
"type": "net.datafaker.providers.base.FunnyName",
64806519
"methods": [
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)