13 | * Adds noise to a {@link Sample}. Noise is chosen at random from one of the
14 | * built-in noise files:
15 | *
16 | *
17 | *
18 | *
{@code radio_tuning.wav}
19 | *
{@code restaurant.wav}
20 | *
{@code swimming.wav}
21 | *
22 | *
23 | * @author James Childers
24 | * @author Paul Hoadley
25 | * @since 1.0
26 | */
27 | public class RandomNoiseProducer implements NoiseProducer {
28 | /**
29 | * Relative volume of background noise
30 | */
31 | private static final double NOISE_VOLUME = 0.6;
32 |
33 | /**
34 | * Random number generator
35 | */
36 | private static final Random RAND = new Random();
37 |
38 | /**
39 | * Built-in noise samples
40 | */
41 | private static final String[] BUILT_IN_NOISES = {
42 | "/sounds/noises/radio_tuning.wav",
43 | "/sounds/noises/restaurant.wav",
44 | "/sounds/noises/swimming.wav", };
45 |
46 | /**
47 | * Noise files to use
48 | */
49 | private final String[] noiseFiles;
50 |
51 | /**
52 | * Constructor: object will use built-in noise files.
53 | */
54 | public RandomNoiseProducer() {
55 | this(BUILT_IN_NOISES);
56 | }
57 |
58 | /**
59 | * Constructor taking an array of noise filenames.
60 | *
61 | * @param noiseFiles noise filenames
62 | */
63 | public RandomNoiseProducer(String[] noiseFiles) {
64 | this.noiseFiles = Arrays.copyOf(noiseFiles, noiseFiles.length);
65 | return;
66 | }
67 |
68 | /**
69 | * Concatenates {@code samples}, then adds a random background noise sample
70 | * (from this object's list of samples), returning the resulting {@link Sample}.
71 | *
72 | * @param samples a list of {@link Sample}s
73 | * @return concatenated {@link Sample}s with added noise
74 | */
75 | @Override
76 | public Sample addNoise(List samples) {
77 | Sample appended = Mixer.concatenate(samples);
78 | String noiseFile = noiseFiles[RAND.nextInt(noiseFiles.length)];
79 | Sample noise = new Sample(noiseFile);
80 | // Decrease the volume of the noise to make sure the voices can be heard
81 | return Mixer.mix(appended, 1.0, noise, NOISE_VOLUME);
82 | }
83 |
84 | @Override
85 | public String toString() {
86 | StringBuffer sb = new StringBuffer(34);
87 | sb.append("[RandomNoiseProducer: noiseFiles=").append(Arrays.asList(noiseFiles).stream().collect(Collectors.joining(", "))).append("]");
88 | return sb.toString();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/noise/StraightLineNoiseProducer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.noise;
2 |
3 | import java.awt.Color;
4 | import java.awt.Graphics;
5 | import java.awt.Graphics2D;
6 | import java.awt.image.BufferedImage;
7 | import java.util.Random;
8 |
9 | /**
10 | * Draws a straight line through the given image.
11 | *
12 | * @author James Childers
13 | * @author Paul Hoadley
14 | * @since 1.0
15 | */
16 | public class StraightLineNoiseProducer implements NoiseProducer {
17 | /**
18 | * Random number generator
19 | */
20 | private static final Random RAND = new Random();
21 |
22 | /**
23 | * Default line {@link Color}
24 | */
25 | private static final Color DEFAULT_COLOR = Color.RED;
26 |
27 | /**
28 | * Default line width
29 | */
30 | private static final int DEFAULT_WIDTH = 4;
31 |
32 | /**
33 | * Line {@link Color}
34 | */
35 | private final Color lineColor;
36 |
37 | /**
38 | * Line width
39 | */
40 | private final int lineWidth;
41 |
42 | /**
43 | * Constructor using default values.
44 | */
45 | public StraightLineNoiseProducer() {
46 | this(DEFAULT_COLOR, DEFAULT_WIDTH);
47 | return;
48 | }
49 |
50 | /**
51 | * Constructor taking a line {@link Color} and line width.
52 | *
53 | * @param lineColor line {@link Color}
54 | * @param lineWidth line width
55 | */
56 | public StraightLineNoiseProducer(Color lineColor, int lineWidth) {
57 | this.lineColor = lineColor;
58 | this.lineWidth = lineWidth;
59 | return;
60 | }
61 |
62 | @Override
63 | public void makeNoise(BufferedImage image) {
64 | Graphics2D graphics = image.createGraphics();
65 | int height = image.getHeight();
66 | int width = image.getWidth();
67 | int y1 = RAND.nextInt(height) + 1;
68 | int y2 = RAND.nextInt(height) + 1;
69 | drawLine(graphics, y1, width, y2);
70 | }
71 |
72 | private void drawLine(Graphics g, int y1, int x2, int y2) {
73 | int x1 = 0;
74 |
75 | // The thick line is in fact a filled polygon
76 | g.setColor(lineColor);
77 | int dX = x2 - x1;
78 | int dY = y2 - y1;
79 | // line length
80 | double lineLength = Math.sqrt(dX * dX + dY * dY);
81 |
82 | double scale = lineWidth / (2 * lineLength);
83 |
84 | // The x and y increments from an endpoint needed to create a
85 | // rectangle...
86 | double ddx = -scale * dY;
87 | double ddy = scale * dX;
88 | ddx += ddx > 0 ? 0.5 : -0.5;
89 | ddy += ddy > 0 ? 0.5 : -0.5;
90 | int dx = (int) ddx;
91 | int dy = (int) ddy;
92 |
93 | // Now we can compute the corner points...
94 | int[] xPoints = new int[4];
95 | int[] yPoints = new int[4];
96 |
97 | xPoints[0] = x1 + dx;
98 | yPoints[0] = y1 + dy;
99 | xPoints[1] = x1 - dx;
100 | yPoints[1] = y1 - dy;
101 | xPoints[2] = x2 - dx;
102 | yPoints[2] = y2 - dy;
103 | xPoints[3] = x2 + dx;
104 | yPoints[3] = y2 + dy;
105 |
106 | g.fillPolygon(xPoints, yPoints, 4);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/noise/GaussianNoiseProducer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.noise;
2 |
3 | import java.awt.image.BufferedImage;
4 | import java.awt.image.WritableRaster;
5 | import java.util.Random;
6 |
7 | /**
8 | * Adds Gaussian noise to the image. Gaussian noise is statistical noise having a
9 | * probability density function equal to that of the normal distribution, which is
10 | * also known as the Gaussian distribution.
11 | *
12 | * @author bivashy
13 | * @see Gaussian noise on Wikipedia
14 | * @since 2.0
15 | */
16 | public class GaussianNoiseProducer implements NoiseProducer {
17 | /**
18 | * Random number generator.
19 | */
20 | private static final Random RAND = new Random();
21 |
22 | /**
23 | * Default standard deviation.
24 | */
25 | private static final int DEFAULT_STANDARD_DEVIATION = 20;
26 |
27 | /**
28 | * Default mean.
29 | */
30 | private static final int DEFAULT_MEAN = 0;
31 |
32 | /**
33 | * Standard deviation for the Gaussian noise.
34 | */
35 | private final int standardDeviation;
36 |
37 | /**
38 | * Mean for the Gaussian noise.
39 | */
40 | private final int mean;
41 |
42 | /**
43 | * Constructor using default standard deviation and mean.
44 | */
45 | public GaussianNoiseProducer() {
46 | this(DEFAULT_STANDARD_DEVIATION, DEFAULT_MEAN);
47 | return;
48 | }
49 |
50 | /**
51 | * Constructor to create a Gaussian noise producer with specified standard deviation and mean.
52 | *
53 | * @param standardDeviation the standard deviation of the Gaussian noise
54 | * @param mean the mean of the Gaussian noise
55 | */
56 | public GaussianNoiseProducer(int standardDeviation, int mean) {
57 | this.standardDeviation = standardDeviation;
58 | this.mean = mean;
59 | return;
60 | }
61 |
62 | /**
63 | * Applies Gaussian noise to a BufferedImage.
64 | *
65 | * @param image the BufferedImage to which the noise is to be applied
66 | */
67 | @Override
68 | public void makeNoise(BufferedImage image) {
69 | WritableRaster raster = image.getRaster();
70 | for (int y = 0; y < raster.getHeight(); y++) {
71 | for (int x = 0; x < raster.getWidth(); x++) {
72 | int[] pixelSamples = raster.getPixel(x, y, (int[]) null);
73 |
74 | for (int i = 0; i < pixelSamples.length; i++) {
75 | pixelSamples[i] = clamp((int) (pixelSamples[i] + RAND.nextGaussian() * standardDeviation + mean), 0, 255);
76 | }
77 |
78 | raster.setPixel(x, y, pixelSamples);
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * Clamp a value to an interval.
85 | *
86 | * @param a the lower clamp threshold
87 | * @param b the upper clamp threshold
88 | * @param x the input parameter
89 | * @return the clamped value
90 | */
91 | private static int clamp(int x, int a, int b) {
92 | return (x < a) ? a : (x > b) ? b : x;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/noise/CurvedLineNoiseProducer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.noise;
2 |
3 | import java.awt.BasicStroke;
4 | import java.awt.Color;
5 | import java.awt.Graphics2D;
6 | import java.awt.RenderingHints;
7 | import java.awt.geom.CubicCurve2D;
8 | import java.awt.geom.PathIterator;
9 | import java.awt.geom.Point2D;
10 | import java.awt.image.BufferedImage;
11 | import java.util.Random;
12 |
13 | /**
14 | * Adds a randomly curved line to the image.
15 | *
16 | * @author James Childers
17 | * @author Paul Hoadley
18 | * @since 1.0
19 | */
20 | public class CurvedLineNoiseProducer implements NoiseProducer {
21 | /**
22 | * Random number generator
23 | */
24 | private static final Random RAND = new Random();
25 |
26 | /**
27 | * Default line {@link Color}
28 | */
29 | private static final Color DEFAULT_COLOR = Color.BLACK;
30 |
31 | /**
32 | * Default line width
33 | */
34 | private static final float DEFAULT_WIDTH = 3.0f;
35 |
36 | /**
37 | * Line {@link Color}
38 | */
39 | private final Color lineColor;
40 |
41 | /**
42 | * Line width
43 | */
44 | private final float lineWidth;
45 |
46 | /**
47 | * Constructor using default {@link Color} and width.
48 | */
49 | public CurvedLineNoiseProducer() {
50 | this(DEFAULT_COLOR, DEFAULT_WIDTH);
51 | return;
52 | }
53 |
54 | /**
55 | * Constructor taking {@link Color} and width.
56 | *
57 | * @param lineColor line {@link Color}
58 | * @param lineWidth line width
59 | */
60 | public CurvedLineNoiseProducer(Color lineColor, float lineWidth) {
61 | this.lineColor = lineColor;
62 | this.lineWidth = lineWidth;
63 | return;
64 | }
65 |
66 | @Override
67 | public void makeNoise(BufferedImage image) {
68 | int width = image.getWidth();
69 | int height = image.getHeight();
70 |
71 | // the curve from where the points are taken
72 | CubicCurve2D cc = new CubicCurve2D.Float(width * .1f, height * RAND.nextFloat(), width * .1f,
73 | height * RAND.nextFloat(), width * .25f, height * RAND.nextFloat(), width * .9f,
74 | height * RAND.nextFloat());
75 |
76 | // creates an iterator to define the boundary of the flattened curve
77 | PathIterator pi = cc.getPathIterator(null, 2);
78 | Point2D[] tmp = new Point2D[200];
79 | int i = 0;
80 |
81 | float[] coords;
82 | // while pi is iterating the curve, adds points to tmp array
83 | while (!pi.isDone()) {
84 | coords = new float[6];
85 | if (pi.currentSegment(coords) == PathIterator.SEG_MOVETO
86 | || pi.currentSegment(coords) == PathIterator.SEG_LINETO) {
87 | tmp[i] = new Point2D.Float(coords[0], coords[1]);
88 | }
89 | i++;
90 | pi.next();
91 | }
92 |
93 | // the points where the line changes the stroke and direction
94 | Point2D[] pts = new Point2D[i];
95 | // copies points from tmp to pts
96 | System.arraycopy(tmp, 0, pts, 0, i);
97 |
98 | Graphics2D graph = (Graphics2D) image.getGraphics();
99 | graph.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
100 | graph.setColor(lineColor);
101 |
102 | // for the maximum 3 point change the stroke and direction
103 | for (i = 0; i < pts.length - 1; i++) {
104 | if (i < 3) {
105 | graph.setStroke(new BasicStroke(lineWidth));
106 | }
107 | graph.drawLine((int) pts[i].getX(), (int) pts[i].getY(), (int) pts[i + 1].getX(), (int) pts[i + 1].getY());
108 | }
109 | graph.dispose();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/filter/FishEyeImageFilter.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.filter;
2 |
3 | import java.awt.Color;
4 | import java.awt.Graphics2D;
5 | import java.awt.image.BufferedImage;
6 |
7 | /**
8 | * Overlays a warped grid to the image.
9 | *
10 | * @author James Childers
11 | * @author Paul Hoadley
12 | * @since 1.0
13 | */
14 | public class FishEyeImageFilter implements ImageFilter {
15 | /**
16 | * Default {@link Color} for lines
17 | */
18 | private static final Color DEFAULT_COLOR = Color.BLACK;
19 |
20 | /**
21 | * Horizontal line {@link Color}
22 | */
23 | private final Color hColor;
24 |
25 | /**
26 | * Verical line {@link Color}
27 | */
28 | private final Color vColor;
29 |
30 | /**
31 | * Constructor using default line colours.
32 | */
33 | public FishEyeImageFilter() {
34 | this(DEFAULT_COLOR, DEFAULT_COLOR);
35 | return;
36 | }
37 |
38 | /**
39 | * Constructor taking colours for lines.
40 | *
41 | * @param hColor horizontal line {@link Color}
42 | * @param vColor vertical line {@link Color}
43 | */
44 | public FishEyeImageFilter(Color hColor, Color vColor) {
45 | this.hColor = hColor;
46 | this.vColor = vColor;
47 | return;
48 | }
49 |
50 | @Override
51 | public void filter(BufferedImage image) {
52 | int height = image.getHeight();
53 | int width = image.getWidth();
54 |
55 | int hstripes = height / 7;
56 | int vstripes = width / 7;
57 |
58 | // Calculate space between lines
59 | int hspace = height / (hstripes + 1);
60 | int vspace = width / (vstripes + 1);
61 |
62 | Graphics2D graph = (Graphics2D) image.getGraphics();
63 | // Draw the horizontal stripes
64 | for (int i = hspace; i < height; i = i + hspace) {
65 | graph.setColor(hColor);
66 | graph.drawLine(0, i, width, i);
67 | }
68 |
69 | // Draw the vertical stripes
70 | for (int i = vspace; i < width; i = i + vspace) {
71 | graph.setColor(vColor);
72 | graph.drawLine(i, 0, i, height);
73 | }
74 |
75 | // Create a pixel array of the original image.
76 | // we need this later to do the operations on..
77 | int[] pix = new int[height * width];
78 | int j = 0;
79 |
80 | for (int j1 = 0; j1 < width; j1++) {
81 | for (int k1 = 0; k1 < height; k1++) {
82 | pix[j] = image.getRGB(j1, k1);
83 | j++;
84 | }
85 | }
86 |
87 | double distance = ranInt(width / 4, width / 3);
88 |
89 | // put the distortion in the (dead) middle
90 | int wMid = image.getWidth() / 2;
91 | int hMid = image.getHeight() / 2;
92 |
93 | // again iterate over all pixels..
94 | for (int x = 0; x < image.getWidth(); x++) {
95 | for (int y = 0; y < image.getHeight(); y++) {
96 |
97 | int relX = x - wMid;
98 | int relY = y - hMid;
99 |
100 | double d1 = Math.sqrt(relX * relX + relY * relY);
101 | if (d1 < distance) {
102 |
103 | int j2 = wMid + (int) (((fishEyeFormula(d1 / distance) * distance) / d1) * (x - wMid));
104 | int k2 = hMid + (int) (((fishEyeFormula(d1 / distance) * distance) / d1) * (y - hMid));
105 | image.setRGB(x, y, pix[j2 * height + k2]);
106 | }
107 | }
108 | }
109 |
110 | graph.dispose();
111 | }
112 |
113 | private int ranInt(int i, int j) {
114 | double d = Math.random();
115 | return (int) (i + ((j - i) + 1) * d);
116 | }
117 |
118 | private double fishEyeFormula(double s) {
119 | // implementation of:
120 | // g(s) = - (3/4)s3 + (3/2)s2 + (1/4)s, with s from 0 to 1.
121 | if (s < 0.0D) {
122 | return 0.0D;
123 | }
124 | if (s > 1.0D) {
125 | return s;
126 | }
127 |
128 | return -0.75D * s * s * s + 1.5D * s * s + 0.25D * s;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://opensource.org/licenses/BSD-3-Clause)
3 |
4 | NanoCaptcha
5 | ===========
6 |
7 | What is this?
8 | -------------
9 | NanoCaptcha is a Java library for generating image and audio
10 | CAPTCHAs. NanoCaptcha is intended to be:
11 |
12 | * Self-contained: no network API hits to any external services.
13 |
14 | * Minimally-dependent: using NanoCaptcha should not involve pulling in
15 | a plethora of JARs, and ideally none at all.
16 |
17 | Getting started
18 | ---------------
19 | You can build a minimal image CAPTCHA very easily:
20 |
21 | ImageCaptcha imageCaptcha = ImageCaptcha.create();
22 |
23 | This creates a 200 x 50 pixel image and adds five random characters
24 | from the Latin alphabet. The `getImage()` method returns the image as
25 | a `BufferedImage` object. `isCorrect(String)` will verify the supplied
26 | string against the text content of the image. If you need the text
27 | content itself, call `getContent()`. Image CAPTCHAs can be further
28 | customised by:
29 |
30 | * Using different `ContentProducer`s (e.g., `ChineseContentProducer`).
31 | * Supplying your own `Color`s and `Font`s.
32 | * Adding noise using a `NoiseProducer`.
33 | * Adding various `ImageFilter`s.
34 | * Adding a background or a border.
35 |
36 | To create a custom CAPTCHA, you can use an `ImageCaptcha.Builder`,
37 | e.g.:
38 |
39 | ImageCaptcha imageCaptcha = new ImageCaptcha.Builder(400, 100)
40 | .addContent(new LatinContentProducer(7),
41 | new DefaultWordRenderer.Builder()
42 | .randomColor(Color.BLACK, Color.BLUE, Color.CYAN, Color.RED)
43 | .build())
44 | .addBackground(new GradiatedBackgroundProducer())
45 | .addNoise(new CurvedLineNoiseProducer())
46 | .build();
47 |
48 | Building a minimal audio CAPTCHA is just as easy:
49 |
50 | AudioCaptcha audioCaptcha = AudioCaptcha.create();
51 |
52 | This creates a CAPTCHA with an audio clip containing five numbers read
53 | out in English (unless the default `Locale` has been changed). To
54 | customise your CAPTCHA, you can use `AudioCaptcha.Builder`.
55 |
56 | There is support for different languages. (Currently English, German
57 | and French are supported.) You can set the system property
58 | `net.logicsquad.nanocaptcha.audio.producer.RandomNumberVoiceProducer.defaultLanguage`
59 | to a 2-digit code for a supported language, e.g., `de`, and the
60 | `Builder` above will return German digit vocalizations. Alternatively,
61 | you can supply a `RandomNumberVoiceProducer` explicitly:
62 |
63 | AudioCaptcha audioCaptcha = new AudioCaptcha.Builder()
64 | .addContent()
65 | .addVoice(new RandomNumberVoiceProducer(Locale.GERMAN))
66 | .build();
67 |
68 | You can even mix languages by calling `addVoice(Locale)` more than
69 | once.
70 |
71 | As with image CAPTCHAs, these can be further customised by:
72 |
73 | * Adding background noise with a `NoiseProducer`.
74 |
75 | Playing the audio is probably application-dependent, but the following
76 | snippet will play the clip locally:
77 |
78 | Clip clip = AudioSystem.getClip();
79 | clip.open(audioCaptcha.getAudio().getAudioInputStream());
80 | clip.start();
81 | Thread.sleep(10000);
82 |
83 | (The call to `Thread.sleep()` is simply to keep the JVM alive long
84 | enough to play the clip.)
85 |
86 | Using NanoCaptcha
87 | -----------------
88 | You can use NanoCaptcha in your projects by including it as a Maven dependency:
89 |
90 |
91 | net.logicsquad
92 | nanocaptcha
93 | 2.1
94 |
95 |
96 | Contributing
97 | ------------
98 | By all means, open issue tickets and pull requests if you have something
99 | to contribute.
100 |
101 | References
102 | ----------
103 | NanoCaptcha is based on
104 | [SimpleCaptcha](https://sourceforge.net/p/simplecaptcha/),
105 | and incorporates code from
106 | [JH Labs Java Image Filters](http://huxtable.com/ip/filters/).
107 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This project adheres to [Semantic
4 | Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | ## Release 1.0 (2019-12-31)
7 |
8 | Initial public release. SimpleCaptcha source was imported and tidied
9 | up, including: Javadoc comments, visibility tightening, API pruning.
10 |
11 |
12 | ## Release 1.1 (2020-01-26)
13 |
14 | ### Added
15 | - New `FastWordRenderer` can render image CAPTCHAs about 5X faster
16 | (with a reduction in configurability).
17 |
18 | ### Changed
19 | - Several speed improvements to `ImageCaptcha` and
20 | `DefaultWordRenderer`.
21 | - Minor improvements to documentation, including `README.md` and
22 | Javadocs.
23 | - Minor code improvements suggested by PMD, FindBugs, SpotBugs and
24 | Checkstyle.
25 | - Substitutes `Random` for `SecureRandom`.
26 |
27 | ### Fixed
28 | - `ImageCaptcha.isCorrect()` returns `false` for a `null`
29 | argument. [#1](https://github.com/logicsquad/nanocaptcha/issues/1)
30 |
31 |
32 | ## Release 1.2 (2021-02-14)
33 |
34 | ### Changed
35 | - Removed dependency on `com.jhlabs.filters`.
36 | [#4](https://github.com/logicsquad/nanocaptcha/issues/4)
37 |
38 | ### Security
39 | - Updated JUnit 4.12 → 4.13.1.
40 | [#2](https://github.com/logicsquad/nanocaptcha/issues/2)
41 |
42 |
43 | ## Release 1.3 (2022-10-05)
44 |
45 | ### Fixed
46 | - Inserted a `BufferedInputStream` into the `Sample(InputStream)`
47 | constructor to allow audio to be played from resources in the
48 | JAR. [#6](https://github.com/logicsquad/nanocaptcha/issues/6)
49 |
50 | ### Security
51 | - Updated SLF4J 2.9.0 → 2.18.0.
52 |
53 |
54 | ## Release 1.4 (2023-03-12)
55 |
56 | ### Added
57 | - Improved support for alternate languages, and added German digit
58 | samples for audio
59 | CAPTCHAs. [#7](https://github.com/logicsquad/nanocaptcha/issues/7)
60 | - Added support in `Builder` classes for setting content
61 | length. [#9](https://github.com/logicsquad/nanocaptcha/issues/9)
62 | - Added support for randomising the y-offset in image CAPTCHAs, which
63 | improves variability in "tall"
64 | images. [#13](https://github.com/logicsquad/nanocaptcha/issues/13)
65 |
66 |
67 | ## Release 1.5 (2023-02-22)
68 |
69 | ### Fixed
70 | - `WordRenderer` implementations now use built-in fonts by default: we
71 | were making assumptions about font availability that were rarely
72 | true. Ships with "Courier Prime" and "Public
73 | Sans". [#14](https://github.com/logicsquad/nanocaptcha/issues/14)
74 | - `FastWordRenderer` was initialising static variables in its
75 | constructor. This has been moved out to a static
76 | block. [#15](https://github.com/logicsquad/nanocaptcha/issues/15)
77 |
78 |
79 | ## Release 2.0 (2023-12-27)
80 |
81 | ### Added
82 | - Improved colour support in `WordRenderer`
83 | implementations. `DefaultWordRenderer` loses the deprecated
84 | `DefaultWordRenderer(List colors, List fonts)`
85 | constructor, and colours are now handled by additions to its
86 | `Builder` (via `AbstractWordRenderer.Builder`). `FastWordRenderer`
87 | benefits in the same way, and it is no longer restricted to a single
88 | colour. Colour options can now be supplied by the `Builder`'s
89 | `color()` and `randomColor()`
90 | methods. [#18](https://github.com/logicsquad/nanocaptcha/issues/18)
91 | - Added two new `NoiseProducer` implementations:
92 | `GaussianNoiseProducer` and
93 | `SaltAndPepperNoiseProducer`. [#19](https://github.com/logicsquad/nanocaptcha/issues/19)
94 | - Added new `create()` static factory method in both `ImageCaptcha`
95 | and `AudioCaptcha` to make the simplest case even
96 | simpler. [#12](https://github.com/logicsquad/nanocaptcha/issues/12)
97 | - Added an SLF4J implementation for unit tests to
98 | use. [#20](https://github.com/logicsquad/nanocaptcha/issues/20)
99 |
100 | ### Changed
101 | - Removed deprecated constructors in `RandomNumberVoiceProducer`,
102 | `DefaultWordRenderer` and
103 | `FastWordRenderer`. [#11](https://github.com/logicsquad/nanocaptcha/issues/11)
104 |
105 |
106 | ## Release 2.1 (2024-01-04)
107 |
108 | ### Added
109 | - Added custom font support via `AbstractWordRenderer.Builder`, with
110 | methods analogous to recent additions for `Color` support (in
111 | 2.0). (Note that while `DefaultWordRenderer` will honour custom
112 | fonts set, `FastWordRenderer` uses only the two built-in fonts.)
113 | [#21](https://github.com/logicsquad/nanocaptcha/issues/21)
114 |
115 | ### Fixed
116 | - Reverted the visibility reduction of `AbstractWordRenderer.Builder`
117 | to public. (The change in 2.0 effectively completely broke usage of
118 | the `Builder`s in both `WordRenderer` implementations!)
119 | [#22](https://github.com/logicsquad/nanocaptcha/issues/22)
120 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/audio/Mixer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.audio;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.InputStream;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import java.util.Objects;
8 |
9 | import javax.sound.sampled.AudioInputStream;
10 |
11 | /**
12 | * Helper class for operating on audio {@link Sample}s.
13 | *
14 | * @author James Childers
15 | * @author Paul Hoadley
16 | * @since 1.0
17 | */
18 | public final class Mixer {
19 | /**
20 | * Private constructor for non-instantiability.
21 | */
22 | private Mixer() {
23 | throw new AssertionError();
24 | }
25 |
26 | /**
27 | * Returns the concatenation of the supplied {@link Sample}s as a new
28 | * {@link Sample}. If {@code samples} is empty, this method returns a new, empty
29 | * {@link Sample}.
30 | *
31 | * @param samples a list of {@link Sample}s
32 | * @return concatenation {@link Sample}
33 | * @throws NullPointerException if {@code samples} is {@code null}
34 | */
35 | public static Sample concatenate(List samples) {
36 | Objects.requireNonNull(samples);
37 |
38 | // If we have no samples, return an empty Sample
39 | if (samples.isEmpty()) {
40 | return buildSample(0, new double[0]);
41 | } else {
42 | int sampleCount = 0;
43 | // append voices to each other
44 | double[] first = samples.get(0).getInterleavedSamples();
45 | sampleCount += samples.get(0).getSampleCount();
46 | double[][] samplesArray = new double[samples.size() - 1][];
47 | for (int i = 0; i < samplesArray.length; i++) {
48 | samplesArray[i] = samples.get(i + 1).getInterleavedSamples();
49 | sampleCount += samples.get(i + 1).getSampleCount();
50 | }
51 | double[] appended = concatenate(first, samplesArray);
52 | return buildSample(sampleCount, appended);
53 | }
54 | }
55 |
56 | /**
57 | * Returns {@code sample1} mixed with {@code sample2} as a new {@link Sample}.
58 | * Additionally, {@code sample1}'s volume is adjusted by the multiplier
59 | * {@code volume1}, and {@code sample2}'s by {@code volume2}.
60 | *
61 | * @param sample1 first {@link Sample}
62 | * @param volume1 first multiplier
63 | * @param sample2 second {@link Sample}
64 | * @param volume2 second multiplier
65 | * @return mixed {@link Sample}
66 | * @throws NullPointerException if {@code sample1} or {@code sample2} is
67 | * {@code null}
68 | */
69 | public static Sample mix(Sample sample1, double volume1, Sample sample2, double volume2) {
70 | Objects.requireNonNull(sample1);
71 | Objects.requireNonNull(sample2);
72 | double[] s1Array = sample1.getInterleavedSamples();
73 | double[] s2Array = sample2.getInterleavedSamples();
74 | double[] mixed = mix(s1Array, volume1, s2Array, volume2);
75 | return buildSample(sample1.getSampleCount(), mixed);
76 | }
77 |
78 | /**
79 | * Concatenates the supplied arrays of {@code double}s and returns the resulting
80 | * array.
81 | *
82 | * @param first an array of {@code double}s
83 | * @param rest additional arrays of {@code double}s
84 | * @return concatenated array
85 | */
86 | private static double[] concatenate(double[] first, double[]... rest) {
87 | int totalLength = first.length;
88 | for (double[] array : rest) {
89 | totalLength += array.length;
90 | }
91 | double[] result = Arrays.copyOf(first, totalLength);
92 | int offset = first.length;
93 | for (double[] array : rest) {
94 | System.arraycopy(array, 0, result, offset, array.length);
95 | offset += array.length;
96 | }
97 | return result;
98 | }
99 |
100 | /**
101 | * Returns {@code sample1} mixed with {@code sample2} as a new raw array of
102 | * {@code double}s. Additionally, {@code sample1}'s volume is adjusted by the
103 | * multiplier {@code volume1}, and {@code sample2}'s by {@code volume2}.
104 | *
105 | * @param sample1 first sample
106 | * @param volume1 first multiplier
107 | * @param sample2 second sample
108 | * @param volume2 second multiplier
109 | * @return mixed sample
110 | */
111 | private static double[] mix(double[] sample1, double volume1, double[] sample2, double volume2) {
112 | for (int i = 0; i < sample1.length; i++) {
113 | if (i >= sample2.length) {
114 | sample1[i] = 0;
115 | break;
116 | }
117 | sample1[i] = sample1[i] * volume1 + sample2[i] * volume2;
118 | }
119 | return sample1;
120 | }
121 |
122 | /**
123 | * Returns a {@link Sample} created from the raw {@code sample} data.
124 | *
125 | * @param sampleCount number of samples
126 | * @param sample raw sample data
127 | * @return {@link Sample} from raw samples
128 | */
129 | private static Sample buildSample(long sampleCount, double[] sample) {
130 | // I'm reasonably sure we don't need to ask for sampleCount here: it's just
131 | // going to match sample.length, isn't it?
132 | byte[] buffer = asByteArray(sampleCount, sample);
133 | InputStream bais = new ByteArrayInputStream(buffer);
134 | AudioInputStream ais = new AudioInputStream(bais, Sample.SC_AUDIO_FORMAT, sampleCount);
135 | return new Sample(ais);
136 | }
137 |
138 | /**
139 | * Returns a sample encoded as {@code double[]} as a {@code byte[]}.
140 | *
141 | * @param sampleCount number of samples
142 | * @param sample raw sample data
143 | * @return sample encoded as {@code byte[]}
144 | */
145 | private static byte[] asByteArray(long sampleCount, double[] sample) {
146 | int bufferLength = (int) sampleCount * (Sample.SC_AUDIO_FORMAT.getSampleSizeInBits() / 8);
147 | byte[] buffer = new byte[bufferLength];
148 | int in;
149 | for (int i = 0; i < sample.length; i++) {
150 | in = (int) (sample[i] * 32_767);
151 | buffer[2 * i] = (byte) (in & 255);
152 | buffer[2 * i + 1] = (byte) (in >> 8);
153 | }
154 | return buffer;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/renderer/FastWordRenderer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.renderer;
2 |
3 | import java.awt.Color;
4 | import java.awt.Font;
5 | import java.awt.Graphics2D;
6 | import java.awt.image.BufferedImage;
7 | import java.util.concurrent.atomic.AtomicInteger;
8 | import java.util.function.Supplier;
9 |
10 | /**
11 | *
12 | * Based on the {@link DefaultWordRenderer}, this implementation strips down to the basics to render {@link BufferedImage}s as much as 5X
13 | * faster. (This class will render almost 70,000 {@link BufferedImage}s per second on an iMac with a 4GHz Intel Core i7 CPU.) It has the
14 | * following restrictions compared to {@link DefaultWordRenderer}:
15 | *
16 | *
17 | *
18 | *
{@link Font} choices are limited: renders with "Courier Prime" and "Public Sans".
19 | *
Rendered text is not anti-aliased.
20 | *
{@link DefaultWordRenderer} measures the size of each glyph it renders to calculate horizontal spacing. This class uses fixed
21 | * spacing, but will "fudge" each glyph's position horizontally and vertically: see below.
22 | *
{@link Font} choice is only random for the first 100 choices: this class pre-computes a list of random indexes into the {@link Font}
23 | * array, and then re-uses those indexes by cycling through them repeatedly.
24 | *
25 | *
26 | *
27 | * As noted above, this class will render each glyph with a random horizontal and vertical fudge factor between (-5, 5) from the baseline.
28 | * The effect is that glyphs can move around and bunch together (or spread apart) more. As with {@link Font} choice, there is only limited
29 | * randomness here: again, we pre-compute a list of 100 random fudge values in the range, and cycle through that list repeatedly.
30 | *
31 | *
32 | * @author Paul Hoadley
33 | * @author bivashy
34 | * @since 1.1
35 | */
36 | public final class FastWordRenderer extends AbstractWordRenderer {
37 | /**
38 | * Horizontal space between glyphs (in pixels)
39 | */
40 | private static final int SHIFT = 20;
41 |
42 | /**
43 | * Size of list of pre-computed indexes (into {@link Font} list)
44 | */
45 | private static final int FONT_INDEX_SIZE = 100;
46 |
47 | /**
48 | * Pre-computed indexes into {@link Font} list
49 | */
50 | private static final int[] INDEXES = new int[FONT_INDEX_SIZE];
51 |
52 | /**
53 | * Current index pointer
54 | */
55 | private static AtomicInteger idxPointer = new AtomicInteger(0);
56 |
57 | /**
58 | * Minimum fudge value
59 | */
60 | private static final int FUDGE_MIN = -5;
61 |
62 | /**
63 | * Maximum fudge value
64 | */
65 | private static final int FUDGE_MAX = 5;
66 |
67 | /**
68 | * Size of list of pre-computed fudge values
69 | */
70 | private static final int FUDGE_INDEX_SIZE = 100;
71 |
72 | /**
73 | * Pre-computed fudge values
74 | */
75 | private static final int[] FUDGES = new int[FUDGE_INDEX_SIZE];
76 |
77 | /**
78 | * Current fudge pointer
79 | */
80 | private static AtomicInteger fudgePointer = new AtomicInteger(0);
81 |
82 | /**
83 | * Available {@link Font}s
84 | */
85 | private static final Font[] FONTS = new Font[2];
86 |
87 | // Set up Font list, pre-computed values
88 | static {
89 | FONTS[0] = DEFAULT_FONTS.get(0);
90 | FONTS[1] = DEFAULT_FONTS.get(1);
91 |
92 | for (int i = 0; i < FONT_INDEX_SIZE; i++) {
93 | INDEXES[i] = RAND.nextInt(FONTS.length);
94 | }
95 | for (int i = 0; i < FUDGE_INDEX_SIZE; i++) {
96 | FUDGES[i] = RAND.nextInt((FUDGE_MAX - FUDGE_MIN) + 1) + FUDGE_MIN;
97 | }
98 | }
99 |
100 | /**
101 | * Constructor taking x- and y-axis offsets
102 | *
103 | * @param xOffset x-axis offset
104 | * @param yOffset y-axis offset
105 | * @param wordColorSupplier {@link Color} supplier
106 | * @param fontSupplier {@link Font} supplier
107 | * @since 1.4
108 | */
109 | private FastWordRenderer(double xOffset, double yOffset, Supplier wordColorSupplier, Supplier fontSupplier) {
110 | super(xOffset, yOffset, wordColorSupplier, fontSupplier);
111 | return;
112 | }
113 |
114 | @Override
115 | public void render(final String word, BufferedImage image) {
116 | Graphics2D g = image.createGraphics();
117 | int xBaseline = (int) (image.getWidth() * xOffset());
118 | int yBaseline = image.getHeight() - (int) (image.getHeight() * yOffset());
119 | char[] chars = new char[1];
120 | for (char c : word.toCharArray()) {
121 | chars[0] = c;
122 | g.setColor(colorSupplier().get());
123 | g.setFont(nextFont());
124 | int xFudge = nextFudge();
125 | int yFudge = nextFudge();
126 | g.drawChars(chars, 0, 1, xBaseline + xFudge, yBaseline - yFudge);
127 | xBaseline = xBaseline + SHIFT;
128 | }
129 | }
130 |
131 | /**
132 | * Returns the next {@link Font} to use.
133 | *
134 | * @return next {@link Font}
135 | */
136 | private Font nextFont() {
137 | if (FONTS.length == 1) {
138 | return FONTS[0];
139 | } else {
140 | return FONTS[INDEXES[idxPointer.getAndIncrement() % FONT_INDEX_SIZE]];
141 | }
142 | }
143 |
144 | /**
145 | * Returns the next fudge value to use.
146 | *
147 | * @return fudge value
148 | */
149 | private int nextFudge() {
150 | return FUDGES[fudgePointer.getAndIncrement() % FUDGE_INDEX_SIZE];
151 | }
152 |
153 | /**
154 | * Builder for {@link FastWordRenderer}. Note that calls to the {@link Font}-related methods inherited from
155 | * {@link AbstractWordRenderer.Builder} are effectively ignored: {@code FastWordRenderer} uses a fixed set of two {@link Font}s.
156 | *
157 | * @since 1.4
158 | */
159 | public static class Builder extends AbstractWordRenderer.Builder {
160 | @Override
161 | public FastWordRenderer build() {
162 | return new FastWordRenderer(xOffset, yOffset, colorSupplier, fontSupplier);
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/audio/producer/RandomNumberVoiceProducer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.audio.producer;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.HashMap;
6 | import java.util.List;
7 | import java.util.Locale;
8 | import java.util.Map;
9 | import java.util.Objects;
10 | import java.util.Random;
11 |
12 | import net.logicsquad.nanocaptcha.audio.Sample;
13 |
14 | /**
15 | * A {@link VoiceProducer} that can generate a vocalization for a given number
16 | * in a randomly chosen voice.
17 | *
18 | * @author James Childers
19 | * @author Paul Hoadley
20 | * @since 1.0
21 | */
22 | public class RandomNumberVoiceProducer implements VoiceProducer {
23 | /**
24 | * Random number generator
25 | */
26 | private static final Random RAND = new Random();
27 |
28 | /**
29 | * List of supported languages
30 | */
31 | private static final List SUPPORTED_LANGUAGES = Arrays.asList(Locale.ENGLISH, Locale.GERMAN, Locale.FRENCH);
32 |
33 | /**
34 | * Property key for declaring a default language (which will be used in the
35 | * no-args constructor) via 2-digit ISO 639 code
36 | */
37 | static final String DEFAULT_LANGUAGE_KEY = "net.logicsquad.nanocaptcha.audio.producer.RandomNumberVoiceProducer.defaultLanguage";
38 |
39 | /**
40 | * Default language of last resort if there's nothing set by property
41 | */
42 | private static final Locale FALLBACK_LANGUAGE = Locale.ENGLISH;
43 |
44 | /**
45 | * Prefix for locating voices
46 | */
47 | private static final String PATH_PREFIX_TEMPLATE = "/sounds/%s/numbers/";
48 |
49 | /**
50 | * English voices
51 | */
52 | private static final List VOICES_EN = Arrays.asList("a", "b", "c", "d", "e", "f", "g");
53 |
54 | /**
55 | * German voices
56 | */
57 | private static final List VOICES_DE = Arrays.asList("a", "b");
58 |
59 | /**
60 | * French voices
61 | */
62 | private static final List VOICES_FR = Arrays.asList("a", "b");
63 |
64 | /**
65 | * Map from language to list of voice names
66 | */
67 | private static final Map> VOICES = new HashMap<>();
68 |
69 | static {
70 | VOICES.put(Locale.ENGLISH, VOICES_EN);
71 | VOICES.put(Locale.GERMAN, VOICES_DE);
72 | VOICES.put(Locale.FRENCH, VOICES_FR);
73 | }
74 |
75 | /**
76 | * Default {@link Locale}
77 | */
78 | static volatile Locale defaultLanguage;
79 |
80 | /**
81 | * Map from each single digit to list of vocalizations to choose from for that
82 | * digit
83 | */
84 | private Map> vocalizations;
85 |
86 | /**
87 | * Language to use for vocalizations
88 | */
89 | final Locale language;
90 |
91 | /**
92 | * Prefix to path for vocalizations
93 | */
94 | private String pathPrefix;
95 |
96 | /**
97 | * Constructor resulting in object providing built-in voices to vocalize digits.
98 | */
99 | public RandomNumberVoiceProducer() {
100 | this(defaultLanguage());
101 | }
102 |
103 | /**
104 | * Constructor taking a language {@link Locale}. If {@code language} is not a
105 | * supported language, the default language will be used.
106 | *
107 | * @param language a {@link Locale} representing a language
108 | * @see #7
109 | * @since 1.4
110 | */
111 | public RandomNumberVoiceProducer(Locale language) {
112 | Objects.requireNonNull(language);
113 | this.language = SUPPORTED_LANGUAGES.contains(language) ? language : defaultLanguage();
114 | return;
115 | }
116 |
117 | @Override
118 | public final Sample getVocalization(char number) {
119 | String stringNumber = Character.toString(number);
120 | try {
121 | int idx = Integer.parseInt(stringNumber);
122 | List files = vocalizations().get(idx);
123 | String filename = files.get(RAND.nextInt(files.size()));
124 | return new Sample(filename);
125 | } catch (NumberFormatException e) {
126 | throw new IllegalArgumentException("RandomNumberVoiceProducer can only vocalize numbers.", e);
127 | }
128 | }
129 |
130 | /**
131 | * Returns a default {@link Locale} to use when not explicitly declared by constructor.
132 | *
133 | * @return default {@link Locale}
134 | * @see #7
135 | * @since 1.4
136 | */
137 | static Locale defaultLanguage() {
138 | if (defaultLanguage == null) {
139 | synchronized (RandomNumberVoiceProducer.class) {
140 | if (defaultLanguage == null) {
141 | String language = System.getProperty(DEFAULT_LANGUAGE_KEY);
142 | if (language == null || !SUPPORTED_LANGUAGES.stream().map(l -> l.getLanguage()).anyMatch(s -> s.equals(language))) {
143 | defaultLanguage = FALLBACK_LANGUAGE;
144 | } else {
145 | defaultLanguage = new Locale(language);
146 | }
147 | }
148 | }
149 | }
150 | return defaultLanguage;
151 | }
152 |
153 | /**
154 | * Returns a localized path prefix to find the vocalizations.
155 | *
156 | * @return path prefix
157 | * @see #7
158 | * @since 1.4
159 | */
160 | private String pathPrefix() {
161 | if (pathPrefix == null) {
162 | pathPrefix = String.format(PATH_PREFIX_TEMPLATE, language.getLanguage());
163 | }
164 | return pathPrefix;
165 | }
166 |
167 | /**
168 | * Returns the map from numbers to vocalization samples.
169 | *
170 | * @return map of vocalizations
171 | * @see #7
172 | * @since 1.4
173 | */
174 | private Map> vocalizations() {
175 | if (vocalizations == null) {
176 | vocalizations = new HashMap<>();
177 | List sampleNames;
178 | for (int i = 0; i < 10; i++) {
179 | sampleNames = new ArrayList<>();
180 | StringBuilder sb;
181 | for (String name : VOICES.get(language)) {
182 | sb = new StringBuilder(pathPrefix());
183 | sb.append(i).append("_").append(name).append(".wav");
184 | sampleNames.add(sb.toString());
185 | }
186 | vocalizations.put(i, sampleNames);
187 | }
188 | }
189 | return vocalizations;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/audio/AudioCaptcha.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.audio;
2 |
3 | import java.time.OffsetDateTime;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 | import java.util.Random;
7 |
8 | import net.logicsquad.nanocaptcha.audio.noise.NoiseProducer;
9 | import net.logicsquad.nanocaptcha.audio.noise.RandomNoiseProducer;
10 | import net.logicsquad.nanocaptcha.audio.producer.RandomNumberVoiceProducer;
11 | import net.logicsquad.nanocaptcha.audio.producer.VoiceProducer;
12 | import net.logicsquad.nanocaptcha.content.ContentProducer;
13 | import net.logicsquad.nanocaptcha.content.NumbersContentProducer;
14 |
15 | /**
16 | * An audio CAPTCHA.
17 | *
18 | * @author James Childers
19 | * @author Paul Hoadley
20 | * @since 1.0
21 | */
22 | public final class AudioCaptcha {
23 | /**
24 | * Generated audio
25 | */
26 | private final Sample audio;
27 |
28 | /**
29 | * Text content of audio
30 | */
31 | private final String content;
32 |
33 | /**
34 | * Creation timestamp
35 | */
36 | private final OffsetDateTime created;
37 |
38 | /**
39 | * Constructor
40 | *
41 | * @param builder a {@link Builder} object
42 | */
43 | private AudioCaptcha(Builder builder) {
44 | audio = builder.audio;
45 | content = builder.content;
46 | created = OffsetDateTime.now();
47 | return;
48 | }
49 |
50 | /**
51 | *
52 | * Returns a new {@code AudioCaptcha} with some very basic settings:
53 | *
54 | *
55 | *
56 | *
{@link NumbersContentProducer} with length 5; and
57 | *
{@link RandomNumberVoiceProducer} (in the default {@link java.util.Locale Locale}).
58 | *
59 | *
60 | *
61 | * That is, the audio clip will contain five numbers read out in English (unless the default {@code Locale} has been changed).
62 | *
63 | *
64 | * @return new {@code AudioCaptcha}
65 | * @since 2.0
66 | */
67 | public static AudioCaptcha create() {
68 | return new AudioCaptcha.Builder().addContent().build();
69 | }
70 |
71 | /**
72 | * Build for an {@link AudioCaptcha}.
73 | */
74 | public static class Builder implements net.logicsquad.nanocaptcha.Builder {
75 | /**
76 | * Random number generator
77 | */
78 | private static final Random RAND = new Random();
79 |
80 | /**
81 | * Text content
82 | */
83 | private String content = "";
84 |
85 | /**
86 | * Generated audio sample
87 | */
88 | private Sample audio;
89 |
90 | /**
91 | * {@link VoiceProducer}s
92 | */
93 | private final List voiceProducers;
94 |
95 | /**
96 | * {@link NoiseProducer}s
97 | */
98 | private final List noiseProducers;
99 |
100 | /**
101 | * Constructor
102 | */
103 | public Builder() {
104 | voiceProducers = new ArrayList<>();
105 | noiseProducers = new ArrayList<>();
106 | return;
107 | }
108 |
109 | /**
110 | * Adds content using the default {@link ContentProducer} ({@link NumbersContentProducer}).
111 | *
112 | * @return this
113 | */
114 | public Builder addContent() {
115 | return addContent(new NumbersContentProducer());
116 | }
117 |
118 | /**
119 | * Adds content (of length {@code length}) using the default {@link ContentProducer} ({@link NumbersContentProducer}).
120 | *
121 | * @param length number of content units to add
122 | * @return this
123 | * @see #9
124 | * @since 1.4
125 | */
126 | public Builder addContent(int length) {
127 | return addContent(new NumbersContentProducer(length));
128 | }
129 |
130 | /**
131 | * Adds content using {@code contentProducer}.
132 | *
133 | * @param contentProducer a {@link ContentProducer}
134 | * @return this
135 | */
136 | public Builder addContent(ContentProducer contentProducer) {
137 | content += contentProducer.getContent();
138 | return this;
139 | }
140 |
141 | /**
142 | * Adds the default {@link VoiceProducer} ({@link RandomNumberVoiceProducer}).
143 | *
144 | * @return this
145 | */
146 | public Builder addVoice() {
147 | voiceProducers.add(new RandomNumberVoiceProducer());
148 | return this;
149 | }
150 |
151 | /**
152 | * Adds {@code voiceProducer}.
153 | *
154 | * @param voiceProducer a {@link VoiceProducer}
155 | * @return this
156 | */
157 | public Builder addVoice(VoiceProducer voiceProducer) {
158 | voiceProducers.add(voiceProducer);
159 | return this;
160 | }
161 |
162 | /**
163 | * Adds background noise using default {@link NoiseProducer}
164 | * ({@link RandomNoiseProducer}).
165 | *
166 | * @return this
167 | */
168 | public Builder addNoise() {
169 | return addNoise(new RandomNoiseProducer());
170 | }
171 |
172 | /**
173 | * Adds noise using {@code noiseProducer}.
174 | *
175 | * @param noiseProducer a {@link NoiseProducer}
176 | * @return this
177 | */
178 | public Builder addNoise(NoiseProducer noiseProducer) {
179 | noiseProducers.add(noiseProducer);
180 | return this;
181 | }
182 |
183 | /**
184 | * Builds the audio CAPTCHA described by this object.
185 | *
186 | * @return {@link AudioCaptcha} as described by this {@code Builder}
187 | */
188 | @Override
189 | public AudioCaptcha build() {
190 | // Make sure we have at least one voiceProducer
191 | if (voiceProducers.isEmpty()) {
192 | addVoice();
193 | }
194 |
195 | // Convert answer to an array
196 | char[] ansAry = content.toCharArray();
197 |
198 | // Make a List of Samples for each character
199 | VoiceProducer vProd;
200 | List samples = new ArrayList<>();
201 | for (char c : ansAry) {
202 | // Create Sample for this character from one of the
203 | // VoiceProducers
204 | vProd = voiceProducers.get(RAND.nextInt(voiceProducers.size()));
205 | samples.add(vProd.getVocalization(c));
206 | }
207 |
208 | // 3. Add noise, if any, and return the result
209 | if (!noiseProducers.isEmpty()) {
210 | NoiseProducer nProd = noiseProducers.get(RAND.nextInt(noiseProducers.size()));
211 | audio = nProd.addNoise(samples);
212 | return new AudioCaptcha(this);
213 | }
214 |
215 | audio = Mixer.concatenate(samples);
216 | return new AudioCaptcha(this);
217 | }
218 | }
219 |
220 | /**
221 | * Does CAPTCHA content match supplied {@code answer}?
222 | *
223 | * @param answer a candidate content match
224 | * @return {@code true} if {@code answer} matches CAPTCHA content, otherwise
225 | * {@code false}
226 | */
227 | public boolean isCorrect(String answer) {
228 | return answer.equals(content);
229 | }
230 |
231 | /**
232 | * Returns content of this CAPTCHA.
233 | *
234 | * @return content
235 | */
236 | public String getContent() {
237 | return content;
238 | }
239 |
240 | /**
241 | * Returns the audio for this {@code AudioCaptcha}.
242 | *
243 | * @return CAPTCHA audio
244 | */
245 | public Sample getAudio() {
246 | return audio;
247 | }
248 |
249 | @Override
250 | public String toString() {
251 | StringBuilder sb = new StringBuilder(35);
252 | sb.append("[AudioCaptcha: created=").append(created).append(" content='").append(content).append("']");
253 | return sb.toString();
254 | }
255 |
256 | /**
257 | * Returns creation timestamp.
258 | *
259 | * @return creation timestamp
260 | */
261 | public OffsetDateTime getCreated() {
262 | return created;
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/audio/Sample.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.audio;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.util.Objects;
7 |
8 | import javax.sound.sampled.AudioFormat;
9 | import javax.sound.sampled.AudioInputStream;
10 | import javax.sound.sampled.AudioSystem;
11 | import javax.sound.sampled.UnsupportedAudioFileException;
12 |
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | /**
17 | *
18 | * Class representing a sound sample, typically read in from a file. Note that
19 | * at this time this class only supports wav files with the following
20 | * characteristics:
21 | *
22 | *
23 | *
24 | *
Sample rate: 16KHz
25 | *
Sample size: 16 bits
26 | *
Channels: 1
27 | *
Signed: true
28 | *
Big Endian: false
29 | *
30 | *
31 | *
32 | * Data files in other formats will cause an
33 | * IllegalArgumentException to be thrown.
34 | *
35 | *
36 | * @author James Childers
37 | * @author Paul Hoadley
38 | * @since 1.0
39 | */
40 | public class Sample {
41 | /**
42 | * Logger
43 | */
44 | private static final Logger LOG = LoggerFactory.getLogger(Sample.class);
45 |
46 | /**
47 | * {@link AudioFormat} for all {@code Sample}s
48 | */
49 | public static final AudioFormat SC_AUDIO_FORMAT = new AudioFormat(16_000, // sample rate
50 | 16, // sample size in bits
51 | 1, // channels
52 | true, // signed?
53 | false); // big endian?;
54 |
55 | /**
56 | * {@link AudioInputStream} for this {@code Sample}
57 | */
58 | private final AudioInputStream audioInputStream;
59 |
60 | /**
61 | * Constructor taking a filename.
62 | *
63 | * @param filename filename
64 | * @throws NullPointerException if {@code filename} is {@code null}
65 | */
66 | public Sample(String filename) {
67 | this(Sample.class.getResourceAsStream(Objects.requireNonNull(filename)));
68 | }
69 |
70 | /**
71 | * Constructor taking an {@link InputStream}.
72 | *
73 | * @param is an {@link InputStream}
74 | * @throws NullPointerException if {@code is} is {@code null}
75 | * @throws IllegalArgumentException if the audio format is unsupported
76 | * @throws RuntimeException if
77 | * {@link AudioSystem#getAudioInputStream(InputStream)}
78 | * is unable to read the audio stream
79 | */
80 | public Sample(InputStream is) {
81 | Objects.requireNonNull(is);
82 | if (is instanceof AudioInputStream) {
83 | audioInputStream = (AudioInputStream) is;
84 | } else {
85 | try {
86 | audioInputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(is));
87 | } catch (UnsupportedAudioFileException | IOException e) {
88 | LOG.error("Unable to get audio input stream.", e);
89 | throw new RuntimeException(e);
90 | }
91 | }
92 | if (!audioInputStream.getFormat().matches(SC_AUDIO_FORMAT)) {
93 | throw new IllegalArgumentException("Unsupported audio format.");
94 | }
95 | return;
96 | }
97 |
98 | /**
99 | * Returns {@link AudioInputStream} for this {@code Sample}.
100 | *
101 | * @return {@link AudioInputStream}
102 | */
103 | public AudioInputStream getAudioInputStream() {
104 | return audioInputStream;
105 | }
106 |
107 | /**
108 | * Returns {@link AudioFormat} for this {@code Sample}.
109 | *
110 | * @return {@link AudioFormat}
111 | */
112 | private AudioFormat getFormat() {
113 | return audioInputStream.getFormat();
114 | }
115 |
116 | /**
117 | * Return the number of samples for all channels.
118 | *
119 | * @return number of samples for all channels
120 | */
121 | long getSampleCount() {
122 | long total = (audioInputStream.getFrameLength() * getFormat().getFrameSize() * 8)
123 | / getFormat().getSampleSizeInBits();
124 | return total / getFormat().getChannels();
125 | }
126 |
127 | /**
128 | * Returns interleaved samples for this {@code Sample}.
129 | *
130 | * @return interleaved samples
131 | */
132 | double[] getInterleavedSamples() {
133 | double[] samples = new double[(int) getSampleCount()];
134 | try {
135 | getInterleavedSamples(0, getSampleCount(), samples);
136 | } catch (IllegalArgumentException | IOException e) {
137 | LOG.error("Unable to get interleaved samples.", e);
138 | }
139 |
140 | return samples;
141 | }
142 |
143 | /**
144 | * Returns the interleaved decoded samples for all channels, from sample index
145 | * {@code start} (included) to sample index {@code end} (excluded) and copy them
146 | * into {@code samples}. {@code end} must not exceed {@code getSampleCount()},
147 | * and the number of samples must not be so large that the associated byte array
148 | * cannot be allocated.
149 | *
150 | * @param start start index
151 | * @param end end index
152 | * @param samples destination array
153 | * @return interleaved decoded samples for all channels
154 | * @throws IOException if unable to read from
155 | * {@link AudioInputStream}
156 | * @throws IllegalArgumentException if sample is too large
157 | */
158 | private double[] getInterleavedSamples(long start, long end, double[] samples) throws IOException {
159 | long nbSamples = end - start;
160 | long nbBytes = nbSamples * (getFormat().getSampleSizeInBits() / 8) * getFormat().getChannels();
161 | if (nbBytes > Integer.MAX_VALUE) {
162 | throw new IllegalArgumentException("Too many samples. Try using a smaller wav.");
163 | }
164 | // allocate a byte buffer
165 | byte[] inBuffer = new byte[(int) nbBytes];
166 | // read bytes from audio file
167 | audioInputStream.read(inBuffer, 0, inBuffer.length);
168 | // decode bytes into samples.
169 | decodeBytes(inBuffer, samples);
170 |
171 | return samples;
172 | }
173 |
174 | /**
175 | * Decodes audio as bytes in {@code audioBytes} into audio as samples and writes
176 | * the result into {@code audioSamples}.
177 | *
178 | * @param audioBytes source audio as bytes
179 | * @param audioSamples destination audio as samples
180 | */
181 | private void decodeBytes(byte[] audioBytes, double[] audioSamples) {
182 | int sampleSizeInBytes = getFormat().getSampleSizeInBits() / 8;
183 | int[] sampleBytes = new int[sampleSizeInBytes];
184 | int k = 0; // index in audioBytes
185 | for (int i = 0; i < audioSamples.length; i++) {
186 | // collect sample byte in big-endian order
187 | if (getFormat().isBigEndian()) {
188 | // bytes start with MSB
189 | for (int j = 0; j < sampleSizeInBytes; j++) {
190 | sampleBytes[j] = audioBytes[k++];
191 | }
192 | } else {
193 | // bytes start with LSB
194 | for (int j = sampleSizeInBytes - 1; j >= 0; j--) {
195 | sampleBytes[j] = audioBytes[k++];
196 | }
197 | }
198 | // get integer value from bytes
199 | int ival = 0;
200 | for (int j = 0; j < sampleSizeInBytes; j++) {
201 | ival += sampleBytes[j];
202 | if (j < sampleSizeInBytes - 1) {
203 | ival <<= 8;
204 | }
205 | }
206 | // decode value
207 | double ratio = Math.pow(2., getFormat().getSampleSizeInBits() - 1);
208 | double val = ((double) ival) / ratio;
209 | audioSamples[i] = val;
210 | }
211 | }
212 |
213 | @Override
214 | public String toString() {
215 | StringBuilder sb = new StringBuilder(26);
216 | sb.append("[Sample: samples=").append(getSampleCount()).append(" format=").append(getFormat()).append(']');
217 | return sb.toString();
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/renderer/AbstractWordRenderer.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.renderer;
2 |
3 | import java.awt.Color;
4 | import java.awt.Font;
5 | import java.awt.FontFormatException;
6 | import java.awt.image.BufferedImage;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.Collections;
12 | import java.util.List;
13 | import java.util.Random;
14 | import java.util.function.Supplier;
15 |
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 |
19 | /**
20 | * Superclass for {@link WordRenderer} implementations.
21 | *
22 | * @author Paul Hoadley
23 | * @author bivashy
24 | * @since 1.4
25 | */
26 | public abstract class AbstractWordRenderer implements WordRenderer {
27 | /**
28 | * Logger
29 | */
30 | private static final Logger LOG = LoggerFactory.getLogger(AbstractWordRenderer.class);
31 |
32 | /**
33 | * Resource path to "Courier Prime"
34 | */
35 | private static final String COURIER_PRIME_FONT = "/fonts/CourierPrime-Bold.ttf";
36 |
37 | /**
38 | * Resource path to "Public Sans"
39 | */
40 | private static final String PUBLIC_SANS_FONT = "/fonts/PublicSans-Bold.ttf";
41 |
42 | /**
43 | * Random number generator
44 | */
45 | protected static final Random RAND = new Random();
46 |
47 | /**
48 | * Default {@link Color}s
49 | */
50 | protected static final List DEFAULT_COLORS;
51 |
52 | /**
53 | * Default fonts
54 | */
55 | protected static final List DEFAULT_FONTS;
56 |
57 | // Set up default Colors, Fonts
58 | static {
59 | List defaultColors = Arrays.asList(Color.BLACK);
60 | DEFAULT_COLORS = Collections.unmodifiableList(defaultColors);
61 | List defaultFonts = Arrays.asList(fontFromResource(COURIER_PRIME_FONT), fontFromResource(PUBLIC_SANS_FONT));
62 | DEFAULT_FONTS = Collections.unmodifiableList(defaultFonts);
63 | }
64 |
65 | /**
66 | * Default supplier for {@link Color}
67 | */
68 | protected static final Supplier DEFAULT_COLOR_SUPPLIER = () -> DEFAULT_COLORS.get(RAND.nextInt(DEFAULT_COLORS.size()));
69 |
70 | /**
71 | * Default supplier for {@link Font}
72 | */
73 | protected static final Supplier DEFAULT_FONT_SUPPLIER = () -> DEFAULT_FONTS.get(RAND.nextInt(DEFAULT_FONTS.size()));
74 |
75 | /**
76 | * Font size (in points)
77 | */
78 | protected static final int FONT_SIZE = 40;
79 |
80 | /**
81 | * Default percentage offset along x-axis
82 | */
83 | protected static final double X_OFFSET_DEFAULT = 0.05;
84 |
85 | /**
86 | * Default percentage offset along y-axis
87 | */
88 | protected static final double Y_OFFSET_DEFAULT = 0.25;
89 |
90 | /**
91 | * Minimum for y-offset if randomised
92 | */
93 | private static final double Y_OFFSET_MIN = 0.0;
94 |
95 | /**
96 | * Maximum for y-offset if randomised
97 | */
98 | private static final double Y_OFFSET_MAX = 0.75;
99 |
100 | /**
101 | * Percentage offset along x-axis
102 | */
103 | private final double xOffset;
104 |
105 | /**
106 | * Percentage offset along y-axis
107 | */
108 | private final double yOffset;
109 |
110 | /**
111 | * Supplier of {@link Color}
112 | */
113 | private final Supplier colorSupplier;
114 |
115 | /**
116 | * Supplier for {@link Font}
117 | */
118 | private final Supplier fontSupplier;
119 |
120 | /**
121 | * Constructor taking x- and y-offset overrides
122 | *
123 | * @param xOffset x-axis offset
124 | * @param yOffset y-axis offset
125 | * @param colorSupplier {@link Color} supplier
126 | * @param fontSupplier {@link Font} supplier
127 | */
128 | protected AbstractWordRenderer(double xOffset, double yOffset, Supplier colorSupplier, Supplier fontSupplier) {
129 | this.xOffset = xOffset;
130 | this.yOffset = yOffset;
131 | this.colorSupplier = colorSupplier;
132 | this.fontSupplier = fontSupplier;
133 | return;
134 | }
135 |
136 | @Override
137 | public abstract void render(String word, BufferedImage image);
138 |
139 | /**
140 | * Builder for {@code AbstractWordRenderer}.
141 | */
142 | public abstract static class Builder implements net.logicsquad.nanocaptcha.Builder {
143 | /**
144 | * X-axis offset
145 | */
146 | protected double xOffset;
147 |
148 | /**
149 | * Y-axis offset
150 | */
151 | protected double yOffset;
152 |
153 | /**
154 | * Supplier for {@link Color}
155 | */
156 | protected Supplier colorSupplier;
157 |
158 | /**
159 | * Supplier for {@link Font}
160 | */
161 | protected Supplier fontSupplier;
162 |
163 | /**
164 | * Constructor
165 | */
166 | protected Builder() {
167 | xOffset = X_OFFSET_DEFAULT;
168 | yOffset = Y_OFFSET_DEFAULT;
169 | colorSupplier = DEFAULT_COLOR_SUPPLIER;
170 | fontSupplier = DEFAULT_FONT_SUPPLIER;
171 | return;
172 | }
173 |
174 | /**
175 | * Sets y-offset value.
176 | *
177 | * @param yOffset y-offset (in [0, 1])
178 | * @return this
179 | */
180 | public Builder yOffset(double yOffset) {
181 | this.yOffset = yOffset;
182 | return this;
183 | }
184 |
185 | /**
186 | * Sets x-offset value.
187 | *
188 | * @param xOffset x-offset (in [0, 1])
189 | * @return this
190 | */
191 | public Builder xOffset(double xOffset) {
192 | this.xOffset = xOffset;
193 | return this;
194 | }
195 |
196 | /**
197 | * Selects a random value for y-offset.
198 | *
199 | * @return this
200 | */
201 | public Builder randomiseYOffset() {
202 | this.yOffset = Y_OFFSET_MIN + (Y_OFFSET_MAX - Y_OFFSET_MIN) * RAND.nextDouble();
203 | return this;
204 | }
205 |
206 | /**
207 | * Sets {@link #colorSupplier} to randomly select a {@link Color} from the given {@link Color}s.
208 | *
209 | * @param color the first {@link Color}
210 | * @param colors additional {@link Color}s (optional)
211 | * @return this
212 | * @since 2.0
213 | */
214 | public Builder randomColor(Color color, Color... colors) {
215 | List colorList = new ArrayList<>();
216 | colorList.add(color);
217 | Collections.addAll(colorList, colors);
218 | return randomColor(colorList);
219 | }
220 |
221 | /**
222 | * Sets {@link #colorSupplier} to randomly select a {@link Color} from the provided {@code colors}. If the list is empty, no changes are
223 | * made to the current {@link #colorSupplier}.
224 | *
225 | * @param colors the list of {@link Color}s to choose from
226 | * @return this
227 | * @since 2.0
228 | */
229 | public Builder randomColor(List colors) {
230 | if (!colors.isEmpty()) {
231 | colorSupplier = () -> colors.get(RAND.nextInt(colors.size()));
232 | }
233 | return this;
234 | }
235 |
236 | /**
237 | * Sets {@link #colorSupplier} to provide a specified {@link Color}.
238 | *
239 | * @param color the {@link Color} to be supplied by {@link #colorSupplier}
240 | * @return this
241 | * @since 2.0
242 | */
243 | public Builder color(Color color) {
244 | colorSupplier = () -> color;
245 | return this;
246 | }
247 |
248 | /**
249 | * Sets {@link #fontSupplier} to randomly select a {@link Font} from the given {@link Font}s.
250 | *
251 | * @param font the first {@link Font}
252 | * @param fonts additional {@link Font}s (optional)
253 | * @return this
254 | * @since 2.1
255 | */
256 | public Builder randomFont(Font font, Font... fonts) {
257 | List fontList = new ArrayList<>();
258 | fontList.add(font);
259 | Collections.addAll(fontList, fonts);
260 | return randomFont(fontList);
261 | }
262 |
263 | /**
264 | * Sets {@link #fontSupplier} to randomly select a {@link Font} from the provided {@code fonts}. If the list is empty, no changes are made
265 | * to the current {@link #fontSupplier}.
266 | *
267 | * @param fonts the list of {@link Font}s to choose from
268 | * @return this
269 | * @since 2.1
270 | */
271 | public Builder randomFont(List fonts) {
272 | if (!fonts.isEmpty()) {
273 | fontSupplier = () -> fonts.get(RAND.nextInt(fonts.size()));
274 | }
275 | return this;
276 | }
277 |
278 | /**
279 | * Sets {@link #fontSupplier} to provide a specified {@link Font}.
280 | *
281 | * @param font the {@link Font} to be supplied by {@link #fontSupplier}
282 | * @return this
283 | * @since 2.1
284 | */
285 | public Builder font(Font font) {
286 | fontSupplier = () -> font;
287 | return this;
288 | }
289 | }
290 |
291 | /**
292 | * Returns x-axis offset.
293 | *
294 | * @return x-axis offset
295 | */
296 | protected double xOffset() {
297 | return xOffset;
298 | }
299 |
300 | /**
301 | * Returns y-axis offset.
302 | *
303 | * @return y-axis offset
304 | */
305 | protected double yOffset() {
306 | return yOffset;
307 | }
308 |
309 | /**
310 | * Returns {@link Color} supplier.
311 | *
312 | * @return {@link Color} supplier
313 | * @since 2.0
314 | */
315 | protected Supplier colorSupplier() {
316 | return colorSupplier;
317 | }
318 |
319 | /**
320 | * Returns {@link Font} supplier.
321 | *
322 | * @return {@link Font} supplier
323 | * @since 2.1
324 | */
325 | protected Supplier fontSupplier() {
326 | return fontSupplier;
327 | }
328 |
329 | /**
330 | * Returns a {@link Font} loaded from supplied {@code resourceName}, or {@code null} if unable to load the
331 | * resource.
332 | *
333 | * @param resourceName path to resource
334 | * @return loaded {@link Font}
335 | * @since 1.5
336 | */
337 | private static Font fontFromResource(String resourceName) {
338 | try (InputStream is = DefaultWordRenderer.class.getResourceAsStream(resourceName)) {
339 | return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont((long) FONT_SIZE);
340 | } catch (IOException | FontFormatException e) {
341 | LOG.error("Unable to load font '{}'.", resourceName, e);
342 | return null;
343 | }
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/ImageCaptcha.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image;
2 |
3 | import java.awt.AlphaComposite;
4 | import java.awt.Color;
5 | import java.awt.Graphics2D;
6 | import java.awt.image.BufferedImage;
7 | import java.time.OffsetDateTime;
8 |
9 | import net.logicsquad.nanocaptcha.content.ContentProducer;
10 | import net.logicsquad.nanocaptcha.content.LatinContentProducer;
11 | import net.logicsquad.nanocaptcha.image.backgrounds.BackgroundProducer;
12 | import net.logicsquad.nanocaptcha.image.backgrounds.TransparentBackgroundProducer;
13 | import net.logicsquad.nanocaptcha.image.filter.ImageFilter;
14 | import net.logicsquad.nanocaptcha.image.filter.RippleImageFilter;
15 | import net.logicsquad.nanocaptcha.image.noise.CurvedLineNoiseProducer;
16 | import net.logicsquad.nanocaptcha.image.noise.NoiseProducer;
17 | import net.logicsquad.nanocaptcha.image.renderer.DefaultWordRenderer;
18 | import net.logicsquad.nanocaptcha.image.renderer.WordRenderer;
19 |
20 | /**
21 | * An image CAPTCHA.
22 | *
23 | * @author James Childers
24 | * @author Paul Hoadley
25 | * @since 1.0
26 | */
27 | public final class ImageCaptcha {
28 | /**
29 | * Key for {@code defaultX} property
30 | */
31 | private static final String DEFAULT_X_KEY = "net.logicsquad.nanocaptcha.image.ImageCaptcha.defaultX";
32 |
33 | /**
34 | * Key for {@code defaultY} property
35 | */
36 | private static final String DEFAULT_Y_KEY = "net.logicsquad.nanocaptcha.image.ImageCaptcha.defaultY";
37 |
38 | /**
39 | * Default x-value if {@code defaultX} not set
40 | */
41 | private static final int DEFAULT_X = 200;
42 |
43 | /**
44 | * Default y-value if {@code defaultY} not set
45 | */
46 | private static final int DEFAULT_Y = 50;
47 |
48 | /**
49 | * Generated image
50 | */
51 | private final BufferedImage image;
52 |
53 | /**
54 | * Text content of image
55 | */
56 | private final String content;
57 |
58 | /**
59 | * Creation timestamp
60 | */
61 | private final OffsetDateTime created;
62 |
63 | /**
64 | * Constructor
65 | *
66 | * @param builder a {@link Builder} object
67 | */
68 | private ImageCaptcha(Builder builder) {
69 | image = builder.image;
70 | content = builder.content;
71 | created = OffsetDateTime.now();
72 | return;
73 | }
74 |
75 | /**
76 | *
77 | * Returns a new {@code ImageCaptcha} with some very basic settings:
78 | *
79 | *
80 | *
81 | *
x- and y-dimensions 200 x 50, unless overridden by properties;
82 | *
{@link LatinContentProducer} with length 5; and
83 | *
{@link DefaultWordRenderer} with its defaults.
84 | *
85 | *
86 | *
87 | * To override the x- and y-dimensions for your project, you can set these properties:
88 | *
104 | * Builder for an {@link ImageCaptcha}. Elements are added to the image on the
105 | * fly, so call the methods in an order that makes sense, e.g.:
106 | *
111 | */
112 | public static class Builder implements net.logicsquad.nanocaptcha.Builder {
113 | /**
114 | * Text content
115 | */
116 | private String content = "";
117 |
118 | /**
119 | * Generated image
120 | */
121 | private BufferedImage image;
122 |
123 | /**
124 | * Background for generated image
125 | */
126 | private BufferedImage background;
127 |
128 | /**
129 | * Should we add a border?
130 | */
131 | private boolean addBorder;
132 |
133 | /**
134 | * Constructor taking a width and height (in pixels) for the generated image.
135 | *
136 | * @param width image width
137 | * @param height image height
138 | */
139 | public Builder(int width, int height) {
140 | image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
141 | return;
142 | }
143 |
144 | /**
145 | * Adds a background using the default {@link BackgroundProducer} (a
146 | * {@link TransparentBackgroundProducer}).
147 | *
148 | * @return this
149 | */
150 | public Builder addBackground() {
151 | return addBackground(new TransparentBackgroundProducer());
152 | }
153 |
154 | /**
155 | * Adds a background using the given {@link BackgroundProducer}. Note that
156 | * adding more than one background does not have an additive effect: the last
157 | * background added is the winner.
158 | *
159 | * @param backgroundProducer a {@link BackgroundProducer}
160 | * @return this
161 | */
162 | public Builder addBackground(BackgroundProducer backgroundProducer) {
163 | background = backgroundProducer.getBackground(image.getWidth(), image.getHeight());
164 | return this;
165 | }
166 |
167 | /**
168 | * Adds content to the CAPTCHA using the default {@link ContentProducer}.
169 | *
170 | * @return this
171 | */
172 | public Builder addContent() {
173 | return addContent(new LatinContentProducer());
174 | }
175 |
176 | /**
177 | * Adds content (of length {@code length}) to the CAPTCHA using the default {@link ContentProducer}.
178 | *
179 | * @param length number of content units to add
180 | * @return this
181 | * @see #9
182 | * @since 1.4
183 | */
184 | public Builder addContent(int length) {
185 | return addContent(new LatinContentProducer(length));
186 | }
187 |
188 | /**
189 | * Adds content to the CAPTCHA using the given {@link ContentProducer}.
190 | *
191 | * @param contentProducer a {@link ContentProducer}
192 | * @return this
193 | */
194 | public Builder addContent(ContentProducer contentProducer) {
195 | return addContent(contentProducer, new DefaultWordRenderer.Builder().build());
196 | }
197 |
198 | /**
199 | * Adds content to the CAPTCHA using the given {@link ContentProducer}, and
200 | * render it to the image using the given {@link WordRenderer}.
201 | *
202 | * @param contentProducer a {@link ContentProducer}
203 | * @param wordRenderer a {@link WordRenderer}
204 | * @return this
205 | */
206 | public Builder addContent(ContentProducer contentProducer, WordRenderer wordRenderer) {
207 | content += contentProducer.getContent();
208 | wordRenderer.render(content, image);
209 | return this;
210 | }
211 |
212 | /**
213 | * Adds noise using the default {@link NoiseProducer} (a
214 | * {@link CurvedLineNoiseProducer}).
215 | *
216 | * @return this
217 | */
218 | public Builder addNoise() {
219 | return addNoise(new CurvedLineNoiseProducer());
220 | }
221 |
222 | /**
223 | * Adds noise using the given {@link NoiseProducer}.
224 | *
225 | * @param noiseProducer a {@link NoiseProducer}
226 | * @return this
227 | */
228 | public Builder addNoise(NoiseProducer noiseProducer) {
229 | noiseProducer.makeNoise(image);
230 | return this;
231 | }
232 |
233 | /**
234 | * Filters the image using the default {@link ImageFilter} (a
235 | * {@link RippleImageFilter}).
236 | *
237 | * @return this
238 | */
239 | public Builder addFilter() {
240 | return addFilter(new RippleImageFilter());
241 | }
242 |
243 | /**
244 | * Filters the image using the given {@link ImageFilter}.
245 | *
246 | * @param filter an {@link ImageFilter}
247 | * @return this
248 | */
249 | public Builder addFilter(ImageFilter filter) {
250 | filter.filter(image);
251 | return this;
252 | }
253 |
254 | /**
255 | * Draws a single-pixel wide black border around the image.
256 | *
257 | * @return this
258 | */
259 | public Builder addBorder() {
260 | addBorder = true;
261 | return this;
262 | }
263 |
264 | /**
265 | * Builds the image CAPTCHA described by this object.
266 | *
267 | * @return {@link ImageCaptcha} as described by this {@code Builder}
268 | */
269 | @Override
270 | public ImageCaptcha build() {
271 | if (background != null) {
272 | // Paint the main image over the background
273 | Graphics2D g = background.createGraphics();
274 | g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
275 | g.drawImage(image, null, null);
276 | image = background;
277 | }
278 | if (addBorder) {
279 | Graphics2D g = image.createGraphics();
280 | int width = image.getWidth();
281 | int height = image.getHeight();
282 | g.setColor(Color.BLACK);
283 | g.drawLine(0, 0, 0, width);
284 | g.drawLine(0, 0, width, 0);
285 | g.drawLine(0, height - 1, width, height - 1);
286 | g.drawLine(width - 1, height - 1, width - 1, 0);
287 | }
288 | return new ImageCaptcha(this);
289 | }
290 | }
291 |
292 | /**
293 | * Does CAPTCHA content match supplied {@code answer}? If {@code answer} is
294 | * {@code null}, this method returns {@code false}.
295 | *
296 | * @param answer a candidate content match
297 | * @return {@code true} if {@code answer} matches CAPTCHA content, otherwise
298 | * {@code false}
299 | */
300 | public boolean isCorrect(String answer) {
301 | if (answer == null) {
302 | return false;
303 | }
304 | return answer.equals(content);
305 | }
306 |
307 | /**
308 | * Returns content of this CAPTCHA.
309 | *
310 | * @return content
311 | */
312 | public String getContent() {
313 | return content;
314 | }
315 |
316 | /**
317 | * Returns the image for this {@code ImageCaptcha}.
318 | *
319 | * @return CAPTCHA image
320 | */
321 | public BufferedImage getImage() {
322 | return image;
323 | }
324 |
325 | /**
326 | * Returns creation timestamp.
327 | *
328 | * @return creation timestamp
329 | */
330 | public OffsetDateTime getCreated() {
331 | return created;
332 | }
333 |
334 | @Override
335 | public String toString() {
336 | StringBuilder sb = new StringBuilder(35);
337 | sb.append("[ImageCaptcha: created=").append(created).append(" content='").append(content).append("']");
338 | return sb.toString();
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | net.logicsquad
4 | nanocaptcha
5 | 2.2-SNAPSHOT
6 | NanoCaptcha
7 | A Java-based CAPTCHA implementation with a minimum of dependencies.
8 | https://github.com/logicsquad/nanocaptcha
9 | 2019
10 |
11 |
12 | Logic Squad
13 | https://logicsquad.net/
14 |
15 |
16 |
17 |
18 | 3-Clause BSD License
19 | https://opensource.org/licenses/BSD-3-Clause
20 | repo
21 | See LICENSE.txt in this project.
22 |
23 |
24 |
25 |
26 |
27 | paulh
28 | Paul Hoadley
29 | paulh@logicsquad.net
30 | Logic Squad
31 | Australia/Adelaide
32 |
33 |
34 |
35 |
36 | scm:git:git://github.com/logicsquad/nanocaptcha.git
37 | scm:git:ssh://github.com:logicsquad/nanocaptcha.git
38 | https://github.com/logicsquad/nanocaptcha/tree/master
39 |
40 |
41 |
42 |
43 | UTF-8
44 | UTF-8
45 | 1.8
46 | 1.8
47 | 7.10.0
48 | 3.21.0
49 |
50 |
51 |
52 |
53 | disable-java8-doclint
54 |
55 | [1.8,)
56 |
57 |
58 | -Xdoclint:none
59 |
60 |
61 |
62 |
63 | release
64 |
65 |
66 | release
67 |
68 |
69 |
70 |
71 |
72 | org.apache.maven.plugins
73 | maven-javadoc-plugin
74 |
75 |
76 | org.apache.maven.plugins
77 | maven-source-plugin
78 |
79 |
80 | org.apache.maven.plugins
81 | maven-gpg-plugin
82 |
83 |
84 | org.sonatype.plugins
85 | nexus-staging-maven-plugin
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | ossrh
95 | https://oss.sonatype.org/content/repositories/snapshots
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | org.apache.maven.plugins
104 | maven-pmd-plugin
105 | 3.26.0
106 |
107 |
108 | net.sourceforge.pmd
109 | pmd-core
110 | ${pmd.version}
111 |
112 |
113 | net.sourceforge.pmd
114 | pmd-java
115 | ${pmd.version}
116 |
117 |
118 | net.sourceforge.pmd
119 | pmd-javascript
120 | ${pmd.version}
121 |
122 |
123 | net.sourceforge.pmd
124 | pmd-jsp
125 | ${pmd.version}
126 |
127 |
128 |
129 |
130 | org.sonatype.plugins
131 | nexus-staging-maven-plugin
132 | 1.6.7
133 | true
134 |
135 | ossrh
136 | https://oss.sonatype.org/
137 | true
138 |
139 |
140 |
141 | org.apache.maven.plugins
142 | maven-source-plugin
143 | 3.3.0
144 |
145 |
146 | attach-sources
147 |
148 | jar-no-fork
149 |
150 |
151 |
152 |
153 |
154 | org.apache.maven.plugins
155 | maven-javadoc-plugin
156 | 3.6.3
157 |
158 |
159 |
160 |
161 |
162 |
163 | build-javadocs
164 | package
165 |
166 | javadoc
167 |
168 |
169 |
170 | attach-javadocs
171 |
172 | jar
173 |
174 |
175 |
176 |
177 |
178 | org.apache.maven.plugins
179 | maven-gpg-plugin
180 | 3.1.0
181 |
182 |
183 | --batch
184 | --yes
185 | --pinentry-mode
186 | loopback
187 |
188 |
189 |
190 |
191 | sign-artifacts
192 | verify
193 |
194 | sign
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | maven-surefire-plugin
205 | 3.2.3
206 |
207 |
208 | default-test
209 | test
210 |
211 | test
212 |
213 |
214 |
215 |
216 |
217 | org.apache.maven.plugins
218 | maven-site-plugin
219 | ${maven-site-plugin.version}
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | org.apache.maven.plugins
228 | maven-project-info-reports-plugin
229 | 3.5.0
230 |
231 | false
232 |
233 |
234 |
235 | org.apache.maven.plugins
236 | maven-javadoc-plugin
237 |
238 |
239 | https://docs.oracle.com/javase/8/docs/api/
240 |
241 |
242 |
243 |
244 | org.apache.maven.plugins
245 | maven-jxr-plugin
246 | 3.0.0
247 |
248 |
249 | org.apache.maven.plugins
250 | maven-pmd-plugin
251 |
252 | true
253 | utf-8
254 | 100
255 | 1.8
256 | false
257 |
258 | http://artefacts.logicsquad.net.s3.amazonaws.com/logicsquad_pmd.xml
259 |
260 |
261 |
262 |
263 | org.apache.maven.plugins
264 | maven-checkstyle-plugin
265 | 3.3.1
266 |
267 | http://artefacts.logicsquad.net.s3.amazonaws.com/logicsquad_checks.xml
268 |
269 |
270 |
271 | com.github.spotbugs
272 | spotbugs-maven-plugin
273 | 4.8.2.0
274 |
275 | Max
276 | Low
277 |
278 |
279 |
280 | org.codehaus.mojo
281 | versions-maven-plugin
282 | 2.16.2
283 |
284 |
285 |
286 | dependency-updates-report
287 | plugin-updates-report
288 | property-updates-report
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 | org.junit
300 | junit-bom
301 | 5.10.1
302 | pom
303 | import
304 |
305 |
306 |
307 |
308 |
309 |
310 | org.slf4j
311 | slf4j-api
312 | 2.0.9
313 |
314 |
315 | org.apache.logging.log4j
316 | log4j-slf4j2-impl
317 | 2.22.0
318 | test
319 |
320 |
321 | org.junit.jupiter
322 | junit-jupiter
323 | test
324 |
325 |
326 |
327 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019, Logic Squad
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of the copyright holder nor the names of its
14 | contributors may be used to endorse or promote products derived
15 | from this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
18 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LOGIC SQUAD BE
21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
24 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | NanoCaptcha is based on SimpleCaptcha, and contains source code from
30 | that project used under license.
31 |
32 | Copyright (c) 2008, James Childers
33 | All rights reserved.
34 |
35 | Redistribution and use in source and binary forms, with or without
36 | modification, are permitted provided that the following conditions are
37 | met:
38 | * Redistributions of source code must retain the above copyright
39 | notice, this list of conditions and the following disclaimer.
40 | * Redistributions in binary form must reproduce the above copyright
41 | notice, this list of conditions and the following disclaimer in the
42 | documentation and/or other materials provided with the
43 | distribution.
44 | * Neither the name of SimpleCaptcha nor the names of its contributors
45 | may be used to endorse or promote products derived from this
46 | software without specific prior written permission.
47 |
48 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
49 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
50 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
51 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
52 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
53 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
54 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
55 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
56 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
57 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
58 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59 |
60 | NanoCaptcha includes source code from JH Labs
61 | (http://www.jhlabs.com/ip/filters/) used under license.
62 |
63 | Copyright (c) 2006, Jerry Huxtable
64 |
65 | Apache License
66 | Version 2.0, January 2004
67 | http://www.apache.org/licenses/
68 |
69 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
70 |
71 | 1. Definitions.
72 |
73 | "License" shall mean the terms and conditions for use, reproduction,
74 | and distribution as defined by Sections 1 through 9 of this document.
75 |
76 | "Licensor" shall mean the copyright owner or entity authorized by
77 | the copyright owner that is granting the License.
78 |
79 | "Legal Entity" shall mean the union of the acting entity and all
80 | other entities that control, are controlled by, or are under common
81 | control with that entity. For the purposes of this definition,
82 | "control" means (i) the power, direct or indirect, to cause the
83 | direction or management of such entity, whether by contract or
84 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
85 | outstanding shares, or (iii) beneficial ownership of such entity.
86 |
87 | "You" (or "Your") shall mean an individual or Legal Entity
88 | exercising permissions granted by this License.
89 |
90 | "Source" form shall mean the preferred form for making modifications,
91 | including but not limited to software source code, documentation
92 | source, and configuration files.
93 |
94 | "Object" form shall mean any form resulting from mechanical
95 | transformation or translation of a Source form, including but
96 | not limited to compiled object code, generated documentation,
97 | and conversions to other media types.
98 |
99 | "Work" shall mean the work of authorship, whether in Source or
100 | Object form, made available under the License, as indicated by a
101 | copyright notice that is included in or attached to the work
102 | (an example is provided in the Appendix below).
103 |
104 | "Derivative Works" shall mean any work, whether in Source or Object
105 | form, that is based on (or derived from) the Work and for which the
106 | editorial revisions, annotations, elaborations, or other modifications
107 | represent, as a whole, an original work of authorship. For the purposes
108 | of this License, Derivative Works shall not include works that remain
109 | separable from, or merely link (or bind by name) to the interfaces of,
110 | the Work and Derivative Works thereof.
111 |
112 | "Contribution" shall mean any work of authorship, including
113 | the original version of the Work and any modifications or additions
114 | to that Work or Derivative Works thereof, that is intentionally
115 | submitted to Licensor for inclusion in the Work by the copyright owner
116 | or by an individual or Legal Entity authorized to submit on behalf of
117 | the copyright owner. For the purposes of this definition, "submitted"
118 | means any form of electronic, verbal, or written communication sent
119 | to the Licensor or its representatives, including but not limited to
120 | communication on electronic mailing lists, source code control systems,
121 | and issue tracking systems that are managed by, or on behalf of, the
122 | Licensor for the purpose of discussing and improving the Work, but
123 | excluding communication that is conspicuously marked or otherwise
124 | designated in writing by the copyright owner as "Not a Contribution."
125 |
126 | "Contributor" shall mean Licensor and any individual or Legal Entity
127 | on behalf of whom a Contribution has been received by Licensor and
128 | subsequently incorporated within the Work.
129 |
130 | 2. Grant of Copyright License. Subject to the terms and conditions of
131 | this License, each Contributor hereby grants to You a perpetual,
132 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
133 | copyright license to reproduce, prepare Derivative Works of,
134 | publicly display, publicly perform, sublicense, and distribute the
135 | Work and such Derivative Works in Source or Object form.
136 |
137 | 3. Grant of Patent License. Subject to the terms and conditions of
138 | this License, each Contributor hereby grants to You a perpetual,
139 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
140 | (except as stated in this section) patent license to make, have made,
141 | use, offer to sell, sell, import, and otherwise transfer the Work,
142 | where such license applies only to those patent claims licensable
143 | by such Contributor that are necessarily infringed by their
144 | Contribution(s) alone or by combination of their Contribution(s)
145 | with the Work to which such Contribution(s) was submitted. If You
146 | institute patent litigation against any entity (including a
147 | cross-claim or counterclaim in a lawsuit) alleging that the Work
148 | or a Contribution incorporated within the Work constitutes direct
149 | or contributory patent infringement, then any patent licenses
150 | granted to You under this License for that Work shall terminate
151 | as of the date such litigation is filed.
152 |
153 | 4. Redistribution. You may reproduce and distribute copies of the
154 | Work or Derivative Works thereof in any medium, with or without
155 | modifications, and in Source or Object form, provided that You
156 | meet the following conditions:
157 |
158 | (a) You must give any other recipients of the Work or
159 | Derivative Works a copy of this License; and
160 |
161 | (b) You must cause any modified files to carry prominent notices
162 | stating that You changed the files; and
163 |
164 | (c) You must retain, in the Source form of any Derivative Works
165 | that You distribute, all copyright, patent, trademark, and
166 | attribution notices from the Source form of the Work,
167 | excluding those notices that do not pertain to any part of
168 | the Derivative Works; and
169 |
170 | (d) If the Work includes a "NOTICE" text file as part of its
171 | distribution, then any Derivative Works that You distribute must
172 | include a readable copy of the attribution notices contained
173 | within such NOTICE file, excluding those notices that do not
174 | pertain to any part of the Derivative Works, in at least one
175 | of the following places: within a NOTICE text file distributed
176 | as part of the Derivative Works; within the Source form or
177 | documentation, if provided along with the Derivative Works; or,
178 | within a display generated by the Derivative Works, if and
179 | wherever such third-party notices normally appear. The contents
180 | of the NOTICE file are for informational purposes only and
181 | do not modify the License. You may add Your own attribution
182 | notices within Derivative Works that You distribute, alongside
183 | or as an addendum to the NOTICE text from the Work, provided
184 | that such additional attribution notices cannot be construed
185 | as modifying the License.
186 |
187 | You may add Your own copyright statement to Your modifications and
188 | may provide additional or different license terms and conditions
189 | for use, reproduction, or distribution of Your modifications, or
190 | for any such Derivative Works as a whole, provided Your use,
191 | reproduction, and distribution of the Work otherwise complies with
192 | the conditions stated in this License.
193 |
194 | 5. Submission of Contributions. Unless You explicitly state otherwise,
195 | any Contribution intentionally submitted for inclusion in the Work
196 | by You to the Licensor shall be under the terms and conditions of
197 | this License, without any additional terms or conditions.
198 | Notwithstanding the above, nothing herein shall supersede or modify
199 | the terms of any separate license agreement you may have executed
200 | with Licensor regarding such Contributions.
201 |
202 | 6. Trademarks. This License does not grant permission to use the trade
203 | names, trademarks, service marks, or product names of the Licensor,
204 | except as required for reasonable and customary use in describing the
205 | origin of the Work and reproducing the content of the NOTICE file.
206 |
207 | 7. Disclaimer of Warranty. Unless required by applicable law or
208 | agreed to in writing, Licensor provides the Work (and each
209 | Contributor provides its Contributions) on an "AS IS" BASIS,
210 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
211 | implied, including, without limitation, any warranties or conditions
212 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
213 | PARTICULAR PURPOSE. You are solely responsible for determining the
214 | appropriateness of using or redistributing the Work and assume any
215 | risks associated with Your exercise of permissions under this License.
216 |
217 | 8. Limitation of Liability. In no event and under no legal theory,
218 | whether in tort (including negligence), contract, or otherwise,
219 | unless required by applicable law (such as deliberate and grossly
220 | negligent acts) or agreed to in writing, shall any Contributor be
221 | liable to You for damages, including any direct, indirect, special,
222 | incidental, or consequential damages of any character arising as a
223 | result of this License or out of the use or inability to use the
224 | Work (including but not limited to damages for loss of goodwill,
225 | work stoppage, computer failure or malfunction, or any and all
226 | other commercial damages or losses), even if such Contributor
227 | has been advised of the possibility of such damages.
228 |
229 | 9. Accepting Warranty or Additional Liability. While redistributing
230 | the Work or Derivative Works thereof, You may choose to offer,
231 | and charge a fee for, acceptance of support, warranty, indemnity,
232 | or other liability obligations and/or rights consistent with this
233 | License. However, in accepting such obligations, You may act only
234 | on Your own behalf and on Your sole responsibility, not on behalf
235 | of any other Contributor, and only if You agree to indemnify,
236 | defend, and hold each Contributor harmless for any liability
237 | incurred by, or claims asserted against, such Contributor by reason
238 | of your accepting any such warranty or additional liability.
239 |
240 | NanoCaptcha contains the "Courier Prime" and "Public Sans" fonts, both
241 | used under license.
242 |
243 | Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime).
244 | Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans)
245 |
246 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
247 | This license is copied below, and is also available with a FAQ at:
248 | http://scripts.sil.org/OFL
249 |
250 |
251 | -----------------------------------------------------------
252 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
253 | -----------------------------------------------------------
254 |
255 | PREAMBLE
256 | The goals of the Open Font License (OFL) are to stimulate worldwide
257 | development of collaborative font projects, to support the font creation
258 | efforts of academic and linguistic communities, and to provide a free and
259 | open framework in which fonts may be shared and improved in partnership
260 | with others.
261 |
262 | The OFL allows the licensed fonts to be used, studied, modified and
263 | redistributed freely as long as they are not sold by themselves. The
264 | fonts, including any derivative works, can be bundled, embedded,
265 | redistributed and/or sold with any software provided that any reserved
266 | names are not used by derivative works. The fonts and derivatives,
267 | however, cannot be released under any other type of license. The
268 | requirement for fonts to remain under this license does not apply
269 | to any document created using the fonts or their derivatives.
270 |
271 | DEFINITIONS
272 | "Font Software" refers to the set of files released by the Copyright
273 | Holder(s) under this license and clearly marked as such. This may
274 | include source files, build scripts and documentation.
275 |
276 | "Reserved Font Name" refers to any names specified as such after the
277 | copyright statement(s).
278 |
279 | "Original Version" refers to the collection of Font Software components as
280 | distributed by the Copyright Holder(s).
281 |
282 | "Modified Version" refers to any derivative made by adding to, deleting,
283 | or substituting -- in part or in whole -- any of the components of the
284 | Original Version, by changing formats or by porting the Font Software to a
285 | new environment.
286 |
287 | "Author" refers to any designer, engineer, programmer, technical
288 | writer or other person who contributed to the Font Software.
289 |
290 | PERMISSION & CONDITIONS
291 | Permission is hereby granted, free of charge, to any person obtaining
292 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
293 | redistribute, and sell modified and unmodified copies of the Font
294 | Software, subject to the following conditions:
295 |
296 | 1) Neither the Font Software nor any of its individual components,
297 | in Original or Modified Versions, may be sold by itself.
298 |
299 | 2) Original or Modified Versions of the Font Software may be bundled,
300 | redistributed and/or sold with any software, provided that each copy
301 | contains the above copyright notice and this license. These can be
302 | included either as stand-alone text files, human-readable headers or
303 | in the appropriate machine-readable metadata fields within text or
304 | binary files as long as those fields can be easily viewed by the user.
305 |
306 | 3) No Modified Version of the Font Software may use the Reserved Font
307 | Name(s) unless explicit written permission is granted by the corresponding
308 | Copyright Holder. This restriction only applies to the primary font name as
309 | presented to the users.
310 |
311 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
312 | Software shall not be used to promote, endorse or advertise any
313 | Modified Version, except to acknowledge the contribution(s) of the
314 | Copyright Holder(s) and the Author(s) or with their explicit written
315 | permission.
316 |
317 | 5) The Font Software, modified or unmodified, in part or in whole,
318 | must be distributed entirely under this license, and must not be
319 | distributed under any other license. The requirement for fonts to
320 | remain under this license does not apply to any document created
321 | using the Font Software.
322 |
323 | TERMINATION
324 | This license becomes null and void if any of the above conditions are
325 | not met.
326 |
327 | DISCLAIMER
328 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
329 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
330 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
331 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
332 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
333 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
334 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
335 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
336 | OTHER DEALINGS IN THE FONT SOFTWARE.
337 |
--------------------------------------------------------------------------------
/src/main/java/net/logicsquad/nanocaptcha/image/filter/RippleImageFilter.java:
--------------------------------------------------------------------------------
1 | package net.logicsquad.nanocaptcha.image.filter;
2 |
3 | import java.awt.Rectangle;
4 | import java.awt.RenderingHints;
5 | import java.awt.geom.Point2D;
6 | import java.awt.geom.Rectangle2D;
7 | import java.awt.image.BufferedImage;
8 | import java.awt.image.BufferedImageOp;
9 | import java.awt.image.ColorModel;
10 | import java.util.Random;
11 |
12 | /**
13 | * Applies a {@link RippleFilter} to the image.
14 | *
15 | * @author James Childers
16 | * @author Paul Hoadley
17 | * @author Jerry Huxtable
18 | * @since 1.0
19 | */
20 | public class RippleImageFilter implements ImageFilter {
21 | @Override
22 | public void filter(BufferedImage image) {
23 | RippleFilter filter = new RippleFilter();
24 | filter.setWaveType(RippleFilter.SINE);
25 | filter.setXAmplitude(2.6f);
26 | filter.setYAmplitude(1.7f);
27 | filter.setXWavelength(15);
28 | filter.setYWavelength(5);
29 | ImageFilter.applyFilter(image, filter);
30 | }
31 |
32 | // The following code has been modified by Logic Squad, and originally carried
33 | // the following license:
34 | /*
35 | Copyright 2006 Jerry Huxtable
36 |
37 | Licensed under the Apache License, Version 2.0 (the "License");
38 | you may not use this file except in compliance with the License.
39 | You may obtain a copy of the License at
40 |
41 | http://www.apache.org/licenses/LICENSE-2.0
42 |
43 | Unless required by applicable law or agreed to in writing, software
44 | distributed under the License is distributed on an "AS IS" BASIS,
45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46 | See the License for the specific language governing permissions and
47 | limitations under the License.
48 | */
49 | /**
50 | * A filter which distorts an image by rippling it in the X or Y directions. The
51 | * amplitude and wavelength of rippling can be specified as well as whether
52 | * pixels going off the edges are wrapped or not.
53 | *
54 | * @author Jerry Huxtable
55 | */
56 | private static class RippleFilter extends TransformFilter {
57 | /**
58 | * Sine wave ripples.
59 | */
60 | public final static int SINE = 0;
61 |
62 | /**
63 | * Sawtooth wave ripples.
64 | */
65 | public final static int SAWTOOTH = 1;
66 |
67 | /**
68 | * Triangle wave ripples.
69 | */
70 | public final static int TRIANGLE = 2;
71 |
72 | /**
73 | * Noise ripples.
74 | */
75 | public final static int NOISE = 3;
76 |
77 | private float xAmplitude, yAmplitude;
78 | private float xWavelength, yWavelength;
79 | private int waveType;
80 |
81 | /**
82 | * Construct a RippleFilter.
83 | */
84 | public RippleFilter() {
85 | xAmplitude = 5.0f;
86 | yAmplitude = 0.0f;
87 | xWavelength = yWavelength = 16.0f;
88 | }
89 |
90 | /**
91 | * Set the amplitude of ripple in the X direction.
92 | *
93 | * @param xAmplitude the amplitude (in pixels).
94 | * @see #getXAmplitude
95 | */
96 | public void setXAmplitude(float xAmplitude) {
97 | this.xAmplitude = xAmplitude;
98 | }
99 |
100 | /**
101 | * Set the wavelength of ripple in the X direction.
102 | *
103 | * @param xWavelength the wavelength (in pixels).
104 | * @see #getXWavelength
105 | */
106 | public void setXWavelength(float xWavelength) {
107 | this.xWavelength = xWavelength;
108 | }
109 |
110 | /**
111 | * Set the amplitude of ripple in the Y direction.
112 | *
113 | * @param yAmplitude the amplitude (in pixels).
114 | * @see #getYAmplitude
115 | */
116 | public void setYAmplitude(float yAmplitude) {
117 | this.yAmplitude = yAmplitude;
118 | }
119 |
120 | /**
121 | * Set the wavelength of ripple in the Y direction.
122 | *
123 | * @param yWavelength the wavelength (in pixels).
124 | * @see #getYWavelength
125 | */
126 | public void setYWavelength(float yWavelength) {
127 | this.yWavelength = yWavelength;
128 | }
129 |
130 | /**
131 | * Set the wave type.
132 | *
133 | * @param waveType the type.
134 | * @see #getWaveType
135 | */
136 | public void setWaveType(int waveType) {
137 | this.waveType = waveType;
138 | }
139 |
140 | @Override
141 | protected void transformSpace(Rectangle r) {
142 | if (edgeAction == ZERO) {
143 | r.x -= (int) xAmplitude;
144 | r.width += (int) (2 * xAmplitude);
145 | r.y -= (int) yAmplitude;
146 | r.height += (int) (2 * yAmplitude);
147 | }
148 | }
149 |
150 | @Override
151 | protected void transformInverse(int x, int y, float[] out) {
152 | float nx = (float) y / xWavelength;
153 | float ny = (float) x / yWavelength;
154 | float fx, fy;
155 | switch (waveType) {
156 | case SINE:
157 | default:
158 | fx = (float) Math.sin(nx);
159 | fy = (float) Math.sin(ny);
160 | break;
161 | case SAWTOOTH:
162 | fx = ImageMath.mod(nx, 1);
163 | fy = ImageMath.mod(ny, 1);
164 | break;
165 | case TRIANGLE:
166 | fx = ImageMath.triangle(nx);
167 | fy = ImageMath.triangle(ny);
168 | break;
169 | case NOISE:
170 | fx = Noise.noise1(nx);
171 | fy = Noise.noise1(ny);
172 | break;
173 | }
174 | out[0] = x + xAmplitude * fx;
175 | out[1] = y + yAmplitude * fy;
176 | }
177 |
178 | @Override
179 | public String toString() {
180 | return "Distort/Ripple...";
181 | }
182 |
183 | @Override
184 | public RenderingHints getRenderingHints() {
185 | return null;
186 | }
187 | }
188 |
189 | // The following code has been modified by Logic Squad, and originally carried
190 | // the following license:
191 | /*
192 | Copyright 2006 Jerry Huxtable
193 |
194 | Licensed under the Apache License, Version 2.0 (the "License");
195 | you may not use this file except in compliance with the License.
196 | You may obtain a copy of the License at
197 |
198 | http://www.apache.org/licenses/LICENSE-2.0
199 |
200 | Unless required by applicable law or agreed to in writing, software
201 | distributed under the License is distributed on an "AS IS" BASIS,
202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
203 | See the License for the specific language governing permissions and
204 | limitations under the License.
205 | */
206 | /**
207 | * An abstract superclass for filters which distort images in some way. The
208 | * subclass only needs to override two methods to provide the mapping between
209 | * source and destination pixels.
210 | */
211 | private static abstract class TransformFilter extends AbstractBufferedImageOp {
212 |
213 | /**
214 | * Treat pixels off the edge as zero.
215 | */
216 | public final static int ZERO = 0;
217 |
218 | /**
219 | * Clamp pixels to the image edges.
220 | */
221 | public final static int CLAMP = 1;
222 |
223 | /**
224 | * Wrap pixels off the edge onto the oppsoite edge.
225 | */
226 | public final static int WRAP = 2;
227 |
228 | /**
229 | * Clamp pixels RGB to the image edges, but zero the alpha. This prevents gray
230 | * borders on your image.
231 | */
232 | public final static int RGB_CLAMP = 3;
233 |
234 | /**
235 | * Use nearest-neighbout interpolation.
236 | */
237 | public final static int NEAREST_NEIGHBOUR = 0;
238 |
239 | /**
240 | * Use bilinear interpolation.
241 | */
242 | public final static int BILINEAR = 1;
243 |
244 | /**
245 | * The action to take for pixels off the image edge.
246 | */
247 | protected int edgeAction = RGB_CLAMP;
248 |
249 | /**
250 | * The type of interpolation to use.
251 | */
252 | protected int interpolation = BILINEAR;
253 |
254 | /**
255 | * The output image rectangle.
256 | */
257 | protected Rectangle transformedSpace;
258 |
259 | /**
260 | * Inverse transform a point. This method needs to be overriden by all
261 | * subclasses.
262 | *
263 | * @param x the X position of the pixel in the output image
264 | * @param y the Y position of the pixel in the output image
265 | * @param out the position of the pixel in the input image
266 | */
267 | protected abstract void transformInverse(int x, int y, float[] out);
268 |
269 | /**
270 | * Forward transform a rectangle. Used to determine the size of the output
271 | * image.
272 | *
273 | * @param rect the rectangle to transform
274 | */
275 | protected abstract void transformSpace(Rectangle rect);
276 |
277 | @Override
278 | public BufferedImage filter(BufferedImage src, BufferedImage dst) {
279 | int width = src.getWidth();
280 | int height = src.getHeight();
281 |
282 | transformedSpace = new Rectangle(0, 0, width, height);
283 | transformSpace(transformedSpace);
284 |
285 | if (dst == null) {
286 | ColorModel dstCM = src.getColorModel();
287 | dst = new BufferedImage(dstCM,
288 | dstCM.createCompatibleWritableRaster(transformedSpace.width, transformedSpace.height),
289 | dstCM.isAlphaPremultiplied(), null);
290 | }
291 |
292 | int[] inPixels = getRGB(src, 0, 0, width, height, null);
293 |
294 | if (interpolation == NEAREST_NEIGHBOUR)
295 | return filterPixelsNN(dst, width, height, inPixels, transformedSpace);
296 |
297 | int srcWidth = width;
298 | int srcHeight = height;
299 | int srcWidth1 = width - 1;
300 | int srcHeight1 = height - 1;
301 | int outWidth = transformedSpace.width;
302 | int outHeight = transformedSpace.height;
303 | int outX, outY;
304 | int[] outPixels = new int[outWidth];
305 |
306 | outX = transformedSpace.x;
307 | outY = transformedSpace.y;
308 | float[] out = new float[2];
309 |
310 | for (int y = 0; y < outHeight; y++) {
311 | for (int x = 0; x < outWidth; x++) {
312 | transformInverse(outX + x, outY + y, out);
313 | int srcX = (int) Math.floor(out[0]);
314 | int srcY = (int) Math.floor(out[1]);
315 | float xWeight = out[0] - srcX;
316 | float yWeight = out[1] - srcY;
317 | int nw, ne, sw, se;
318 |
319 | if (srcX >= 0 && srcX < srcWidth1 && srcY >= 0 && srcY < srcHeight1) {
320 | // Easy case, all corners are in the image
321 | int i = srcWidth * srcY + srcX;
322 | nw = inPixels[i];
323 | ne = inPixels[i + 1];
324 | sw = inPixels[i + srcWidth];
325 | se = inPixels[i + srcWidth + 1];
326 | } else {
327 | // Some of the corners are off the image
328 | nw = getPixel(inPixels, srcX, srcY, srcWidth, srcHeight);
329 | ne = getPixel(inPixels, srcX + 1, srcY, srcWidth, srcHeight);
330 | sw = getPixel(inPixels, srcX, srcY + 1, srcWidth, srcHeight);
331 | se = getPixel(inPixels, srcX + 1, srcY + 1, srcWidth, srcHeight);
332 | }
333 | outPixels[x] = ImageMath.bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se);
334 | }
335 | setRGB(dst, 0, y, transformedSpace.width, 1, outPixels);
336 | }
337 | return dst;
338 | }
339 |
340 | final private int getPixel(int[] pixels, int x, int y, int width, int height) {
341 | if (x < 0 || x >= width || y < 0 || y >= height) {
342 | switch (edgeAction) {
343 | case ZERO:
344 | default:
345 | return 0;
346 | case WRAP:
347 | return pixels[(ImageMath.mod(y, height) * width) + ImageMath.mod(x, width)];
348 | case CLAMP:
349 | return pixels[(ImageMath.clamp(y, 0, height - 1) * width) + ImageMath.clamp(x, 0, width - 1)];
350 | case RGB_CLAMP:
351 | return pixels[(ImageMath.clamp(y, 0, height - 1) * width) + ImageMath.clamp(x, 0, width - 1)]
352 | & 0x00ffffff;
353 | }
354 | }
355 | return pixels[y * width + x];
356 | }
357 |
358 | protected BufferedImage filterPixelsNN(BufferedImage dst, int width, int height, int[] inPixels,
359 | Rectangle transformedSpace) {
360 | int srcWidth = width;
361 | int srcHeight = height;
362 | int outWidth = transformedSpace.width;
363 | int outHeight = transformedSpace.height;
364 | int outX, outY, srcX, srcY;
365 | int[] outPixels = new int[outWidth];
366 |
367 | outX = transformedSpace.x;
368 | outY = transformedSpace.y;
369 | float[] out = new float[2];
370 |
371 | for (int y = 0; y < outHeight; y++) {
372 | for (int x = 0; x < outWidth; x++) {
373 | transformInverse(outX + x, outY + y, out);
374 | srcX = (int) out[0];
375 | srcY = (int) out[1];
376 | // int casting rounds towards zero, so we check out[0] < 0, not srcX < 0
377 | if (out[0] < 0 || srcX >= srcWidth || out[1] < 0 || srcY >= srcHeight) {
378 | int p;
379 | switch (edgeAction) {
380 | case ZERO:
381 | default:
382 | p = 0;
383 | break;
384 | case WRAP:
385 | p = inPixels[(ImageMath.mod(srcY, srcHeight) * srcWidth) + ImageMath.mod(srcX, srcWidth)];
386 | break;
387 | case CLAMP:
388 | p = inPixels[(ImageMath.clamp(srcY, 0, srcHeight - 1) * srcWidth)
389 | + ImageMath.clamp(srcX, 0, srcWidth - 1)];
390 | break;
391 | case RGB_CLAMP:
392 | p = inPixels[(ImageMath.clamp(srcY, 0, srcHeight - 1) * srcWidth)
393 | + ImageMath.clamp(srcX, 0, srcWidth - 1)] & 0x00ffffff;
394 | }
395 | outPixels[x] = p;
396 | } else {
397 | int i = srcWidth * srcY + srcX;
398 | outPixels[x] = inPixels[i];
399 | }
400 | }
401 | setRGB(dst, 0, y, transformedSpace.width, 1, outPixels);
402 | }
403 | return dst;
404 | }
405 | }
406 |
407 | // The following code has been modified by Logic Squad, and originally carried
408 | // the following license:
409 | /*
410 | Copyright 2006 Jerry Huxtable
411 |
412 | Licensed under the Apache License, Version 2.0 (the "License");
413 | you may not use this file except in compliance with the License.
414 | You may obtain a copy of the License at
415 |
416 | http://www.apache.org/licenses/LICENSE-2.0
417 |
418 | Unless required by applicable law or agreed to in writing, software
419 | distributed under the License is distributed on an "AS IS" BASIS,
420 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
421 | See the License for the specific language governing permissions and
422 | limitations under the License.
423 | */
424 | /**
425 | * A convenience class which implements those methods of BufferedImageOp which
426 | * are rarely changed.
427 | */
428 | private static abstract class AbstractBufferedImageOp implements BufferedImageOp, Cloneable {
429 | @Override
430 | public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
431 | if (dstCM == null)
432 | dstCM = src.getColorModel();
433 | return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()),
434 | dstCM.isAlphaPremultiplied(), null);
435 | }
436 |
437 | @Override
438 | public Rectangle2D getBounds2D(BufferedImage src) {
439 | return new Rectangle(0, 0, src.getWidth(), src.getHeight());
440 | }
441 |
442 | @Override
443 | public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
444 | if (dstPt == null)
445 | dstPt = new Point2D.Double();
446 | dstPt.setLocation(srcPt.getX(), srcPt.getY());
447 | return dstPt;
448 | }
449 |
450 | @Override
451 | public abstract RenderingHints getRenderingHints();
452 |
453 | /**
454 | * A convenience method for getting ARGB pixels from an image. This tries to
455 | * avoid the performance penalty of BufferedImage.getRGB unmanaging the image.
456 | *
457 | * @param image a BufferedImage object
458 | * @param x the left edge of the pixel block
459 | * @param y the right edge of the pixel block
460 | * @param width the width of the pixel arry
461 | * @param height the height of the pixel arry
462 | * @param pixels the array to hold the returned pixels. May be null.
463 | * @return the pixels
464 | * @see #setRGB
465 | */
466 | public int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
467 | int type = image.getType();
468 | if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB)
469 | return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
470 | return image.getRGB(x, y, width, height, pixels, 0, width);
471 | }
472 |
473 | /**
474 | * A convenience method for setting ARGB pixels in an image. This tries to avoid
475 | * the performance penalty of BufferedImage.setRGB unmanaging the image.
476 | *
477 | * @param image a BufferedImage object
478 | * @param x the left edge of the pixel block
479 | * @param y the right edge of the pixel block
480 | * @param width the width of the pixel arry
481 | * @param height the height of the pixel arry
482 | * @param pixels the array of pixels to set
483 | * @see #getRGB
484 | */
485 | public void setRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
486 | int type = image.getType();
487 | if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB)
488 | image.getRaster().setDataElements(x, y, width, height, pixels);
489 | else
490 | image.setRGB(x, y, width, height, pixels, 0, width);
491 | }
492 |
493 | @Override
494 | public Object clone() {
495 | try {
496 | return super.clone();
497 | } catch (CloneNotSupportedException e) {
498 | return null;
499 | }
500 | }
501 | }
502 |
503 | // The following code has been modified by Logic Squad, and originally carried
504 | // the following license:
505 | /*
506 | Copyright 2006 Jerry Huxtable
507 |
508 | Licensed under the Apache License, Version 2.0 (the "License");
509 | you may not use this file except in compliance with the License.
510 | You may obtain a copy of the License at
511 |
512 | http://www.apache.org/licenses/LICENSE-2.0
513 |
514 | Unless required by applicable law or agreed to in writing, software
515 | distributed under the License is distributed on an "AS IS" BASIS,
516 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
517 | See the License for the specific language governing permissions and
518 | limitations under the License.
519 | */
520 | /**
521 | * A class containing static math methods useful for image processing.
522 | */
523 | private static class ImageMath {
524 | /**
525 | * Clamp a value to an interval.
526 | *
527 | * @param a the lower clamp threshold
528 | * @param b the upper clamp threshold
529 | * @param x the input parameter
530 | * @return the clamped value
531 | */
532 | public static int clamp(int x, int a, int b) {
533 | return (x < a) ? a : (x > b) ? b : x;
534 | }
535 |
536 | /**
537 | * Return a mod b. This differs from the % operator with respect to negative
538 | * numbers.
539 | *
540 | * @param a the dividend
541 | * @param b the divisor
542 | * @return a mod b
543 | */
544 | public static float mod(float a, float b) {
545 | int n = (int) (a / b);
546 |
547 | a -= n * b;
548 | if (a < 0)
549 | return a + b;
550 | return a;
551 | }
552 |
553 | /**
554 | * Return a mod b. This differs from the % operator with respect to negative
555 | * numbers.
556 | *
557 | * @param a the dividend
558 | * @param b the divisor
559 | * @return a mod b
560 | */
561 | public static int mod(int a, int b) {
562 | int n = a / b;
563 |
564 | a -= n * b;
565 | if (a < 0)
566 | return a + b;
567 | return a;
568 | }
569 |
570 | /**
571 | * The triangle function. Returns a repeating triangle shape in the range 0..1
572 | * with wavelength 1.0
573 | *
574 | * @param x the input parameter
575 | * @return the output value
576 | */
577 | public static float triangle(float x) {
578 | float r = mod(x, 1.0f);
579 | return 2.0f * (r < 0.5 ? r : 1 - r);
580 | }
581 |
582 | /**
583 | * Bilinear interpolation of ARGB values.
584 | *
585 | * @param x the X interpolation parameter 0..1
586 | * @param y the y interpolation parameter 0..1
587 | * @param rgb array of four ARGB values in the order NW, NE, SW, SE
588 | * @return the interpolated value
589 | */
590 | public static int bilinearInterpolate(float x, float y, int nw, int ne, int sw, int se) {
591 | float m0, m1;
592 | int a0 = (nw >> 24) & 0xff;
593 | int r0 = (nw >> 16) & 0xff;
594 | int g0 = (nw >> 8) & 0xff;
595 | int b0 = nw & 0xff;
596 | int a1 = (ne >> 24) & 0xff;
597 | int r1 = (ne >> 16) & 0xff;
598 | int g1 = (ne >> 8) & 0xff;
599 | int b1 = ne & 0xff;
600 | int a2 = (sw >> 24) & 0xff;
601 | int r2 = (sw >> 16) & 0xff;
602 | int g2 = (sw >> 8) & 0xff;
603 | int b2 = sw & 0xff;
604 | int a3 = (se >> 24) & 0xff;
605 | int r3 = (se >> 16) & 0xff;
606 | int g3 = (se >> 8) & 0xff;
607 | int b3 = se & 0xff;
608 |
609 | float cx = 1.0f - x;
610 | float cy = 1.0f - y;
611 |
612 | m0 = cx * a0 + x * a1;
613 | m1 = cx * a2 + x * a3;
614 | int a = (int) (cy * m0 + y * m1);
615 |
616 | m0 = cx * r0 + x * r1;
617 | m1 = cx * r2 + x * r3;
618 | int r = (int) (cy * m0 + y * m1);
619 |
620 | m0 = cx * g0 + x * g1;
621 | m1 = cx * g2 + x * g3;
622 | int g = (int) (cy * m0 + y * m1);
623 |
624 | m0 = cx * b0 + x * b1;
625 | m1 = cx * b2 + x * b3;
626 | int b = (int) (cy * m0 + y * m1);
627 |
628 | return (a << 24) | (r << 16) | (g << 8) | b;
629 | }
630 | }
631 |
632 | // The following code has been modified by Logic Squad, and originally carried
633 | // the following license:
634 | /*
635 | Copyright 2006 Jerry Huxtable
636 |
637 | Licensed under the Apache License, Version 2.0 (the "License");
638 | you may not use this file except in compliance with the License.
639 | You may obtain a copy of the License at
640 |
641 | http://www.apache.org/licenses/LICENSE-2.0
642 |
643 | Unless required by applicable law or agreed to in writing, software
644 | distributed under the License is distributed on an "AS IS" BASIS,
645 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
646 | See the License for the specific language governing permissions and
647 | limitations under the License.
648 | */
649 | /**
650 | * Perlin Noise functions
651 | */
652 | private static class Noise implements Function1D, Function2D, Function3D {
653 |
654 | private static Random randomGenerator = new Random();
655 |
656 | @Override
657 | public float evaluate(float x) {
658 | return noise1(x);
659 | }
660 |
661 | @Override
662 | public float evaluate(float x, float y) {
663 | return noise2(x, y);
664 | }
665 |
666 | @Override
667 | public float evaluate(float x, float y, float z) {
668 | return noise3(x, y, z);
669 | }
670 |
671 | private final static int B = 0x100;
672 | private final static int BM = 0xff;
673 | private final static int N = 0x1000;
674 |
675 | static int[] p = new int[B + B + 2];
676 | static float[][] g3 = new float[B + B + 2][3];
677 | static float[][] g2 = new float[B + B + 2][2];
678 | static float[] g1 = new float[B + B + 2];
679 | static boolean start = true;
680 |
681 | private static float sCurve(float t) {
682 | return t * t * (3.0f - 2.0f * t);
683 | }
684 |
685 | /**
686 | * Compute 1-dimensional Perlin noise.
687 | *
688 | * @param x the x value
689 | * @return noise value at x in the range -1..1
690 | */
691 | public static float noise1(float x) {
692 | int bx0, bx1;
693 | float rx0, rx1, sx, t, u, v;
694 |
695 | if (start) {
696 | start = false;
697 | init();
698 | }
699 |
700 | t = x + N;
701 | bx0 = ((int) t) & BM;
702 | bx1 = (bx0 + 1) & BM;
703 | rx0 = t - (int) t;
704 | rx1 = rx0 - 1.0f;
705 |
706 | sx = sCurve(rx0);
707 |
708 | u = rx0 * g1[p[bx0]];
709 | v = rx1 * g1[p[bx1]];
710 | return 2.3f * lerp(sx, u, v);
711 | }
712 |
713 | /**
714 | * Compute 2-dimensional Perlin noise.
715 | *
716 | * @param x the x coordinate
717 | * @param y the y coordinate
718 | * @return noise value at (x,y)
719 | */
720 | public static float noise2(float x, float y) {
721 | int bx0, bx1, by0, by1, b00, b10, b01, b11;
722 | float rx0, rx1, ry0, ry1, q[], sx, sy, a, b, t, u, v;
723 | int i, j;
724 |
725 | if (start) {
726 | start = false;
727 | init();
728 | }
729 |
730 | t = x + N;
731 | bx0 = ((int) t) & BM;
732 | bx1 = (bx0 + 1) & BM;
733 | rx0 = t - (int) t;
734 | rx1 = rx0 - 1.0f;
735 |
736 | t = y + N;
737 | by0 = ((int) t) & BM;
738 | by1 = (by0 + 1) & BM;
739 | ry0 = t - (int) t;
740 | ry1 = ry0 - 1.0f;
741 |
742 | i = p[bx0];
743 | j = p[bx1];
744 |
745 | b00 = p[i + by0];
746 | b10 = p[j + by0];
747 | b01 = p[i + by1];
748 | b11 = p[j + by1];
749 |
750 | sx = sCurve(rx0);
751 | sy = sCurve(ry0);
752 |
753 | q = g2[b00];
754 | u = rx0 * q[0] + ry0 * q[1];
755 | q = g2[b10];
756 | v = rx1 * q[0] + ry0 * q[1];
757 | a = lerp(sx, u, v);
758 |
759 | q = g2[b01];
760 | u = rx0 * q[0] + ry1 * q[1];
761 | q = g2[b11];
762 | v = rx1 * q[0] + ry1 * q[1];
763 | b = lerp(sx, u, v);
764 |
765 | return 1.5f * lerp(sy, a, b);
766 | }
767 |
768 | /**
769 | * Compute 3-dimensional Perlin noise.
770 | *
771 | * @param x the x coordinate
772 | * @param y the y coordinate
773 | * @param y the y coordinate
774 | * @return noise value at (x,y,z)
775 | */
776 | public static float noise3(float x, float y, float z) {
777 | int bx0, bx1, by0, by1, bz0, bz1, b00, b10, b01, b11;
778 | float rx0, rx1, ry0, ry1, rz0, rz1, q[], sy, sz, a, b, c, d, t, u, v;
779 | int i, j;
780 |
781 | if (start) {
782 | start = false;
783 | init();
784 | }
785 |
786 | t = x + N;
787 | bx0 = ((int) t) & BM;
788 | bx1 = (bx0 + 1) & BM;
789 | rx0 = t - (int) t;
790 | rx1 = rx0 - 1.0f;
791 |
792 | t = y + N;
793 | by0 = ((int) t) & BM;
794 | by1 = (by0 + 1) & BM;
795 | ry0 = t - (int) t;
796 | ry1 = ry0 - 1.0f;
797 |
798 | t = z + N;
799 | bz0 = ((int) t) & BM;
800 | bz1 = (bz0 + 1) & BM;
801 | rz0 = t - (int) t;
802 | rz1 = rz0 - 1.0f;
803 |
804 | i = p[bx0];
805 | j = p[bx1];
806 |
807 | b00 = p[i + by0];
808 | b10 = p[j + by0];
809 | b01 = p[i + by1];
810 | b11 = p[j + by1];
811 |
812 | t = sCurve(rx0);
813 | sy = sCurve(ry0);
814 | sz = sCurve(rz0);
815 |
816 | q = g3[b00 + bz0];
817 | u = rx0 * q[0] + ry0 * q[1] + rz0 * q[2];
818 | q = g3[b10 + bz0];
819 | v = rx1 * q[0] + ry0 * q[1] + rz0 * q[2];
820 | a = lerp(t, u, v);
821 |
822 | q = g3[b01 + bz0];
823 | u = rx0 * q[0] + ry1 * q[1] + rz0 * q[2];
824 | q = g3[b11 + bz0];
825 | v = rx1 * q[0] + ry1 * q[1] + rz0 * q[2];
826 | b = lerp(t, u, v);
827 |
828 | c = lerp(sy, a, b);
829 |
830 | q = g3[b00 + bz1];
831 | u = rx0 * q[0] + ry0 * q[1] + rz1 * q[2];
832 | q = g3[b10 + bz1];
833 | v = rx1 * q[0] + ry0 * q[1] + rz1 * q[2];
834 | a = lerp(t, u, v);
835 |
836 | q = g3[b01 + bz1];
837 | u = rx0 * q[0] + ry1 * q[1] + rz1 * q[2];
838 | q = g3[b11 + bz1];
839 | v = rx1 * q[0] + ry1 * q[1] + rz1 * q[2];
840 | b = lerp(t, u, v);
841 |
842 | d = lerp(sy, a, b);
843 |
844 | return 1.5f * lerp(sz, c, d);
845 | }
846 |
847 | public static float lerp(float t, float a, float b) {
848 | return a + t * (b - a);
849 | }
850 |
851 | private static void normalize2(float v[]) {
852 | float s = (float) Math.sqrt(v[0] * v[0] + v[1] * v[1]);
853 | v[0] = v[0] / s;
854 | v[1] = v[1] / s;
855 | }
856 |
857 | static void normalize3(float v[]) {
858 | float s = (float) Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
859 | v[0] = v[0] / s;
860 | v[1] = v[1] / s;
861 | v[2] = v[2] / s;
862 | }
863 |
864 | private static int random() {
865 | return randomGenerator.nextInt() & 0x7fffffff;
866 | }
867 |
868 | private static void init() {
869 | int i, j, k;
870 |
871 | for (i = 0; i < B; i++) {
872 | p[i] = i;
873 |
874 | g1[i] = (float) ((random() % (B + B)) - B) / B;
875 |
876 | for (j = 0; j < 2; j++)
877 | g2[i][j] = (float) ((random() % (B + B)) - B) / B;
878 | normalize2(g2[i]);
879 |
880 | for (j = 0; j < 3; j++)
881 | g3[i][j] = (float) ((random() % (B + B)) - B) / B;
882 | normalize3(g3[i]);
883 | }
884 |
885 | for (i = B - 1; i >= 0; i--) {
886 | k = p[i];
887 | p[i] = p[j = random() % B];
888 | p[j] = k;
889 | }
890 |
891 | for (i = 0; i < B + 2; i++) {
892 | p[B + i] = p[i];
893 | g1[B + i] = g1[i];
894 | for (j = 0; j < 2; j++)
895 | g2[B + i][j] = g2[i][j];
896 | for (j = 0; j < 3; j++)
897 | g3[B + i][j] = g3[i][j];
898 | }
899 | }
900 | }
901 |
902 | // The following code has been modified by Logic Squad, and originally carried
903 | // the following license:
904 | /*
905 | Copyright 2006 Jerry Huxtable
906 |
907 | Licensed under the Apache License, Version 2.0 (the "License");
908 | you may not use this file except in compliance with the License.
909 | You may obtain a copy of the License at
910 |
911 | http://www.apache.org/licenses/LICENSE-2.0
912 |
913 | Unless required by applicable law or agreed to in writing, software
914 | distributed under the License is distributed on an "AS IS" BASIS,
915 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
916 | See the License for the specific language governing permissions and
917 | limitations under the License.
918 | */
919 | private interface Function3D {
920 | public float evaluate(float x, float y, float z);
921 | }
922 |
923 | // The following code has been modified by Logic Squad, and originally carried
924 | // the following license:
925 | /*
926 | Copyright 2006 Jerry Huxtable
927 |
928 | Licensed under the Apache License, Version 2.0 (the "License");
929 | you may not use this file except in compliance with the License.
930 | You may obtain a copy of the License at
931 |
932 | http://www.apache.org/licenses/LICENSE-2.0
933 |
934 | Unless required by applicable law or agreed to in writing, software
935 | distributed under the License is distributed on an "AS IS" BASIS,
936 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
937 | See the License for the specific language governing permissions and
938 | limitations under the License.
939 | */
940 | private interface Function2D {
941 | public float evaluate(float x, float y);
942 | }
943 |
944 | // The following code has been modified by Logic Squad, and originally carried
945 | // the following license:
946 | /*
947 | Copyright 2006 Jerry Huxtable
948 |
949 | Licensed under the Apache License, Version 2.0 (the "License");
950 | you may not use this file except in compliance with the License.
951 | You may obtain a copy of the License at
952 |
953 | http://www.apache.org/licenses/LICENSE-2.0
954 |
955 | Unless required by applicable law or agreed to in writing, software
956 | distributed under the License is distributed on an "AS IS" BASIS,
957 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
958 | See the License for the specific language governing permissions and
959 | limitations under the License.
960 | */
961 | private interface Function1D {
962 | public float evaluate(float v);
963 | }
964 | }
965 |
--------------------------------------------------------------------------------