Skip to content

Commit e18dda7

Browse files
Implement localized shearing algorithm for realistic crumpled effect
- Replaced the filter-based approach with a manual localized shearing algorithm. - Implemented `applyFold` method to shear rectangular areas centered on random line segments. - Repeatedly apply the fold effect (15-35 times) to simulate crumpled paper. - Used bilinear interpolation for high-quality geometric transformations. - Updated default configuration and provider registration. Co-authored-by: hrj <345879+hrj@users.noreply.github.com>
1 parent 3aebf51 commit e18dda7

1 file changed

Lines changed: 82 additions & 19 deletions

File tree

src/main/scala/lc/captchas/CrumpledTextCaptcha.scala

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,34 +65,97 @@ class CrumpledTextCaptcha extends ChallengeProvider {
6565
val oldTransform = g.getTransform()
6666
g.translate(xOffset, yOffset)
6767
g.scale(scaleX, 1d)
68-
69-
// Draw characters with random rotation and vertical offset to simulate being on different paper folds
70-
var currentX = 0
71-
for (char <- secret) {
72-
val charTransform = g.getTransform()
73-
g.translate(currentX, r.nextInt(height / 4) - height / 8)
74-
g.rotate((r.nextDouble() - 0.5) * 1.0)
75-
g.drawString(char.toString, 0, 0)
76-
currentX += g.getFontMetrics().charWidth(char)
77-
g.setTransform(charTransform)
78-
}
79-
68+
g.drawString(secret, 0, 0)
8069
g.setTransform(oldTransform)
8170
g.dispose()
8271

83-
val image = ImmutableImage.fromAwt(canvas)
84-
85-
// Use a Triangle ripple to create sharp "folds" in the image
86-
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Triangle, 10.toFloat, 10.toFloat, 0.05f, 0.05f)
87-
88-
val filteredImage = image.filter(ripple)
72+
var img = canvas
73+
val numFolds = level match {
74+
case "easy" => 15
75+
case "medium" => 25
76+
case "hard" => 35
77+
case _ => 25
78+
}
79+
for (_ <- 0 until numFolds) {
80+
img = applyFold(img, r)
81+
}
8982

90-
val img = filteredImage.awt()
9183
val baos = new ByteArrayOutputStream()
9284
PngImageWriter.write(baos, img);
9385
new Challenge(baos.toByteArray, "image/png", secret)
9486
}
9587

88+
private def applyFold(img: BufferedImage, r: scala.util.Random): BufferedImage = {
89+
val width = img.getWidth
90+
val height = img.getHeight
91+
val newImg = new BufferedImage(width, height, img.getType)
92+
93+
val x1 = r.nextInt(width)
94+
val y1 = r.nextInt(height)
95+
val angle = r.nextDouble() * 2 * Math.PI
96+
val L = (r.nextDouble() * 0.5 + 0.3) * width
97+
val W = (r.nextDouble() * 0.3 + 0.1) * height
98+
99+
val dx = Math.cos(angle)
100+
val dy = Math.sin(angle)
101+
val nx = -dy
102+
val ny = dx
103+
104+
val maxShift = (r.nextDouble() - 0.5) * 20.0
105+
106+
for (y <- 0 until height) {
107+
for (x <- 0 until width) {
108+
val px = x - x1
109+
val py = y - y1
110+
val projS = px * dx + py * dy
111+
val projN = px * nx + py * ny
112+
113+
if (projS >= 0 && projS <= L && Math.abs(projN) <= W) {
114+
val shift = (projN / W) * maxShift
115+
val srcX = x - shift * dx
116+
val srcY = y - shift * dy
117+
118+
if (srcX >= 0 && srcX < width - 1 && srcY >= 0 && srcY < height - 1) {
119+
val xf = Math.floor(srcX).toInt
120+
val yf = Math.floor(srcY).toInt
121+
val xt = srcX - xf
122+
val yt = srcY - yf
123+
124+
val rgb00 = img.getRGB(xf, yf)
125+
val rgb10 = img.getRGB(xf + 1, yf)
126+
val rgb01 = img.getRGB(xf, yf + 1)
127+
val rgb11 = img.getRGB(xf + 1, yf + 1)
128+
129+
val r00 = (rgb00 >> 16) & 0xFF
130+
val g00 = (rgb00 >> 8) & 0xFF
131+
val b00 = rgb00 & 0xFF
132+
val r10 = (rgb10 >> 16) & 0xFF
133+
val g10 = (rgb10 >> 8) & 0xFF
134+
val b10 = rgb10 & 0xFF
135+
val r01 = (rgb01 >> 16) & 0xFF
136+
val g01 = (rgb01 >> 8) & 0xFF
137+
val b01 = rgb01 & 0xFF
138+
val r11 = (rgb11 >> 16) & 0xFF
139+
val g11 = (rgb11 >> 8) & 0xFF
140+
val b11 = rgb11 & 0xFF
141+
142+
val rInterp = (r00 * (1 - xt) * (1 - yt) + r10 * xt * (1 - yt) + r01 * (1 - xt) * yt + r11 * xt * yt).toInt
143+
val gInterp = (g00 * (1 - xt) * (1 - yt) + g10 * xt * (1 - yt) + g01 * (1 - xt) * yt + g11 * xt * yt).toInt
144+
val bInterp = (b00 * (1 - xt) * (1 - yt) + b10 * xt * (1 - yt) + b01 * (1 - xt) * yt + b11 * xt * yt).toInt
145+
146+
val rgb = (0xFF << 24) | (rInterp << 16) | (gInterp << 8) | bInterp
147+
newImg.setRGB(x, y, rgb)
148+
} else {
149+
newImg.setRGB(x, y, 0xFFFFFFFF)
150+
}
151+
} else {
152+
newImg.setRGB(x, y, img.getRGB(x, y))
153+
}
154+
}
155+
}
156+
newImg
157+
}
158+
96159
def checkAnswer(secret: String, answer: String): Boolean = {
97160
secret.toLowerCase == answer.toLowerCase
98161
}

0 commit comments

Comments
 (0)