commit 96a32b2e6579951728a97282b3eaa036ef7e2dc4 Author: Struchkov Mark Date: Sun Jul 10 23:25:06 2022 +0300 First Version Haiti Captcha diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..fbaa16f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,17 @@ +image: maven:3.8.5-openjdk-17 +variables: + MAVEN_OPTS: "-Dmaven.repo.local=./.m2/repository" + +stages: + - deploy + +deploy: + stage: deploy + only: + - /^v.*$/ + except: + - branches + before_script: + - gpg --pinentry-mode loopback --passphrase $GPG_PASSPHRASE --import $GPG_PRIVATE_KEY + script: + - 'mvn --settings $MAVEN_SETTINGS -U -P ossrh,release clean deploy' \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..55aeb59 --- /dev/null +++ b/pom.xml @@ -0,0 +1,169 @@ + + + 4.0.0 + + dev.struchkov.haiti.utils + haiti-utils-captcha + 1.0-SNAPSHOT + + Haiti Captcha + A quick and clean way to add a simple captcha to your app + https://github.com/haiti-projects/haiti-utils-captcha + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + GitHub + https://github.com/haiti-projects/haiti-utils-captcha/issues + + + + 17 + ${java.version} + ${java.version} + UTF-8 + UTF-8 + + 3.10.1 + 1.6.13 + 3.2.1 + 3.4.0 + 3.0.1 + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${plugin.nexus.staging.ver} + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-source-plugin + ${plugin.maven.source.ver} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${plugin.maven.javadoc.ver} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${plugin.maven.gpg.ver} + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${plugin.maven.compiler.ver} + + ${java.version} + ${java.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + + release + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + + + scm:git:https://github.com/haiti-projects/haiti-utils-captcha.git + https://github.com/haiti-projects/haiti-utils-captcha + scm:git:https://github.com/haiti-projects/haiti-utils-captcha.git + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + + + + uPagge + Struchkov Mark + mark@struchkov.dev + https://struchkov.dev + + + + \ No newline at end of file diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/Captcha.java b/src/main/java/dev/struchkov/haiti/util/captcha/Captcha.java new file mode 100644 index 0000000..7076c88 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/Captcha.java @@ -0,0 +1,209 @@ +package dev.struchkov.haiti.util.captcha; + +import dev.struchkov.haiti.util.captcha.background.BackgroundProducer; +import dev.struchkov.haiti.util.captcha.background.TransparentBackgroundProducer; +import dev.struchkov.haiti.util.captcha.noise.CurvedLineNoiseProducer; +import dev.struchkov.haiti.util.captcha.noise.NoiseProducer; +import dev.struchkov.haiti.util.captcha.text.producer.DefaultTextProducer; +import dev.struchkov.haiti.util.captcha.text.producer.TextProducer; +import dev.struchkov.haiti.util.captcha.text.renderer.DefaultWordRenderer; +import dev.struchkov.haiti.util.captcha.text.renderer.WordRenderer; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.time.LocalDateTime; + +/** + * A builder for generating a CAPTCHA image/answer pair. + * + *

+ * Example for generating a new CAPTCHA: + *

+ *
Captcha captcha = new Captcha.Builder(200, 50)
+ * 	.addText()
+ * 	.addBackground()
+ * 	.build();
+ *

Note that the build() must always be called last. Other methods are optional, + * and can sometimes be repeated. For example:

+ *
Captcha captcha = new Captcha.Builder(200, 50)
+ * 	.addText()
+ * 	.addNoise()
+ * 	.addNoise()
+ * 	.addNoise()
+ * 	.addBackground()
+ * 	.build();
+ *

Adding multiple backgrounds has no affect; the last background added will simply be the + * one that is eventually rendered.

+ *

To validate that answerStr is a correct answer to the CAPTCHA:

+ * + * captcha.isCorrect(answerStr); + */ +public final class Captcha { + + private final String answer; + private final BufferedImage img; + private final LocalDateTime timeStamp; + + private Captcha(Builder builder) { + img = builder.img; + answer = builder.answer; + timeStamp = builder.timeStamp; + } + + public static Builder builder(int width, int height) { + return new Builder(width, height); + } + + public boolean isCorrect(String answer) { + return this.answer.equals(answer); + } + + public String getAnswer() { + return answer; + } + + /** + * @return A png captcha image. + */ + public BufferedImage getImage() { + return img; + } + + public LocalDateTime getTimeStamp() { + return timeStamp; + } + + @Override + public String toString() { + return "[Answer: " + + answer + + "][Timestamp: " + + timeStamp + + "][Image: " + + img + + "]"; + } + + public static class Builder { + + private String answer = ""; + private BufferedImage img; + private BufferedImage backGround; + private LocalDateTime timeStamp; + private boolean addBorder = false; + + public Builder(int width, int height) { + img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + /** + * Add a background using the default {@link BackgroundProducer} (a {@link TransparentBackgroundProducer}). + */ + public Builder addBackground() { + return addBackground(new TransparentBackgroundProducer()); + } + + /** + * Add a background using the given {@link BackgroundProducer}. + */ + public Builder addBackground(BackgroundProducer bgProd) { + backGround = bgProd.getBackground(img.getWidth(), img.getHeight()); + return this; + } + + /** + * Generate the answer to the CAPTCHA using the {@link DefaultTextProducer}. + */ + public Builder addText() { + return addText(new DefaultTextProducer()); + } + + /** + * Generate the answer to the CAPTCHA using the given + * {@link TextProducer}. + */ + public Builder addText(TextProducer txtProd) { + return addText(txtProd, new DefaultWordRenderer()); + } + + /** + * Generate the answer to the CAPTCHA using the default + * {@link TextProducer}, and render it to the image using the given + * {@link WordRenderer}. + */ + public Builder addText(WordRenderer wRenderer) { + return addText(new DefaultTextProducer(), wRenderer); + } + + /** + * Generate the answer to the CAPTCHA using the given + * {@link TextProducer}, and render it to the image using the given + * {@link WordRenderer}. + */ + public Builder addText(TextProducer txtProd, WordRenderer wRenderer) { + answer += txtProd.getText(); + wRenderer.render(answer, img); + return this; + } + + /** + * Add noise using the default {@link NoiseProducer} (a {@link CurvedLineNoiseProducer}). + */ + public Builder addNoise() { + return this.addNoise(new CurvedLineNoiseProducer()); + } + + /** + * Add noise using the given NoiseProducer. + */ + public Builder addNoise(NoiseProducer nProd) { + nProd.makeNoise(img); + return this; + } + + /** + * Draw a single-pixel wide black border around the image. + */ + public Builder addBorder() { + addBorder = true; + return this; + } + + /** + * Build the CAPTCHA. This method should always be called, and should always + * be called last. + * + * @return The constructed CAPTCHA. + */ + public Captcha build() { + if (backGround == null) { + backGround = new TransparentBackgroundProducer().getBackground(img.getWidth(), img.getHeight()); + } + + // Paint the main image over the background + final Graphics2D g = backGround.createGraphics(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); + g.drawImage(img, null, null); + + if (addBorder) { + int width = img.getWidth(); + int height = img.getHeight(); + + g.setColor(Color.BLACK); + g.drawLine(0, 0, 0, width); + g.drawLine(0, 0, width, 0); + g.drawLine(0, height - 1, width, height - 1); + g.drawLine(width - 1, height - 1, width - 1, 0); + } + + img = backGround; + + timeStamp = LocalDateTime.now(); + + return new Captcha(this); + } + + } +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/background/BackgroundProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/background/BackgroundProducer.java new file mode 100644 index 0000000..df0cf96 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/background/BackgroundProducer.java @@ -0,0 +1,25 @@ +package dev.struchkov.haiti.util.captcha.background; + +import java.awt.image.BufferedImage; + +/** + * Used to add a captcha background. + * + * @author upagge 10.07.2022 + */ +public interface BackgroundProducer { + + /** + * Add the background to the given image. + * + * @param image The image onto which the background will be rendered. + * @return The image with the background rendered. + */ + BufferedImage addBackground(BufferedImage image); + + /** + * Returns a gradient background. + */ + BufferedImage getBackground(int width, int height); + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/background/GradiatedBackgroundProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/background/GradiatedBackgroundProducer.java new file mode 100644 index 0000000..fc4ff2f --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/background/GradiatedBackgroundProducer.java @@ -0,0 +1,71 @@ +package dev.struchkov.haiti.util.captcha.background; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; + +import static java.awt.Color.DARK_GRAY; +import static java.awt.Color.WHITE; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; + +/** + * Creates a gradiated background with the given from and to + * Color values. If none are specified they default to light gray and white + * respectively. + */ +public class GradiatedBackgroundProducer implements BackgroundProducer { + + private Color fromColor; + private Color toColor; + + public GradiatedBackgroundProducer() { + this(DARK_GRAY, WHITE); + } + + public GradiatedBackgroundProducer(Color from, Color to) { + fromColor = from; + toColor = to; + } + + public void setFromColor(Color fromColor) { + this.fromColor = fromColor; + } + + public void setToColor(Color toColor) { + this.toColor = toColor; + } + + @Override + public BufferedImage getBackground(int width, int height) { + // create an opaque image + final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + final Graphics2D g = img.createGraphics(); + final RenderingHints hints = new RenderingHints(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + + g.setRenderingHints(hints); + + // create the gradient color + final GradientPaint ytow = new GradientPaint(0, 0, fromColor, width, height, toColor); + + g.setPaint(ytow); + // draw gradient color + g.fill(new Rectangle2D.Double(0, 0, width, height)); + + // draw the transparent image over the background + g.drawImage(img, 0, 0, null); + g.dispose(); + + return img; + } + + @Override + public BufferedImage addBackground(BufferedImage image) { + return getBackground(image.getWidth(), image.getHeight()); + } + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/background/TransparentBackgroundProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/background/TransparentBackgroundProducer.java new file mode 100644 index 0000000..faa0090 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/background/TransparentBackgroundProducer.java @@ -0,0 +1,29 @@ +package dev.struchkov.haiti.util.captcha.background; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Transparency; +import java.awt.image.BufferedImage; + +/** + * Generates a transparent background. + */ +public class TransparentBackgroundProducer implements BackgroundProducer { + + @Override + public BufferedImage addBackground(BufferedImage image) { + return getBackground(image.getWidth(), image.getHeight()); + } + + @Override + public BufferedImage getBackground(int width, int height) { + final BufferedImage bg = new BufferedImage(width, height, Transparency.TRANSLUCENT); + final Graphics2D g = bg.createGraphics(); + + g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f)); + g.fillRect(0, 0, width, height); + + return bg; + } + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/noise/CurvedLineNoiseProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/noise/CurvedLineNoiseProducer.java new file mode 100644 index 0000000..6a832e0 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/noise/CurvedLineNoiseProducer.java @@ -0,0 +1,91 @@ +package dev.struchkov.haiti.util.captcha.noise; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.CubicCurve2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.security.SecureRandom; +import java.util.Random; + +import static java.awt.geom.PathIterator.SEG_LINETO; +import static java.awt.geom.PathIterator.SEG_MOVETO; + +/** + * Adds a randomly curved line to the image. + */ +public class CurvedLineNoiseProducer implements NoiseProducer { + + private static final Random RAND = new SecureRandom(); + + private final Color color; + private final float width; + + public CurvedLineNoiseProducer() { + this(Color.BLACK, 3.0f); + } + + public CurvedLineNoiseProducer(Color color, float width) { + this.color = color; + this.width = width; + } + + @Override + public void makeNoise(BufferedImage image) { + final int imgWidth = image.getWidth(); + final int imgHeight = image.getHeight(); + + // the curve from where the points are taken + final CubicCurve2D cc = new CubicCurve2D.Float( + imgWidth * .1f, imgHeight * RAND.nextFloat(), + imgWidth * .1f, imgHeight * RAND.nextFloat(), + imgWidth * .25f, imgHeight * RAND.nextFloat(), + imgWidth * .9f, imgHeight * RAND.nextFloat() + ); + + // creates an iterator to define the boundary of the flattened curve + final PathIterator pi = cc.getPathIterator(null, 2); + final Point2D[] tmp = new Point2D[200]; + int i = 0; + + // while pi is iterating the curve, adds points to tmp array + while (!pi.isDone()) { + float[] coords = new float[6]; + int currentSegment = pi.currentSegment(coords); + if (currentSegment == SEG_MOVETO || currentSegment == SEG_LINETO) { + tmp[i] = new Point2D.Float(coords[0], coords[1]); + } + i++; + pi.next(); + } + + // the points where the line changes the stroke and direction + final Point2D[] pts = new Point2D[i]; + // copies points from tmp to pts + System.arraycopy(tmp, 0, pts, 0, i); + + final Graphics2D graph = (Graphics2D) image.getGraphics(); + graph.setRenderingHints(new RenderingHints( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON)); + + graph.setColor(color); + + // for the maximum 3 point change the stroke and direction + for (i = 0; i < pts.length - 1; i++) { + if (i < 3) { + graph.setStroke(new BasicStroke(this.width)); + } + graph.drawLine( + (int) pts[i].getX(), + (int) pts[i].getY(), + (int) pts[i + 1].getX(), + (int) pts[i + 1].getY() + ); + } + graph.dispose(); + } +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/noise/NoiseProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/noise/NoiseProducer.java new file mode 100644 index 0000000..ffac3a3 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/noise/NoiseProducer.java @@ -0,0 +1,10 @@ +package dev.struchkov.haiti.util.captcha.noise; + +import java.awt.image.BufferedImage; + +@FunctionalInterface +public interface NoiseProducer { + + void makeNoise(BufferedImage image); + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/DefaultTextProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/DefaultTextProducer.java new file mode 100644 index 0000000..ca04c93 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/DefaultTextProducer.java @@ -0,0 +1,48 @@ +package dev.struchkov.haiti.util.captcha.text.producer; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * Produces text of a given length from a given array of characters. + */ +public class DefaultTextProducer implements TextProducer { + + private static final Random RAND = new SecureRandom(); + private static final int DEFAULT_LENGTH = 5; + private static final char[] DEFAULT_CHARS = new char[]{ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'k', 'm', 'n', 'p', 'r', 'w', 'x', 'y', + '2', '3', '4', '5', '6', '7', '8' + }; + + private final int length; + private final char[] srcChars; + + public DefaultTextProducer() { + this(DEFAULT_LENGTH, DEFAULT_CHARS); + } + + public DefaultTextProducer(int length, char[] srcChars) { + this.length = length; + this.srcChars = copyOf(srcChars, srcChars.length); + } + + private static char[] copyOf(char[] original, int newLength) { + char[] copy = new char[newLength]; + System.arraycopy( + original, 0, copy, 0, + Math.min(original.length, newLength) + ); + return copy; + } + + @Override + public String getText() { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(srcChars[RAND.nextInt(srcChars.length)]); + } + return sb.toString(); + } + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/TextProducer.java b/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/TextProducer.java new file mode 100644 index 0000000..3ec29a6 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/text/producer/TextProducer.java @@ -0,0 +1,14 @@ +package dev.struchkov.haiti.util.captcha.text.producer; + +/** + * Generate an answer for the CAPTCHA. + */ +@FunctionalInterface +public interface TextProducer { + + /** + * Generate a series of characters to be used as the answer for the CAPTCHA. + */ + String getText(); + +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/DefaultWordRenderer.java b/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/DefaultWordRenderer.java new file mode 100644 index 0000000..d246f20 --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/DefaultWordRenderer.java @@ -0,0 +1,96 @@ +package dev.struchkov.haiti.util.captcha.text.renderer; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.image.BufferedImage; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_RENDERING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_RENDER_QUALITY; + +/** + * Renders the answer onto the image. + */ +public class DefaultWordRenderer implements WordRenderer { + + private static final Random RAND = new SecureRandom(); + private static final List DEFAULT_COLORS = new ArrayList<>(); + private static final List DEFAULT_FONTS = new ArrayList<>(); + + // The text will be rendered 25%/5% of the image height/width from the X and Y axes + private static final double YOFFSET = 0.25; + private static final double XOFFSET = 0.05; + + static { + DEFAULT_COLORS.add(Color.BLACK); + DEFAULT_FONTS.add(new Font("Arial", Font.BOLD, 40)); + DEFAULT_FONTS.add(new Font("Courier", Font.BOLD, 40)); + } + + private final List colors = new ArrayList<>(); + private final List fonts = new ArrayList<>(); + + /** + * Use the default color (black) and fonts (Arial and Courier). + */ + public DefaultWordRenderer() { + this(DEFAULT_COLORS, DEFAULT_FONTS); + } + + /** + * Build a WordRenderer using the given Colors and + * Fonts. + * + * @param colors + * @param fonts + */ + public DefaultWordRenderer(List colors, List fonts) { + this.colors.addAll(colors); + this.fonts.addAll(fonts); + } + + /** + * Render a word onto a BufferedImage. + * + * @param word The word to be rendered. + * @param image The BufferedImage onto which the word will be painted. + */ + @Override + public void render(final String word, BufferedImage image) { + final Graphics2D g = image.createGraphics(); + + final RenderingHints hints = new RenderingHints(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + hints.add(new RenderingHints(KEY_RENDERING, VALUE_RENDER_QUALITY)); + g.setRenderingHints(hints); + + final FontRenderContext frc = g.getFontRenderContext(); + int xBaseline = (int) Math.round(image.getWidth() * XOFFSET); + final int yBaseline = image.getHeight() - (int) Math.round(image.getHeight() * YOFFSET); + + final char[] chars = new char[1]; + for (char c : word.toCharArray()) { + chars[0] = c; + + g.setColor(colors.get(RAND.nextInt(colors.size()))); + + final int choiceFont = RAND.nextInt(fonts.size()); + Font font = fonts.get(choiceFont); + g.setFont(font); + + GlyphVector gv = font.createGlyphVector(frc, chars); + g.drawChars(chars, 0, chars.length, xBaseline, yBaseline); + + final int width = (int) gv.getVisualBounds().getWidth(); + xBaseline = xBaseline + width; + } + } +} diff --git a/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/WordRenderer.java b/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/WordRenderer.java new file mode 100644 index 0000000..d0d97cf --- /dev/null +++ b/src/main/java/dev/struchkov/haiti/util/captcha/text/renderer/WordRenderer.java @@ -0,0 +1,19 @@ +package dev.struchkov.haiti.util.captcha.text.renderer; + +import java.awt.image.BufferedImage; + +/** + * Render the answer for the CAPTCHA onto the image. + */ +@FunctionalInterface +public interface WordRenderer { + + /** + * Render a word to a BufferedImage. + * + * @param word The sequence of characters to be rendered. + * @param image The image onto which the word will be rendered. + */ + void render(String word, BufferedImage image); + +}