diff --git a/EasySVG.php b/EasySVG.php
new file mode 100644
index 0000000..82b0b55
--- /dev/null
+++ b/EasySVG.php
@@ -0,0 +1,516 @@
+
+ * @version 0.1b
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
+ * @see http://stackoverflow.com/questions/14684846/flattening-svg-matrix-transforms-in-inkscape
+ * @see http://stackoverflow.com/questions/7742148/how-to-convert-text-to-svg-paths
+ */
+class EasySVG {
+
+ protected $font;
+ protected $svg;
+
+ public function __construct() {
+ // default font data
+ $this->font = new stdClass;
+ $this->font->id = '';
+ $this->font->horizAdvX = 0;
+ $this->font->unitsPerEm = 0;
+ $this->font->ascent = 0;
+ $this->font->descent = 0;
+ $this->font->glyphs = array();
+ $this->font->size = 20;
+ $this->font->color = '';
+ $this->font->lineHeight = 1;
+ $this->font->letterSpacing = 0;
+
+ $this->clearSVG();
+ }
+
+ public function clearSVG() {
+ $this->svg = new SimpleXMLElement('');
+ }
+
+ /**
+ * Function takes UTF-8 encoded string and returns unicode number for every character.
+ * @param string $str
+ * @return string
+ */
+ private function _utf8ToUnicode( $str ) {
+ $unicode = array();
+ $values = array();
+ $lookingFor = 1;
+
+ for ($i = 0; $i < strlen( $str ); $i++ ) {
+ $thisValue = ord( $str[ $i ] );
+ if ( $thisValue < 128 ) $unicode[] = $thisValue;
+ else {
+ if ( count( $values ) == 0 ) $lookingFor = ( $thisValue < 224 ) ? 2 : 3;
+ $values[] = $thisValue;
+ if ( count( $values ) == $lookingFor ) {
+ $number = ( $lookingFor == 3 ) ?
+ ( ( $values[0] % 16 ) * 4096 ) + ( ( $values[1] % 64 ) * 64 ) + ( $values[2] % 64 ):
+ ( ( $values[0] % 32 ) * 64 ) + ( $values[1] % 64 );
+
+ $unicode[] = $number;
+ $values = array();
+ $lookingFor = 1;
+ }
+ }
+ }
+
+ return $unicode;
+ }
+
+ /**
+ * Set font params (short-hand method)
+ * @param string $filepath
+ * @param integer $size
+ * @param string $color
+ */
+ public function setFont( $filepath, $size, $color ) {
+ $this->setFontSVG($filepath);
+ $this->setFontSize($size);
+ $this->setFontColor($color);
+ }
+
+ /**
+ * Set font size for display
+ * @param int $size
+ * @return void
+ */
+ public function setFontSize( $size ) {
+ $this->font->size = $size;
+ }
+
+ /**
+ * Set font color
+ * @param string $color
+ * @return void
+ */
+ public function setFontColor( $color ) {
+ $this->font->color = $color;
+ }
+
+ /**
+ * Set the line height from default (1) to custom value
+ * @param float $value
+ * @return void
+ */
+ public function setLineHeight( $value ) {
+ $this->font->lineHeight = $value;
+ }
+
+ /**
+ * Set the letter spacing from default (0) to custom value
+ * @param float $value
+ * @return void
+ */
+ public function setLetterSpacing( $value ) {
+ $this->font->letterSpacing = $value;
+ }
+
+ /**
+ * Function takes path to SVG font (local path) and processes its xml
+ * to get path representation of every character and additional
+ * font parameters
+ * @param string $filepath
+ * @return void
+ */
+ public function setFontSVG( $filepath ) {
+ $this->font->glyphs = array();
+ $z = new XMLReader;
+ $z->open($filepath);
+
+ // move to the first node
+ while ($z->read()) {
+ $name = $z->name;
+
+ if ($z->nodeType == XMLReader::ELEMENT) {
+ if ($name == 'font') {
+ $this->font->id = $z->getAttribute('id');
+ $this->font->horizAdvX = $z->getAttribute('horiz-adv-x');
+ }
+
+ if ($name == 'font-face') {
+ $this->font->unitsPerEm = $z->getAttribute('units-per-em');
+ $this->font->ascent = $z->getAttribute('ascent');
+ $this->font->descent = $z->getAttribute('descent');
+ }
+
+ if ($name == 'glyph') {
+ $unicode = $z->getAttribute('unicode');
+ $unicode = $this->_utf8ToUnicode($unicode);
+
+ if (isset($unicode[0])) {
+ $unicode = $unicode[0];
+
+ $this->font->glyphs[$unicode] = new stdClass();
+ $this->font->glyphs[$unicode]->horizAdvX = $z->getAttribute('horiz-adv-x');
+ if (empty($this->font->glyphs[$unicode]->horizAdvX)) {
+ $this->font->glyphs[$unicode]->horizAdvX = $this->font->horizAdvX;
+ }
+ $this->font->glyphs[$unicode]->d = $z->getAttribute('d');
+
+ // save em value for letter spacing (109 is unicode for the letter 'm')
+ if ($unicode == '109') {
+ $this->font->em = $this->font->glyphs[$unicode]->horizAdvX;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a path to the SVG
+ * @param string $def
+ * @param array $attributes
+ * @return SimpleXMLElement
+ */
+ public function addPath($def, $attributes=array()) {
+ $path = $this->svg->addChild('path');
+ foreach($attributes as $key=>$value){
+ $path->addAttribute($key, $value);
+ }
+ $path->addAttribute('d', $def);
+ return $path;
+ }
+
+ /**
+ * Add a text to the SVG
+ * @param string $def
+ * @param float $x
+ * @param float $y
+ * @param array $attributes
+ * @return SimpleXMLElement
+ */
+ public function addText($text, $x=0, $y=0, $attributes=array()) {
+ $def = $this->textDef($text);
+
+ if($x!=0 || $y!=0){
+ $def = $this->defTranslate($def, $x, $y);
+ }
+
+ if($this->font->color) {
+ $attributes['fill'] = $this->font->color;
+ }
+
+ return $this->addPath($def, $attributes);
+ }
+
+
+ /**
+ * Function takes UTF-8 encoded string and size, returns xml for SVG paths representing this string.
+ * @param string $text UTF-8 encoded text
+ * @return string xml for text converted into SVG paths
+ */
+ public function textDef($text) {
+ $def = array();
+
+ $horizAdvX = 0;
+ $horizAdvY = $this->font->ascent + $this->font->descent;
+ $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
+ $text = $this->_utf8ToUnicode($text);
+
+ for($i = 0; $i < count($text); $i++) {
+
+ $letter = $text[$i];
+
+ // line break support (10 is unicode for linebreak)
+ if($letter==10){
+ $horizAdvX = 0;
+ $horizAdvY += $this->font->lineHeight * ( $this->font->ascent + $this->font->descent );
+ continue;
+ }
+
+ // extract character definition
+ $d = $this->font->glyphs[$letter]->d;
+
+ // transform typo from original SVG format to straight display
+ $d = $this->defScale($d, $fontSize, -$fontSize);
+ $d = $this->defTranslate($d, $horizAdvX, $horizAdvY*$fontSize*2);
+
+ $def[] = $d;
+
+ // next letter's position
+ $horizAdvX += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
+ }
+ return implode(' ', $def);
+ }
+
+
+ /**
+ * Function takes UTF-8 encoded string and size, returns width and height of the whole text
+ * @param string $text UTF-8 encoded text
+ * @return array ($width, $height)
+ */
+ public function textDimensions($text) {
+ $def = array();
+
+ $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
+ $text = $this->_utf8ToUnicode($text);
+
+ $lineWidth = 0;
+ $lineHeight = ( $this->font->ascent + $this->font->descent ) * $fontSize * 2;
+
+ $width = 0;
+ $height = $lineHeight;
+
+ for($i = 0; $i < count($text); $i++) {
+
+ $letter = $text[$i];
+
+ // line break support (10 is unicode for linebreak)
+ if($letter==10){
+ $width = $lineWidth>$width ? $lineWidth : $width;
+ $height += $lineHeight * $this->font->lineHeight;
+ $lineWidth = 0;
+ continue;
+ }
+
+ $lineWidth += $this->font->glyphs[$letter]->horizAdvX * $fontSize + $this->font->em * $this->font->letterSpacing * $fontSize;
+ }
+
+ // only keep the widest line's width
+ $width = $lineWidth>$width ? $lineWidth : $width;
+
+ return array($width, $height);
+ }
+
+
+ /**
+ * Function takes unicode character and returns the UTF-8 equivalent
+ * @param string $str
+ * @return string
+ */
+ public function unicodeDef( $unicode ) {
+
+ $horizAdvY = $this->font->ascent + $this->font->descent;
+ $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
+
+ // extract character definition
+ $d = $this->font->glyphs[hexdec($unicode)]->d;
+
+ // transform typo from original SVG format to straight display
+ $d = $this->defScale($d, $fontSize, -$fontSize);
+ $d = $this->defTranslate($d, 0, $horizAdvY*$fontSize*2);
+
+ return $d;
+ }
+
+ /**
+ * Returns the character width, as set in the font file
+ * @param string $str
+ * @param boolean $is_unicode
+ * @return float
+ */
+ public function characterWidth( $char, $is_unicode = false ) {
+ if ($is_unicode){
+ $letter = hexdec($char);
+ }
+ else {
+ $letter = $this->_utf8ToUnicode($char);
+ }
+
+ if (!isset($this->font->glyphs[$letter]))
+ return NULL;
+
+ $fontSize = floatval($this->font->size) / $this->font->unitsPerEm;
+ return $this->font->glyphs[$letter]->horizAdvX * $fontSize;
+ }
+
+
+ /**
+ * Applies a translate transformation to definition
+ * @param string $def definition
+ * @param float $x
+ * @param float $y
+ * @return string
+ */
+ public function defTranslate($def, $x=0, $y=0){
+ return $this->defApplyMatrix($def, array(1, 0, 0, 1, $x, $y));
+ }
+
+ /**
+ * Applies a translate transformation to definition
+ * @param string $def Definition
+ * @param integer $angle Rotation angle (degrees)
+ * @param integer $x X coordinate of rotation center
+ * @param integer $y Y coordinate of rotation center
+ * @return string
+ */
+ public function defRotate($def, $angle, $x=0, $y=0){
+ if($x==0 && $y==0){
+ $angle = deg2rad($angle);
+ return $this->defApplyMatrix($def, array(cos($angle), sin($angle), -sin($angle), cos($angle), 0, 0));
+ }
+
+ // rotate by a given point
+ $def = $this->defTranslate($def, $x, $y);
+ $def = $this->defRotate($def, $angle);
+ $def = $this->defTranslate($def, -$x, -$y);
+ return $def;
+ }
+
+ /**
+ * Applies a scale transformation to definition
+ * @param string $def definition
+ * @param integer $x
+ * @param integer $y
+ * @return string
+ */
+ public function defScale($def, $x=1, $y=1){
+ return $this->defApplyMatrix($def, array($x, 0, 0, $y, 0, 0));
+ }
+
+ /**
+ * Calculates the new definition with the matrix applied
+ * @param string $def
+ * @param array $matrix
+ * @return string
+ */
+ public function defApplyMatrix($def, $matrix){
+
+ // if there are several shapes in this definition, do the operation for each
+ preg_match_all('/M[^zZ]*[zZ]/', $def, $shapes);
+ $shapes = $shapes[0];
+ if(count($shapes)>1){
+ foreach($shapes as &$shape)
+ $shape = $this->defApplyMatrix($shape, $matrix);
+ return implode(' ', $shapes);
+ }
+
+ preg_match_all('/[a-zA-Z]+[^a-zA-Z]*/', $def, $instructions);
+ $instructions = $instructions[0];
+
+ $return = '';
+ foreach($instructions as &$instruction){
+ $i = preg_replace('/[^a-zA-Z]*/', '', $instruction);
+ preg_match_all('/\-?[0-9\.]+/', $instruction, $coords);
+ $coords = $coords[0];
+
+ if(empty($coords)){
+ continue;
+ }
+
+ $new_coords = array();
+ while(count($coords)>0){
+
+ // do the matrix calculation stuff
+ list($a, $b, $c, $d, $e, $f) = $matrix;
+
+ // exception for relative instruction
+ if( preg_match('/[a-z]/', $i) ){
+ $e = 0;
+ $f = 0;
+ }
+
+ // convert horizontal lineto (relative)
+ if( $i=='h' ){
+ $i = 'l';
+ $x = floatval( array_shift($coords) );
+ $y = 0;
+
+ // add new point's coordinates
+ $current_point = array(
+ $a*$x + $c*$y + $e,
+ $b*$x + $d*$y + $f,
+ );
+ $new_coords = array_merge($new_coords, $current_point);
+ }
+
+ // convert vertical lineto (relative)
+ elseif( $i=='v' ){
+ $i = 'l';
+ $x = 0;
+ $y = floatval( array_shift($coords) );
+
+ // add new point's coordinates
+ $current_point = array(
+ $a*$x + $c*$y + $e,
+ $b*$x + $d*$y + $f,
+ );
+ $new_coords = array_merge($new_coords, $current_point);
+ }
+
+ // convert quadratic bezier curve (relative)
+ elseif( $i=='q' ){
+ $x = floatval( array_shift($coords) );
+ $y = floatval( array_shift($coords) );
+
+ // add new point's coordinates
+ $current_point = array(
+ $a*$x + $c*$y + $e,
+ $b*$x + $d*$y + $f,
+ );
+ $new_coords = array_merge($new_coords, $current_point);
+
+ // same for 2nd point
+ $x = floatval( array_shift($coords) );
+ $y = floatval( array_shift($coords) );
+
+ // add new point's coordinates
+ $current_point = array(
+ $a*$x + $c*$y + $e,
+ $b*$x + $d*$y + $f,
+ );
+ $new_coords = array_merge($new_coords, $current_point);
+ }
+
+ // every other commands
+ // @TODO: handle 'a,c,s' (elliptic arc curve) commands
+ // cf. http://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands
+ else{
+ $x = floatval( array_shift($coords) );
+ $y = floatval( array_shift($coords) );
+
+ // add new point's coordinates
+ $current_point = array(
+ $a*$x + $c*$y + $e,
+ $b*$x + $d*$y + $f,
+ );
+ $new_coords = array_merge($new_coords, $current_point);
+ }
+
+
+ }
+
+ $instruction = $i . implode(',', $new_coords);
+
+ // remove useless commas
+ $instruction = preg_replace('/,\-/','-', $instruction);
+ }
+
+ return implode('', $instructions);
+ }
+
+
+
+ /**
+ *
+ * Short-hand methods
+ *
+ */
+
+
+ /**
+ * Return full SVG XML
+ * @return string
+ */
+ public function asXML(){
+ return $this->svg->asXML();
+ }
+
+ /**
+ * Adds an attribute to the SVG
+ * @param string $key
+ * @param string $value
+ */
+ public function addAttribute($key, $value){
+ return $this->svg->addAttribute($key, $value);
+ }
+}
diff --git a/conf/default.php b/conf/default.php
index a46580b..426dc17 100644
--- a/conf/default.php
+++ b/conf/default.php
@@ -9,7 +9,7 @@ $conf['mode'] = 'js';
$conf['forusers'] = 0;
$conf['loginprotect']= 0;
$conf['lettercount'] = 5;
-$conf['width'] = 115;
-$conf['height'] = 22;
+$conf['width'] = 125;
+$conf['height'] = 30;
$conf['question'] = 'What\'s the answer to life, the universe and everything?';
$conf['answer'] = '42';
diff --git a/conf/metadata.php b/conf/metadata.php
index 84ddfaf..099a6a1 100644
--- a/conf/metadata.php
+++ b/conf/metadata.php
@@ -5,7 +5,7 @@
* @author Andreas Gohr
*/
-$meta['mode'] = array('multichoice', '_choices' => array('js', 'text', 'math', 'question', 'image', 'audio', 'figlet'));
+$meta['mode'] = array('multichoice', '_choices' => array('js', 'text', 'math', 'question', 'image', 'audio', 'svg', 'svgaudio', 'figlet'));
$meta['forusers'] = array('onoff');
$meta['loginprotect']= array('onoff');
$meta['lettercount'] = array('numeric', '_min' => 3, '_max' => 16);
diff --git a/fonts/README b/fonts/README
index 69edcbe..beafa3b 100644
--- a/fonts/README
+++ b/fonts/README
@@ -2,7 +2,12 @@ All fonts placed in this directory will be used randomly for the image captcha.
The more and exotic fonts you use, the harder it will be to OCR. However you
should be aware that most fonts are very hard to read when used for small sizes.
-Provided fonts:
+Provided TTF fonts for use with image CAPTCHA:
VeraSe.ttf - Bitsream Vera, http://www-old.gnome.org/fonts/
Rufscript010.ttf - Rufscript, http://openfontlibrary.org/en/font/rufscript
+
+Provided SVG fonts for use with SVG CAPTCHA:
+
+ostrich-sans-black.svg - Ostrich Sans Black, https://github.com/theleagueof/ostrich-sans
+goudy_bookletter_1911-webfont.svg - Goudy Bookletter 1911, https://github.com/theleagueof/goudy-bookletter-1911
diff --git a/fonts/goudy_bookletter_1911-webfont.svg b/fonts/goudy_bookletter_1911-webfont.svg
new file mode 100644
index 0000000..b60239e
--- /dev/null
+++ b/fonts/goudy_bookletter_1911-webfont.svg
@@ -0,0 +1,145 @@
+
+
+
\ No newline at end of file
diff --git a/fonts/ostrich-sans-black.svg b/fonts/ostrich-sans-black.svg
new file mode 100644
index 0000000..5001bbc
--- /dev/null
+++ b/fonts/ostrich-sans-black.svg
@@ -0,0 +1,141 @@
+
+
+
\ No newline at end of file
diff --git a/helper.php b/helper.php
index 5a7e999..0d4f883 100644
--- a/helper.php
+++ b/helper.php
@@ -73,6 +73,20 @@ class helper_plugin_captcha extends DokuWiki_Plugin {
case 'js':
$out .= ''.$this->_obfuscateText($code).'';
break;
+ case 'svg':
+ $out .= '';
+ $out .= $this->_svgCAPTCHA($code);
+ $out .= '';
+ break;
+ case 'svgaudio':
+ $out .= '';
+ $out .= $this->_svgCAPTCHA($code);
+ $out .= '';
+ $out .= '';
+ $out .= '
';
+ break;
case 'image':
$out .= '
';
@@ -378,6 +392,43 @@ class helper_plugin_captcha extends DokuWiki_Plugin {
imagedestroy($img);
}
+ /**
+ * Create an SVG of the given text
+ *
+ * @param string $text
+ * @return string
+ */
+ public function _svgCAPTCHA($text) {
+ require_once(__DIR__ . '/EasySVG.php');
+
+ $fonts = glob(__DIR__ . '/fonts/*.svg');
+
+ $x = 0; // where we start to draw
+ $y = 100; // our max height
+
+ $svg = new EasySVG();
+
+ // draw the letters
+ $txtlen = strlen($text);
+ for($i = 0; $i < $txtlen; $i++) {
+ $char = $text[$i];
+ $size = rand($y / 2, $y - $y * 0.1); // 50-90%
+ $svg->setFontSVG($fonts[array_rand($fonts)]);
+
+ $svg->setFontSize($size);
+ $svg->setLetterSpacing(round(rand(1, 4) / 10, 2)); // 0.1 - 0.4
+ $svg->addText($char, $x, rand(0, round($y - $size))); // random up and down
+
+ list($w) = $svg->textDimensions($char);
+ $x += $w;
+ }
+
+ $svg->addAttribute('width', $x . 'px');
+ $svg->addAttribute('height', $y . 'px');
+ $svg->addAttribute('viewbox', "0 0 $x $y");
+ return $svg->asXML();
+ }
+
/**
* Encrypt the given string with the cookie salt
*
diff --git a/lang/en/settings.php b/lang/en/settings.php
index fe1714c..c3c33aa 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -12,6 +12,8 @@ $lang['mode_o_math'] = "Math Problem";
$lang['mode_o_question'] = "Fixed Question";
$lang['mode_o_image'] = "Image (bad accessibility)";
$lang['mode_o_audio'] = "Image+Audio (better accessibility)";
+$lang['mode_o_svg'] = "SVG (bad accessibility, readable)";
+$lang['mode_o_svgaudio'] = "SVG+Audio (better accessibility, readable)";
$lang['mode_o_figlet'] = "Figlet ASCII Art (bad accessibility)";
$lang['forusers'] = "Use CAPTCHA for logged in users, too?";
diff --git a/style.css b/style.css
index 9f6ec0c..e29676b 100644
--- a/style.css
+++ b/style.css
@@ -14,6 +14,19 @@
padding: 0;
}
+.dokuwiki #plugin__captcha_wrapper .svg {
+ display: inline-block;
+ background-color: __background__;
+ vertical-align: bottom;
+}
+
+.dokuwiki #plugin__captcha_wrapper svg {
+ width: 100%;
+ height: 100%;
+}
+.dokuwiki #plugin__captcha_wrapper svg path {
+ fill: __text__;
+}
.dokuwiki #plugin__captcha_wrapper .no {
display: none;
}
diff --git a/wav.php b/wav.php
index 996d52a..7cc98cf 100644
--- a/wav.php
+++ b/wav.php
@@ -16,7 +16,7 @@ $ID = $_REQUEST['id'];
/** @var $plugin helper_plugin_captcha */
$plugin = plugin_load('helper', 'captcha');
-if($plugin->getConf('mode') != 'audio') {
+if($plugin->getConf('mode') != 'audio' && $plugin->getConf('mode') != 'svgaudio') {
http_status(404);
exit;
}