├── .gitignore ├── src ├── test │ ├── resources │ │ ├── 0-alex.wav │ │ ├── hello.mp3 │ │ ├── hello.wav │ │ ├── input.png │ │ ├── output.png │ │ └── log4j2.properties │ └── java │ │ └── net │ │ └── logicsquad │ │ └── nanocaptcha │ │ ├── content │ │ ├── FiveLetterFirstNameContentProducerTest.java │ │ ├── ChineseContentProducerTest.java │ │ └── NumbersContentProducerTest.java │ │ ├── audio │ │ ├── producer │ │ │ └── RandomNumberVoiceProducerTest.java │ │ └── SampleTest.java │ │ └── image │ │ └── filter │ │ └── RippleImageFilterTest.java ├── main │ ├── resources │ │ ├── fonts │ │ │ ├── PublicSans-Bold.ttf │ │ │ └── CourierPrime-Bold.ttf │ │ └── sounds │ │ │ ├── de │ │ │ └── numbers │ │ │ │ ├── 0_a.wav │ │ │ │ ├── 0_b.wav │ │ │ │ ├── 1_a.wav │ │ │ │ ├── 1_b.wav │ │ │ │ ├── 2_a.wav │ │ │ │ ├── 2_b.wav │ │ │ │ ├── 3_a.wav │ │ │ │ ├── 3_b.wav │ │ │ │ ├── 4_a.wav │ │ │ │ ├── 4_b.wav │ │ │ │ ├── 5_a.wav │ │ │ │ ├── 5_b.wav │ │ │ │ ├── 6_a.wav │ │ │ │ ├── 6_b.wav │ │ │ │ ├── 7_a.wav │ │ │ │ ├── 7_b.wav │ │ │ │ ├── 8_a.wav │ │ │ │ ├── 8_b.wav │ │ │ │ ├── 9_a.wav │ │ │ │ └── 9_b.wav │ │ │ ├── en │ │ │ └── numbers │ │ │ │ ├── 0_a.wav │ │ │ │ ├── 0_b.wav │ │ │ │ ├── 0_c.wav │ │ │ │ ├── 0_d.wav │ │ │ │ ├── 0_e.wav │ │ │ │ ├── 0_f.wav │ │ │ │ ├── 0_g.wav │ │ │ │ ├── 1_a.wav │ │ │ │ ├── 1_b.wav │ │ │ │ ├── 1_c.wav │ │ │ │ ├── 1_d.wav │ │ │ │ ├── 1_e.wav │ │ │ │ ├── 1_f.wav │ │ │ │ ├── 1_g.wav │ │ │ │ ├── 2_a.wav │ │ │ │ ├── 2_b.wav │ │ │ │ ├── 2_c.wav │ │ │ │ ├── 2_d.wav │ │ │ │ ├── 2_e.wav │ │ │ │ ├── 2_f.wav │ │ │ │ ├── 2_g.wav │ │ │ │ ├── 3_a.wav │ │ │ │ ├── 3_b.wav │ │ │ │ ├── 3_c.wav │ │ │ │ ├── 3_d.wav │ │ │ │ ├── 3_e.wav │ │ │ │ ├── 3_f.wav │ │ │ │ ├── 3_g.wav │ │ │ │ ├── 4_a.wav │ │ │ │ ├── 4_b.wav │ │ │ │ ├── 4_c.wav │ │ │ │ ├── 4_d.wav │ │ │ │ ├── 4_e.wav │ │ │ │ ├── 4_f.wav │ │ │ │ ├── 4_g.wav │ │ │ │ ├── 5_a.wav │ │ │ │ ├── 5_b.wav │ │ │ │ ├── 5_c.wav │ │ │ │ ├── 5_d.wav │ │ │ │ ├── 5_e.wav │ │ │ │ ├── 5_f.wav │ │ │ │ ├── 5_g.wav │ │ │ │ ├── 6_a.wav │ │ │ │ ├── 6_b.wav │ │ │ │ ├── 6_c.wav │ │ │ │ ├── 6_d.wav │ │ │ │ ├── 6_e.wav │ │ │ │ ├── 6_f.wav │ │ │ │ ├── 6_g.wav │ │ │ │ ├── 7_a.wav │ │ │ │ ├── 7_b.wav │ │ │ │ ├── 7_c.wav │ │ │ │ ├── 7_d.wav │ │ │ │ ├── 7_e.wav │ │ │ │ ├── 7_f.wav │ │ │ │ ├── 7_g.wav │ │ │ │ ├── 8_a.wav │ │ │ │ ├── 8_b.wav │ │ │ │ ├── 8_c.wav │ │ │ │ ├── 8_d.wav │ │ │ │ ├── 8_e.wav │ │ │ │ ├── 8_f.wav │ │ │ │ ├── 8_g.wav │ │ │ │ ├── 9_a.wav │ │ │ │ ├── 9_b.wav │ │ │ │ ├── 9_c.wav │ │ │ │ ├── 9_d.wav │ │ │ │ ├── 9_e.wav │ │ │ │ ├── 9_f.wav │ │ │ │ └── 9_g.wav │ │ │ ├── fr │ │ │ └── numbers │ │ │ │ ├── 0_a.wav │ │ │ │ ├── 0_b.wav │ │ │ │ ├── 1_a.wav │ │ │ │ ├── 1_b.wav │ │ │ │ ├── 2_a.wav │ │ │ │ ├── 2_b.wav │ │ │ │ ├── 3_a.wav │ │ │ │ ├── 3_b.wav │ │ │ │ ├── 4_a.wav │ │ │ │ ├── 4_b.wav │ │ │ │ ├── 5_a.wav │ │ │ │ ├── 5_b.wav │ │ │ │ ├── 6_a.wav │ │ │ │ ├── 6_b.wav │ │ │ │ ├── 7_a.wav │ │ │ │ ├── 7_b.wav │ │ │ │ ├── 8_a.wav │ │ │ │ ├── 8_b.wav │ │ │ │ ├── 9_a.wav │ │ │ │ └── 9_b.wav │ │ │ └── noises │ │ │ ├── swimming.wav │ │ │ ├── zombie.wav │ │ │ ├── restaurant.wav │ │ │ └── radio_tuning.wav │ └── java │ │ └── net │ │ └── logicsquad │ │ └── nanocaptcha │ │ ├── audio │ │ ├── package-info.java │ │ ├── noise │ │ │ ├── package-info.java │ │ │ ├── NoiseProducer.java │ │ │ └── RandomNoiseProducer.java │ │ ├── producer │ │ │ ├── package-info.java │ │ │ ├── VoiceProducer.java │ │ │ └── RandomNumberVoiceProducer.java │ │ ├── Mixer.java │ │ ├── AudioCaptcha.java │ │ └── Sample.java │ │ ├── image │ │ ├── package-info.java │ │ ├── noise │ │ │ ├── package-info.java │ │ │ ├── NoiseProducer.java │ │ │ ├── SaltAndPepperNoiseProducer.java │ │ │ ├── StraightLineNoiseProducer.java │ │ │ ├── GaussianNoiseProducer.java │ │ │ └── CurvedLineNoiseProducer.java │ │ ├── filter │ │ │ ├── package-info.java │ │ │ ├── ImageFilter.java │ │ │ ├── StretchImageFilter.java │ │ │ ├── ShearImageFilter.java │ │ │ ├── FishEyeImageFilter.java │ │ │ └── RippleImageFilter.java │ │ ├── backgrounds │ │ │ ├── package-info.java │ │ │ ├── BackgroundProducer.java │ │ │ ├── TransparentBackgroundProducer.java │ │ │ ├── SquigglesBackgroundProducer.java │ │ │ ├── FlatColorBackgroundProducer.java │ │ │ └── GradiatedBackgroundProducer.java │ │ ├── renderer │ │ │ ├── package-info.java │ │ │ ├── WordRenderer.java │ │ │ ├── DefaultWordRenderer.java │ │ │ ├── FastWordRenderer.java │ │ │ └── AbstractWordRenderer.java │ │ └── ImageCaptcha.java │ │ ├── package-info.java │ │ ├── content │ │ ├── package-info.java │ │ ├── ContentProducer.java │ │ ├── LatinContentProducer.java │ │ ├── NumbersContentProducer.java │ │ ├── ArabicContentProducer.java │ │ ├── ChineseContentProducer.java │ │ └── AbstractContentProducer.java │ │ └── Builder.java └── site │ └── site.xml ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── README.md ├── CHANGELOG.md ├── pom.xml └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | .classpath 3 | .project 4 | .DS_Store 5 | bin 6 | build 7 | target 8 | *~ 9 | -------------------------------------------------------------------------------- /src/test/resources/0-alex.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/test/resources/0-alex.wav -------------------------------------------------------------------------------- /src/test/resources/hello.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/test/resources/hello.mp3 -------------------------------------------------------------------------------- /src/test/resources/hello.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/test/resources/hello.wav -------------------------------------------------------------------------------- /src/test/resources/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/test/resources/input.png -------------------------------------------------------------------------------- /src/test/resources/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/test/resources/output.png -------------------------------------------------------------------------------- /src/main/resources/fonts/PublicSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/fonts/PublicSans-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/0_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/0_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/0_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/0_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/1_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/1_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/1_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/1_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/2_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/2_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/2_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/2_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/3_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/3_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/3_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/3_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/4_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/4_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/4_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/4_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/5_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/5_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/5_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/5_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/6_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/6_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/6_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/6_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/7_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/7_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/7_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/7_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/8_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/8_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/8_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/8_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/9_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/9_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/de/numbers/9_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/de/numbers/9_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/0_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/0_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/1_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/1_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/2_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/2_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/3_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/3_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/4_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/4_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/5_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/5_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/6_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/6_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/7_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/7_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/8_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/8_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_c.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_d.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_e.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_f.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/en/numbers/9_g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/en/numbers/9_g.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/0_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/0_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/0_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/0_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/1_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/1_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/1_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/1_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/2_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/2_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/2_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/2_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/3_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/3_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/3_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/3_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/4_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/4_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/4_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/4_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/5_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/5_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/5_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/5_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/6_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/6_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/6_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/6_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/7_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/7_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/7_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/7_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/8_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/8_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/8_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/8_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/9_a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/9_a.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/fr/numbers/9_b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/fr/numbers/9_b.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/noises/swimming.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/noises/swimming.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/noises/zombie.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/noises/zombie.wav -------------------------------------------------------------------------------- /src/main/resources/fonts/CourierPrime-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/fonts/CourierPrime-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/sounds/noises/restaurant.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/noises/restaurant.wav -------------------------------------------------------------------------------- /src/main/resources/sounds/noises/radio_tuning.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsquad/nanocaptcha/HEAD/src/main/resources/sounds/noises/radio_tuning.wav -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides audio CAPTCHA generation. 3 | */ 4 | package net.logicsquad.nanocaptcha.audio; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides text CAPTCHA generation. 3 | */ 4 | package net.logicsquad.nanocaptcha.image; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides text and audio CAPTCHA generation. 3 | */ 4 | package net.logicsquad.nanocaptcha; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/noise/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides noise for image CAPTCHAs. 3 | */ 4 | package net.logicsquad.nanocaptcha.image.noise; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides generators for CAPTCHA challenge content. 3 | */ 4 | package net.logicsquad.nanocaptcha.content; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/filter/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides filters for image CAPTCHAs. 3 | */ 4 | package net.logicsquad.nanocaptcha.image.filter; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides backgrounds for image CAPTCHAs. 3 | */ 4 | package net.logicsquad.nanocaptcha.image.backgrounds; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/noise/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides producers of background noise for audio samples. 3 | */ 4 | package net.logicsquad.nanocaptcha.audio.noise; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/renderer/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides renderers for components of image CAPTCHAs. 3 | */ 4 | package net.logicsquad.nanocaptcha.image.renderer; 5 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/producer/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides voice producers for components of audio CAPTCHAs. 3 | */ 4 | package net.logicsquad.nanocaptcha.audio.producer; 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Set up JDK 1.8 10 | uses: actions/setup-java@v1 11 | with: 12 | java-version: 1.8 13 | - name: Build with Maven 14 | run: mvn --batch-mode --update-snapshots verify 15 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/Builder.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha; 2 | 3 | /** 4 | * Generic object builder. 5 | * 6 | * @author Paul Hoadley 7 | * @param concrete class 8 | * @since 1.4 9 | */ 10 | public interface Builder { 11 | /** 12 | * Builds and returns an object of type {@code T} based on the state of this object. 13 | * 14 | * @return object of type {@code T} 15 | */ 16 | T build(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/noise/NoiseProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.noise; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * An object that can add noise to an image. 7 | * 8 | * @author James Childers 9 | * @since 1.0 10 | */ 11 | public interface NoiseProducer { 12 | /** 13 | * Adds noise to {@code image}. 14 | * 15 | * @param image a {@link BufferedImage} 16 | */ 17 | void makeNoise(BufferedImage image); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | status = error 2 | name = PropertiesConfig 3 | 4 | filters = threshold 5 | 6 | filter.threshold.type = ThresholdFilter 7 | filter.threshold.level = debug 8 | 9 | appenders = console 10 | 11 | appender.console.type = Console 12 | appender.console.name = STDOUT 13 | appender.console.layout.type = PatternLayout 14 | appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n 15 | 16 | rootLogger.level = debug 17 | rootLogger.appenderRefs = stdout 18 | rootLogger.appenderRef.stdout.ref = STDOUT 19 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/ContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | /** 4 | * Object that can generate text content for a CAPTCHA. 5 | * 6 | * @author James Childers 7 | * @author Paul Hoadley 8 | * @since 1.0 9 | */ 10 | public interface ContentProducer { 11 | /** 12 | * Returns a string of characters to be used as the answer for the CAPTCHA. 13 | * 14 | * @return CAPTCHA content 15 | */ 16 | String getContent(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/producer/VoiceProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.audio.producer; 2 | 3 | import net.logicsquad.nanocaptcha.audio.Sample; 4 | 5 | /** 6 | * An object that can produce vocalized components of audio CAPTCHAs. 7 | * 8 | * @author James Childers 9 | * @author Paul Hoadley 10 | * @since 1.0 11 | */ 12 | public interface VoiceProducer { 13 | /** 14 | * Generates a vocalization for a single character. 15 | * 16 | * @param letter character to vocalize 17 | * @return a {@link Sample} containing the vocalization 18 | */ 19 | Sample getVocalization(char letter); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/renderer/WordRenderer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.renderer; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * Renders the content for the CAPTCHA onto the image. 7 | * 8 | * @author James Childers 9 | * @author Paul Hoadley 10 | * @since 1.0 11 | */ 12 | public interface WordRenderer { 13 | /** 14 | * Renders {@code word} to a {@link BufferedImage}. 15 | * 16 | * @param word string to be rendered 17 | * @param image image onto which the word will be rendered 18 | */ 19 | void render(String word, BufferedImage image); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/noise/NoiseProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.audio.noise; 2 | 3 | import java.util.List; 4 | 5 | import net.logicsquad.nanocaptcha.audio.Sample; 6 | 7 | /** 8 | * An object that can add background noise to {@link Sample}s. 9 | * 10 | * @author James Childers 11 | * @author Paul Hoadley 12 | * @since 1.0 13 | */ 14 | public interface NoiseProducer { 15 | /** 16 | * Concatenates {@code samples}, adds background noise and returns the result. 17 | * 18 | * @param samples a list of {@link Sample}s 19 | * @return concatenated {@link Sample}s with added noise 20 | */ 21 | Sample addNoise(List samples); 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/content/FiveLetterFirstNameContentProducerTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | /** 6 | * Unit tests on {@link FiveLetterFirstNameContentProducer} class. 7 | * 8 | * @author Paul Hoadley 9 | * @since 1.0 10 | */ 11 | public class FiveLetterFirstNameContentProducerTest { 12 | private ContentProducer producer = new FiveLetterFirstNameContentProducer(); 13 | 14 | // All we're doing here is making sure getContent() probably doesn't make an 15 | // index calculation error. 16 | @Test 17 | public void getContentShouldSucceed() { 18 | for (int i = 0; i < 200_000; i++) { 19 | producer.getContent(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/BackgroundProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.backgrounds; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * An object that can produce a background image. 7 | * 8 | * @author James Childers 9 | * @author Paul Hoadley 10 | * @since 1.0 11 | */ 12 | public interface BackgroundProducer { 13 | /** 14 | * Returns a background image that can be added to a CAPTCHA image. 15 | * 16 | * @param width required width 17 | * @param height required height 18 | * @return background image of {@code width} x {@code height} pixels 19 | */ 20 | BufferedImage getBackground(int width, int height); 21 | } 22 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | org.apache.maven.skins 8 | maven-fluido-skin 9 | 1.5 10 | 11 | 12 | ${project.name} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/TransparentBackgroundProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.backgrounds; 2 | 3 | import java.awt.AlphaComposite; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * Generates a transparent background. 9 | * 10 | * @author James Childers 11 | * @author Paul Hoadley 12 | * @since 1.0 13 | */ 14 | public class TransparentBackgroundProducer implements BackgroundProducer { 15 | @Override 16 | public BufferedImage getBackground(int width, int height) { 17 | BufferedImage bg = new BufferedImage(width, height, BufferedImage.TRANSLUCENT); 18 | Graphics2D g = bg.createGraphics(); 19 | 20 | g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f)); 21 | g.fillRect(0, 0, width, height); 22 | 23 | return bg; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/content/ChineseContentProducerTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * Unit tests on {@link ChineseContentProducer} class. 9 | * 10 | * @author Paul Hoadley 11 | * @since 1.1 12 | */ 13 | public class ChineseContentProducerTest { 14 | // All we're doing here is checking that the static char array is initialized as 15 | // expected. 16 | @Test 17 | public void confirmCharactersArrayIsLoadedAsExpected() { 18 | assertEquals(ChineseContentProducer.CODE_POINT_END - ChineseContentProducer.CODE_POINT_START, 19 | ChineseContentProducer.CHARS.length); 20 | for (int i = 0; i < ChineseContentProducer.CHARS.length; i++) { 21 | assertEquals((char) (ChineseContentProducer.CODE_POINT_START + i), ChineseContentProducer.CHARS[i]); 22 | } 23 | return; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/LatinContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | /** 4 | * Generates random strings using a subset of the Latin alphabet. 5 | * 6 | * @author Paul Hoadley 7 | * @since 1.0 8 | */ 9 | public class LatinContentProducer extends AbstractContentProducer { 10 | /** 11 | * Source characters 12 | */ 13 | private static final char[] DEFAULT_CHARS = { 'a', 'b', 'c', 'd', 14 | 'e', 'f', 'g', 'h', 'k', 'm', 'n', 'p', 'r', 'w', 'x', 'y', 15 | '2', '3', '4', '5', '6', '7', '8', }; 16 | 17 | /** 18 | * Constructor for object returning content of default length. 19 | */ 20 | public LatinContentProducer() { 21 | this(DEFAULT_LENGTH); 22 | } 23 | 24 | /** 25 | * Constructor taking a length specifier. 26 | * 27 | * @param length content length 28 | */ 29 | public LatinContentProducer(int length) { 30 | super(length, DEFAULT_CHARS); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/NumbersContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | /** 4 | * {@link ContentProducer} implementation that will return a series of numbers. 5 | * 6 | * @author James Childers 7 | * @author Paul Hoadley 8 | * @since 1.0 9 | */ 10 | public class NumbersContentProducer extends AbstractContentProducer { 11 | /** 12 | * Character set 13 | */ 14 | private static final char[] NUMBERS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; 15 | 16 | /** 17 | * Constructor for object returning content of default length. 18 | */ 19 | public NumbersContentProducer() { 20 | this(DEFAULT_LENGTH); 21 | } 22 | 23 | /** 24 | * Constructor taking a {@code length} specifier. 25 | * 26 | * @param length length of returned content strings 27 | * @throws IllegalArgumentException if {@code length} is not positive 28 | */ 29 | public NumbersContentProducer(int length) { 30 | super(length, NUMBERS); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up JDK 1.8 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 1.8 17 | 18 | - name: Import GPG Owner Trust 19 | run: echo ${{secrets.GPG_OWNERTRUST}} | base64 --decode | gpg --import-ownertrust 20 | 21 | - name: Import GPG key 22 | run: echo ${{secrets.GPG_SECRET_KEYS}} | base64 --decode | gpg --import --no-tty --batch --yes 23 | 24 | - name: Clean settings.xml 25 | run: rm -rf ~/.m2/settings.xml 26 | 27 | - name: Create settings.xml 28 | uses: s4u/maven-settings-action@v1 29 | with: 30 | servers: '[{"id": "ossrh", "username": "${{secrets.OSSRH_USERNAME}}", "password": "${{secrets.OSSRH_PASSWORD}}"}]' 31 | 32 | - name: Build and deploy to Maven Central 33 | run: mvn --batch-mode --update-snapshots clean deploy -Prelease -Dgpg.keyname=${{secrets.GPG_KEYNAME}} -Dgpg.passphrase=${{secrets.GPG_PASSPHRASE}} 34 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/ArabicContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | /** 4 | * Generates random strings using a subset of the Arabic alphabet. 5 | * 6 | * @author James Childers 7 | * @author Paul Hoadley 8 | * @since 1.0 9 | */ 10 | public class ArabicContentProducer extends AbstractContentProducer { 11 | /** 12 | * Array containing some Arabic characters 13 | */ 14 | private static final char[] ARABIC_CHARS = { '\u0627', '\u0628', '\u062a', '\u062b', '\u062c', '\u062d', '\u062e', 15 | '\u062f', '\u0630', '\u0631', '\u0632', '\u0633', '\u0634', '\u0635', '\u0636', '\u0637', '\u0638', 16 | '\u0639', '\u063a', '\u0641', '\u0642', '\u0643', '\u0644', '\u0645', '\u0646', '\u0647', '\u0648', 17 | '\u064a' }; 18 | 19 | /** 20 | * Constructor for object returning content of default length. 21 | */ 22 | public ArabicContentProducer() { 23 | this(DEFAULT_LENGTH); 24 | } 25 | 26 | /** 27 | * Constructor taking a length specifier. 28 | * 29 | * @param length content length 30 | */ 31 | public ArabicContentProducer(int length) { 32 | super(length, ARABIC_CHARS); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/filter/ImageFilter.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.filter; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.Image; 5 | import java.awt.Toolkit; 6 | import java.awt.image.BufferedImage; 7 | import java.awt.image.BufferedImageFilter; 8 | import java.awt.image.BufferedImageOp; 9 | import java.awt.image.FilteredImageSource; 10 | 11 | /** 12 | * A filter that can distort an image CAPTCHA in some way. 13 | * 14 | * @author James Childers 15 | * @author Paul Hoadley 16 | * @since 1.0 17 | */ 18 | public interface ImageFilter { 19 | /** 20 | * Transforms {@code image} in-place. 21 | * 22 | * @param image {@link BufferedImage} to transform 23 | */ 24 | void filter(BufferedImage image); 25 | 26 | /** 27 | * Applies {@code filter} to {@code img}. 28 | * 29 | * @param img a {@link BufferedImage} 30 | * @param filter a {@link BufferedImageOp} 31 | */ 32 | static void applyFilter(BufferedImage img, BufferedImageOp filter) { 33 | FilteredImageSource src = new FilteredImageSource(img.getSource(), new BufferedImageFilter(filter)); 34 | Image fImg = Toolkit.getDefaultToolkit().createImage(src); 35 | Graphics2D g = img.createGraphics(); 36 | g.drawImage(fImg, 0, 0, null, null); 37 | g.dispose(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/ChineseContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | /** 4 | * Generates random strings using a subset of the Chinese alphabet. 5 | * 6 | * @author James Childers 7 | * @author Paul Hoadley 8 | * @since 1.0 9 | */ 10 | public class ChineseContentProducer extends AbstractContentProducer { 11 | /** 12 | * Code point at start of range (inclusive) 13 | */ 14 | static final int CODE_POINT_START = 0x4E00; 15 | 16 | /** 17 | * Code point at end of range (exclusive) 18 | */ 19 | static final int CODE_POINT_END = 0x4F6F; 20 | 21 | /** 22 | * Array of source characters 23 | */ 24 | static final char[] CHARS; 25 | 26 | static { 27 | CHARS = new char[CODE_POINT_END - CODE_POINT_START]; 28 | for (int i = 0; i < (CODE_POINT_END - CODE_POINT_START); i++) { 29 | CHARS[i] = (char) (CODE_POINT_START + i); 30 | } 31 | } 32 | 33 | /** 34 | * Constructor for object returning content of default length. 35 | */ 36 | public ChineseContentProducer() { 37 | this(DEFAULT_LENGTH); 38 | } 39 | 40 | /** 41 | * Constructor taking a length specifier. 42 | * 43 | * @param length content length 44 | */ 45 | public ChineseContentProducer(int length) { 46 | super(length, CHARS); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/SquigglesBackgroundProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.backgrounds; 2 | 3 | import java.awt.AlphaComposite; 4 | import java.awt.BasicStroke; 5 | import java.awt.Graphics2D; 6 | import java.awt.geom.Arc2D; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * Adds squiggles to background. 11 | * 12 | * @author James Childers 13 | * @author Paul Hoadley 14 | * @since 1.0 15 | */ 16 | public class SquigglesBackgroundProducer implements BackgroundProducer { 17 | /** 18 | * Alpha value of background 19 | */ 20 | private static final float ALPHA = 0.75f; 21 | 22 | @Override 23 | public BufferedImage getBackground(int width, int height) { 24 | BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 25 | Graphics2D graphics = result.createGraphics(); 26 | 27 | BasicStroke bs = new BasicStroke(2.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, 28 | new float[] { 2.0f, 2.0f }, 0.0f); 29 | graphics.setStroke(bs); 30 | AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, ALPHA); 31 | graphics.setComposite(ac); 32 | 33 | graphics.translate(width * -1.0, 0.0); 34 | double delta = 5.0; 35 | double xt; 36 | Arc2D arc; 37 | for (xt = 0.0; xt < (2.0 * width); xt += delta) { 38 | arc = new Arc2D.Double(0, 0, width, height, 0.0, 360.0, Arc2D.OPEN); 39 | graphics.draw(arc); 40 | graphics.translate(delta, 0.0); 41 | } 42 | graphics.dispose(); 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/content/NumbersContentProducerTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * Unit tests on {@link NumbersContentProducer} class. 9 | * 10 | * @author Paul Hoadley 11 | * @since 1.0 12 | */ 13 | public class NumbersContentProducerTest { 14 | @Test 15 | public void producerReturnsNumber() { 16 | ContentProducer producer = new NumbersContentProducer(); 17 | for (int i = 0; i < 1000; i++) { 18 | assertTrue(isNumeric(producer.getContent())); 19 | } 20 | return; 21 | } 22 | 23 | @Test 24 | public void producerReturnsContentOfExpectedLength() { 25 | for (int i = 1; i <= 1000; i++) { 26 | ContentProducer producer = new NumbersContentProducer(i); 27 | String content = producer.getContent(); 28 | assertEquals(i, content.length()); 29 | assertTrue(isNumeric(content)); 30 | } 31 | return; 32 | } 33 | 34 | @Test 35 | public void constructorThrowsOnZero() { 36 | assertThrows(IllegalArgumentException.class, () -> new NumbersContentProducer(0)); 37 | } 38 | 39 | @Test 40 | public void constructorThrowsOnNegative() { 41 | assertThrows(IllegalArgumentException.class, () -> new NumbersContentProducer(-1)); 42 | } 43 | 44 | /** 45 | * Is {@code number} a number? 46 | * 47 | * @param number candidate number string 48 | * @return {@code true} if {@code number} is a number, otherwise {@code false} 49 | */ 50 | private boolean isNumeric(String number) { 51 | return number.chars().allMatch(Character::isDigit); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/FlatColorBackgroundProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.backgrounds; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.geom.Rectangle2D; 6 | import java.awt.image.BufferedImage; 7 | 8 | /** 9 | * A {@link BackgroundProducer} that generates a solid colour background. 10 | * 11 | * @author James Childers 12 | * @author Paul Hoadley 13 | * @since 1.0 14 | */ 15 | public final class FlatColorBackgroundProducer implements BackgroundProducer { 16 | /** 17 | * Default {@link Color} 18 | */ 19 | private static final Color DEFAULT_COLOR = Color.GRAY; 20 | 21 | /** 22 | * {@link Color} for this producer 23 | */ 24 | private final Color color; 25 | 26 | /** 27 | * Constructor: creates a {@link BackgroundProducer} that uses the default 28 | * {@link Color}. 29 | */ 30 | public FlatColorBackgroundProducer() { 31 | this(DEFAULT_COLOR); 32 | return; 33 | } 34 | 35 | /** 36 | * Constructor taking a {@link Color}. 37 | * 38 | * @param color {@link Color} to set for this {@link BackgroundProducer} 39 | */ 40 | public FlatColorBackgroundProducer(Color color) { 41 | this.color = color; 42 | return; 43 | } 44 | 45 | @Override 46 | public BufferedImage getBackground(int width, int height) { 47 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 48 | Graphics2D graphics = img.createGraphics(); 49 | graphics.setPaint(color); 50 | graphics.fill(new Rectangle2D.Double(0, 0, width, height)); 51 | graphics.drawImage(img, 0, 0, null); 52 | graphics.dispose(); 53 | return img; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/filter/StretchImageFilter.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.filter; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.geom.AffineTransform; 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * Stretches the given image over the x- and y-axes. If no scale is given, 9 | * defaults to an x-axis scale of 1.0 and a y-axis scale of 3.0 (i.e. make the 10 | * image tall but do not affect the width). 11 | * 12 | * @author James Childers 13 | * @author Paul Hoadley 14 | * @since 1.0 15 | */ 16 | public class StretchImageFilter implements ImageFilter { 17 | /** 18 | * Default x-axis multiplier 19 | */ 20 | private static final double XDEFAULT = 1.0; 21 | 22 | /** 23 | * Default y-axis multiplier 24 | */ 25 | private static final double YDEFAULT = 3.0; 26 | 27 | /** 28 | * x-axis multiplier 29 | */ 30 | private final double xScale; 31 | 32 | /** 33 | * y-axis multiplier 34 | */ 35 | private final double yScale; 36 | 37 | /** 38 | * Constructor using default scale multipliers. 39 | */ 40 | public StretchImageFilter() { 41 | this(XDEFAULT, YDEFAULT); 42 | } 43 | 44 | /** 45 | * Constructor taking x- and y-axis scale multipliers. 46 | * 47 | * @param xScale x-axis scale 48 | * @param yScale y-axis scale 49 | */ 50 | public StretchImageFilter(double xScale, double yScale) { 51 | this.xScale = xScale; 52 | this.yScale = yScale; 53 | return; 54 | } 55 | 56 | @Override 57 | public void filter(BufferedImage image) { 58 | Graphics2D g = image.createGraphics(); 59 | AffineTransform at = new AffineTransform(); 60 | at.scale(xScale, yScale); 61 | g.drawRenderedImage(image, at); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/content/AbstractContentProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.content; 2 | 3 | import java.util.Arrays; 4 | import java.util.Random; 5 | 6 | /** 7 | * Parent class for {@link ContentProducer}s that produce text of a given length 8 | * from a given array of characters. Subclasses just need to supply the 9 | * permitted characters. 10 | * 11 | * @author James Childers 12 | * @author Paul Hoadley 13 | * @since 1.0 14 | */ 15 | public abstract class AbstractContentProducer implements ContentProducer { 16 | /** 17 | * Default length for produced content 18 | */ 19 | protected static final int DEFAULT_LENGTH = 5; 20 | 21 | /** 22 | * Random number generator 23 | */ 24 | private static final Random RAND = new Random(); 25 | 26 | /** 27 | * Length of strings produced by this object 28 | */ 29 | private final int length; 30 | 31 | /** 32 | * Source characters 33 | */ 34 | private final char[] srcChars; 35 | 36 | /** 37 | * Constructor taking a length and an array of source characters. 38 | * 39 | * @param length text length 40 | * @param srcChars source characters 41 | * @throws IllegalArgumentException if {@code length} is not positive 42 | */ 43 | public AbstractContentProducer(int length, char[] srcChars) { 44 | if (length <= 0) { 45 | throw new IllegalArgumentException("Content length must be positive."); 46 | } 47 | this.length = length; 48 | this.srcChars = Arrays.copyOf(srcChars, srcChars.length); 49 | return; 50 | } 51 | 52 | @Override 53 | public String getContent() { 54 | StringBuilder sb = new StringBuilder(); 55 | for (int i = 0; i < length; i++) { 56 | sb.append(srcChars[RAND.nextInt(srcChars.length)]); 57 | } 58 | return sb.toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/audio/producer/RandomNumberVoiceProducerTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.audio.producer; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.Locale; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * Unit tests on {@link RandomNumberVoiceProducer} class. 12 | * 13 | * @author Paul Hoadley 14 | * @since 1.0 15 | */ 16 | public class RandomNumberVoiceProducerTest { 17 | @BeforeEach 18 | public void setup() { 19 | RandomNumberVoiceProducer.defaultLanguage = null; 20 | System.clearProperty(RandomNumberVoiceProducer.DEFAULT_LANGUAGE_KEY); 21 | return; 22 | } 23 | 24 | @Test 25 | public void constructorThrowsOnNull() { 26 | assertThrows(NullPointerException.class, () -> new RandomNumberVoiceProducer((Locale) null)); 27 | return; 28 | } 29 | 30 | @Test 31 | public void defaultLocaleReturnsEnglishIfPropertyNotSet() { 32 | assertEquals(Locale.ENGLISH, RandomNumberVoiceProducer.defaultLanguage()); 33 | return; 34 | } 35 | 36 | @Test 37 | public void defaultLocaleReturnsRequestedLocaleForLanguageIfPropertySetAndSupported() { 38 | System.setProperty(RandomNumberVoiceProducer.DEFAULT_LANGUAGE_KEY, "de"); 39 | assertEquals(Locale.GERMAN, RandomNumberVoiceProducer.defaultLanguage()); 40 | return; 41 | } 42 | 43 | @Test 44 | public void defaultLocaleReturnsEnglishIfRequestedLanguageIsUnsupported() { 45 | System.setProperty(RandomNumberVoiceProducer.DEFAULT_LANGUAGE_KEY, "xx"); 46 | assertEquals(Locale.ENGLISH, RandomNumberVoiceProducer.defaultLanguage()); 47 | return; 48 | } 49 | 50 | @Test 51 | public void localeConstructorReturnsObjectWithExpectedLanguage() { 52 | RandomNumberVoiceProducer r1 = new RandomNumberVoiceProducer(Locale.ENGLISH); 53 | assertEquals(r1.language, Locale.ENGLISH); 54 | RandomNumberVoiceProducer r2 = new RandomNumberVoiceProducer(Locale.GERMAN); 55 | assertEquals(r2.language, Locale.GERMAN); 56 | RandomNumberVoiceProducer r3 = new RandomNumberVoiceProducer(Locale.FRENCH); 57 | assertEquals(r3.language, Locale.FRENCH); 58 | // We don't support Italian yet 59 | RandomNumberVoiceProducer r4 = new RandomNumberVoiceProducer(Locale.ITALIAN); 60 | assertEquals(r4.language, Locale.ENGLISH); 61 | return; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/image/filter/RippleImageFilterTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.filter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.io.IOException; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | /** 14 | * Unit tests on {@link RippleImageFilter} class. 15 | * 16 | * @author Paul Hoadley 17 | * @since 1.2 18 | */ 19 | public class RippleImageFilterTest { 20 | private ImageFilter rippleImageFilter; 21 | 22 | @BeforeEach 23 | public void setup() { 24 | rippleImageFilter = new RippleImageFilter(); 25 | return; 26 | } 27 | 28 | /** 29 | * Compares a known expected result to an actual transformation. This method is 30 | * to allow us to make changes to the implementation of 31 | * {@link RippleImageFilter} and confirm its functionality is unchanged. 32 | * 33 | * @throws IOException if there is a problem reading images 34 | */ 35 | @Test 36 | public void rippleImageFilterProducesExpectedTransformation() throws IOException { 37 | BufferedImage input = ImageIO.read(RippleImageFilterTest.class.getClassLoader().getResourceAsStream("input.png")); 38 | BufferedImage expected = ImageIO.read(RippleImageFilterTest.class.getClassLoader().getResourceAsStream("output.png")); 39 | rippleImageFilter.filter(input); 40 | assertTrue(bufferedImagesEqual(expected, input)); 41 | return; 42 | } 43 | 44 | /** 45 | * 46 | * @param expected expected {@link BufferedImage} 47 | * @param actual actual {@link BufferedImage} 48 | * @return {@code true} if {@code expected} and {@code actual} are the same, 49 | * otherwise {@code false} 50 | * @see Stack 52 | * Overflow 53 | */ 54 | private boolean bufferedImagesEqual(BufferedImage expected, BufferedImage actual) { 55 | if (expected.getWidth() == actual.getWidth() && expected.getHeight() == actual.getHeight()) { 56 | for (int x = 0; x < expected.getWidth(); x++) { 57 | for (int y = 0; y < expected.getHeight(); y++) { 58 | if (expected.getRGB(x, y) != actual.getRGB(x, y)) 59 | return false; 60 | } 61 | } 62 | } else { 63 | return false; 64 | } 65 | return true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/net/logicsquad/nanocaptcha/audio/SampleTest.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.audio; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | import javax.sound.sampled.AudioInputStream; 9 | import javax.sound.sampled.AudioSystem; 10 | import javax.sound.sampled.UnsupportedAudioFileException; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Unit tests on {@link Sample} class. 16 | * 17 | * @author Paul Hoadley 18 | * @since 1.0 19 | */ 20 | public class SampleTest { 21 | // An MP3 file can't be used at all 22 | private static final String MP3_FILENAME = "/hello.mp3"; 23 | 24 | // This sample has the wrong encoding parameters for Sample 25 | private static final String WAV_BAD_FILENAME = "/hello.wav"; 26 | 27 | // This sample is a copy of one of the known-good samples 28 | private static final String WAV_GOOD_FILENAME = "/0-alex.wav"; 29 | 30 | // Known sample count 31 | private static final int WAV_GOOD_SAMPLES = 9847; 32 | 33 | @Test 34 | public void stringConstructorThrowsOnNull() { 35 | assertThrows(NullPointerException.class, () -> new Sample((String) null)); 36 | return; 37 | } 38 | 39 | @Test 40 | public void inputStreamConstructorThrowsOnNull() { 41 | assertThrows(NullPointerException.class, () -> new Sample((InputStream) null)); 42 | return; 43 | } 44 | 45 | @Test 46 | public void constructorThrowsOnWrongFormat() { 47 | assertThrows(RuntimeException.class, () -> new Sample(MP3_FILENAME)); 48 | return; 49 | } 50 | 51 | @Test 52 | public void stringConstructorThrowsOnWrongAudioParameters() { 53 | assertThrows(IllegalArgumentException.class, () -> new Sample(WAV_BAD_FILENAME)); 54 | return; 55 | } 56 | 57 | @Test 58 | public void inputStreamConstructorThrowsOnWrongAudioParameters() throws UnsupportedAudioFileException, IOException { 59 | AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(SampleTest.class.getResourceAsStream(WAV_BAD_FILENAME)); 60 | assertNotNull(audioInputStream); 61 | assertThrows(IllegalArgumentException.class, () -> new Sample(audioInputStream)); 62 | return; 63 | } 64 | 65 | @Test 66 | public void canCreateSampleFromSuitableInput() { 67 | Sample sample = new Sample(WAV_GOOD_FILENAME); 68 | assertNotNull(sample); 69 | assertEquals(WAV_GOOD_SAMPLES, sample.getSampleCount()); 70 | return; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/backgrounds/GradiatedBackgroundProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.backgrounds; 2 | 3 | import java.awt.Color; 4 | import java.awt.GradientPaint; 5 | import java.awt.Graphics2D; 6 | import java.awt.RenderingHints; 7 | import java.awt.geom.Rectangle2D; 8 | import java.awt.image.BufferedImage; 9 | 10 | /** 11 | * Creates a gradiated background between two {@link Color} values. Default 12 | * {@link Color}s are dark gray and white. 13 | * 14 | * @author James Childers 15 | * @author Paul Hoadley 16 | * @since 1.0 17 | */ 18 | public class GradiatedBackgroundProducer implements BackgroundProducer { 19 | /** 20 | * Default from {@link Color} 21 | */ 22 | private static final Color DEFAULT_FROM_COLOR = Color.DARK_GRAY; 23 | 24 | /** 25 | * Default to {@link Color} 26 | */ 27 | private static final Color DEFAULT_TO_COLOR = Color.WHITE; 28 | 29 | /** 30 | * From {@link Color} for this producer 31 | */ 32 | private final Color fromColor; 33 | 34 | /** 35 | * To {@link Color} for this producer 36 | */ 37 | private final Color toColor; 38 | 39 | /** 40 | * Constructor creating a producer with default {@link Color}s. 41 | */ 42 | public GradiatedBackgroundProducer() { 43 | this(DEFAULT_FROM_COLOR, DEFAULT_TO_COLOR); 44 | return; 45 | } 46 | 47 | /** 48 | * Constructor taking from and to {@link Color}s. 49 | * 50 | * @param fromColor from {@link Color} 51 | * @param toColor to {@link Color} 52 | */ 53 | public GradiatedBackgroundProducer(Color fromColor, Color toColor) { 54 | this.fromColor = fromColor; 55 | this.toColor = toColor; 56 | return; 57 | } 58 | 59 | @Override 60 | public BufferedImage getBackground(int width, int height) { 61 | // create an opaque image 62 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 63 | 64 | Graphics2D g = img.createGraphics(); 65 | RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 66 | 67 | g.setRenderingHints(hints); 68 | 69 | // create the gradient color 70 | GradientPaint ytow = new GradientPaint(0, 0, fromColor, width, height, toColor); 71 | 72 | g.setPaint(ytow); 73 | // draw gradient color 74 | g.fill(new Rectangle2D.Double(0, 0, width, height)); 75 | 76 | // draw the transparent image over the background 77 | g.drawImage(img, 0, 0, null); 78 | g.dispose(); 79 | 80 | return img; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/filter/ShearImageFilter.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 | import java.util.Random; 7 | 8 | /** 9 | * Applies a shear effect to the image. 10 | * 11 | * @author James Childers 12 | * @author Paul Hoadley 13 | * @since 1.0 14 | */ 15 | public class ShearImageFilter implements ImageFilter { 16 | /** 17 | * 2 * pi 18 | */ 19 | private static final double TWO_PI = 6.2831853071795862; 20 | 21 | /** 22 | * Default {@link Color} 23 | */ 24 | private static final Color DEFAULT_COLOR = Color.GRAY; 25 | 26 | /** 27 | * Random number generator 28 | */ 29 | private static final Random RAND = new Random(); 30 | 31 | /** 32 | * {@link Color} to use in filter 33 | */ 34 | private final Color color; 35 | 36 | /** 37 | * Constructor using default {@link Color}. 38 | */ 39 | public ShearImageFilter() { 40 | this(DEFAULT_COLOR); 41 | return; 42 | } 43 | 44 | /** 45 | * Constructor taking a {@link Color} for the effect. 46 | * 47 | * @param color effect {@link Color} 48 | */ 49 | public ShearImageFilter(Color color) { 50 | this.color = color; 51 | return; 52 | } 53 | 54 | @Override 55 | public void filter(BufferedImage bi) { 56 | Graphics2D g = bi.createGraphics(); 57 | shearX(g, bi.getWidth(), bi.getHeight()); 58 | shearY(g, bi.getWidth(), bi.getHeight()); 59 | g.dispose(); 60 | } 61 | 62 | private void shearX(Graphics2D g, int w1, int h1) { 63 | int period = RAND.nextInt(10) + 5; 64 | boolean borderGap = true; 65 | int frames = 15; 66 | int phase = RAND.nextInt(5) + 2; 67 | for (int i = 0; i < h1; i++) { 68 | double d = (period >> 1) * Math.sin((double) i / (double) period + (TWO_PI * phase) / frames); 69 | g.copyArea(0, i, w1, 1, (int) d, 0); 70 | if (borderGap) { 71 | g.setColor(color); 72 | g.drawLine((int) d, i, 0, i); 73 | g.drawLine((int) d + w1, i, w1, i); 74 | } 75 | } 76 | } 77 | 78 | private void shearY(Graphics2D g, int w1, int h1) { 79 | int period = RAND.nextInt(30) + 10; 80 | boolean borderGap = true; 81 | int frames = 15; 82 | int phase = 7; 83 | for (int i = 0; i < w1; i++) { 84 | double d = (period >> 1) * Math.sin((float) i / period + (TWO_PI * phase) / frames); 85 | g.copyArea(i, 0, 1, h1, 0, (int) d); 86 | if (borderGap) { 87 | g.setColor(color); 88 | g.drawLine(i, (int) d, i, 0); 89 | g.drawLine(i, (int) d + h1, i, h1); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/renderer/DefaultWordRenderer.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.RenderingHints; 7 | import java.awt.font.FontRenderContext; 8 | import java.awt.font.GlyphVector; 9 | import java.awt.image.BufferedImage; 10 | import java.util.function.Supplier; 11 | 12 | /** 13 | * Renders the content onto the image. 14 | * 15 | * @author James Childers 16 | * @author Paul Hoadley 17 | * @author bivashy 18 | * @since 1.0 19 | */ 20 | public final class DefaultWordRenderer extends AbstractWordRenderer { 21 | /** 22 | * Constructor taking x- and y-axis offsets 23 | * 24 | * @param xOffset x-axis offset 25 | * @param yOffset y-axis offset 26 | * @param colorSupplier {@link Color} supplier 27 | * @param fontSupplier {@link Font} supplier 28 | * @since 1.4 29 | */ 30 | private DefaultWordRenderer(double xOffset, double yOffset, Supplier colorSupplier, Supplier fontSupplier) { 31 | super(xOffset, yOffset, colorSupplier, fontSupplier); 32 | return; 33 | } 34 | 35 | @Override 36 | public void render(final String word, BufferedImage image) { 37 | Graphics2D g = image.createGraphics(); 38 | 39 | RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 40 | hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); 41 | g.setRenderingHints(hints); 42 | 43 | FontRenderContext frc = g.getFontRenderContext(); 44 | int xBaseline = (int) Math.round(image.getWidth() * xOffset()); 45 | int yBaseline = image.getHeight() - (int) Math.round(image.getHeight() * yOffset()); 46 | 47 | char[] chars = new char[1]; 48 | for (char c : word.toCharArray()) { 49 | chars[0] = c; 50 | 51 | g.setColor(colorSupplier().get()); 52 | Font font = fontSupplier().get(); 53 | g.setFont(font); 54 | GlyphVector gv = font.createGlyphVector(frc, chars); 55 | g.drawChars(chars, 0, chars.length, xBaseline, yBaseline); 56 | 57 | int width = (int) gv.getVisualBounds().getWidth(); 58 | xBaseline = xBaseline + width; 59 | } 60 | } 61 | 62 | /** 63 | * Builder for {@code DefaultWordRenderer}. 64 | * 65 | * @since 1.4 66 | */ 67 | public static class Builder extends AbstractWordRenderer.Builder { 68 | @Override 69 | public DefaultWordRenderer build() { 70 | return new DefaultWordRenderer(xOffset, yOffset, colorSupplier, fontSupplier); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/image/noise/SaltAndPepperNoiseProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.image.noise; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.util.Random; 6 | 7 | /** 8 | * Applies salt and pepper noise to an image. This noise type randomly changes some of the pixels to black or white, creating a 'salt and 9 | * pepper' effect. 10 | * 11 | * @author bivashy 12 | * @see Salt and pepper on Wikipedia 13 | * @since 2.0 14 | */ 15 | public class SaltAndPepperNoiseProducer implements NoiseProducer { 16 | /** 17 | * Random number generator. 18 | */ 19 | private static final Random RAND = new Random(); 20 | 21 | /** 22 | * Default noise density. 23 | */ 24 | private static final double DEFAULT_NOISE_DENSITY = 0.15; 25 | 26 | /** 27 | * RGB value of the "pepper". 28 | */ 29 | private static final int PEPPER = Color.BLACK.getRGB(); 30 | 31 | /** 32 | * RGB value of the "salt". 33 | */ 34 | private static final int SALT = Color.WHITE.getRGB(); 35 | 36 | /** 37 | * Noise density. 38 | */ 39 | private final double noiseDensity; 40 | 41 | /** 42 | * Constructor using default standard deviation and mean. 43 | */ 44 | public SaltAndPepperNoiseProducer() { 45 | this(DEFAULT_NOISE_DENSITY); 46 | return; 47 | } 48 | 49 | /** 50 | * Constructor for salt and pepper noise producer. 51 | * 52 | * @param noiseDensity The density of the noise to be applied (0 to 1 range). 53 | * @throws IllegalArgumentException when the noise density is not in the range of 0 to 1. 54 | */ 55 | public SaltAndPepperNoiseProducer(double noiseDensity) { 56 | if (noiseDensity < 0 || noiseDensity > 1) { 57 | throw new IllegalArgumentException("Noise density must be between 0 and 1."); 58 | } 59 | this.noiseDensity = noiseDensity; 60 | return; 61 | } 62 | 63 | /** 64 | * Applies salt and pepper noise to the given image. 65 | * 66 | * @param image The BufferedImage to apply noise to 67 | */ 68 | @Override 69 | public void makeNoise(BufferedImage image) { 70 | int width = image.getWidth(); 71 | int height = image.getHeight(); 72 | 73 | for (int x = 0; x < width; x++) { 74 | for (int y = 0; y < height; y++) { 75 | if (RAND.nextDouble() < noiseDensity) { 76 | int color = RAND.nextBoolean() ? PEPPER : SALT; 77 | image.setRGB(x, y, color); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/net/logicsquad/nanocaptcha/audio/noise/RandomNoiseProducer.java: -------------------------------------------------------------------------------- 1 | package net.logicsquad.nanocaptcha.audio.noise; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Random; 6 | import java.util.stream.Collectors; 7 | 8 | import net.logicsquad.nanocaptcha.audio.Mixer; 9 | import net.logicsquad.nanocaptcha.audio.Sample; 10 | 11 | /** 12 | *

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 | ![](https://github.com/logicsquad/nanocaptcha/workflows/build/badge.svg) 2 | [![License](https://img.shields.io/badge/License-BSD-blue.svg)](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 | *

89 | * 90 | *
    91 | *
  • {@code net.logicsquad.nanocaptcha.image.ImageCaptcha.defaultX}
  • 92 | *
  • {@code net.logicsquad.nanocaptcha.image.ImageCaptcha.defaultY}
  • 93 | *
94 | * 95 | * @return new {@code ImageCaptcha} 96 | * @since 2.0 97 | */ 98 | public static ImageCaptcha create() { 99 | return new Builder(Integer.getInteger(DEFAULT_X_KEY, DEFAULT_X), Integer.getInteger(DEFAULT_Y_KEY, DEFAULT_Y)).addContent().build(); 100 | } 101 | 102 | /** 103 | *

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 | *

107 | * 108 | *
109 | 	 * ImageCaptcha image = addBackground().addContent().addNoise().addFilter().addBorder().build();
110 | 	 * 
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 | --------------------------------------------------------------------------------