Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Respect size param #153

Merged
merged 22 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/main/java/lc/captchas/FontFunCaptcha.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ private Font loadCustomFont(String level, String path) {
return null;
}

private byte[] fontFun(String captchaText, String level, String path) {
private byte[] fontFun(final int width, final int height, String captchaText, String level, String path) {
String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"};
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path);
Expand All @@ -81,10 +81,13 @@ private byte[] fontFun(String captchaText, String level, String path) {
return baos.toByteArray();
}

public Challenge returnChallenge() {
public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(7);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
String path = "./lib/fonts/";
return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase());
return new Challenge(fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
}

public boolean checkAnswer(String secret, String answer) {
Expand Down
37 changes: 21 additions & 16 deletions src/main/java/lc/captchas/PoppingCharactersCaptcha.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import java.util.LinkedList;
import java.util.List;

import javax.imageio.stream.MemoryCacheImageOutputStream;
Expand All @@ -20,28 +19,26 @@
import lc.misc.GifSequenceWriter;

public class PoppingCharactersCaptcha implements ChallengeProvider {
private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
private final int width = 250;
private final int height = 100;

private Integer[] computeOffsets(final String text) {
private int[] computeOffsets(final Font font, final int width, final int height, final String text) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics();
final var frc = graphics2D.getFontRenderContext();
final var advances = new LinkedList<Integer>();
final var advances = new int[text.length() + 1];
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
var currX = 0;
for (int i = 0; i < text.length(); i++) {
final var c = text.charAt(i);
advances.add(currX);
advances[i] = currX;
currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
currX += spacing;
};
advances[text.length()] = currX;
graphics2D.dispose();
return advances.toArray(new Integer[]{});
return advances;
}

private BufferedImage makeImage(final Consumer<Graphics2D> f) {
private BufferedImage makeImage(final Font font, final int width, final int height, final Consumer<Graphics2D> f) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Expand All @@ -56,23 +53,28 @@ private int jitter() {
return HelperFunctions.randomNumber(-2, +2);
}

private byte[] gifCaptcha(final String text) {
private byte[] gifCaptcha(final int width, final int height, final String text) {
try {
final var fontHeight = (int) (height * 0.5);
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
final var byteArrayOutputStream = new ByteArrayOutputStream();
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
final var writer = new GifSequenceWriter(output, 1, 900, true);
final var advances = computeOffsets(text);
final var advances = computeOffsets(font, width, height, text);
final var expectedWidth = advances[advances.length - 1];
final var scale = width / (float) expectedWidth;
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
IntStream.range(0, text.length()).forEach(i -> {
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f);
final var nextImage = makeImage((g) -> {
final var nextImage = makeImage(font, width, height, (g) -> {
g.scale(scale, 1);
if (i > 0) {
final var prevI = (i - 1) % text.length();
g.setColor(prevColor);
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter());
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), fontHeight*1.1f + jitter());
}
g.setColor(color);
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter());
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), fontHeight*1.1f + jitter());
});
try {
writer.writeToSequence(nextImage);
Expand Down Expand Up @@ -100,9 +102,12 @@ public Map<String, List<String>> supportedParameters() {
"supportedInputType", List.of("text"));
}

public Challenge returnChallenge() {
public Challenge returnChallenge(String level, String size) {
final var secret = HelperFunctions.randomString(6);
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase());
}

public boolean checkAnswer(String secret, String answer) {
Expand Down
48 changes: 28 additions & 20 deletions src/main/java/lc/captchas/ShadowTextCaptcha.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.awt.RenderingHints;
import java.awt.Color;
import java.awt.Font;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
Expand Down Expand Up @@ -38,32 +37,38 @@ public boolean checkAnswer(String secret, String answer) {
return answer.toLowerCase().equals(secret);
}

private byte[] shadowText(String text) {
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
Graphics2D graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
private float[] makeKernel(int size) {
final int N = size * size;
final float weight = 1.0f / (N);
final float[] kernel = new float[N];
java.util.Arrays.fill(kernel, weight);
return kernel;
};

TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext());
private byte[] shadowText(final int width, final int height, String text) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final int fontHeight = (int) (height * 0.5f);
Font font = new Font("Arial", Font.PLAIN, fontHeight);
Graphics2D graphics2D = img.createGraphics();
HelperFunctions.setRenderingHints(graphics2D);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, 350, 100);
graphics2D.fillRect(0, 0, width, height);
graphics2D.setPaint(Color.BLACK);
textLayout.draw(graphics2D, 15, 50);
graphics2D.setFont(font);
final var stringWidth = graphics2D.getFontMetrics().stringWidth(text);
final var scaleX = (stringWidth > width) ? width/((double) stringWidth) : 1d;
graphics2D.scale(scaleX, 1d);
graphics2D.drawString(text, 0, fontHeight*1.1f);
graphics2D.dispose();
float[] kernel = {
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f
};
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
ConvolveOp op = new ConvolveOp(new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)), ConvolveOp.EDGE_NO_OP, null);
BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d);
g2d.setPaint(Color.WHITE);
textLayout.draw(g2d, 13, 50);
g2d.scale(scaleX, 1d);
g2d.setFont(font);
g2d.drawString(text, -kernelSize, fontHeight*1.1f);
g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
Expand All @@ -74,8 +79,11 @@ private byte[] shadowText(String text) {
return baos.toByteArray();
}

public Challenge returnChallenge() {
public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(6);
return new Challenge(shadowText(secret), "image/png", secret.toLowerCase());
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public interface ChallengeProvider {
public String getId();

public Challenge returnChallenge();
public Challenge returnChallenge(String level, String size);

public boolean checkAnswer(String secret, String answer);

Expand Down
10 changes: 9 additions & 1 deletion src/main/java/lc/misc/HelperFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ public static synchronized void setSeed(long seed) {
random.setSeed(seed);
}

public static int[] parseSize2D(final String size) {
final String[] fields = size.split("x");
final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])};
return result;
}

public static void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(
Expand All @@ -23,7 +30,8 @@ public static void setRenderingHints(Graphics2D g2d) {
public static final String safeNumbers = "23456789";
public static final String allNumbers = safeNumbers + "10";
public static final String specialCharacters = "$#%@&?";
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
public static final String safeAlphaNum = safeAlphabets + safeNumbers;
public static final String safeCharacters = safeAlphaNum + specialCharacters;

public static String randomString(final int n) {
return randomString(n, safeCharacters);
Expand Down
13 changes: 9 additions & 4 deletions src/main/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@
const levelInput = document.getElementById("levelInput").value
const mediaInput = document.getElementById("mediaInput").value
const typeInput = document.getElementById("typeInput").value
fetch("/v1/captcha", {
const sizeInput = document.getElementById("sizeInput").value
fetch("/v2/captcha", {
method: 'POST',
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput})
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
}).then(async function(resp) {
const respJson = await resp.json()
if (resp.ok) {
const id = respJson.id
const resultDiv = document.getElementById("result")
const result = `
<p>Id: ${id}</p>
<p><img src="/v1/media?id=${id}" /> </p>
<p><img src="/v2/media?id=${id}" /> </p>
<input type="text" id="answerInput" />
<button onClick="submitAnswer('${id}')">Submit</button>
<div id="answerResult" />
Expand All @@ -43,7 +44,7 @@
}
async function submitAnswer(id) {
const ans = document.getElementById("answerInput").value;
const resp = await fetch("/v1/answer", {
const resp = await fetch("/v2/answer", {
method: 'POST',
body: JSON.stringify({id: id, answer: ans})
})
Expand All @@ -70,6 +71,10 @@
<span>Input Type</span>
<input type="text" id="typeInput" value="text" />
</div>
<div class="inputGroup">
<span>Input Size</span>
<input type="text" id="sizeInput" value="150x100" />
</div>
<div class="inputGroup">
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/main/scala/lc/background/taskThread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
(config.captchaConfig).flatMap { captcha =>
(captcha.allowedLevels).flatMap { level =>
(captcha.allowedMedia).flatMap { media =>
(captcha.allowedInputType).map { inputType =>
Parameters(level, media, inputType, Some(Size(0, 0)))
(captcha.allowedInputType).flatMap { inputType =>
(captcha.allowedSizes).map {size =>
Parameters(level, media, inputType, size)
}
}
}
}
Expand All @@ -58,8 +60,9 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
val level = pickRandom(captcha.allowedLevels)
val media = pickRandom(captcha.allowedMedia)
val inputType = pickRandom(captcha.allowedInputType)
val size = pickRandom(captcha.allowedSizes)

Parameters(level, media, inputType, Some(Size(0, 0)))
Parameters(level, media, inputType, size)
}

private def pickRandom[T](list: List[T]): T = {
Expand Down
13 changes: 8 additions & 5 deletions src/main/scala/lc/captchas/DebugCaptcha.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
matches
}

private def simpleText(text: String): Array[Byte] = {
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB)
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
val graphics2D = img.createGraphics()
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
HelperFunctions.setRenderingHints(graphics2D)
graphics2D.setPaint(Color.WHITE)
graphics2D.fillRect(0, 0, 350, 100)
graphics2D.fillRect(0, 0, width, height)
graphics2D.setPaint(Color.BLACK)
textLayout.draw(graphics2D, 15, 50)
graphics2D.dispose()
Expand All @@ -66,8 +66,11 @@ class DebugCaptcha extends ChallengeProvider {
baos.toByteArray()
}

def returnChallenge(): Challenge = {
def returnChallenge(level: String, size: String): Challenge = {
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
}
}
Loading