Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/vendor/dapphp/securimage
diff options
context:
space:
mode:
authorAndreas Baumann <mail@andreasbaumann.cc>2020-02-01 09:05:48 +0100
committerAndreas Baumann <mail@andreasbaumann.cc>2020-02-01 09:05:48 +0100
commit6854cb3f4d8219cf1829e32122eb2502a916eae9 (patch)
tree350feb504587d932e02837a1442b059759927646 /vendor/dapphp/securimage
initial checkin
Diffstat (limited to 'vendor/dapphp/securimage')
-rw-r--r--vendor/dapphp/securimage/.gitattributes5
-rw-r--r--vendor/dapphp/securimage/AHGBold.ttfbin0 -> 144556 bytes
-rw-r--r--vendor/dapphp/securimage/LICENSE.txt25
-rw-r--r--vendor/dapphp/securimage/README.FONT.txt12
-rw-r--r--vendor/dapphp/securimage/README.md244
-rw-r--r--vendor/dapphp/securimage/README.txt222
-rw-r--r--vendor/dapphp/securimage/WavFile.php1913
-rw-r--r--vendor/dapphp/securimage/audio/.htaccess11
-rw-r--r--vendor/dapphp/securimage/composer.json27
-rw-r--r--vendor/dapphp/securimage/config.inc.php1
-rw-r--r--vendor/dapphp/securimage/database/.htaccess11
-rw-r--r--vendor/dapphp/securimage/database/index.html1
-rw-r--r--vendor/dapphp/securimage/database/securimage.sq3bin0 -> 4096 bytes
-rw-r--r--vendor/dapphp/securimage/images/audio_icon.pngbin0 -> 1684 bytes
-rw-r--r--vendor/dapphp/securimage/images/loading.pngbin0 -> 1136 bytes
-rw-r--r--vendor/dapphp/securimage/images/refresh.pngbin0 -> 4835 bytes
-rw-r--r--vendor/dapphp/securimage/securimage.css41
-rw-r--r--vendor/dapphp/securimage/securimage.js252
-rw-r--r--vendor/dapphp/securimage/securimage.php3468
-rw-r--r--vendor/dapphp/securimage/securimage_play.php70
-rw-r--r--vendor/dapphp/securimage/securimage_show.php79
-rw-r--r--vendor/dapphp/securimage/words/words.txt15457
22 files changed, 21839 insertions, 0 deletions
diff --git a/vendor/dapphp/securimage/.gitattributes b/vendor/dapphp/securimage/.gitattributes
new file mode 100644
index 0000000..c3c14a0
--- /dev/null
+++ b/vendor/dapphp/securimage/.gitattributes
@@ -0,0 +1,5 @@
+/examples/ export-ignore
+/captcha.html export-ignore
+/config.inc.php.SAMPLE export-ignore
+/example_form.ajax.php export-ignore
+/example_form.php export-ignore
diff --git a/vendor/dapphp/securimage/AHGBold.ttf b/vendor/dapphp/securimage/AHGBold.ttf
new file mode 100644
index 0000000..764b23d
--- /dev/null
+++ b/vendor/dapphp/securimage/AHGBold.ttf
Binary files differ
diff --git a/vendor/dapphp/securimage/LICENSE.txt b/vendor/dapphp/securimage/LICENSE.txt
new file mode 100644
index 0000000..889bc2c
--- /dev/null
+++ b/vendor/dapphp/securimage/LICENSE.txt
@@ -0,0 +1,25 @@
+COPYRIGHT:
+ Copyright (c) 2011 Drew Phillips
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/vendor/dapphp/securimage/README.FONT.txt b/vendor/dapphp/securimage/README.FONT.txt
new file mode 100644
index 0000000..d4770de
--- /dev/null
+++ b/vendor/dapphp/securimage/README.FONT.txt
@@ -0,0 +1,12 @@
+AHGBold.ttf is used by Securimage under the following license:
+
+Alte Haas Grotesk is a typeface that look like an helvetica printed in an old Muller-Brockmann Book.
+
+These fonts are freeware and can be distributed as long as they are
+together with this text file.
+
+I would appreciate very much to see what you have done with it anyway.
+
+yann le coroller
+www.yannlecoroller.com
+yann@lecoroller.com \ No newline at end of file
diff --git a/vendor/dapphp/securimage/README.md b/vendor/dapphp/securimage/README.md
new file mode 100644
index 0000000..e935ea2
--- /dev/null
+++ b/vendor/dapphp/securimage/README.md
@@ -0,0 +1,244 @@
+## Name:
+
+**Securimage** - A PHP class for creating captcha images and audio with many options.
+
+## Version:
+
+**3.6.6**
+
+## Author:
+
+Drew Phillips <drew@drew-phillips.com>
+
+## Download:
+
+The latest version can always be found at [phpcaptcha.org](https://www.phpcaptcha.org)
+
+## Documentation:
+
+Online documentation of the class, methods, and variables can be found
+at http://www.phpcaptcha.org/Securimage_Docs/
+
+## Requirements:
+
+* PHP 5.2 or greater
+* GD 2.0
+* FreeType (Required, for TTF fonts)
+* PDO (if using Sqlite, MySQL, or PostgreSQL)
+
+## Synopsis:
+
+**Within your HTML form**
+
+ <form method="post" action="">
+ .. form elements
+
+ <div>
+ <?php
+ require_once 'securimage.php';
+ echo Securimage::getCaptchaHtml();
+ ?>
+ </div>
+ </form>
+
+
+**Within your PHP form processor**
+
+ require_once 'securimage.php';
+
+ // Code Validation
+
+ $image = new Securimage();
+ if ($image->check($_POST['captcha_code']) == true) {
+ echo "Correct!";
+ } else {
+ echo "Sorry, wrong code.";
+ }
+
+## Description:
+
+What is **Securimage**?
+
+Securimage is a PHP class that is used to generate and validate CAPTCHA images.
+
+The classes uses an existing PHP session or creates its own if none is found to
+store the CAPTCHA code. In addition, a database can be used instead of
+session storage.
+
+Variables within the class are used to control the style and display of the
+image. The class uses TTF fonts and effects for strengthening the security of
+the image.
+
+It also creates audible codes which are played for visually impared users.
+
+## UPGRADE NOTICE:
+
+**3.6.3 and below:**
+Securimage 3.6.4 fixed a XSS vulnerability in example_form.ajax.php. It is
+recommended to upgrade to the latest version or delete example_form.ajax.php
+from the securimage directory on your website.
+
+**3.6.2 and above:**
+
+If you are upgrading to 3.6.2 or greater *AND* are using database storage,
+the table structure has changed in 3.6.2 adding an audio_data column for
+storing audio files in the database in order to support HTTP range
+requests. Delete your tables and have Securimage recreate them or see
+the function createDatabaseTables() in securimage.php for the new structure
+depending on which database backend you are using and alter the tables as
+needed. If using SQLite, just overwrite your existing securimage.sq3 file
+with the one from this release.
+
+*If you are not using database tables for storage, ignore this notice.*
+
+## Copyright:
+Script
+ Copyright (c) 2016 Drew Phillips
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+## Licenses:
+
+**WavFile.php**
+
+ The WavFile.php class used in Securimage by Drew Phillips and Paul Voegler
+ is used under the BSD License. See WavFile.php for details.
+ Many thanks to Paul Voegler (http://www.voegler.eu/) for contributing to
+ Securimage.
+Script
+---------------------------------------------------------------------------
+
+**Flash code for Securimage**
+
+Flash code created by Age Bosma & Mario Romero (animario@hotmail.com)
+Many thanks for releasing this to the project!
+
+---------------------------------------------------------------------------
+
+**HKCaptcha**
+
+Portions of Securimage contain code from Han-Kwang Nienhuys' PHP captcha
+
+ Han-Kwang Nienhuys' PHP captcha
+ Copyright June 2007
+
+ This copyright message and attribution must be preserved upon
+ modification. Redistribution under other licenses is expressly allowed.
+ Other licenses include GPL 2 or higher, BSD, and non-free licenses.
+ The original, unrestricted version can be obtained from
+ http://www.lagom.nl/linux/hkcaptcha/
+
+---------------------------------------------------------------------------
+
+**AHGBold.ttf**
+
+ AHGBold.ttf (AlteHaasGroteskBold.ttf) font was created by Yann Le Coroller
+ and is distributed as freeware.
+
+ Alte Haas Grotesk is a typeface that look like an helvetica printed in an
+ old Muller-Brockmann Book.
+
+ These fonts are freeware and can be distributed as long as they are
+ together with this text file.
+
+ I would appreciate very much to see what you have done with it anyway.
+
+ yann le coroller
+ www.yannlecoroller.com
+ yann@lecoroller.com
+
+---------------------------------------------------------------------------
+
+**PopForge Flash Library**
+
+Portions of securimage_play.swf use the PopForge flash library for playing audio
+
+ /**
+ * Copyright(C) 2007 Andre Michelle and Joa Ebert
+ *
+ * PopForge is an ActionScript3 code sandbox developed by Andre Michelle
+ * and Joa Ebert
+ * http://sandbox.popforge.de
+ *
+ * PopforgeAS3Audio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PopforgeAS3Audio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ */
+
+--------------------------------------------------------------------------
+
+**Graphics**
+
+Some graphics used are from the Humility Icon Pack by WorLord
+
+ License: GNU/GPL (http://findicons.com/pack/1723/humility)
+ http://findicons.com/icon/192558/gnome_volume_control
+ http://findicons.com/icon/192562/gtk_refresh
+
+--------------------------------------------------------------------------
+
+
+**Background noise sound files are from SoundJay.com**
+
+http://www.soundjay.com/tos.html
+
+ All sound effects on this website are created by us and protected under
+ the copyright laws, international treaty provisions and other applicable
+ laws. By downloading sounds, music or any material from this site implies
+ that you have read and accepted these terms and conditions:
+
+ Sound Effects
+ You are allowed to use the sounds free of charge and royalty free in your
+ projects (such as films, videos, games, presentations, animations, stage
+ plays, radio plays, audio books, apps) be it for commercial or
+ non-commercial purposes.
+
+ But you are NOT allowed to
+ - post the sounds (as sound effects or ringtones) on any website for
+ others to download, copy or use
+ - use them as a raw material to create sound effects or ringtones that
+ you will sell, distribute or offer for downloading
+ - sell, re-sell, license or re-license the sounds (as individual sound
+ effects or as a sound effects library) to anyone else
+ - claim the sounds as yours
+ - link directly to individual sound files
+ - distribute the sounds in apps or computer programs that are clearly
+ sound related in nature (such as sound machine, sound effect
+ generator, ringtone maker, funny sounds app, sound therapy app, etc.)
+ or in apps or computer programs that use the sounds as the program's
+ sound resource library for other people's use (such as animation
+ creator, digital book creator, song maker software, etc.). If you are
+ developing such computer programs, contact us for licensing options.
+
+ If you use the sound effects, please consider giving us a credit and
+ linking back to us but it's not required.
+
diff --git a/vendor/dapphp/securimage/README.txt b/vendor/dapphp/securimage/README.txt
new file mode 100644
index 0000000..57898cd
--- /dev/null
+++ b/vendor/dapphp/securimage/README.txt
@@ -0,0 +1,222 @@
+NAME:
+
+ Securimage - A PHP class for creating captcha images and audio with many options.
+
+VERSION:
+
+ 3.6.6
+
+AUTHOR:
+
+ Drew Phillips <drew@drew-phillips.com>
+
+DOWNLOAD:
+
+ The latest version can always be
+ found at http://www.phpcaptcha.org
+
+DOCUMENTATION:
+
+ Online documentation of the class, methods, and variables can
+ be found at http://www.phpcaptcha.org/Securimage_Docs/
+
+REQUIREMENTS:
+
+ PHP 5.2 or greater
+ GD 2.0
+ FreeType (Required, for TTF fonts)
+ PDO (if using Sqlite, MySQL, or PostgreSQL)
+
+SYNOPSIS:
+
+ require_once 'securimage.php';
+
+ **Within your HTML form**
+
+ <form method="post" action="">
+ .. form elements
+
+ <div>
+ <?php echo Securimage::getCaptchaHtml() ?>
+ </div>
+ </form>
+
+
+ **Within your PHP form processor**
+
+ // Code Validation
+
+ $image = new Securimage();
+ if ($image->check($_POST['captcha_code']) == true) {
+ echo "Correct!";
+ } else {
+ echo "Sorry, wrong code.";
+ }
+
+DESCRIPTION:
+
+ What is Securimage?
+
+ Securimage is a PHP class that is used to generate and validate CAPTCHA
+ images.
+
+ The classes uses an existing PHP session or creates its own if
+ none is found to store the CAPTCHA code. In addition, a database can be
+ used instead of session storage.
+
+ Variables within the class are used to control the style and display of
+ the image. The class uses TTF fonts and effects for strengthening the
+ security of the image.
+
+ It also creates audible codes which are played for visually impared users.
+
+UPGRADE NOTICE:
+ 3.6.3 and below:
+ Securimage 3.6.4 fixed a XSS vulnerability in example_form.ajax.php. It is
+ recommended to upgrade to the latest version or delete example_form.ajax.php
+ from the securimage directory on your website.
+
+ 3.6.2 and above:
+ If you are upgrading to 3.6.2 or greater AND are using database storage,
+ the table structure has changed in 3.6.2 adding an audio_data column for
+ storing audio files in the database in order to support HTTP range
+ requests. Delete your tables and have Securimage recreate them or see
+ the function createDatabaseTables() in securimage.php for the new structure
+ depending on which database backend you are using and alter the tables as
+ needed. If using SQLite, just overwrite your existing securimage.sq3 file
+ with the one from this release.
+
+ If you are not using database tables for storage, ignore this notice.
+
+COPYRIGHT:
+
+ Copyright (c) 2016 Drew Phillips
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+LICENSES:
+
+ The WavFile.php class used in Securimage by Drew Phillips and Paul Voegler
+ is used under the BSD License. See WavFile.php for details.
+ Many thanks to Paul Voegler (http://www.voegler.eu/) for contributing to
+ Securimage.
+
+ ---------------------------------------------------------------------------
+ Flash code created by Age Bosma & Mario Romero (animario@hotmail.com)
+ Many thanks for releasing this to the project!
+
+ ---------------------------------------------------------------------------
+ Portions of Securimage contain code from Han-Kwang Nienhuys' PHP captcha
+
+ Han-Kwang Nienhuys' PHP captcha
+ Copyright June 2007
+
+ This copyright message and attribution must be preserved upon
+ modification. Redistribution under other licenses is expressly allowed.
+ Other licenses include GPL 2 or higher, BSD, and non-free licenses.
+ The original, unrestricted version can be obtained from
+ http://www.lagom.nl/linux/hkcaptcha/
+
+ ---------------------------------------------------------------------------
+ AHGBold.ttf (AlteHaasGroteskBold.ttf) font was created by Yann Le Coroller
+ and is distributed as freeware.
+
+ Alte Haas Grotesk is a typeface that look like an helvetica printed in an
+ old Muller-Brockmann Book.
+
+ These fonts are freeware and can be distributed as long as they are
+ together with this text file.
+
+ I would appreciate very much to see what you have done with it anyway.
+
+ yann le coroller
+ www.yannlecoroller.com
+ yann@lecoroller.com
+
+ ---------------------------------------------------------------------------
+ Portions of securimage_play.swf use the PopForge flash library for
+ playing audio
+
+ /**
+ * Copyright(C) 2007 Andre Michelle and Joa Ebert
+ *
+ * PopForge is an ActionScript3 code sandbox developed by Andre Michelle
+ * and Joa Ebert
+ * http://sandbox.popforge.de
+ *
+ * PopforgeAS3Audio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PopforgeAS3Audio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ */
+
+ --------------------------------------------------------------------------
+ Some graphics used are from the Humility Icon Pack by WorLord
+
+ License: GNU/GPL (http://findicons.com/pack/1723/humility)
+ http://findicons.com/icon/192558/gnome_volume_control
+ http://findicons.com/icon/192562/gtk_refresh
+
+ --------------------------------------------------------------------------
+ Background noise sound files are from SoundJay.com
+ http://www.soundjay.com/tos.html
+
+ All sound effects on this website are created by us and protected under
+ the copyright laws, international treaty provisions and other applicable
+ laws. By downloading sounds, music or any material from this site implies
+ that you have read and accepted these terms and conditions:
+
+ Sound Effects
+ You are allowed to use the sounds free of charge and royalty free in your
+ projects (such as films, videos, games, presentations, animations, stage
+ plays, radio plays, audio books, apps) be it for commercial or
+ non-commercial purposes.
+
+ But you are NOT allowed to
+ - post the sounds (as sound effects or ringtones) on any website for
+ others to download, copy or use
+ - use them as a raw material to create sound effects or ringtones that
+ you will sell, distribute or offer for downloading
+ - sell, re-sell, license or re-license the sounds (as individual sound
+ effects or as a sound effects library) to anyone else
+ - claim the sounds as yours
+ - link directly to individual sound files
+ - distribute the sounds in apps or computer programs that are clearly
+ sound related in nature (such as sound machine, sound effect
+ generator, ringtone maker, funny sounds app, sound therapy app, etc.)
+ or in apps or computer programs that use the sounds as the program's
+ sound resource library for other people's use (such as animation
+ creator, digital book creator, song maker software, etc.). If you are
+ developing such computer programs, contact us for licensing options.
+
+ If you use the sound effects, please consider giving us a credit and
+ linking back to us but it's not required.
+
diff --git a/vendor/dapphp/securimage/WavFile.php b/vendor/dapphp/securimage/WavFile.php
new file mode 100644
index 0000000..8702d22
--- /dev/null
+++ b/vendor/dapphp/securimage/WavFile.php
@@ -0,0 +1,1913 @@
+<?php
+
+// error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
+
+/**
+* Project: PHPWavUtils: Classes for creating, reading, and manipulating WAV files in PHP<br />
+* File: WavFile.php<br />
+*
+* Copyright (c) 2014, Drew Phillips
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without modification,
+* are permitted provided that the following conditions are met:
+*
+* - Redistributions of source code must retain the above copyright notice,
+* this list of conditions and the following disclaimer.
+* - Redistributions in binary form must reproduce the above copyright notice,
+* this list of conditions and the following disclaimer in the documentation
+* and/or other materials provided with the distribution.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+* POSSIBILITY OF SUCH DAMAGE.
+*
+* Any modifications to the library should be indicated clearly in the source code
+* to inform users that the changes are not a part of the original software.<br /><br />
+*
+* @copyright 2014 Drew Phillips
+* @author Drew Phillips <drew@drew-phillips.com>
+* @author Paul Voegler <http://www.voegler.eu/>
+* @version 1.1.1 (Sep 2015)
+* @package PHPWavUtils
+* @license BSD License
+*
+* Changelog:
+* 1.1.1 (09/08/2015)
+* - Fix degrade() method to call filter correctly (Rasmus Lerdorf)
+*
+* 1.1 (02/8/2014)
+* - Add method setIgnoreChunkSizes() to allow reading of wav data with bogus chunk sizes set.
+* This allows streamed wav data to be processed where the chunk sizes were not known when
+* writing the header. Instead calculates the chunk sizes automatically.
+* - Add simple volume filter to attenuate or amplify the audio signal.
+*
+* 1.0 (10/2/2012)
+* - Fix insertSilence() creating invalid block size
+*
+* 1.0 RC1 (4/20/2012)
+* - Initial release candidate
+* - Supports 8, 16, 24, 32 bit PCM, 32-bit IEEE FLOAT, Extensible Format
+* - Support for 18 channels of audio
+* - Ability to read an offset from a file to reduce memory footprint with large files
+* - Single-pass audio filter processing
+* - Highly accurate and efficient mix and normalization filters (http://www.voegler.eu/pub/audio/)
+* - Utility filters for degrading audio, and inserting silence
+*
+* 0.6 (4/12/2012)
+* - Support 8, 16, 24, 32 bit and PCM float (Paul Voegler)
+* - Add normalize filter, misc improvements and fixes (Paul Voegler)
+* - Normalize parameters to filter() to use filter constants as array indices
+* - Add option to mix filter to loop the target file if the source is longer
+*
+* 0.5 (4/3/2012)
+* - Fix binary pack routine (Paul Voegler)
+* - Add improved mixing function (Paul Voegler)
+*
+*/
+
+class WavFile
+{
+ /*%******************************************************************************************%*/
+ // Class constants
+
+ /** @var int Filter flag for mixing two files */
+ const FILTER_MIX = 0x01;
+
+ /** @var int Filter flag for normalizing audio data */
+ const FILTER_NORMALIZE = 0x02;
+
+ /** @var int Filter flag for degrading audio data */
+ const FILTER_DEGRADE = 0x04;
+
+ /** @var int Filter flag for amplifying or attenuating audio data. */
+ const FILTER_VOLUME = 0x08;
+
+ /** @var int Maximum number of channels */
+ const MAX_CHANNEL = 18;
+
+ /** @var int Maximum sample rate */
+ const MAX_SAMPLERATE = 192000;
+
+ /** Channel Locations for ChannelMask */
+ const SPEAKER_DEFAULT = 0x000000;
+ const SPEAKER_FRONT_LEFT = 0x000001;
+ const SPEAKER_FRONT_RIGHT = 0x000002;
+ const SPEAKER_FRONT_CENTER = 0x000004;
+ const SPEAKER_LOW_FREQUENCY = 0x000008;
+ const SPEAKER_BACK_LEFT = 0x000010;
+ const SPEAKER_BACK_RIGHT = 0x000020;
+ const SPEAKER_FRONT_LEFT_OF_CENTER = 0x000040;
+ const SPEAKER_FRONT_RIGHT_OF_CENTER = 0x000080;
+ const SPEAKER_BACK_CENTER = 0x000100;
+ const SPEAKER_SIDE_LEFT = 0x000200;
+ const SPEAKER_SIDE_RIGHT = 0x000400;
+ const SPEAKER_TOP_CENTER = 0x000800;
+ const SPEAKER_TOP_FRONT_LEFT = 0x001000;
+ const SPEAKER_TOP_FRONT_CENTER = 0x002000;
+ const SPEAKER_TOP_FRONT_RIGHT = 0x004000;
+ const SPEAKER_TOP_BACK_LEFT = 0x008000;
+ const SPEAKER_TOP_BACK_CENTER = 0x010000;
+ const SPEAKER_TOP_BACK_RIGHT = 0x020000;
+ const SPEAKER_ALL = 0x03FFFF;
+
+ /** @var int PCM Audio Format */
+ const WAVE_FORMAT_PCM = 0x0001;
+
+ /** @var int IEEE FLOAT Audio Format */
+ const WAVE_FORMAT_IEEE_FLOAT = 0x0003;
+
+ /** @var int EXTENSIBLE Audio Format - actual audio format defined by SubFormat */
+ const WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+
+ /** @var string PCM Audio Format SubType - LE hex representation of GUID {00000001-0000-0010-8000-00AA00389B71} */
+ const WAVE_SUBFORMAT_PCM = "0100000000001000800000aa00389b71";
+
+ /** @var string IEEE FLOAT Audio Format SubType - LE hex representation of GUID {00000003-0000-0010-8000-00AA00389B71} */
+ const WAVE_SUBFORMAT_IEEE_FLOAT = "0300000000001000800000aa00389b71";
+
+
+ /*%******************************************************************************************%*/
+ // Properties
+
+ /** @var array Log base modifier lookup table for a given threshold (in 0.05 steps) used by normalizeSample.
+ * Adjusts the slope (1st derivative) of the log function at the threshold to 1 for a smooth transition
+ * from linear to logarithmic amplitude output. */
+ protected static $LOOKUP_LOGBASE = array(
+ 2.513, 2.667, 2.841, 3.038, 3.262,
+ 3.520, 3.819, 4.171, 4.589, 5.093,
+ 5.711, 6.487, 7.483, 8.806, 10.634,
+ 13.302, 17.510, 24.970, 41.155, 96.088
+ );
+
+ /** @var int The actual physical file size */
+ protected $_actualSize;
+
+ /** @var int The size of the file in RIFF header */
+ protected $_chunkSize;
+
+ /** @var int The size of the "fmt " chunk */
+ protected $_fmtChunkSize;
+
+ /** @var int The size of the extended "fmt " data */
+ protected $_fmtExtendedSize;
+
+ /** @var int The size of the "fact" chunk */
+ protected $_factChunkSize;
+
+ /** @var int Size of the data chunk */
+ protected $_dataSize;
+
+ /** @var int Size of the data chunk in the opened wav file */
+ protected $_dataSize_fp;
+
+ /** @var bool Does _dataSize really reflect strlen($_samples)? Case when a wav file is read with readData = false */
+ protected $_dataSize_valid;
+
+ /** @var int Starting offset of data chunk */
+ protected $_dataOffset;
+
+ /** @var int The audio format - WavFile::WAVE_FORMAT_* */
+ protected $_audioFormat;
+
+ /** @var int|string|null The audio subformat - WavFile::WAVE_SUBFORMAT_* */
+ protected $_audioSubFormat;
+
+ /** @var int Number of channels in the audio file */
+ protected $_numChannels;
+
+ /** @var int The channel mask */
+ protected $_channelMask;
+
+ /** @var int Samples per second */
+ protected $_sampleRate;
+
+ /** @var int Number of bits per sample */
+ protected $_bitsPerSample;
+
+ /** @var int Number of valid bits per sample */
+ protected $_validBitsPerSample;
+
+ /** @var int NumChannels * BitsPerSample/8 */
+ protected $_blockAlign;
+
+ /** @var int Number of sample blocks */
+ protected $_numBlocks;
+
+ /** @var int Bytes per second */
+ protected $_byteRate;
+
+ /** @var bool Ignore chunk sizes when reading wav data (useful when reading data from a stream where chunk sizes contain dummy values) */
+ protected $_ignoreChunkSizes;
+
+ /** @var string Binary string of samples */
+ protected $_samples;
+
+ /** @var resource|null The file pointer used for reading wavs from file or memory */
+ protected $_fp;
+
+
+ /*%******************************************************************************************%*/
+ // Special methods
+
+ /**
+ * WavFile Constructor.
+ *
+ * <code>
+ * $wav1 = new WavFile(2, 44100, 16); // new wav with 2 channels, at 44100 samples/sec and 16 bits per sample
+ * $wav2 = new WavFile('./audio/sound.wav'); // open and read wav file
+ * </code>
+ *
+ * @param string|int $numChannelsOrFileName (Optional) If string, the filename of the wav file to open. The number of channels otherwise. Defaults to 1.
+ * @param int|bool $sampleRateOrReadData (Optional) If opening a file and boolean, decides whether to read the data chunk or not. Defaults to true. The sample rate in samples per second otherwise. 8000 = standard telephone, 16000 = wideband telephone, 32000 = FM radio and 44100 = CD quality. Defaults to 8000.
+ * @param int $bitsPerSample (Optional) The number of bits per sample. Has to be 8, 16 or 24 for PCM audio or 32 for IEEE FLOAT audio. 8 = telephone, 16 = CD and 24 or 32 = studio quality. Defaults to 8.
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ public function __construct($numChannelsOrFileName = null, $sampleRateOrReadData = null, $bitsPerSample = null)
+ {
+ $this->_actualSize = 44;
+ $this->_chunkSize = 36;
+ $this->_fmtChunkSize = 16;
+ $this->_fmtExtendedSize = 0;
+ $this->_factChunkSize = 0;
+ $this->_dataSize = 0;
+ $this->_dataSize_fp = 0;
+ $this->_dataSize_valid = true;
+ $this->_dataOffset = 44;
+ $this->_audioFormat = self::WAVE_FORMAT_PCM;
+ $this->_audioSubFormat = null;
+ $this->_numChannels = 1;
+ $this->_channelMask = self::SPEAKER_DEFAULT;
+ $this->_sampleRate = 8000;
+ $this->_bitsPerSample = 8;
+ $this->_validBitsPerSample = 8;
+ $this->_blockAlign = 1;
+ $this->_numBlocks = 0;
+ $this->_byteRate = 8000;
+ $this->_ignoreChunkSizes = false;
+ $this->_samples = '';
+ $this->_fp = null;
+
+
+ if (is_string($numChannelsOrFileName)) {
+ $this->openWav($numChannelsOrFileName, is_bool($sampleRateOrReadData) ? $sampleRateOrReadData : true);
+
+ } else {
+ $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
+ ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
+ ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
+ }
+ }
+
+ public function __destruct() {
+ if (is_resource($this->_fp)) $this->closeWav();
+ }
+
+ public function __clone() {
+ $this->_fp = null;
+ }
+
+ /**
+ * Output the wav file headers and data.
+ *
+ * @return string The encoded file.
+ */
+ public function __toString()
+ {
+ return $this->makeHeader() .
+ $this->getDataSubchunk();
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Static methods
+
+ /**
+ * Unpacks a single binary sample to numeric value.
+ *
+ * @param string $sampleBinary (Required) The sample to decode.
+ * @param int $bitDepth (Optional) The bits per sample to decode. If omitted, derives it from the length of $sampleBinary.
+ * @return int|float|null The numeric sample value. Float for 32-bit samples. Returns null for unsupported bit depths.
+ */
+ public static function unpackSample($sampleBinary, $bitDepth = null)
+ {
+ if ($bitDepth === null) {
+ $bitDepth = strlen($sampleBinary) * 8;
+ }
+
+ switch ($bitDepth) {
+ case 8:
+ // unsigned char
+ return ord($sampleBinary);
+
+ case 16:
+ // signed short, little endian
+ $data = unpack('v', $sampleBinary);
+ $sample = $data[1];
+ if ($sample >= 0x8000) {
+ $sample -= 0x10000;
+ }
+ return $sample;
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ $data = unpack('C3', $sampleBinary);
+ $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
+ if ($sample >= 0x800000) {
+ $sample -= 0x1000000;
+ }
+ return $sample;
+
+ case 32:
+ // 32-bit float
+ $data = unpack('f', $sampleBinary);
+ return $data[1];
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Packs a single numeric sample to binary.
+ *
+ * @param int|float $sample (Required) The sample to encode. Has to be within valid range for $bitDepth. Float values only for 32 bits.
+ * @param int $bitDepth (Required) The bits per sample to encode with.
+ * @return string|null The encoded binary sample. Returns null for unsupported bit depths.
+ */
+ public static function packSample($sample, $bitDepth)
+ {
+ switch ($bitDepth) {
+ case 8:
+ // unsigned char
+ return chr($sample);
+
+ case 16:
+ // signed short, little endian
+ if ($sample < 0) {
+ $sample += 0x10000;
+ }
+ return pack('v', $sample);
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ if ($sample < 0) {
+ $sample += 0x1000000;
+ }
+ return pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
+
+ case 32:
+ // 32-bit float
+ return pack('f', $sample);
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Unpacks a binary sample block to numeric values.
+ *
+ * @param string $sampleBlock (Required) The binary sample block (all channels).
+ * @param int $bitDepth (Required) The bits per sample to decode.
+ * @param int $numChannels (Optional) The number of channels to decode. If omitted, derives it from the length of $sampleBlock and $bitDepth.
+ * @return array The sample values as an array of integers of floats for 32 bits. First channel is array index 1.
+ */
+ public static function unpackSampleBlock($sampleBlock, $bitDepth, $numChannels = null) {
+ $sampleBytes = $bitDepth / 8;
+ if ($numChannels === null) {
+ $numChannels = strlen($sampleBlock) / $sampleBytes;
+ }
+
+ $samples = array();
+ for ($i = 0; $i < $numChannels; $i++) {
+ $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
+ $samples[$i + 1] = self::unpackSample($sampleBinary, $bitDepth);
+ }
+
+ return $samples;
+ }
+
+ /**
+ * Packs an array of numeric channel samples to a binary sample block.
+ *
+ * @param array $samples (Required) The array of channel sample values. Expects float values for 32 bits and integer otherwise.
+ * @param int $bitDepth (Required) The bits per sample to encode with.
+ * @return string The encoded binary sample block.
+ */
+ public static function packSampleBlock($samples, $bitDepth) {
+ $sampleBlock = '';
+ foreach($samples as $sample) {
+ $sampleBlock .= self::packSample($sample, $bitDepth);
+ }
+
+ return $sampleBlock;
+ }
+
+ /**
+ * Normalizes a float audio sample. Maximum input range assumed for compression is [-2, 2].
+ * See http://www.voegler.eu/pub/audio/ for more information.
+ *
+ * @param float $sampleFloat (Required) The float sample to normalize.
+ * @param float $threshold (Required) The threshold or gain factor for normalizing the amplitude. <ul>
+ * <li> >= 1 - Normalize by multiplying by the threshold (boost - positive gain). <br />
+ * A value of 1 in effect means no normalization (and results in clipping). </li>
+ * <li> <= -1 - Normalize by dividing by the the absolute value of threshold (attenuate - negative gain). <br />
+ * A factor of 2 (-2) is about 6dB reduction in volume.</li>
+ * <li> [0, 1) - (open inverval - not including 1) - The threshold
+ * above which amplitudes are comressed logarithmically. <br />
+ * e.g. 0.6 to leave amplitudes up to 60% "as is" and compress above. </li>
+ * <li> (-1, 0) - (open inverval - not including -1 and 0) - The threshold
+ * above which amplitudes are comressed linearly. <br />
+ * e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above. </li></ul>
+ * @return float The normalized sample.
+ **/
+ public static function normalizeSample($sampleFloat, $threshold) {
+ // apply positive gain
+ if ($threshold >= 1) {
+ return $sampleFloat * $threshold;
+ }
+
+ // apply negative gain
+ if ($threshold <= -1) {
+ return $sampleFloat / -$threshold;
+ }
+
+ $sign = $sampleFloat < 0 ? -1 : 1;
+ $sampleAbs = abs($sampleFloat);
+
+ // logarithmic compression
+ if ($threshold >= 0 && $threshold < 1 && $sampleAbs > $threshold) {
+ $loga = self::$LOOKUP_LOGBASE[(int)($threshold * 20)]; // log base modifier
+ return $sign * ($threshold + (1 - $threshold) * log(1 + $loga * ($sampleAbs - $threshold) / (2 - $threshold)) / log(1 + $loga));
+ }
+
+ // linear compression
+ $thresholdAbs = abs($threshold);
+ if ($threshold > -1 && $threshold < 0 && $sampleAbs > $thresholdAbs) {
+ return $sign * ($thresholdAbs + (1 - $thresholdAbs) / (2 - $thresholdAbs) * ($sampleAbs - $thresholdAbs));
+ }
+
+ // else ?
+ return $sampleFloat;
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Getter and Setter methods for properties
+
+ public function getActualSize() {
+ return $this->_actualSize;
+ }
+
+ /** @param int $actualSize */
+ protected function setActualSize($actualSize = null) {
+ if (is_null($actualSize)) {
+ $this->_actualSize = 8 + $this->_chunkSize; // + "RIFF" header (ID + size)
+ } else {
+ $this->_actualSize = $actualSize;
+ }
+
+ return $this;
+ }
+
+ public function getChunkSize() {
+ return $this->_chunkSize;
+ }
+
+ /** @param int $chunkSize */
+ protected function setChunkSize($chunkSize = null) {
+ if (is_null($chunkSize)) {
+ $this->_chunkSize = 4 + // "WAVE" chunk
+ 8 + $this->_fmtChunkSize + // "fmt " subchunk
+ ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
+ 8 + $this->_dataSize + // "data" subchunk
+ ($this->_dataSize & 1); // padding byte
+ } else {
+ $this->_chunkSize = $chunkSize;
+ }
+
+ $this->setActualSize();
+
+ return $this;
+ }
+
+ public function getFmtChunkSize() {
+ return $this->_fmtChunkSize;
+ }
+
+ /** @param int $fmtChunkSize */
+ protected function setFmtChunkSize($fmtChunkSize = null) {
+ if (is_null($fmtChunkSize)) {
+ $this->_fmtChunkSize = 16 + $this->_fmtExtendedSize;
+ } else {
+ $this->_fmtChunkSize = $fmtChunkSize;
+ }
+
+ $this->setChunkSize() // implicit setActualSize()
+ ->setDataOffset();
+
+ return $this;
+ }
+
+ public function getFmtExtendedSize() {
+ return $this->_fmtExtendedSize;
+ }
+
+ /** @param int $fmtExtendedSize */
+ protected function setFmtExtendedSize($fmtExtendedSize = null) {
+ if (is_null($fmtExtendedSize)) {
+ if ($this->_audioFormat == self::WAVE_FORMAT_EXTENSIBLE) {
+ $this->_fmtExtendedSize = 2 + 22; // extension size for WAVE_FORMAT_EXTENSIBLE
+ } elseif ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
+ $this->_fmtExtendedSize = 2 + 0; // empty extension
+ } else {
+ $this->_fmtExtendedSize = 0; // no extension, only for WAVE_FORMAT_PCM
+ }
+ } else {
+ $this->_fmtExtendedSize = $fmtExtendedSize;
+ }
+
+ $this->setFmtChunkSize(); // implicit setSize(), setActualSize(), setDataOffset()
+
+ return $this;
+ }
+
+ public function getFactChunkSize() {
+ return $this->_factChunkSize;
+ }
+
+ /** @param int $factChunkSize */
+ protected function setFactChunkSize($factChunkSize = null) {
+ if (is_null($factChunkSize)) {
+ if ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
+ $this->_factChunkSize = 4;
+ } else {
+ $this->_factChunkSize = 0;
+ }
+ } else {
+ $this->_factChunkSize = $factChunkSize;
+ }
+
+ $this->setChunkSize() // implicit setActualSize()
+ ->setDataOffset();
+
+ return $this;
+ }
+
+ public function getDataSize() {
+ return $this->_dataSize;
+ }
+
+ /** @param int $dataSize */
+ protected function setDataSize($dataSize = null) {
+ if (is_null($dataSize)) {
+ $this->_dataSize = strlen($this->_samples);
+ } else {
+ $this->_dataSize = $dataSize;
+ }
+
+ $this->setChunkSize() // implicit setActualSize()
+ ->setNumBlocks();
+ $this->_dataSize_valid = true;
+
+ return $this;
+ }
+
+ public function getDataOffset() {
+ return $this->_dataOffset;
+ }
+
+ /** @param int $dataOffset */
+ protected function setDataOffset($dataOffset = null) {
+ if (is_null($dataOffset)) {
+ $this->_dataOffset = 8 + // "RIFF" header (ID + size)
+ 4 + // "WAVE" chunk
+ 8 + $this->_fmtChunkSize + // "fmt " subchunk
+ ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
+ 8; // "data" subchunk
+ } else {
+ $this->_dataOffset = $dataOffset;
+ }
+
+ return $this;
+ }
+
+ public function getAudioFormat() {
+ return $this->_audioFormat;
+ }
+
+ /** @param int $audioFormat */
+ protected function setAudioFormat($audioFormat = null) {
+ if (is_null($audioFormat)) {
+ if (($this->_bitsPerSample <= 16 || $this->_bitsPerSample == 32)
+ && $this->_validBitsPerSample == $this->_bitsPerSample
+ && $this->_channelMask == self::SPEAKER_DEFAULT
+ && $this->_numChannels <= 2) {
+ if ($this->_bitsPerSample <= 16) {
+ $this->_audioFormat = self::WAVE_FORMAT_PCM;
+ } else {
+ $this->_audioFormat = self::WAVE_FORMAT_IEEE_FLOAT;
+ }
+ } else {
+ $this->_audioFormat = self::WAVE_FORMAT_EXTENSIBLE;
+ }
+ } else {
+ $this->_audioFormat = $audioFormat;
+ }
+
+ $this->setAudioSubFormat()
+ ->setFactChunkSize() // implicit setSize(), setActualSize(), setDataOffset()
+ ->setFmtExtendedSize(); // implicit setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
+
+ return $this;
+ }
+
+ public function getAudioSubFormat() {
+ return $this->_audioSubFormat;
+ }
+
+ /** @param int $audioSubFormat */
+ protected function setAudioSubFormat($audioSubFormat = null) {
+ if (is_null($audioSubFormat)) {
+ if ($this->_bitsPerSample == 32) {
+ $this->_audioSubFormat = self::WAVE_SUBFORMAT_IEEE_FLOAT; // 32 bits are IEEE FLOAT in this class
+ } else {
+ $this->_audioSubFormat = self::WAVE_SUBFORMAT_PCM; // 8, 16 and 24 bits are PCM in this class
+ }
+ } else {
+ $this->_audioSubFormat = $audioSubFormat;
+ }
+
+ return $this;
+ }
+
+ public function getNumChannels() {
+ return $this->_numChannels;
+ }
+
+ /** @param int $numChannels */
+ public function setNumChannels($numChannels) {
+ if ($numChannels < 1 || $numChannels > self::MAX_CHANNEL) {
+ throw new WavFileException('Unsupported number of channels. Only up to ' . self::MAX_CHANNEL . ' channels are supported.');
+ } elseif ($this->_samples !== '') {
+ trigger_error('Wav already has sample data. Changing the number of channels does not convert and may corrupt the data.', E_USER_NOTICE);
+ }
+
+ $this->_numChannels = (int)$numChannels;
+
+ $this->setAudioFormat() // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
+ ->setByteRate()
+ ->setBlockAlign(); // implicit setNumBlocks()
+
+ return $this;
+ }
+
+ public function getChannelMask() {
+ return $this->_channelMask;
+ }
+
+ public function setChannelMask($channelMask = self::SPEAKER_DEFAULT) {
+ if ($channelMask != 0) {
+ // count number of set bits - Hamming weight
+ $c = (int)$channelMask;
+ $n = 0;
+ while ($c > 0) {
+ $n += $c & 1;
+ $c >>= 1;
+ }
+ if ($n != $this->_numChannels || (((int)$channelMask | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
+ throw new WavFileException('Invalid channel mask. The number of channels does not match the number of locations in the mask.');
+ }
+ }
+
+ $this->_channelMask = (int)$channelMask;
+
+ $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
+
+ return $this;
+ }
+
+ public function getSampleRate() {
+ return $this->_sampleRate;
+ }
+
+ public function setSampleRate($sampleRate) {
+ if ($sampleRate < 1 || $sampleRate > self::MAX_SAMPLERATE) {
+ throw new WavFileException('Invalid sample rate.');
+ } elseif ($this->_samples !== '') {
+ trigger_error('Wav already has sample data. Changing the sample rate does not convert the data and may yield undesired results.', E_USER_NOTICE);
+ }
+
+ $this->_sampleRate = (int)$sampleRate;
+
+ $this->setByteRate();
+
+ return $this;
+ }
+
+ public function getBitsPerSample() {
+ return $this->_bitsPerSample;
+ }
+
+ public function setBitsPerSample($bitsPerSample) {
+ if (!in_array($bitsPerSample, array(8, 16, 24, 32))) {
+ throw new WavFileException('Unsupported bits per sample. Only 8, 16, 24 and 32 bits are supported.');
+ } elseif ($this->_samples !== '') {
+ trigger_error('Wav already has sample data. Changing the bits per sample does not convert and may corrupt the data.', E_USER_NOTICE);
+ }
+
+ $this->_bitsPerSample = (int)$bitsPerSample;
+
+ $this->setValidBitsPerSample() // implicit setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset()
+ ->setByteRate()
+ ->setBlockAlign(); // implicit setNumBlocks()
+
+ return $this;
+ }
+
+ public function getValidBitsPerSample() {
+ return $this->_validBitsPerSample;
+ }
+
+ protected function setValidBitsPerSample($validBitsPerSample = null) {
+ if (is_null($validBitsPerSample)) {
+ $this->_validBitsPerSample = $this->_bitsPerSample;
+ } else {
+ if ($validBitsPerSample < 1 || $validBitsPerSample > $this->_bitsPerSample) {
+ throw new WavFileException('ValidBitsPerSample cannot be greater than BitsPerSample.');
+ }
+ $this->_validBitsPerSample = (int)$validBitsPerSample;
+ }
+
+ $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
+
+ return $this;
+ }
+
+ public function getBlockAlign() {
+ return $this->_blockAlign;
+ }
+
+ /** @param int $blockAlign */
+ protected function setBlockAlign($blockAlign = null) {
+ if (is_null($blockAlign)) {
+ $this->_blockAlign = $this->_numChannels * $this->_bitsPerSample / 8;
+ } else {
+ $this->_blockAlign = $blockAlign;
+ }
+
+ $this->setNumBlocks();
+
+ return $this;
+ }
+
+ public function getNumBlocks()
+ {
+ return $this->_numBlocks;
+ }
+
+ /** @param int $numBlocks */
+ protected function setNumBlocks($numBlocks = null) {
+ if (is_null($numBlocks)) {
+ $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign); // do not count incomplete sample blocks
+ } else {
+ $this->_numBlocks = $numBlocks;
+ }
+
+ return $this;
+ }
+
+ public function getByteRate() {
+ return $this->_byteRate;
+ }
+
+ /** @param int $byteRate */
+ protected function setByteRate($byteRate = null) {
+ if (is_null($byteRate)) {
+ $this->_byteRate = $this->_sampleRate * $this->_numChannels * $this->_bitsPerSample / 8;
+ } else {
+ $this->_byteRate = $byteRate;
+ }
+
+ return $this;
+ }
+
+ public function getIgnoreChunkSizes()
+ {
+ return $this->_ignoreChunkSizes;
+ }
+
+ public function setIgnoreChunkSizes($ignoreChunkSizes)
+ {
+ $this->_ignoreChunkSizes = (bool)$ignoreChunkSizes;
+ return $this;
+ }
+
+ public function getSamples() {
+ return $this->_samples;
+ }
+
+ public function setSamples(&$samples = '') {
+ if (strlen($samples) % $this->_blockAlign != 0) {
+ throw new WavFileException('Incorrect samples size. Has to be a multiple of BlockAlign.');
+ }
+
+ $this->_samples = $samples;
+
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ return $this;
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Getters
+
+ public function getMinAmplitude()
+ {
+ if ($this->_bitsPerSample == 8) {
+ return 0;
+ } elseif ($this->_bitsPerSample == 32) {
+ return -1.0;
+ } else {
+ return -(1 << ($this->_bitsPerSample - 1));
+ }
+ }
+
+ public function getZeroAmplitude()
+ {
+ if ($this->_bitsPerSample == 8) {
+ return 0x80;
+ } elseif ($this->_bitsPerSample == 32) {
+ return 0.0;
+ } else {
+ return 0;
+ }
+ }
+
+ public function getMaxAmplitude()
+ {
+ if($this->_bitsPerSample == 8) {
+ return 0xFF;
+ } elseif($this->_bitsPerSample == 32) {
+ return 1.0;
+ } else {
+ return (1 << ($this->_bitsPerSample - 1)) - 1;
+ }
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Wave file methods
+
+ /**
+ * Construct a wav header from this object. Includes "fact" chunk if necessary.
+ * http://www-mmsp.ece.mcgill.ca/documents/audioformats/wave/wave.html
+ *
+ * @return string The RIFF header data.
+ */
+ public function makeHeader()
+ {
+ // reset and recalculate
+ $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
+ $this->setNumBlocks();
+
+ // RIFF header
+ $header = pack('N', 0x52494646); // ChunkID - "RIFF"
+ $header .= pack('V', $this->getChunkSize()); // ChunkSize
+ $header .= pack('N', 0x57415645); // Format - "WAVE"
+
+ // "fmt " subchunk
+ $header .= pack('N', 0x666d7420); // SubchunkID - "fmt "
+ $header .= pack('V', $this->getFmtChunkSize()); // SubchunkSize
+ $header .= pack('v', $this->getAudioFormat()); // AudioFormat
+ $header .= pack('v', $this->getNumChannels()); // NumChannels
+ $header .= pack('V', $this->getSampleRate()); // SampleRate
+ $header .= pack('V', $this->getByteRate()); // ByteRate
+ $header .= pack('v', $this->getBlockAlign()); // BlockAlign
+ $header .= pack('v', $this->getBitsPerSample()); // BitsPerSample
+ if($this->getFmtExtendedSize() == 24) {
+ $header .= pack('v', 22); // extension size = 24 bytes, cbSize: 24 - 2 = 22 bytes
+ $header .= pack('v', $this->getValidBitsPerSample()); // ValidBitsPerSample
+ $header .= pack('V', $this->getChannelMask()); // ChannelMask
+ $header .= pack('H32', $this->getAudioSubFormat()); // SubFormat
+ } elseif ($this->getFmtExtendedSize() == 2) {
+ $header .= pack('v', 0); // extension size = 2 bytes, cbSize: 2 - 2 = 0 bytes
+ }
+
+ // "fact" subchunk
+ if ($this->getFactChunkSize() == 4) {
+ $header .= pack('N', 0x66616374); // SubchunkID - "fact"
+ $header .= pack('V', 4); // SubchunkSize
+ $header .= pack('V', $this->getNumBlocks()); // SampleLength (per channel)
+ }
+
+ return $header;
+ }
+
+ /**
+ * Construct wav DATA chunk.
+ *
+ * @return string The DATA header and chunk.
+ */
+ public function getDataSubchunk()
+ {
+ // check preconditions
+ if (!$this->_dataSize_valid) {
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+ }
+
+
+ // create subchunk
+ return pack('N', 0x64617461) . // SubchunkID - "data"
+ pack('V', $this->getDataSize()) . // SubchunkSize
+ $this->_samples . // Subchunk data
+ ($this->getDataSize() & 1 ? chr(0) : ''); // padding byte
+ }
+
+ /**
+ * Save the wav data to a file.
+ *
+ * @param string $filename (Required) The file path to save the wav to.
+ * @throws WavFileException
+ */
+ public function save($filename)
+ {
+ $fp = @fopen($filename, 'w+b');
+ if (!is_resource($fp)) {
+ throw new WavFileException('Failed to open "' . $filename . '" for writing.');
+ }
+
+ fwrite($fp, $this->makeHeader());
+ fwrite($fp, $this->getDataSubchunk());
+ fclose($fp);
+
+ return $this;
+ }
+
+ /**
+ * Reads a wav header and data from a file.
+ *
+ * @param string $filename (Required) The path to the wav file to read.
+ * @param bool $readData (Optional) If true, also read the data chunk.
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ public function openWav($filename, $readData = true)
+ {
+ // check preconditions
+ if (!file_exists($filename)) {
+ throw new WavFileException('Failed to open "' . $filename . '". File not found.');
+ } elseif (!is_readable($filename)) {
+ throw new WavFileException('Failed to open "' . $filename . '". File is not readable.');
+ } elseif (is_resource($this->_fp)) {
+ $this->closeWav();
+ }
+
+
+ // open the file
+ $this->_fp = @fopen($filename, 'rb');
+ if (!is_resource($this->_fp)) {
+ throw new WavFileException('Failed to open "' . $filename . '".');
+ }
+
+ // read the file
+ return $this->readWav($readData);
+ }
+
+ /**
+ * Close a with openWav() previously opened wav file or free the buffer of setWavData().
+ * Not necessary if the data has been read (readData = true) already.
+ */
+ public function closeWav() {
+ if (is_resource($this->_fp)) fclose($this->_fp);
+
+ return $this;
+ }
+
+ /**
+ * Set the wav file data and properties from a wav file in a string.
+ *
+ * @param string $data (Required) The wav file data. Passed by reference.
+ * @param bool $free (Optional) True to free the passed $data after copying.
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ public function setWavData(&$data, $free = true)
+ {
+ // check preconditions
+ if (is_resource($this->_fp)) $this->closeWav();
+
+
+ // open temporary stream in memory
+ $this->_fp = @fopen('php://memory', 'w+b');
+ if (!is_resource($this->_fp)) {
+ throw new WavFileException('Failed to open memory stream to write wav data. Use openWav() instead.');
+ }
+
+ // prepare stream
+ fwrite($this->_fp, $data);
+ rewind($this->_fp);
+
+ // free the passed data
+ if ($free) $data = null;
+
+ // read the stream like a file
+ return $this->readWav(true);
+ }
+
+ /**
+ * Read wav file from a stream.
+ *
+ * @param bool $readData (Optional) If true, also read the data chunk.
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ protected function readWav($readData = true)
+ {
+ if (!is_resource($this->_fp)) {
+ throw new WavFileException('No wav file open. Use openWav() first.');
+ }
+
+ try {
+ $this->readWavHeader();
+ } catch (WavFileException $ex) {
+ $this->closeWav();
+ throw $ex;
+ }
+
+ if ($readData) return $this->readWavData();
+
+ return $this;
+ }
+
+ /**
+ * Parse a wav header.
+ * http://www-mmsp.ece.mcgill.ca/documents/audioformats/wave/wave.html
+ *
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ protected function readWavHeader()
+ {
+ if (!is_resource($this->_fp)) {
+ throw new WavFileException('No wav file open. Use openWav() first.');
+ }
+
+ // get actual file size
+ $stat = fstat($this->_fp);
+ $actualSize = $stat['size'];
+
+ $this->_actualSize = $actualSize;
+
+
+ // read the common header
+ $header = fread($this->_fp, 36); // minimum size of the wav header
+ if (strlen($header) < 36) {
+ throw new WavFormatException('Not wav format. Header too short.', 1);
+ }
+
+
+ // check "RIFF" header
+ $RIFF = unpack('NChunkID/VChunkSize/NFormat', $header);
+
+ if ($RIFF['ChunkID'] != 0x52494646) { // "RIFF"
+ throw new WavFormatException('Not wav format. "RIFF" signature missing.', 2);
+ }
+
+ if ($this->getIgnoreChunkSizes()) {
+ $RIFF['ChunkSize'] = $actualSize - 8;
+ } else if ($actualSize - 8 < $RIFF['ChunkSize']) {
+ trigger_error('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', E_USER_NOTICE);
+ $RIFF['ChunkSize'] = $actualSize - 8;
+ }
+
+ if ($RIFF['Format'] != 0x57415645) { // "WAVE"
+ throw new WavFormatException('Not wav format. "RIFF" chunk format is not "WAVE".', 4);
+ }
+
+ $this->_chunkSize = $RIFF['ChunkSize'];
+
+
+ // check common "fmt " subchunk
+ $fmt = unpack('NSubchunkID/VSubchunkSize/vAudioFormat/vNumChannels/'
+ .'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
+ substr($header, 12));
+
+ if ($fmt['SubchunkID'] != 0x666d7420) { // "fmt "
+ throw new WavFormatException('Bad wav header. Expected "fmt " subchunk.', 11);
+ }
+
+ if ($fmt['SubchunkSize'] < 16) {
+ throw new WavFormatException('Bad "fmt " subchunk size.', 12);
+ }
+
+ if ( $fmt['AudioFormat'] != self::WAVE_FORMAT_PCM
+ && $fmt['AudioFormat'] != self::WAVE_FORMAT_IEEE_FLOAT
+ && $fmt['AudioFormat'] != self::WAVE_FORMAT_EXTENSIBLE)
+ {
+ throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
+ }
+
+ if ($fmt['NumChannels'] < 1 || $fmt['NumChannels'] > self::MAX_CHANNEL) {
+ throw new WavFormatException('Invalid number of channels in "fmt " subchunk.', 14);
+ }
+
+ if ($fmt['SampleRate'] < 1 || $fmt['SampleRate'] > self::MAX_SAMPLERATE) {
+ throw new WavFormatException('Invalid sample rate in "fmt " subchunk.', 15);
+ }
+
+ if ( ($fmt['AudioFormat'] == self::WAVE_FORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
+ || ($fmt['AudioFormat'] == self::WAVE_FORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32)
+ || ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE && !in_array($fmt['BitsPerSample'], array(8, 16, 24, 32))))
+ {
+ throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
+ }
+
+ $blockAlign = $fmt['NumChannels'] * $fmt['BitsPerSample'] / 8;
+ if ($blockAlign != $fmt['BlockAlign']) {
+ trigger_error('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', E_USER_NOTICE);
+ $fmt['BlockAlign'] = $blockAlign;
+ }
+
+ $byteRate = $fmt['SampleRate'] * $blockAlign;
+ if ($byteRate != $fmt['ByteRate']) {
+ trigger_error('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', E_USER_NOTICE);
+ $fmt['ByteRate'] = $byteRate;
+ }
+
+ $this->_fmtChunkSize = $fmt['SubchunkSize'];
+ $this->_audioFormat = $fmt['AudioFormat'];
+ $this->_numChannels = $fmt['NumChannels'];
+ $this->_sampleRate = $fmt['SampleRate'];
+ $this->_byteRate = $fmt['ByteRate'];
+ $this->_blockAlign = $fmt['BlockAlign'];
+ $this->_bitsPerSample = $fmt['BitsPerSample'];
+
+
+ // read extended "fmt " subchunk data
+ $extendedFmt = '';
+ if ($fmt['SubchunkSize'] > 16) {
+ // possibly handle malformed subchunk without a padding byte
+ $extendedFmt = fread($this->_fp, $fmt['SubchunkSize'] - 16 + ($fmt['SubchunkSize'] & 1)); // also read padding byte
+ if (strlen($extendedFmt) < $fmt['SubchunkSize'] - 16) {
+ throw new WavFormatException('Not wav format. Header too short.', 1);
+ }
+ }
+
+
+ // check extended "fmt " for EXTENSIBLE Audio Format
+ if ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE) {
+ if (strlen($extendedFmt) < 24) {
+ throw new WavFormatException('Invalid EXTENSIBLE "fmt " subchunk size. Found ' . $fmt['SubchunkSize'] . ', expected at least 40.', 19);
+ }
+
+ $extensibleFmt = unpack('vSize/vValidBitsPerSample/VChannelMask/H32SubFormat', substr($extendedFmt, 0, 24));
+
+ if ( $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_PCM
+ && $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_IEEE_FLOAT)
+ {
+ throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
+ }
+
+ if ( ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
+ || ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32))
+ {
+ throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
+ }
+
+ if ($extensibleFmt['Size'] != 22) {
+ trigger_error('Invaid extension size in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
+ $extensibleFmt['Size'] = 22;
+ }
+
+ if ($extensibleFmt['ValidBitsPerSample'] != $fmt['BitsPerSample']) {
+ trigger_error('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
+ $extensibleFmt['ValidBitsPerSample'] = $fmt['BitsPerSample'];
+ }
+
+ if ($extensibleFmt['ChannelMask'] != 0) {
+ // count number of set bits - Hamming weight
+ $c = (int)$extensibleFmt['ChannelMask'];
+ $n = 0;
+ while ($c > 0) {
+ $n += $c & 1;
+ $c >>= 1;
+ }
+ if ($n != $fmt['NumChannels'] || (((int)$extensibleFmt['ChannelMask'] | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
+ trigger_error('Invalid channel mask in EXTENSIBLE "fmt " subchunk. The number of channels does not match the number of locations in the mask.', E_USER_NOTICE);
+ $extensibleFmt['ChannelMask'] = 0;
+ }
+ }
+
+ $this->_fmtExtendedSize = strlen($extendedFmt);
+ $this->_validBitsPerSample = $extensibleFmt['ValidBitsPerSample'];
+ $this->_channelMask = $extensibleFmt['ChannelMask'];
+ $this->_audioSubFormat = $extensibleFmt['SubFormat'];
+
+ } else {
+ $this->_fmtExtendedSize = strlen($extendedFmt);
+ $this->_validBitsPerSample = $fmt['BitsPerSample'];
+ $this->_channelMask = 0;
+ $this->_audioSubFormat = null;
+ }
+
+
+ // read additional subchunks until "data" subchunk is found
+ $factSubchunk = array();
+ $dataSubchunk = array();
+
+ while (!feof($this->_fp)) {
+ $subchunkHeader = fread($this->_fp, 8);
+ if (strlen($subchunkHeader) < 8) {
+ throw new WavFormatException('Missing "data" subchunk.', 101);
+ }
+
+ $subchunk = unpack('NSubchunkID/VSubchunkSize', $subchunkHeader);
+
+ if ($subchunk['SubchunkID'] == 0x66616374) { // "fact"
+ // possibly handle malformed subchunk without a padding byte
+ $subchunkData = fread($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1)); // also read padding byte
+ if (strlen($subchunkData) < 4) {
+ throw new WavFormatException('Invalid "fact" subchunk.', 102);
+ }
+
+ $factParams = unpack('VSampleLength', substr($subchunkData, 0, 4));
+ $factSubchunk = array_merge($subchunk, $factParams);
+
+ } elseif ($subchunk['SubchunkID'] == 0x64617461) { // "data"
+ $dataSubchunk = $subchunk;
+
+ break;
+
+ } elseif ($subchunk['SubchunkID'] == 0x7761766C) { // "wavl"
+ throw new WavFormatException('Wave List Chunk ("wavl" subchunk) is not supported.', 106);
+ } else {
+ // skip all other (unknown) subchunks
+ // possibly handle malformed subchunk without a padding byte
+ if ( $subchunk['SubchunkSize'] < 0
+ || fseek($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1), SEEK_CUR) !== 0) { // also skip padding byte
+ throw new WavFormatException('Invalid subchunk (0x' . dechex($subchunk['SubchunkID']) . ') encountered.', 103);
+ }
+ }
+ }
+
+ if (empty($dataSubchunk)) {
+ throw new WavFormatException('Missing "data" subchunk.', 101);
+ }
+
+ // check "data" subchunk
+ $dataOffset = ftell($this->_fp);
+ if ($this->getIgnoreChunkSizes()) {
+ $dataSubchunk['SubchunkSize'] = $actualSize - $dataOffset;
+ } elseif ($dataSubchunk['SubchunkSize'] < 0 || $actualSize - $dataOffset < $dataSubchunk['SubchunkSize']) {
+ trigger_error("Invalid \"data\" subchunk size (found {$dataSubchunk['SubchunkSize']}.", E_USER_NOTICE);
+ $dataSubchunk['SubchunkSize'] = $actualSize - $dataOffset;
+ }
+
+ $this->_dataOffset = $dataOffset;
+ $this->_dataSize = $dataSubchunk['SubchunkSize'];
+ $this->_dataSize_fp = $dataSubchunk['SubchunkSize'];
+ $this->_dataSize_valid = false;
+ $this->_samples = '';
+
+
+ // check "fact" subchunk
+ $numBlocks = (int)($dataSubchunk['SubchunkSize'] / $fmt['BlockAlign']);
+
+ if (empty($factSubchunk)) { // construct fake "fact" subchunk
+ $factSubchunk = array('SubchunkSize' => 0, 'SampleLength' => $numBlocks);
+ }
+
+ if ($factSubchunk['SampleLength'] != $numBlocks) {
+ trigger_error('Invalid sample length in "fact" subchunk.', E_USER_NOTICE);
+ $factSubchunk['SampleLength'] = $numBlocks;
+ }
+
+ $this->_factChunkSize = $factSubchunk['SubchunkSize'];
+ $this->_numBlocks = $factSubchunk['SampleLength'];
+
+
+ return $this;
+
+ }
+
+ /**
+ * Read the wav data from the file into the buffer.
+ *
+ * @param int $dataOffset (Optional) The byte offset to skip before starting to read. Must be a multiple of BlockAlign.
+ * @param int $dataSize (Optional) The size of the data to read in bytes. Must be a multiple of BlockAlign. Defaults to all data.
+ * @throws WavFileException
+ */
+ public function readWavData($dataOffset = 0, $dataSize = null)
+ {
+ // check preconditions
+ if (!is_resource($this->_fp)) {
+ throw new WavFileException('No wav file open. Use openWav() first.');
+ }
+
+ if ($dataOffset < 0 || $dataOffset % $this->getBlockAlign() > 0) {
+ throw new WavFileException('Invalid data offset. Has to be a multiple of BlockAlign.');
+ }
+
+ if (is_null($dataSize)) {
+ $dataSize = $this->_dataSize_fp - ($this->_dataSize_fp % $this->getBlockAlign()); // only read complete blocks
+ } elseif ($dataSize < 0 || $dataSize % $this->getBlockAlign() > 0) {
+ throw new WavFileException('Invalid data size to read. Has to be a multiple of BlockAlign.');
+ }
+
+
+ // skip offset
+ if ($dataOffset > 0 && fseek($this->_fp, $dataOffset, SEEK_CUR) !== 0) {
+ throw new WavFileException('Seeking to data offset failed.');
+ }
+
+ // read data
+ $this->_samples .= fread($this->_fp, $dataSize); // allow appending
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ // close file or memory stream
+ return $this->closeWav();
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Sample manipulation methods
+
+ /**
+ * Return a single sample block from the file.
+ *
+ * @param int $blockNum (Required) The sample block number. Zero based.
+ * @return string|null The binary sample block (all channels). Returns null if the sample block number was out of range.
+ */
+ public function getSampleBlock($blockNum)
+ {
+ // check preconditions
+ if (!$this->_dataSize_valid) {
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+ }
+
+ $offset = $blockNum * $this->_blockAlign;
+ if ($offset + $this->_blockAlign > $this->_dataSize || $offset < 0) {
+ return null;
+ }
+
+
+ // read data
+ return substr($this->_samples, $offset, $this->_blockAlign);
+ }
+
+ /**
+ * Set a single sample block. <br />
+ * Allows to append a sample block.
+ *
+ * @param string $sampleBlock (Required) The binary sample block (all channels).
+ * @param int $blockNum (Required) The sample block number. Zero based.
+ * @throws WavFileException
+ */
+ public function setSampleBlock($sampleBlock, $blockNum)
+ {
+ // check preconditions
+ $blockAlign = $this->_blockAlign;
+ if (!isset($sampleBlock[$blockAlign - 1]) || isset($sampleBlock[$blockAlign])) { // faster than: if (strlen($sampleBlock) != $blockAlign)
+ throw new WavFileException('Incorrect sample block size. Got ' . strlen($sampleBlock) . ', expected ' . $blockAlign . '.');
+ }
+
+ if (!$this->_dataSize_valid) {
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+ }
+
+ $numBlocks = (int)($this->_dataSize / $blockAlign);
+ $offset = $blockNum * $blockAlign;
+ if ($blockNum > $numBlocks || $blockNum < 0) { // allow appending
+ throw new WavFileException('Sample block number is out of range.');
+ }
+
+
+ // replace or append data
+ if ($blockNum == $numBlocks) {
+ // append
+ $this->_samples .= $sampleBlock;
+ $this->_dataSize += $blockAlign;
+ $this->_chunkSize += $blockAlign;
+ $this->_actualSize += $blockAlign;
+ $this->_numBlocks++;
+ } else {
+ // replace
+ for ($i = 0; $i < $blockAlign; ++$i) {
+ $this->_samples[$offset + $i] = $sampleBlock[$i];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get a float sample value for a specific sample block and channel number.
+ *
+ * @param int $blockNum (Required) The sample block number to fetch. Zero based.
+ * @param int $channelNum (Required) The channel number within the sample block to fetch. First channel is 1.
+ * @return float|null The float sample value. Returns null if the sample block number was out of range.
+ * @throws WavFileException
+ */
+ public function getSampleValue($blockNum, $channelNum)
+ {
+ // check preconditions
+ if ($channelNum < 1 || $channelNum > $this->_numChannels) {
+ throw new WavFileException('Channel number is out of range.');
+ }
+
+ if (!$this->_dataSize_valid) {
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+ }
+
+ $sampleBytes = $this->_bitsPerSample / 8;
+ $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
+ if ($offset + $sampleBytes > $this->_dataSize || $offset < 0) {
+ return null;
+ }
+
+ // read binary value
+ $sampleBinary = substr($this->_samples, $offset, $sampleBytes);
+
+ // convert binary to value
+ switch ($this->_bitsPerSample) {
+ case 8:
+ // unsigned char
+ return (float)((ord($sampleBinary) - 0x80) / 0x80);
+
+ case 16:
+ // signed short, little endian
+ $data = unpack('v', $sampleBinary);
+ $sample = $data[1];
+ if ($sample >= 0x8000) {
+ $sample -= 0x10000;
+ }
+ return (float)($sample / 0x8000);
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ $data = unpack('C3', $sampleBinary);
+ $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
+ if ($sample >= 0x800000) {
+ $sample -= 0x1000000;
+ }
+ return (float)($sample / 0x800000);
+
+ case 32:
+ // 32-bit float
+ $data = unpack('f', $sampleBinary);
+ return (float)$data[1];
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Sets a float sample value for a specific sample block number and channel. <br />
+ * Converts float values to appropriate integer values and clips properly. <br />
+ * Allows to append samples (in order).
+ *
+ * @param float $sampleFloat (Required) The float sample value to set. Converts float values and clips if necessary.
+ * @param int $blockNum (Required) The sample block number to set or append. Zero based.
+ * @param int $channelNum (Required) The channel number within the sample block to set or append. First channel is 1.
+ * @throws WavFileException
+ */
+ public function setSampleValue($sampleFloat, $blockNum, $channelNum)
+ {
+ // check preconditions
+ if ($channelNum < 1 || $channelNum > $this->_numChannels) {
+ throw new WavFileException('Channel number is out of range.');
+ }
+
+ if (!$this->_dataSize_valid) {
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+ }
+
+ $dataSize = $this->_dataSize;
+ $bitsPerSample = $this->_bitsPerSample;
+ $sampleBytes = $bitsPerSample / 8;
+ $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
+ if (($offset + $sampleBytes > $dataSize && $offset != $dataSize) || $offset < 0) { // allow appending
+ throw new WavFileException('Sample block or channel number is out of range.');
+ }
+
+
+ // convert to value, quantize and clip
+ if ($bitsPerSample == 32) {
+ $sample = $sampleFloat < -1.0 ? -1.0 : ($sampleFloat > 1.0 ? 1.0 : $sampleFloat);
+ } else {
+ $p = 1 << ($bitsPerSample - 1); // 2 to the power of _bitsPerSample divided by 2
+
+ // project and quantize (round) float to integer values
+ $sample = $sampleFloat < 0 ? (int)($sampleFloat * $p - 0.5) : (int)($sampleFloat * $p + 0.5);
+
+ // clip if necessary to [-$p, $p - 1]
+ if ($sample < -$p) {
+ $sample = -$p;
+ } elseif ($sample > $p - 1) {
+ $sample = $p - 1;
+ }
+ }
+
+ // convert to binary
+ switch ($bitsPerSample) {
+ case 8:
+ // unsigned char
+ $sampleBinary = chr($sample + 0x80);
+ break;
+
+ case 16:
+ // signed short, little endian
+ if ($sample < 0) {
+ $sample += 0x10000;
+ }
+ $sampleBinary = pack('v', $sample);
+ break;
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ if ($sample < 0) {
+ $sample += 0x1000000;
+ }
+ $sampleBinary = pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
+ break;
+
+ case 32:
+ // 32-bit float
+ $sampleBinary = pack('f', $sample);
+ break;
+
+ default:
+ $sampleBinary = null;
+ $sampleBytes = 0;
+ break;
+ }
+
+ // replace or append data
+ if ($offset == $dataSize) {
+ // append
+ $this->_samples .= $sampleBinary;
+ $this->_dataSize += $sampleBytes;
+ $this->_chunkSize += $sampleBytes;
+ $this->_actualSize += $sampleBytes;
+ $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign);
+ } else {
+ // replace
+ for ($i = 0; $i < $sampleBytes; ++$i) {
+ $this->_samples{$offset + $i} = $sampleBinary{$i};
+ }
+ }
+
+ return $this;
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Audio processing methods
+
+ /**
+ * Run samples through audio processing filters.
+ *
+ * <code>
+ * $wav->filter(
+ * array(
+ * WavFile::FILTER_MIX => array( // Filter for mixing 2 WavFile instances.
+ * 'wav' => $wav2, // (Required) The WavFile to mix into this WhavFile. If no optional arguments are given, can be passed without the array.
+ * 'loop' => true, // (Optional) Loop the selected portion (with warping to the beginning at the end).
+ * 'blockOffset' => 0, // (Optional) Block number to start mixing from.
+ * 'numBlocks' => null // (Optional) Number of blocks to mix in or to select for looping. Defaults to the end or all data for looping.
+ * ),
+ * WavFile::FILTER_NORMALIZE => 0.6, // (Required) Normalization of (mixed) audio samples - see threshold parameter for normalizeSample().
+ * WavFile::FILTER_DEGRADE => 0.9 // (Required) Introduce random noise. The quality relative to the amplitude. 1 = no noise, 0 = max. noise.
+ * WavFile::FILTER_VOLUME => 1.0 // (Required) Amplify or attenuate the audio signal. Beware of clipping when amplifying. Values range from >= 0 - <= 2. 1 = no change in volume; 0.5 = 50% reduction of volume; 1.5 = 150% increase in volume.
+ * ),
+ * 0, // (Optional) The block number of this WavFile to start with.
+ * null // (Optional) The number of blocks to process.
+ * );
+ * </code>
+ *
+ * @param array $filters (Required) An array of 1 or more audio processing filters.
+ * @param int $blockOffset (Optional) The block number to start precessing from.
+ * @param int $numBlocks (Optional) The maximum number of blocks to process.
+ * @throws WavFileException
+ */
+ public function filter($filters, $blockOffset = 0, $numBlocks = null)
+ {
+ // check preconditions
+ $totalBlocks = $this->getNumBlocks();
+ $numChannels = $this->getNumChannels();
+ if (is_null($numBlocks)) $numBlocks = $totalBlocks - $blockOffset;
+
+ if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
+ // nothing to do
+ return $this;
+ }
+
+ // check filtes
+ $filter_mix = false;
+ if (array_key_exists(self::FILTER_MIX, $filters)) {
+ if (!is_array($filters[self::FILTER_MIX])) {
+ // assume the 'wav' parameter
+ $filters[self::FILTER_MIX] = array('wav' => $filters[self::FILTER_MIX]);
+ }
+
+ $mix_wav = @$filters[self::FILTER_MIX]['wav'];
+ if (!($mix_wav instanceof WavFile)) {
+ throw new WavFileException("WavFile to mix is missing or invalid.");
+ } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
+ throw new WavFileException("Sample rate of WavFile to mix does not match.");
+ } else if ($mix_wav->getNumChannels() != $this->getNumChannels()) {
+ throw new WavFileException("Number of channels of WavFile to mix does not match.");
+ }
+
+ $mix_loop = @$filters[self::FILTER_MIX]['loop'];
+ if (is_null($mix_loop)) $mix_loop = false;
+
+ $mix_blockOffset = @$filters[self::FILTER_MIX]['blockOffset'];
+ if (is_null($mix_blockOffset)) $mix_blockOffset = 0;
+
+ $mix_totalBlocks = $mix_wav->getNumBlocks();
+ $mix_numBlocks = @$filters[self::FILTER_MIX]['numBlocks'];
+ if (is_null($mix_numBlocks)) $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
+ $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
+
+ $filter_mix = true;
+ }
+
+ $filter_normalize = false;
+ if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
+ $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
+
+ if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) $filter_normalize = true;
+ }
+
+ $filter_degrade = false;
+ if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
+ $degrade_quality = @$filters[self::FILTER_DEGRADE];
+ if (is_null($degrade_quality)) $degrade_quality = 1;
+
+ if ($degrade_quality >= 0 && $degrade_quality < 1) $filter_degrade = true;
+ }
+
+ $filter_vol = false;
+ if (array_key_exists(self::FILTER_VOLUME, $filters)) {
+ $volume_amount = @$filters[self::FILTER_VOLUME];
+ if (is_null($volume_amount)) $volume_amount = 1;
+
+ if ($volume_amount >= 0 && $volume_amount <= 2 && $volume_amount != 1.0) {
+ $filter_vol = true;
+ }
+ }
+
+
+ // loop through all sample blocks
+ for ($block = 0; $block < $numBlocks; ++$block) {
+ // loop through all channels
+ for ($channel = 1; $channel <= $numChannels; ++$channel) {
+ // read current sample
+ $currentBlock = $blockOffset + $block;
+ $sampleFloat = $this->getSampleValue($currentBlock, $channel);
+
+
+ /************* MIX FILTER ***********************/
+ if ($filter_mix) {
+ if ($mix_loop) {
+ $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
+ } else {
+ $mixBlock = $mix_blockOffset + $block;
+ }
+
+ if ($mixBlock < $mix_maxBlock) {
+ $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
+ }
+ }
+
+ /************* NORMALIZE FILTER *******************/
+ if ($filter_normalize) {
+ $sampleFloat = $this->normalizeSample($sampleFloat, $normalize_threshold);
+ }
+
+ /************* DEGRADE FILTER *******************/
+ if ($filter_degrade) {
+ $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
+ }
+
+ /************* VOLUME FILTER *******************/
+ if ($filter_vol) {
+ $sampleFloat *= $volume_amount;
+ }
+
+ // write current sample
+ $this->setSampleValue($sampleFloat, $currentBlock, $channel);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Append a wav file to the current wav. <br />
+ * The wav files must have the same sample rate, number of bits per sample, and number of channels.
+ *
+ * @param WavFile $wav (Required) The wav file to append.
+ * @throws WavFileException
+ */
+ public function appendWav(WavFile $wav) {
+ // basic checks
+ if ($wav->getSampleRate() != $this->getSampleRate()) {
+ throw new WavFileException("Sample rate for wav files do not match.");
+ } else if ($wav->getBitsPerSample() != $this->getBitsPerSample()) {
+ throw new WavFileException("Bits per sample for wav files do not match.");
+ } else if ($wav->getNumChannels() != $this->getNumChannels()) {
+ throw new WavFileException("Number of channels for wav files do not match.");
+ }
+
+ $this->_samples .= $wav->_samples;
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ return $this;
+ }
+
+ /**
+ * Mix 2 wav files together. <br />
+ * Both wavs must have the same sample rate and same number of channels.
+ *
+ * @param WavFile $wav (Required) The WavFile to mix.
+ * @param float $normalizeThreshold (Optional) See normalizeSample for an explanation.
+ * @throws WavFileException
+ */
+ public function mergeWav(WavFile $wav, $normalizeThreshold = null) {
+ return $this->filter(array(
+ WavFile::FILTER_MIX => $wav,
+ WavFile::FILTER_NORMALIZE => $normalizeThreshold
+ ));
+ }
+
+ /**
+ * Add silence to the wav file.
+ *
+ * @param float $duration (Optional) How many seconds of silence. If negative, add to the beginning of the file. Defaults to 1s.
+ */
+ public function insertSilence($duration = 1.0)
+ {
+ $numSamples = (int)($this->getSampleRate() * abs($duration));
+ $numChannels = $this->getNumChannels();
+
+ $data = str_repeat(self::packSample($this->getZeroAmplitude(), $this->getBitsPerSample()), $numSamples * $numChannels);
+ if ($duration >= 0) {
+ $this->_samples .= $data;
+ } else {
+ $this->_samples = $data . $this->_samples;
+ }
+
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ return $this;
+ }
+
+ /**
+ * Degrade the quality of the wav file by introducing random noise.
+ *
+ * @param float quality (Optional) The quality relative to the amplitude. 1 = no noise, 0 = max. noise.
+ */
+ public function degrade($quality = 1.0)
+ {
+ return $this->filter(array(
+ self::FILTER_DEGRADE => $quality
+ ));
+ }
+
+ /**
+ * Generate noise at the end of the wav for the specified duration and volume.
+ *
+ * @param float $duration (Optional) Number of seconds of noise to generate.
+ * @param float $percent (Optional) The percentage of the maximum amplitude to use. 100 = full amplitude.
+ */
+ public function generateNoise($duration = 1.0, $percent = 100)
+ {
+ $numChannels = $this->getNumChannels();
+ $numSamples = $this->getSampleRate() * $duration;
+ $minAmp = $this->getMinAmplitude();
+ $maxAmp = $this->getMaxAmplitude();
+ $bitDepth = $this->getBitsPerSample();
+
+ for ($s = 0; $s < $numSamples; ++$s) {
+ if ($bitDepth == 32) {
+ $val = rand(-$percent * 10000, $percent * 10000) / 1000000;
+ } else {
+ $val = rand($minAmp, $maxAmp);
+ $val = (int)($val * $percent / 100);
+ }
+
+ $this->_samples .= str_repeat(self::packSample($val, $bitDepth), $numChannels);
+ }
+
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ return $this;
+ }
+
+ /**
+ * Convert sample data to different bits per sample.
+ *
+ * @param int $bitsPerSample (Required) The new number of bits per sample;
+ * @throws WavFileException
+ */
+ public function convertBitsPerSample($bitsPerSample) {
+ if ($this->getBitsPerSample() == $bitsPerSample) {
+ return $this;
+ }
+
+ $tempWav = new WavFile($this->getNumChannels(), $this->getSampleRate(), $bitsPerSample);
+ $tempWav->filter(
+ array(self::FILTER_MIX => $this),
+ 0,
+ $this->getNumBlocks()
+ );
+
+ $this->setSamples() // implicit setDataSize(), setSize(), setActualSize(), setNumBlocks()
+ ->setBitsPerSample($bitsPerSample); // implicit setValidBitsPerSample(), setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset(), setByteRate(), setBlockAlign(), setNumBlocks()
+ $this->_samples = $tempWav->_samples;
+ $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
+
+ return $this;
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Miscellaneous methods
+
+ /**
+ * Output information about the wav object.
+ */
+ public function displayInfo()
+ {
+ $s = "File Size: %u\n"
+ ."Chunk Size: %u\n"
+ ."fmt Subchunk Size: %u\n"
+ ."Extended fmt Size: %u\n"
+ ."fact Subchunk Size: %u\n"
+ ."Data Offset: %u\n"
+ ."Data Size: %u\n"
+ ."Audio Format: %s\n"
+ ."Audio SubFormat: %s\n"
+ ."Channels: %u\n"
+ ."Channel Mask: 0x%s\n"
+ ."Sample Rate: %u\n"
+ ."Bits Per Sample: %u\n"
+ ."Valid Bits Per Sample: %u\n"
+ ."Sample Block Size: %u\n"
+ ."Number of Sample Blocks: %u\n"
+ ."Byte Rate: %uBps\n";
+
+ $s = sprintf($s, $this->getActualSize(),
+ $this->getChunkSize(),
+ $this->getFmtChunkSize(),
+ $this->getFmtExtendedSize(),
+ $this->getFactChunkSize(),
+ $this->getDataOffset(),
+ $this->getDataSize(),
+ $this->getAudioFormat() == self::WAVE_FORMAT_PCM ? 'PCM' : ($this->getAudioFormat() == self::WAVE_FORMAT_IEEE_FLOAT ? 'IEEE FLOAT' : 'EXTENSIBLE'),
+ $this->getAudioSubFormat() == self::WAVE_SUBFORMAT_PCM ? 'PCM' : 'IEEE FLOAT',
+ $this->getNumChannels(),
+ dechex($this->getChannelMask()),
+ $this->getSampleRate(),
+ $this->getBitsPerSample(),
+ $this->getValidBitsPerSample(),
+ $this->getBlockAlign(),
+ $this->getNumBlocks(),
+ $this->getByteRate());
+
+ if (php_sapi_name() == 'cli') {
+ return $s;
+ } else {
+ return nl2br($s);
+ }
+ }
+}
+
+
+/*%******************************************************************************************%*/
+// Exceptions
+
+/**
+ * WavFileException indicates an illegal state or argument in this class.
+ */
+class WavFileException extends Exception {}
+
+/**
+ * WavFormatException indicates a malformed or unsupported wav file header.
+ */
+class WavFormatException extends WavFileException {}
diff --git a/vendor/dapphp/securimage/audio/.htaccess b/vendor/dapphp/securimage/audio/.htaccess
new file mode 100644
index 0000000..4fdb24a
--- /dev/null
+++ b/vendor/dapphp/securimage/audio/.htaccess
@@ -0,0 +1,11 @@
+# Deny access to this folder
+
+# Apache 2.4
+<IfModule mod_authz_core.c>
+ Require all denied
+</IfModule>
+
+# Apache 2.2
+<IfModule !mod_authz_core.c>
+ Deny from all
+</IfModule>
diff --git a/vendor/dapphp/securimage/composer.json b/vendor/dapphp/securimage/composer.json
new file mode 100644
index 0000000..ada7882
--- /dev/null
+++ b/vendor/dapphp/securimage/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "dapphp/securimage",
+ "type": "library",
+ "vesion": "3.6.3",
+ "description": "PHP CAPTCHA Library",
+ "keywords": ["captcha","security"],
+ "homepage": "https://www.phpcaptcha.org",
+ "license": "BSD",
+ "authors": [
+ {
+ "name": "Drew Phillips",
+ "email": "drew@drew-phillips.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.2.0",
+ "ext-gd": "*"
+ },
+ "suggest": {
+ "ext-pdo": "For database storage support",
+ "ext-pdo_mysql": "For MySQL database support",
+ "ext-pdo_sqlite": "For SQLite3 database support"
+ },
+ "autoload": {
+ "classmap": ["securimage.php"]
+ }
+}
diff --git a/vendor/dapphp/securimage/config.inc.php b/vendor/dapphp/securimage/config.inc.php
new file mode 100644
index 0000000..2c83f03
--- /dev/null
+++ b/vendor/dapphp/securimage/config.inc.php
@@ -0,0 +1 @@
+<?php return array("session_name"=>"flyspray"); ?>
diff --git a/vendor/dapphp/securimage/database/.htaccess b/vendor/dapphp/securimage/database/.htaccess
new file mode 100644
index 0000000..4fdb24a
--- /dev/null
+++ b/vendor/dapphp/securimage/database/.htaccess
@@ -0,0 +1,11 @@
+# Deny access to this folder
+
+# Apache 2.4
+<IfModule mod_authz_core.c>
+ Require all denied
+</IfModule>
+
+# Apache 2.2
+<IfModule !mod_authz_core.c>
+ Deny from all
+</IfModule>
diff --git a/vendor/dapphp/securimage/database/index.html b/vendor/dapphp/securimage/database/index.html
new file mode 100644
index 0000000..8d1c8b6
--- /dev/null
+++ b/vendor/dapphp/securimage/database/index.html
@@ -0,0 +1 @@
+
diff --git a/vendor/dapphp/securimage/database/securimage.sq3 b/vendor/dapphp/securimage/database/securimage.sq3
new file mode 100644
index 0000000..a3fcbd7
--- /dev/null
+++ b/vendor/dapphp/securimage/database/securimage.sq3
Binary files differ
diff --git a/vendor/dapphp/securimage/images/audio_icon.png b/vendor/dapphp/securimage/images/audio_icon.png
new file mode 100644
index 0000000..9922ef1
--- /dev/null
+++ b/vendor/dapphp/securimage/images/audio_icon.png
Binary files differ
diff --git a/vendor/dapphp/securimage/images/loading.png b/vendor/dapphp/securimage/images/loading.png
new file mode 100644
index 0000000..1711568
--- /dev/null
+++ b/vendor/dapphp/securimage/images/loading.png
Binary files differ
diff --git a/vendor/dapphp/securimage/images/refresh.png b/vendor/dapphp/securimage/images/refresh.png
new file mode 100644
index 0000000..f5e7d82
--- /dev/null
+++ b/vendor/dapphp/securimage/images/refresh.png
Binary files differ
diff --git a/vendor/dapphp/securimage/securimage.css b/vendor/dapphp/securimage/securimage.css
new file mode 100644
index 0000000..0cffdb9
--- /dev/null
+++ b/vendor/dapphp/securimage/securimage.css
@@ -0,0 +1,41 @@
+@CHARSET "UTF-8";
+
+@-webkit-keyframes rotating /* Safari and Chrome */ {
+ from {
+ -ms-transform: rotate(0deg);
+ -moz-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ to {
+ -ms-transform: rotate(360deg);
+ -moz-transform: rotate(360deg);
+ -webkit-transform: rotate(360deg);
+ -o-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+@keyframes rotating {
+ from {
+ -ms-transform: rotate(0deg);
+ -moz-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ to {
+ -ms-transform: rotate(360deg);
+ -moz-transform: rotate(360deg);
+ -webkit-transform: rotate(360deg);
+ -o-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+.rotating {
+ -webkit-animation: rotating 1.5s linear infinite;
+ -moz-animation: rotating 1.5s linear infinite;
+ -ms-animation: rotating 1.5s linear infinite;
+ -o-animation: rotating 1.5s linear infinite;
+ animation: rotating 1.5s linear infinite;
+} \ No newline at end of file
diff --git a/vendor/dapphp/securimage/securimage.js b/vendor/dapphp/securimage/securimage.js
new file mode 100644
index 0000000..481e9e6
--- /dev/null
+++ b/vendor/dapphp/securimage/securimage.js
@@ -0,0 +1,252 @@
+/*!
+ * Securimage CAPTCHA Audio Library
+ * https://www.phpcaptcha.org/
+ *
+ * Copyright 2015 phpcaptcha.org
+ * Released under the BSD-3 license
+ * See https://github.com/dapphp/securimage/blob/master/README.md
+ */
+
+var SecurimageAudio = function(options) {
+ this.html5Support = true;
+ this.flashFallback = false;
+ this.captchaId = null;
+ this.playing = false;
+ this.reload = false;
+ this.audioElement = null;
+ this.controlsElement = null;
+ this.playButton = null;
+ this.playButtonImage = null;
+ this.loadingImage = null;
+
+ if (options.audioElement) {
+ this.audioElement = document.getElementById(options.audioElement);
+ }
+ if (options.controlsElement) {
+ this.controlsElement = document.getElementById(options.controlsElement);
+ }
+
+ this.init();
+}
+
+SecurimageAudio.prototype.init = function() {
+ var ua = navigator.userAgent.toLowerCase();
+ var ieVer = (ua.indexOf('msie') != -1) ? parseInt(ua.split('msie')[1]) : false;
+ // ie 11+ detection
+ if (!ieVer && null != (ieVer = ua.match(/trident\/.*rv:(\d+\.\d+)/)))
+ ieVer = parseInt(ieVer[1]);
+
+ var objAu = this.audioElement.getElementsByTagName('object');
+ if (objAu.length > 0) {
+ objAu = objAu[0];
+ } else {
+ objAu = null;
+ }
+
+ if (ieVer) {
+ if (ieVer < 9) {
+ // no html5 audio support, hide player controls
+ this.controlsElement.style.display = 'none';
+ this.html5Support = false;
+ return ;
+ } else if ('' == this.audioElement.canPlayType('audio/wav')) {
+ // check for mpeg <source> tag - if not found then fallback to flash
+ var sources = this.audioElement.getElementsByTagName('source');
+ var mp3support = false;
+ var type;
+
+ if (objAu) {
+ this.flashFallback = true;
+ }
+
+ for (var i = 0; i < sources.length; ++i) {
+ type = sources[i].attributes["type"].value;
+ if (type.toLowerCase().indexOf('mpeg') >= 0 || type.toLowerCase().indexOf('mp3') >= 0) {
+ mp3support = true;
+ break;
+ }
+ }
+
+ if (false == mp3support) {
+ // browser supports <audio> but does not support WAV audio and no flash audio available
+ this.html5Support = false;
+
+ if (this.flashFallback) {
+ // ie9+? bug - flash object does not display when moved from within audio tag to other dom node
+ var newObjAu = document.createElement('object');
+ var newParams = document.createElement('param');
+ var oldParams = objAu.getElementsByTagName('param');
+ this.copyElementAttributes(newObjAu, objAu);
+ if (oldParams.length > 0) {
+ this.copyElementAttributes(newParams, oldParams[0]);
+ newObjAu.appendChild(newParams);
+ }
+ objAu.parentNode.removeChild(objAu);
+ this.audioElement.parentNode.appendChild(newObjAu);
+ }
+
+ this.audioElement.parentNode.removeChild(this.audioElement);
+ this.controlsElement.parentNode.removeChild(this.controlsElement);
+
+ return ;
+ }
+ }
+ }
+
+ this.audioElement.addEventListener('playing', this.updateControls.bind(this), false);
+ this.audioElement.addEventListener('ended', this.audioStopped.bind(this), false);
+
+ // find the element used as the play button and register click event to play/stop audio
+ var children = this.controlsElement.getElementsByTagName('*');
+ for (var i = 0; i < children.length; ++i) {
+ var el = children[i];
+ if (undefined != el.className) {
+ if (el.className.indexOf('play_button') >= 0) {
+ this.playButton = el;
+ el.addEventListener('click', this.play.bind(this), false);
+ } else if (el.className.indexOf('play_image') >= 0) {
+ this.playButtonImage = el;
+ } else if (el.className.indexOf('loading_image') >= 0) {
+ this.loadingImage = el;
+ }
+ }
+ }
+
+ if (objAu) {
+ // remove flash object from DOM
+ objAu.parentNode.removeChild(objAu);
+ }
+}
+
+SecurimageAudio.prototype.play = function(evt) {
+ if (null != this.playButton) {
+ this.playButton.blur();
+ }
+
+ if (this.reload) {
+ this.replaceElements();
+ this.reload = false;
+ }
+
+ try {
+ if (!this.playing) {
+ if (this.playButtonImage != null) {
+ this.playButtonImage.style.display = 'none';
+ }
+ if (this.loadingImage != null) {
+ this.loadingImage.style.display = '';
+ }
+ //TODO: FIX, most likely browser doesn't support audio type
+ this.audioElement.onerror = this.audioError;
+ try {
+ this.audioElement.play();
+ } catch(ex) {
+ alert('Audio error: ' + ex);
+ }
+ } else {
+ this.audioElement.pause();
+ if (this.loadingImage != null) {
+ this.loadingImage.style.display = 'none';
+ }
+ if (this.playButtonImage != null) {
+ this.playButtonImage.style.display = '';
+ }
+ this.playing = false;
+ }
+ } catch (ex) {
+ alert('Audio error: ' + ex);
+ }
+
+ if (undefined !== evt) {
+ evt.preventDefault();
+ }
+ return false;
+}
+
+SecurimageAudio.prototype.refresh = function(captchaId) {
+ if (!this.html5Support) {
+ return;
+ }
+
+ if (undefined !== captchaId) {
+ this.captchaId = captchaId;
+ }
+
+ this.playing = true;
+ this.reload = false;
+ this.play(); // stops audio if playing
+ this.reload = true;
+
+ return false;
+}
+
+SecurimageAudio.prototype.copyElementAttributes = function(newEl, el) {
+ for (var i = 0, atts = el.attributes, n = atts.length; i < n; ++i) {
+ newEl.setAttribute(atts[i].nodeName, atts[i].value);
+ }
+
+ return newEl;
+}
+
+SecurimageAudio.prototype.replaceElements = function() {
+ var parent = this.audioElement.parentNode;
+ parent.removeChild(this.audioElement);
+
+ var newAudioEl = document.createElement('audio');
+ newAudioEl.setAttribute('style', 'display: none;');
+ newAudioEl.setAttribute('preload', 'false');
+ newAudioEl.setAttribute('id', this.audioElement.id);
+
+ for (var c = 0; c < this.audioElement.children.length; ++c) {
+ if (this.audioElement.children[c].tagName.toLowerCase() != 'source') continue;
+ var sourceEl = document.createElement('source');
+ this.copyElementAttributes(sourceEl, this.audioElement.children[c]);
+ var cid = (null !== this.captchaId) ? this.captchaId : (Math.random() + '').replace('0.', '');
+ sourceEl.src = sourceEl.src.replace(/([?|&])id=[a-zA-Z0-9]+/, '$1id=' + cid);
+ newAudioEl.appendChild(sourceEl);
+ }
+
+ this.audioElement = null;
+ this.audioElement = newAudioEl;
+ parent.appendChild(this.audioElement);
+
+ this.audioElement.addEventListener('playing', this.updateControls.bind(this), false);
+ this.audioElement.addEventListener('ended', this.audioStopped.bind(this), false);
+}
+
+SecurimageAudio.prototype.updateControls = function() {
+ this.playing = true;
+ if (this.loadingImage != null) {
+ this.loadingImage.style.display = 'none';
+ }
+ if (this.playButtonImage != null) {
+ this.playButtonImage.style.display = '';
+ }
+}
+
+SecurimageAudio.prototype.audioStopped = function() {
+ this.playing = false;
+}
+
+SecurimageAudio.prototype.audioError = function(err) {
+ var msg = null;
+ switch(err.target.error.code) {
+ case err.target.error.MEDIA_ERR_ABORTED:
+ break;
+ case err.target.error.MEDIA_ERR_NETWORK:
+ msg = 'A network error caused the audio download to fail.';
+ break;
+ case err.target.error.MEDIA_ERR_DECODE:
+ alert('An error occurred while decoding the audio');
+ break;
+ case err.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
+ alert('The audio format is not supported by your browser.');
+ break;
+ default:
+ alert('An unknown error occurred trying to play the audio.');
+ break;
+ }
+ if (msg) {
+ alert('Audio playback error: ' + msg);
+ }
+}
diff --git a/vendor/dapphp/securimage/securimage.php b/vendor/dapphp/securimage/securimage.php
new file mode 100644
index 0000000..1582b81
--- /dev/null
+++ b/vendor/dapphp/securimage/securimage.php
@@ -0,0 +1,3468 @@
+<?php
+
+// error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
+
+/**
+ * Project: Securimage: A PHP class dealing with CAPTCHA images, audio, and validation
+ * File: securimage.php
+ *
+ * Copyright (c) 2017, Drew Phillips
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Any modifications to the library should be indicated clearly in the source code
+ * to inform users that the changes are not a part of the original software.
+ *
+ * If you found this script useful, please take a quick moment to rate it.
+ * http://www.hotscripts.com/rate/49400.html Thanks.
+ *
+ * @link http://www.phpcaptcha.org Securimage PHP CAPTCHA
+ * @link http://www.phpcaptcha.org/latest.zip Download Latest Version
+ * @link http://www.phpcaptcha.org/Securimage_Docs/ Online Documentation
+ * @copyright 2017 Drew Phillips
+ * @author Drew Phillips <drew@drew-phillips.com>
+ * @version 3.6.6 (Nov 20 2017)
+ * @package Securimage
+ *
+ */
+
+/**
+
+ ChangeLog
+ 3.6.6
+ - Not critical: Fix potential HTML injection in example form via HTTP_USER_AGENT (CVE-2017-14077)
+
+ 3.6.5
+ - Fix regex in replaceElements in securimage.js
+ - Update examples
+ - Exclude certain examples from Git autogenerated archives
+
+ 3.6.4
+ - Fix XSS vulnerability in example_form.ajax.php (Discovered by RedTeam. advisory rt-sa-2016-002)
+ - Update example_form.ajax.php to use Securimage::getCaptchaHtml()
+
+ 3.6.3
+ - Add support for multibyte wordlist files
+ - Fix code generation issues with UTF-8 charsets
+ - Add parameter to getCaptchaHtml() method to control display components of captcha HTML
+ - Fix database audio storage issue with multiple namespaces
+
+ 3.6.2
+ - Support HTTP range requests with audio playback (iOS requirement)
+ - Add optional config.inc.php for storing global configuration settings
+
+ 3.6.1
+ - Fix copyElement bug in securimage.js for IE Flash fallback
+
+ 3.6
+ - Implement CAPTCHA audio using HTML5 <audio> with optional Flash fallback
+ - Support MP3 audio using LAME MP3 Encoder (Internet Explorer 9+ does not support WAV format in <audio> tags)
+ - Add getCaptchaHtml() options to support full framework integration (ruifil)
+
+ 3.5.4
+ - Fix email validation code in example form files
+ - Fix backslashes in getCaptchaHtml for img attribute on Windows systems
+
+ 3.5.3
+ - Add options for audio button to getCaptchaHtml(), fix urlencoding of flash parameters that was breaking button
+
+ 3.5.2
+
+ - Add Securimage::getCaptchaHtml() for getting automatically generated captcha html code
+ - Option for using SoX to add effects to captcha audio to make identification by neural networks more difficult
+ - Add setNamespace() method
+ - Add getTimeToSolve() method
+ - Add session_status() check so session still starts if one had previously been opened and closed
+ - Add .htaccess file to audio directory to deny access; update audio files
+ - Option to skip checking of database tables during connection
+ - Add composer.json to package, submit to packagist
+ - Add font_ratio variable to determine size of font (github.com/wilkor)
+ - Add hint if sqlite3 database is not writeable. Improve database error handling, add example database options to securimage_play.php
+ - Fixed issue regarding database storage and math captcha breaking audio output (github.com/SoftwareAndOutsourcing)
+
+ 3.5.1
+ - Fix XSS vulnerability in example_form.php (discovered by Gjoko Krstic - <gjoko@zeroscience.mk>)
+
+ 3.5
+ - Release new version
+ - MB string support for charlist
+ - Modify audio file path to use language directories
+ - Changed default captcha appearance
+
+ 3.2RC4
+ - Add MySQL, PostgreSQL, and SQLite3 support for database storage
+ - Deprecate "use_sqlite_db" option and remove SQLite2/sqlite_* functions
+ - Add new captcha type that displays 2 dictionary words on one image
+ - Update examples
+
+ 3.2RC3
+ - Fix canSendHeaders() check which was breaking if a PHP startup error was issued
+
+ 3.2RC2
+ - Add error handler (https://github.com/dapphp/securimage/issues/15)
+ - Fix flash examples to use the correct value name for audio parameter
+
+ 3.2RC1
+ - New audio captcha code. Faster, fully dynamic audio, full WAV support
+ (Paul Voegler, Drew Phillips) <http://voegler.eu/pub/audio>
+ - New Flash audio streaming button. User defined image and size supported
+ - Additional options for customizing captcha (noise_level, send_headers,
+ no_exit, no_session, display_value
+ - Add captcha ID support. Uses sqlite and unique captcha IDs to track captchas,
+ no session used
+ - Add static methods for creating and validating captcha by ID
+ - Automatic clearing of old codes from SQLite database
+
+ 3.0.3Beta
+ - Add improved mixing function to WavFile class (Paul Voegler)
+ - Improve performance and security of captcha audio (Paul Voegler, Drew Phillips)
+ - Add option to use random file as background noise in captcha audio
+ - Add new securimage options for audio files
+
+ 3.0.2Beta
+ - Fix issue with session variables when upgrading from 2.0 - 3.0
+ - Improve audio captcha, switch to use WavFile class, make mathematical captcha audio work
+
+ 3.0.1
+ - Bugfix: removed use of deprecated variable in addSignature method that would cause errors with display_errors on
+
+ 3.0
+ - Rewrite class using PHP5 OOP
+ - Remove support for GD fonts, require FreeType
+ - Remove support for multi-color codes
+ - Add option to make codes case-sensitive
+ - Add namespaces to support multiple captchas on a single page or page specific captchas
+ - Add option to show simple math problems instead of codes
+ - Remove support for mp3 files due to vulnerability in decoding mp3 audio files
+ - Create new flash file to stream wav files instead of mp3
+ - Changed to BSD license
+
+ 2.0.2
+ - Fix pathing to make integration into libraries easier (Nathan Phillip Brink ohnobinki@ohnopublishing.net)
+
+ 2.0.1
+ - Add support for browsers with cookies disabled (requires php5, sqlite) maps users to md5 hashed ip addresses and md5 hashed codes for security
+ - Add fallback to gd fonts if ttf support is not enabled or font file not found (Mike Challis http://www.642weather.com/weather/scripts.php)
+ - Check for previous definition of image type constants (Mike Challis)
+ - Fix mime type settings for audio output
+ - Fixed color allocation issues with multiple colors and background images, consolidate allocation to one function
+ - Ability to let codes expire after a given length of time
+ - Allow HTML color codes to be passed to Securimage_Color (suggested by Mike Challis)
+
+ 2.0.0
+ - Add mathematical distortion to characters (using code from HKCaptcha)
+ - Improved session support
+ - Added Securimage_Color class for easier color definitions
+ - Add distortion to audio output to prevent binary comparison attack (proposed by Sven "SavageTiger" Hagemann [insecurity.nl])
+ - Flash button to stream mp3 audio (Douglas Walsh www.douglaswalsh.net)
+ - Audio output is mp3 format by default
+ - Change font to AlteHaasGrotesk by yann le coroller
+ - Some code cleanup
+
+ 1.0.4 (unreleased)
+ - Ability to output audible codes in mp3 format to stream from flash
+
+ 1.0.3.1
+ - Error reading from wordlist in some cases caused words to be cut off 1 letter short
+
+ 1.0.3
+ - Removed shadow_text from code which could cause an undefined property error due to removal from previous version
+
+ 1.0.2
+ - Audible CAPTCHA Code wav files
+ - Create codes from a word list instead of random strings
+
+ 1.0
+ - Added the ability to use a selected character set, rather than a-z0-9 only.
+ - Added the multi-color text option to use different colors for each letter.
+ - Switched to automatic session handling instead of using files for code storage
+ - Added GD Font support if ttf support is not available. Can use internal GD fonts or load new ones.
+ - Added the ability to set line thickness
+ - Added option for drawing arced lines over letters
+ - Added ability to choose image type for output
+
+ */
+
+
+/**
+ * Securimage CAPTCHA Class.
+ *
+ * A class for creating and validating secure CAPTCHA images and audio.
+ *
+ * The class contains many options regarding appearance, security, storage of
+ * captcha data and image/audio generation options.
+ *
+* @package Securimage
+ * @subpackage classes
+ * @author Drew Phillips <drew@drew-phillips.com>
+ *
+ */
+class Securimage
+{
+ // All of the public variables below are securimage options
+ // They can be passed as an array to the Securimage constructor, set below,
+ // or set from securimage_show.php and securimage_play.php
+
+ /**
+ * Constant for rendering captcha as a JPEG image
+ * @var int
+ */
+ const SI_IMAGE_JPEG = 1;
+
+ /**
+ * Constant for rendering captcha as a PNG image (default)
+ * @var int
+ */
+
+ const SI_IMAGE_PNG = 2;
+ /**
+ * Constant for rendering captcha as a GIF image
+ * @var int
+ */
+ const SI_IMAGE_GIF = 3;
+
+ /**
+ * Constant for generating a normal alphanumeric captcha based on the
+ * character set
+ *
+ * @see Securimage::$charset charset property
+ * @var int
+ */
+ const SI_CAPTCHA_STRING = 0;
+
+ /**
+ * Constant for generating a captcha consisting of a simple math problem
+ *
+ * @var int
+ */
+ const SI_CAPTCHA_MATHEMATIC = 1;
+
+ /**
+ * Constant for generating a word based captcha using 2 words from a list
+ *
+ * @var int
+ */
+ const SI_CAPTCHA_WORDS = 2;
+
+ /**
+ * MySQL option identifier for database storage option
+ *
+ * @var string
+ */
+ const SI_DRIVER_MYSQL = 'mysql';
+
+ /**
+ * PostgreSQL option identifier for database storage option
+ *
+ * @var string
+ */
+ const SI_DRIVER_PGSQL = 'pgsql';
+
+ /**
+ * SQLite option identifier for database storage option
+ *
+ * @var string
+ */
+ const SI_DRIVER_SQLITE3 = 'sqlite';
+
+ /**
+ * getCaptchaHtml() display constant for HTML Captcha Image
+ *
+ * @var integer
+ */
+ const HTML_IMG = 1;
+
+ /**
+ * getCaptchaHtml() display constant for HTML5 Audio code
+ *
+ * @var integer
+ */
+ const HTML_AUDIO = 2;
+
+ /**
+ * getCaptchaHtml() display constant for Captcha Input text box
+ *
+ * @var integer
+ */
+ const HTML_INPUT = 4;
+
+ /**
+ * getCaptchaHtml() display constant for Captcha Text HTML label
+ *
+ * @var integer
+ */
+ const HTML_INPUT_LABEL = 8;
+
+ /**
+ * getCaptchaHtml() display constant for HTML Refresh button
+ *
+ * @var integer
+ */
+ const HTML_ICON_REFRESH = 16;
+
+ /**
+ * getCaptchaHtml() display constant for all HTML elements (default)
+ *
+ * @var integer
+ */
+ const HTML_ALL = 0xffffffff;
+
+ /*%*********************************************************************%*/
+ // Properties
+
+ /**
+ * The width of the captcha image
+ * @var int
+ */
+ public $image_width = 215;
+
+ /**
+ * The height of the captcha image
+ * @var int
+ */
+ public $image_height = 80;
+
+ /**
+ * Font size is calculated by image height and this ratio. Leave blank for
+ * default ratio of 0.4.
+ *
+ * Valid range: 0.1 - 0.99.
+ *
+ * Depending on image_width, values > 0.6 are probably too large and
+ * values < 0.3 are too small.
+ *
+ * @var float
+ */
+ public $font_ratio;
+
+ /**
+ * The type of the image, default = png
+ *
+ * @see Securimage::SI_IMAGE_PNG SI_IMAGE_PNG
+ * @see Securimage::SI_IMAGE_JPEG SI_IMAGE_JPEG
+ * @see Securimage::SI_IMAGE_GIF SI_IMAGE_GIF
+ * @var int
+ */
+ public $image_type = self::SI_IMAGE_PNG;
+
+ /**
+ * The background color of the captcha
+ * @var Securimage_Color|string
+ */
+ public $image_bg_color = '#ffffff';
+
+ /**
+ * The color of the captcha text
+ * @var Securimage_Color|string
+ */
+ public $text_color = '#707070';
+
+ /**
+ * The color of the lines over the captcha
+ * @var Securimage_Color|string
+ */
+ public $line_color = '#707070';
+
+ /**
+ * The color of the noise that is drawn
+ * @var Securimage_Color|string
+ */
+ public $noise_color = '#707070';
+
+ /**
+ * How transparent to make the text.
+ *
+ * 0 = completely opaque, 100 = invisible
+ *
+ * @var int
+ */
+ public $text_transparency_percentage = 20;
+
+ /**
+ * Whether or not to draw the text transparently.
+ *
+ * true = use transparency, false = no transparency
+ *
+ * @var bool
+ */
+ public $use_transparent_text = true;
+
+ /**
+ * The length of the captcha code
+ * @var int
+ */
+ public $code_length = 6;
+
+ /**
+ * Whether the captcha should be case sensitive or not.
+ *
+ * Not recommended, use only for maximum protection.
+ *
+ * @var bool
+ */
+ public $case_sensitive = false;
+
+ /**
+ * The character set to use for generating the captcha code
+ * @var string
+ */
+ public $charset = 'ABCDEFGHKLMNPRSTUVWYZabcdefghklmnprstuvwyz23456789';
+
+ /**
+ * How long in seconds a captcha remains valid, after this time it will be
+ * considered incorrect.
+ *
+ * @var int
+ */
+ public $expiry_time = 900;
+
+ /**
+ * The session name securimage should use.
+ *
+ * Only use if your application uses a custom session name (e.g. Joomla).
+ * It is recommended to set this value here so it is used by all securimage
+ * scripts (i.e. securimage_show.php)
+ *
+ * @var string
+ */
+ public $session_name = null;
+
+ /**
+ * true to use the wordlist file, false to generate random captcha codes
+ * @var bool
+ */
+ public $use_wordlist = false;
+
+ /**
+ * The level of distortion.
+ *
+ * 0.75 = normal, 1.0 = very high distortion
+ *
+ * @var double
+ */
+ public $perturbation = 0.85;
+
+ /**
+ * How many lines to draw over the captcha code to increase security
+ * @var int
+ */
+ public $num_lines = 5;
+
+ /**
+ * The level of noise (random dots) to place on the image, 0-10
+ * @var int
+ */
+ public $noise_level = 2;
+
+ /**
+ * The signature text to draw on the bottom corner of the image
+ * @var string
+ */
+ public $image_signature = '';
+
+ /**
+ * The color of the signature text
+ * @var Securimage_Color|string
+ */
+ public $signature_color = '#707070';
+
+ /**
+ * The path to the ttf font file to use for the signature text.
+ * Defaults to $ttf_file (AHGBold.ttf)
+ *
+ * @see Securimage::$ttf_file
+ * @var string
+ */
+ public $signature_font;
+
+ /**
+ * No longer used.
+ *
+ * Use an SQLite database to store data (for users that do not support cookies)
+ *
+ * @var bool
+ * @see Securimage::$database_driver database_driver property
+ * @deprecated 3.2RC4
+ */
+ public $use_sqlite_db = false;
+
+ /**
+ * Use a database backend for code storage.
+ * Provides a fallback to users with cookies disabled.
+ * Required when using captcha IDs.
+ *
+ * @see Securimage::$database_driver
+ * @var bool
+ */
+ public $use_database = false;
+
+ /**
+ * Whether or not to skip checking if Securimage tables exist when using a
+ * database.
+ *
+ * Turn this to true once database functionality is working to improve
+ * performance.
+ *
+ * @var bool true to not check if captcha_codes tables are set up, false
+ * to check (and create if necessary)
+ */
+ public $skip_table_check = false;
+
+ /**
+ * Database driver to use for database support.
+ * Allowable values: *mysql*, *pgsql*, *sqlite*.
+ * Default: sqlite
+ *
+ * @var string
+ */
+ public $database_driver = self::SI_DRIVER_SQLITE3;
+
+ /**
+ * Database host to connect to when using mysql or postgres
+ *
+ * On Linux use "localhost" for Unix domain socket, otherwise uses TCP/IP
+ *
+ * Does not apply to SQLite
+ *
+ * @var string
+ */
+ public $database_host = 'localhost';
+
+ /**
+ * Database username for connection (mysql, postgres only)
+ * Default is an empty string
+ *
+ * @var string
+ */
+ public $database_user = '';
+
+ /**
+ * Database password for connection (mysql, postgres only)
+ * Default is empty string
+ *
+ * @var string
+ */
+ public $database_pass = '';
+
+ /**
+ * Name of the database to select (mysql, postgres only)
+ *
+ * @see Securimage::$database_file for SQLite
+ * @var string
+ */
+ public $database_name = '';
+
+ /**
+ * Database table where captcha codes are stored
+ *
+ * Note: Securimage will attempt to create this table for you if it does
+ * not exist. If the table cannot be created, an E_USER_WARNING is emitted
+ *
+ * @var string
+ */
+ public $database_table = 'captcha_codes';
+
+ /**
+ * Fully qualified path to the database file when using SQLite3.
+ *
+ * This value is only used when $database_driver == sqlite and does
+ * not apply when no database is used, or when using MySQL or PostgreSQL.
+ *
+ * On *nix, file must have permissions of 0666.
+ *
+ * **Make sure the directory containing this file is NOT web accessible**
+ *
+ * @var string
+ */
+ public $database_file;
+
+ /**
+ * The type of captcha to create.
+ *
+ * Either alphanumeric based on *charset*, a simple math problem, or an
+ * image consisting of 2 words from the word list.
+ *
+ * @see Securimage::SI_CAPTCHA_STRING SI_CAPTCHA_STRING
+ * @see Securimage::SI_CAPTCHA_MATHEMATIC SI_CAPTCHA_MATHEMATIC
+ * @see Securimage::SI_CAPTCHA_WORDS SI_CAPTCHA_WORDS
+ * @see Securimage::$charset charset property
+ * @see Securimage::$wordlist_file wordlist_file property
+ * @var int
+ */
+ public $captcha_type = self::SI_CAPTCHA_STRING; // or self::SI_CAPTCHA_MATHEMATIC, or self::SI_CAPTCHA_WORDS;
+
+ /**
+ * The captcha namespace used for having multiple captchas on a page or
+ * to separate captchas from differen forms on your site.
+ * Example:
+ *
+ * <?php
+ * // use <img src="securimage_show.php?namespace=contact_form">
+ * // or manually in securimage_show.php
+ * $img->setNamespace('contact_form');
+ *
+ * // in form validator
+ * $img->setNamespace('contact_form');
+ * if ($img->check($code) == true) {
+ * echo "Valid!";
+ * }
+ *
+ * @var string
+ */
+ public $namespace;
+
+ /**
+ * The TTF font file to use to draw the captcha code.
+ *
+ * Leave blank for default font AHGBold.ttf
+ *
+ * @var string
+ */
+ public $ttf_file;
+
+ /**
+ * The path to the wordlist file to use.
+ *
+ * Leave blank for default words/words.txt
+ *
+ * @var string
+ */
+ public $wordlist_file;
+
+ /**
+ * Character encoding of the wordlist file.
+ * Requires PHP Multibyte String (mbstring) support.
+ * Allows word list to contain characters other than US-ASCII (requires compatible TTF font).
+ *
+ * @var string The character encoding (e.g. UTF-8, UTF-7, EUC-JP, GB2312)
+ * @see http://php.net/manual/en/mbstring.supported-encodings.php
+ * @since 3.6.3
+ */
+ public $wordlist_file_encoding = null;
+
+ /**
+ * The directory to scan for background images, if set a random background
+ * will be chosen from this folder
+ *
+ * @var string
+ */
+ public $background_directory;
+
+ /**
+ * No longer used
+ *
+ * The path to the SQLite database file to use
+ *
+ * @deprecated 3.2RC4
+ * @see Securimage::$database_file database_file property
+ * @var string
+ */
+ public $sqlite_database;
+
+ /**
+ * The path to the audio files to be used for audio captchas.
+ *
+ * Can also be set in securimage_play.php
+ *
+ * Example:
+ *
+ * $img->audio_path = '/home/yoursite/public_html/securimage/audio/en/';
+ *
+ * @var string
+ */
+ public $audio_path;
+
+ /**
+ * Use SoX (The Swiss Army knife of audio manipulation) for audio effects
+ * and processing.
+ *
+ * Using SoX should make it more difficult for bots to solve audio captchas
+ *
+ * @see Securimage::$sox_binary_path sox_binary_path property
+ * @var bool true to use SoX, false to use PHP
+ */
+ public $audio_use_sox = false;
+
+ /**
+ * The path to the SoX binary on your system
+ *
+ * @var string
+ */
+ public $sox_binary_path = '/usr/bin/sox';
+
+ /**
+ * The path to the lame (mp3 encoder) binary on your system
+ * Static so that Securimage::getCaptchaHtml() has access to this value.
+ *
+ * @since 3.6
+ * @var string
+ */
+ public static $lame_binary_path = '/usr/bin/lame';
+
+ /**
+ * The path to the directory containing audio files that will be selected
+ * randomly and mixed with the captcha audio.
+ *
+ * @var string
+ */
+ public $audio_noise_path;
+
+ /**
+ * Whether or not to mix background noise files into captcha audio
+ *
+ * Mixing random background audio with noise can help improve security of
+ * audio captcha.
+ *
+ * Default: securimage/audio/noise
+ *
+ * @since 3.0.3
+ * @see Securimage::$audio_noise_path audio_noise_path property
+ * @var bool true = mix, false = no
+ */
+ public $audio_use_noise;
+
+ /**
+ * The method and threshold (or gain factor) used to normalize the mixing
+ * with background noise.
+ *
+ * See http://www.voegler.eu/pub/audio/ for more information.
+ *
+ * Default: 0.6
+ *
+ * Valid:
+ * >= 1
+ * Normalize by multiplying by the threshold (boost - positive gain).
+ * A value of 1 in effect means no normalization (and results in clipping).
+ *
+ * <= -1
+ * Normalize by dividing by the the absolute value of threshold (attenuate - negative gain).
+ * A factor of 2 (-2) is about 6dB reduction in volume.
+ *
+ * [0, 1) (open inverval - not including 1)
+ * The threshold above which amplitudes are comressed logarithmically.
+ * e.g. 0.6 to leave amplitudes up to 60% "as is" and compressabove.
+ *
+ * (-1, 0) (open inverval - not including -1 and 0)
+ * The threshold above which amplitudes are comressed linearly.
+ * e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above.
+ *
+ * @since 3.0.4
+ * @var float
+ */
+ public $audio_mix_normalization = 0.8;
+
+ /**
+ * Whether or not to degrade audio by introducing random noise.
+ *
+ * Current research shows this may not increase the security of audible
+ * captchas.
+ *
+ * Default: true
+ *
+ * @since 3.0.3
+ * @var bool
+ */
+ public $degrade_audio;
+
+ /**
+ * Minimum delay to insert between captcha audio letters in milliseconds
+ *
+ * @since 3.0.3
+ * @var float
+ */
+ public $audio_gap_min = 0;
+
+ /**
+ * Maximum delay to insert between captcha audio letters in milliseconds
+ *
+ * @since 3.0.3
+ * @var float
+ */
+ public $audio_gap_max = 3000;
+
+ /**
+ * Captcha ID if using static captcha
+ * @var string Unique captcha id
+ */
+ protected static $_captchaId = null;
+
+ /**
+ * The GD image resource of the captcha image
+ *
+ * @var resource
+ */
+ protected $im;
+
+ /**
+ * A temporary GD image resource of the captcha image for distortion
+ *
+ * @var resource
+ */
+ protected $tmpimg;
+
+ /**
+ * The background image GD resource
+ * @var string
+ */
+ protected $bgimg;
+
+ /**
+ * Scale factor for magnification of distorted captcha image
+ *
+ * @var int
+ */
+ protected $iscale = 5;
+
+ /**
+ * Absolute path to securimage directory.
+ *
+ * This is calculated at runtime
+ *
+ * @var string
+ */
+ public $securimage_path = null;
+
+ /**
+ * The captcha challenge value.
+ *
+ * Either the case-sensitive/insensitive word captcha, or the solution to
+ * the math captcha.
+ *
+ * @var string|bool Captcha challenge value
+ */
+ protected $code;
+
+ /**
+ * The display value of the captcha to draw on the image
+ *
+ * Either the word captcha or the math equation to present to the user
+ *
+ * @var string Captcha display value to draw on the image
+ */
+ protected $code_display;
+
+ /**
+ * Alternate text to draw as the captcha image text
+ *
+ * A value that can be passed to the constructor that can be used to
+ * generate a captcha image with a given value.
+ *
+ * This value does not get stored in the session or database and is only
+ * used when calling Securimage::show().
+ *
+ * If a display_value was passed to the constructor and the captcha image
+ * is generated, the display_value will be used as the string to draw on
+ * the captcha image.
+ *
+ * Used only if captcha codes are generated and managed by a 3rd party
+ * app/library
+ *
+ * @var string Captcha code value to display on the image
+ */
+ public $display_value;
+
+ /**
+ * Captcha code supplied by user [set from Securimage::check()]
+ *
+ * @var string
+ */
+ protected $captcha_code;
+
+ /**
+ * Time (in seconds) that the captcha was solved in (correctly or incorrectly).
+ *
+ * This is from the time of code creation, to when validation was attempted.
+ *
+ * @var int
+ */
+ protected $_timeToSolve = 0;
+
+ /**
+ * Flag that can be specified telling securimage not to call exit after
+ * generating a captcha image or audio file
+ *
+ * @var bool If true, script will not terminate; if false script will terminate (default)
+ */
+ protected $no_exit;
+
+ /**
+ * Flag indicating whether or not a PHP session should be started and used
+ *
+ * @var bool If true, no session will be started; if false, session will be started and used to store data (default)
+ */
+ protected $no_session;
+
+ /**
+ * Flag indicating whether or not HTTP headers will be sent when outputting
+ * captcha image/audio
+ *
+ * @var bool If true (default) headers will be sent, if false, no headers are sent
+ */
+ protected $send_headers;
+
+ /**
+ * PDO connection when a database is used
+ *
+ * @var PDO|bool
+ */
+ protected $pdo_conn;
+
+ /**
+ * The GD color for the background color
+ *
+ * @var int
+ */
+ protected $gdbgcolor;
+
+ /**
+ * The GD color for the text color
+ *
+ * @var int
+ */
+ protected $gdtextcolor;
+
+ /**
+ * The GD color for the line color
+ *
+ * @var int
+ */
+ protected $gdlinecolor;
+
+ /**
+ * The GD color for the signature text color
+ *
+ * @var int
+ */
+ protected $gdsignaturecolor;
+
+ /**
+ * Create a new securimage object, pass options to set in the constructor.
+ *
+ * The object can then be used to display a captcha, play an audible captcha, or validate a submission.
+ *
+ * @param array $options Options to initialize the class. May be any class property.
+ *
+ * $options = array(
+ * 'text_color' => new Securimage_Color('#013020'),
+ * 'code_length' => 5,
+ * 'num_lines' => 5,
+ * 'noise_level' => 3,
+ * 'font_file' => Securimage::getPath() . '/custom.ttf'
+ * );
+ *
+ * $img = new Securimage($options);
+ *
+ */
+ public function __construct($options = array())
+ {
+ $this->securimage_path = dirname(__FILE__);
+
+ if (!is_array($options)) {
+ trigger_error(
+ '$options passed to Securimage::__construct() must be an array. ' .
+ gettype($options) . ' given',
+ E_USER_WARNING
+ );
+ $options = array();
+ }
+
+ // check for and load settings from custom config file
+ if (file_exists(dirname(__FILE__) . '/config.inc.php')) {
+ $settings = include dirname(__FILE__) . '/config.inc.php';
+
+ if (is_array($settings)) {
+ $options = array_merge($settings, $options);
+ }
+ }
+
+ if (is_array($options) && sizeof($options) > 0) {
+ foreach($options as $prop => $val) {
+ if ($prop == 'captchaId') {
+ Securimage::$_captchaId = $val;
+ $this->use_database = true;
+ } else if ($prop == 'use_sqlite_db') {
+ trigger_error("The use_sqlite_db option is deprecated, use 'use_database' instead", E_USER_NOTICE);
+ } else {
+ $this->$prop = $val;
+ }
+ }
+ }
+
+ $this->image_bg_color = $this->initColor($this->image_bg_color, '#ffffff');
+ $this->text_color = $this->initColor($this->text_color, '#616161');
+ $this->line_color = $this->initColor($this->line_color, '#616161');
+ $this->noise_color = $this->initColor($this->noise_color, '#616161');
+ $this->signature_color = $this->initColor($this->signature_color, '#616161');
+
+ if (is_null($this->ttf_file)) {
+ $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
+ }
+
+ $this->signature_font = $this->ttf_file;
+
+ if (is_null($this->wordlist_file)) {
+ $this->wordlist_file = $this->securimage_path . '/words/words.txt';
+ }
+
+ if (is_null($this->database_file)) {
+ $this->database_file = $this->securimage_path . '/database/securimage.sq3';
+ }
+
+ if (is_null($this->audio_path)) {
+ $this->audio_path = $this->securimage_path . '/audio/en/';
+ }
+
+ if (is_null($this->audio_noise_path)) {
+ $this->audio_noise_path = $this->securimage_path . '/audio/noise/';
+ }
+
+ if (is_null($this->audio_use_noise)) {
+ $this->audio_use_noise = true;
+ }
+
+ if (is_null($this->degrade_audio)) {
+ $this->degrade_audio = true;
+ }
+
+ if (is_null($this->code_length) || (int)$this->code_length < 1) {
+ $this->code_length = 6;
+ }
+
+ if (is_null($this->perturbation) || !is_numeric($this->perturbation)) {
+ $this->perturbation = 0.75;
+ }
+
+ if (is_null($this->namespace) || !is_string($this->namespace)) {
+ $this->namespace = 'default';
+ }
+
+ if (is_null($this->no_exit)) {
+ $this->no_exit = false;
+ }
+
+ if (is_null($this->no_session)) {
+ $this->no_session = false;
+ }
+
+ if (is_null($this->send_headers)) {
+ $this->send_headers = true;
+ }
+
+ if ($this->no_session != true) {
+ // Initialize session or attach to existing
+ if ( session_id() == '' || (function_exists('session_status') && PHP_SESSION_NONE == session_status()) ) { // no session has been started yet (or it was previousy closed), which is needed for validation
+ if (!is_null($this->session_name) && trim($this->session_name) != '') {
+ session_name(trim($this->session_name)); // set session name if provided
+ }
+ session_start();
+ }
+ }
+ }
+
+ /**
+ * Return the absolute path to the Securimage directory.
+ *
+ * @return string The path to the securimage base directory
+ */
+ public static function getPath()
+ {
+ return dirname(__FILE__);
+ }
+
+ /**
+ * Generate a new captcha ID or retrieve the current ID (if exists).
+ *
+ * @param bool $new If true, generates a new challenge and returns and ID. If false, the existing captcha ID is returned, or null if none exists.
+ * @param array $options Additional options to be passed to Securimage.
+ * $options must include database settings if they are not set directly in securimage.php
+ *
+ * @return null|string Returns null if no captcha id set and new was false, or the captcha ID
+ */
+ public static function getCaptchaId($new = true, array $options = array())
+ {
+ if (is_null($new) || (bool)$new == true) {
+ $id = sha1(uniqid($_SERVER['REMOTE_ADDR'], true));
+ $opts = array('no_session' => true,
+ 'use_database' => true);
+ if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+ $si = new self($opts);
+ Securimage::$_captchaId = $id;
+ $si->createCode();
+
+ return $id;
+ } else {
+ return Securimage::$_captchaId;
+ }
+ }
+
+ /**
+ * Validate a captcha code input against a captcha ID
+ *
+ * @param string $id The captcha ID to check
+ * @param string $value The captcha value supplied by the user
+ * @param array $options Array of options to construct Securimage with.
+ * Options must include database options if they are not set in securimage.php
+ *
+ * @see Securimage::$database_driver
+ * @return bool true if the code was valid for the given captcha ID, false if not or if database failed to open
+ */
+ public static function checkByCaptchaId($id, $value, array $options = array())
+ {
+ $opts = array('captchaId' => $id,
+ 'no_session' => true,
+ 'use_database' => true);
+
+ if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+
+ $si = new self($opts);
+
+ if ($si->openDatabase()) {
+ $code = $si->getCodeFromDatabase();
+
+ if (is_array($code)) {
+ $si->code = $code['code'];
+ $si->code_display = $code['code_disp'];
+ }
+
+ if ($si->check($value)) {
+ $si->clearCodeFromDatabase();
+
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Generates a new challenge and serves a captcha image.
+ *
+ * Appropriate headers will be sent to the browser unless the *send_headers* option is false.
+ *
+ * @param string $background_image The absolute or relative path to the background image to use as the background of the captcha image.
+ *
+ * $img = new Securimage();
+ * $img->code_length = 6;
+ * $img->num_lines = 5;
+ * $img->noise_level = 5;
+ *
+ * $img->show(); // sends the image and appropriate headers to browser
+ * exit;
+ */
+ public function show($background_image = '')
+ {
+ set_error_handler(array(&$this, 'errorHandler'));
+
+ if($background_image != '' && is_readable($background_image)) {
+ $this->bgimg = $background_image;
+ }
+
+ $this->doImage();
+ }
+
+ /**
+ * Checks a given code against the correct value from the session and/or database.
+ *
+ * @param string $code The captcha code to check
+ *
+ * $code = $_POST['code'];
+ * $img = new Securimage();
+ * if ($img->check($code) == true) {
+ * $captcha_valid = true;
+ * } else {
+ * $captcha_valid = false;
+ * }
+ *
+ * @return bool true if the given code was correct, false if not.
+ */
+ public function check($code)
+ {
+ $this->code_entered = $code;
+ $this->validate();
+ return $this->correct_code;
+ }
+
+ /**
+ * Returns HTML code for displaying the captcha image, audio button, and form text input.
+ *
+ * Options can be specified to modify the output of the HTML. Accepted options:
+ *
+ * 'securimage_path':
+ * Optional: The URI to where securimage is installed (e.g. /securimage)
+ * 'show_image_url':
+ * Path to the securimage_show.php script (useful when integrating with a framework or moving outside the securimage directory)
+ * This will be passed as a urlencoded string to the <img> tag for outputting the captcha image
+ * 'audio_play_url':
+ * Same as show_image_url, except this indicates the URL of the audio playback script
+ * 'image_id':
+ * A string that sets the "id" attribute of the captcha image (default: captcha_image)
+ * 'image_alt_text':
+ * The alt text of the captcha image (default: CAPTCHA Image)
+ * 'show_audio_button':
+ * true/false Whether or not to show the audio button (default: true)
+ * 'disable_flash_fallback':)
+ * Allow only HTML5 audio and disable Flash fallback
+ * 'show_refresh_button':
+ * true/false Whether or not to show a button to refresh the image (default: true)
+ * 'audio_icon_url':
+ * URL to the image used for showing the HTML5 audio icon
+ * 'icon_size':
+ * Size (for both height & width) in pixels of the audio and refresh buttons
+ * 'show_text_input':
+ * true/false Whether or not to show the text input for the captcha (default: true)
+ * 'refresh_alt_text':
+ * Alt text for the refresh image (default: Refresh Image)
+ * 'refresh_title_text':
+ * Title text for the refresh image link (default: Refresh Image)
+ * 'input_id':
+ * A string that sets the "id" attribute of the captcha text input (default: captcha_code)
+ * 'input_name':
+ * A string that sets the "name" attribute of the captcha text input (default: same as input_id)
+ * 'input_text':
+ * A string that sets the text of the label for the captcha text input (default: Type the text:)
+ * 'input_attributes':
+ * An array of additional HTML tag attributes to pass to the text input tag (default: empty)
+ * 'image_attributes':
+ * An array of additional HTML tag attributes to pass to the captcha image tag (default: empty)
+ * 'error_html':
+ * Optional HTML markup to be shown above the text input field
+ * 'namespace':
+ * The optional captcha namespace to use for showing the image and playing back the audio. Namespaces are for using multiple captchas on the same page.
+ *
+ * @param array $options Array of options for modifying the HTML code.
+ * @param int $parts Securiage::HTML_* constant controlling what component of the captcha HTML to display
+ *
+ * @return string The generated HTML code for displaying the captcha
+ */
+ public static function getCaptchaHtml($options = array(), $parts = Securimage::HTML_ALL)
+ {
+ static $javascript_init = false;
+
+ if (!isset($options['securimage_path'])) {
+ $docroot = (isset($_SERVER['DOCUMENT_ROOT'])) ? $_SERVER['DOCUMENT_ROOT'] : substr($_SERVER['SCRIPT_FILENAME'], 0, -strlen($_SERVER['SCRIPT_NAME']));
+ $docroot = realpath($docroot);
+ $sipath = dirname(__FILE__);
+ $securimage_path = str_replace($docroot, '', $sipath);
+ } else {
+ $securimage_path = $options['securimage_path'];
+ }
+
+ $show_image_url = (isset($options['show_image_url'])) ? $options['show_image_url'] : null;
+ $image_id = (isset($options['image_id'])) ? $options['image_id'] : 'captcha_image';
+ $image_alt = (isset($options['image_alt_text'])) ? $options['image_alt_text'] : 'CAPTCHA Image';
+ $show_audio_btn = (isset($options['show_audio_button'])) ? (bool)$options['show_audio_button'] : true;
+ $disable_flash_fbk = (isset($options['disable_flash_fallback'])) ? (bool)$options['disable_flash_fallback'] : false;
+ $show_refresh_btn = (isset($options['show_refresh_button'])) ? (bool)$options['show_refresh_button'] : true;
+ $refresh_icon_url = (isset($options['refresh_icon_url'])) ? $options['refresh_icon_url'] : null;
+ $audio_but_bg_col = (isset($options['audio_button_bgcol'])) ? $options['audio_button_bgcol'] : '#ffffff';
+ $audio_icon_url = (isset($options['audio_icon_url'])) ? $options['audio_icon_url'] : null;
+ $loading_icon_url = (isset($options['loading_icon_url'])) ? $options['loading_icon_url'] : null;
+ $icon_size = (isset($options['icon_size'])) ? $options['icon_size'] : 32;
+ $audio_play_url = (isset($options['audio_play_url'])) ? $options['audio_play_url'] : null;
+ $audio_swf_url = (isset($options['audio_swf_url'])) ? $options['audio_swf_url'] : null;
+ $show_input = (isset($options['show_text_input'])) ? (bool)$options['show_text_input'] : true;
+ $refresh_alt = (isset($options['refresh_alt_text'])) ? $options['refresh_alt_text'] : 'Refresh Image';
+ $refresh_title = (isset($options['refresh_title_text'])) ? $options['refresh_title_text'] : 'Refresh Image';
+ $input_text = (isset($options['input_text'])) ? $options['input_text'] : 'Type the text:';
+ $input_id = (isset($options['input_id'])) ? $options['input_id'] : 'captcha_code';
+ $input_name = (isset($options['input_name'])) ? $options['input_name'] : $input_id;
+ $input_attrs = (isset($options['input_attributes'])) ? $options['input_attributes'] : array();
+ $image_attrs = (isset($options['image_attributes'])) ? $options['image_attributes'] : array();
+ $error_html = (isset($options['error_html'])) ? $options['error_html'] : null;
+ $namespace = (isset($options['namespace'])) ? $options['namespace'] : '';
+
+ $rand = md5(uniqid($_SERVER['REMOTE_PORT'], true));
+ $securimage_path = rtrim($securimage_path, '/\\');
+ $securimage_path = str_replace('\\', '/', $securimage_path);
+
+ $image_attr = '';
+ if (!is_array($image_attrs)) $image_attrs = array();
+ if (!isset($image_attrs['style'])) $image_attrs['style'] = 'float: left; padding-right: 5px';
+ $image_attrs['id'] = $image_id;
+
+ $show_path = $securimage_path . '/securimage_show.php?';
+ if ($show_image_url) {
+ if (parse_url($show_image_url, PHP_URL_QUERY)) {
+ $show_path = "{$show_image_url}&";
+ } else {
+ $show_path = "{$show_image_url}?";
+ }
+ }
+ if (!empty($namespace)) {
+ $show_path .= sprintf('namespace=%s&amp;', $namespace);
+ }
+ $image_attrs['src'] = $show_path . $rand;
+
+ $image_attrs['alt'] = $image_alt;
+
+ foreach($image_attrs as $name => $val) {
+ $image_attr .= sprintf('%s="%s" ', $name, htmlspecialchars($val));
+ }
+
+ $swf_path = $securimage_path . '/securimage_play.swf';
+ $play_path = $securimage_path . '/securimage_play.php?';
+ $icon_path = $securimage_path . '/images/audio_icon.png';
+ $load_path = $securimage_path . '/images/loading.png';
+ $js_path = $securimage_path . '/securimage.js';
+
+ if (!empty($audio_icon_url)) {
+ $icon_path = $audio_icon_url;
+ }
+
+ if (!empty($loading_icon_url)) {
+ $load_path = $loading_icon_url;
+ }
+
+ if (!empty($audio_play_url)) {
+ if (parse_url($audio_play_url, PHP_URL_QUERY)) {
+ $play_path = "{$audio_play_url}&";
+ } else {
+ $play_path = "{$audio_play_url}?";
+ }
+ }
+
+ if (!empty($namespace)) {
+ $play_path .= sprintf('namespace=%s&amp;', $namespace);
+ }
+
+ if (!empty($audio_swf_url)) {
+ $swf_path = $audio_swf_url;
+ }
+
+ $audio_obj = $image_id . '_audioObj';
+ $html = '';
+
+ if ( ($parts & Securimage::HTML_IMG) > 0) {
+ $html .= sprintf('<img %s/>', $image_attr);
+ }
+
+ if ( ($parts & Securimage::HTML_AUDIO) > 0 && $show_audio_btn) {
+ // html5 audio
+ $html .= sprintf('<div id="%s_audio_div">', $image_id) . "\n" .
+ sprintf('<audio id="%s_audio" preload="none" style="display: none">', $image_id) . "\n";
+
+ // check for existence and executability of LAME binary
+ // prefer mp3 over wav by sourcing it first, if available
+ if (is_executable(Securimage::$lame_binary_path)) {
+ $html .= sprintf('<source id="%s_source_mp3" src="%sid=%s&amp;format=mp3" type="audio/mpeg">', $image_id, $play_path, uniqid()) . "\n";
+ }
+
+ // output wav source
+ $html .= sprintf('<source id="%s_source_wav" src="%sid=%s" type="audio/wav">', $image_id, $play_path, uniqid()) . "\n";
+
+ // flash audio button
+ if (!$disable_flash_fbk) {
+ $html .= sprintf('<object type="application/x-shockwave-flash" data="%s?bgcol=%s&amp;icon_file=%s&amp;audio_file=%s" height="%d" width="%d">',
+ htmlspecialchars($swf_path),
+ urlencode($audio_but_bg_col),
+ urlencode($icon_path),
+ urlencode(html_entity_decode($play_path)),
+ $icon_size, $icon_size
+ );
+
+ $html .= sprintf('<param name="movie" value="%s?bgcol=%s&amp;icon_file=%s&amp;audio_file=%s" />',
+ htmlspecialchars($swf_path),
+ urlencode($audio_but_bg_col),
+ urlencode($icon_path),
+ urlencode(html_entity_decode($play_path))
+ );
+
+ $html .= '</object><br />';
+ }
+
+ // html5 audio close
+ $html .= "</audio>\n</div>\n";
+
+ // html5 audio controls
+ $html .= sprintf('<div id="%s_audio_controls">', $image_id) . "\n" .
+ sprintf('<a tabindex="-1" class="captcha_play_button" href="%sid=%s" onclick="return false">',
+ $play_path, uniqid()
+ ) . "\n" .
+ sprintf('<img class="captcha_play_image" height="%d" width="%d" src="%s" alt="Play CAPTCHA Audio" style="border: 0px">', $icon_size, $icon_size, htmlspecialchars($icon_path)) . "\n" .
+ sprintf('<img class="captcha_loading_image rotating" height="%d" width="%d" src="%s" alt="Loading audio" style="display: none">', $icon_size, $icon_size, htmlspecialchars($load_path)) . "\n" .
+ "</a>\n<noscript>Enable Javascript for audio controls</noscript>\n" .
+ "</div>\n";
+
+ // html5 javascript
+ if (!$javascript_init) {
+ $html .= sprintf('<script type="text/javascript" src="%s"></script>', $js_path) . "\n";
+ $javascript_init = true;
+ }
+ $html .= '<script type="text/javascript">' .
+ "$audio_obj = new SecurimageAudio({ audioElement: '{$image_id}_audio', controlsElement: '{$image_id}_audio_controls' });" .
+ "</script>\n";
+ }
+
+ if ( ($parts & Securimage::HTML_ICON_REFRESH) > 0 && $show_refresh_btn) {
+ $icon_path = $securimage_path . '/images/refresh.png';
+ if ($refresh_icon_url) {
+ $icon_path = $refresh_icon_url;
+ }
+ $img_tag = sprintf('<img height="%d" width="%d" src="%s" alt="%s" onclick="this.blur()" style="border: 0px; vertical-align: bottom" />',
+ $icon_size, $icon_size, htmlspecialchars($icon_path), htmlspecialchars($refresh_alt));
+
+ $html .= sprintf('<a tabindex="-1" style="border: 0" href="#" title="%s" onclick="%sdocument.getElementById(\'%s\').src = \'%s\' + Math.random(); this.blur(); return false">%s</a><br />',
+ htmlspecialchars($refresh_title),
+ ($audio_obj) ? "if (typeof window.{$audio_obj} !== 'undefined') {$audio_obj}.refresh(); " : '',
+ $image_id,
+ $show_path,
+ $img_tag
+ );
+ }
+
+ if ($parts == Securimage::HTML_ALL) {
+ $html .= '<div style="clear: both"></div>';
+ }
+
+ if ( ($parts & Securimage::HTML_INPUT_LABEL) > 0 && $show_input) {
+ $html .= sprintf('<label for="%s">%s</label> ',
+ htmlspecialchars($input_id),
+ htmlspecialchars($input_text));
+
+ if (!empty($error_html)) {
+ $html .= $error_html;
+ }
+ }
+
+ if ( ($parts & Securimage::HTML_INPUT) > 0 && $show_input) {
+ $input_attr = '';
+ if (!is_array($input_attrs)) $input_attrs = array();
+ $input_attrs['type'] = 'text';
+ $input_attrs['name'] = $input_name;
+ $input_attrs['id'] = $input_id;
+
+ foreach($input_attrs as $name => $val) {
+ $input_attr .= sprintf('%s="%s" ', $name, htmlspecialchars($val));
+ }
+
+ $html .= sprintf('<input %s/>', $input_attr);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get the time in seconds that it took to solve the captcha.
+ *
+ * @return int The time in seconds from when the code was created, to when it was solved
+ */
+ public function getTimeToSolve()
+ {
+ return $this->_timeToSolve;
+ }
+
+ /**
+ * Set the namespace for the captcha being stored in the session or database.
+ *
+ * Namespaces are useful when multiple captchas need to be displayed on a single page.
+ *
+ * @param string $namespace Namespace value, String consisting of characters "a-zA-Z0-9_-"
+ */
+ public function setNamespace($namespace)
+ {
+ $namespace = preg_replace('/[^a-z0-9-_]/i', '', $namespace);
+ $namespace = substr($namespace, 0, 64);
+
+ if (!empty($namespace)) {
+ $this->namespace = $namespace;
+ } else {
+ $this->namespace = 'default';
+ }
+ }
+
+ /**
+ * Generate an audible captcha in WAV format and send it to the browser with appropriate headers.
+ * Example:
+ *
+ * $img = new Securimage();
+ * $img->outputAudioFile(); // outputs a wav file to the browser
+ * exit;
+ *
+ * @param string $format
+ */
+ public function outputAudioFile($format = null)
+ {
+ set_error_handler(array(&$this, 'errorHandler'));
+
+ if (isset($_SERVER['HTTP_RANGE'])) {
+ $range = true;
+ $rangeId = (isset($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) ?
+ 'ID' . $_SERVER['HTTP_X_PLAYBACK_SESSION_ID'] :
+ 'ID' . md5($_SERVER['REQUEST_URI']);
+ $uniq = $rangeId;
+ } else {
+ $uniq = md5(uniqid(microtime()));
+ }
+
+ try {
+ if (!($audio = $this->getAudioData())) {
+ // if previously generated audio not found for current captcha
+ require_once dirname(__FILE__) . '/WavFile.php';
+ $audio = $this->getAudibleCode();
+
+ if (strtolower($format) == 'mp3') {
+ $audio = $this->wavToMp3($audio);
+ }
+
+ $this->saveAudioData($audio);
+ }
+ } catch (Exception $ex) {
+ if (($fp = @fopen(dirname(__FILE__) . '/si.error_log', 'a+')) !== false) {
+ fwrite($fp, date('Y-m-d H:i:s') . ': Securimage audio error "' . $ex->getMessage() . '"' . "\n");
+ fclose($fp);
+ }
+
+ $audio = $this->audioError();
+ }
+
+ if ($this->no_session != true) {
+ // close session to make it available to other requests in the event
+ // streaming the audio takes sevaral seconds or more
+ session_write_close();
+ }
+
+ if ($this->canSendHeaders() || $this->send_headers == false) {
+ if ($this->send_headers) {
+ if ($format == 'mp3') {
+ $ext = 'mp3';
+ $type = 'audio/mpeg';
+ } else {
+ $ext = 'wav';
+ $type = 'audio/wav';
+ }
+
+ header('Accept-Ranges: bytes');
+ header("Content-Disposition: attachment; filename=\"securimage_audio-{$uniq}.{$ext}\"");
+ header('Cache-Control: no-store, no-cache, must-revalidate');
+ header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
+ header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
+ header('Content-type: ' . $type);
+ }
+
+ $this->rangeDownload($audio);
+ } else {
+ echo '<hr /><strong>'
+ .'Failed to generate audio file, content has already been '
+ .'output.<br />This is most likely due to misconfiguration or '
+ .'a PHP error was sent to the browser.</strong>';
+ }
+
+ restore_error_handler();
+
+ if (!$this->no_exit) exit;
+ }
+
+ /**
+ * Output audio data with http range support. Typically this shouldn't be
+ * called directly unless being used with a custom implentation. Use
+ * Securimage::outputAudioFile instead.
+ *
+ * @param string $audio Raw wav or mp3 audio file content
+ */
+ public function rangeDownload($audio)
+ {
+ /* Congratulations Firefox Android/Linux/Windows for being the most
+ * sensible browser of all when streaming HTML5 audio!
+ *
+ * Chrome on Android and iOS on iPad/iPhone both make extra HTTP requests
+ * for the audio whether on WiFi or the mobile network resulting in
+ * multiple downloads of the audio file and wasted bandwidth.
+ *
+ * If I'm doing something wrong in this code or anyone knows why, I'd
+ * love to hear from you.
+ */
+ $audioLength = $size = strlen($audio);
+
+ if (isset($_SERVER['HTTP_RANGE'])) {
+ list( , $range) = explode('=', $_SERVER['HTTP_RANGE']); // bytes=byte-range-set
+ $range = trim($range);
+
+ if (strpos($range, ',') !== false) {
+ // eventually, we should handle requests with multiple ranges
+ // most likely these types of requests will never be sent
+ header('HTTP/1.1 416 Range Not Satisfiable');
+ echo "<h1>Range Not Satisfiable</h1>";
+ exit;
+ } else if (preg_match('/(\d+)-(\d+)/', $range, $match)) {
+ // bytes n - m
+ $range = array(intval($match[1]), intval($match[2]));
+ } else if (preg_match('/(\d+)-$/', $range, $match)) {
+ // bytes n - last byte of file
+ $range = array(intval($match[1]), null);
+ } else if (preg_match('/-(\d+)/', $range, $match)) {
+ // final n bytes of file
+ $range = array($size - intval($match[1]), $size - 1);
+ }
+
+ if ($range[1] === null) $range[1] = $size - 1;
+ $length = $range[1] - $range[0] + 1;
+ $audio = substr($audio, $range[0], $length);
+ $audioLength = strlen($audio);
+
+ header('HTTP/1.1 206 Partial Content');
+ header("Content-Range: bytes {$range[0]}-{$range[1]}/{$size}");
+
+ if ($range[0] < 0 ||$range[1] >= $size || $range[0] >= $size || $range[0] > $range[1]) {
+ header('HTTP/1.1 416 Range Not Satisfiable');
+ echo "<h1>Range Not Satisfiable</h1>";
+ exit;
+ }
+ }
+
+ header('Content-Length: ' . $audioLength);
+
+ echo $audio;
+ }
+
+ /**
+ * Return the code from the session or database (if configured). If none exists or was found, an empty string is returned.
+ *
+ * @param bool $array true to receive an array containing the code and properties, false to receive just the code.
+ * @param bool $returnExisting If true, and the class property *code* is set, it will be returned instead of getting the code from the session or database.
+ * @return array|string Return is an array if $array = true, otherwise a string containing the code
+ */
+ public function getCode($array = false, $returnExisting = false)
+ {
+ $code = array();
+ $time = 0;
+ $disp = 'error';
+
+ if ($returnExisting && strlen($this->code) > 0) {
+ if ($array) {
+ return array(
+ 'code' => $this->code,
+ 'display' => $this->code_display,
+ 'code_display' => $this->code_display,
+ 'time' => 0);
+ } else {
+ return $this->code;
+ }
+ }
+
+ if ($this->no_session != true) {
+ if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
+ trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
+ if ($this->isCodeExpired(
+ $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
+ $code['code'] = $_SESSION['securimage_code_value'][$this->namespace];
+ $code['time'] = $_SESSION['securimage_code_ctime'][$this->namespace];
+ $code['display'] = $_SESSION['securimage_code_disp'] [$this->namespace];
+ }
+ }
+ }
+
+ if (empty($code) && $this->use_database) {
+ // no code in session - may mean user has cookies turned off
+ $this->openDatabase();
+ $code = $this->getCodeFromDatabase();
+
+ if (!empty($code)) {
+ $code['display'] = $code['code_disp'];
+ unset($code['code_disp']);
+ }
+ } else { /* no code stored in session or sqlite database, validation will fail */ }
+
+ if ($array == true) {
+ return $code;
+ } else {
+ return $code['code'];
+ }
+ }
+
+ /**
+ * The main image drawing routing, responsible for constructing the entire image and serving it
+ */
+ protected function doImage()
+ {
+ if( ($this->use_transparent_text == true || $this->bgimg != '') && function_exists('imagecreatetruecolor')) {
+ $imagecreate = 'imagecreatetruecolor';
+ } else {
+ $imagecreate = 'imagecreate';
+ }
+
+ $this->im = $imagecreate($this->image_width, $this->image_height);
+ $this->tmpimg = $imagecreate($this->image_width * $this->iscale, $this->image_height * $this->iscale);
+
+ $this->allocateColors();
+ imagepalettecopy($this->tmpimg, $this->im);
+
+ $this->setBackground();
+
+ $code = '';
+
+ if ($this->getCaptchaId(false) !== null) {
+ // a captcha Id was supplied
+
+ // check to see if a display_value for the captcha image was set
+ if (is_string($this->display_value) && strlen($this->display_value) > 0) {
+ $this->code_display = $this->display_value;
+ $this->code = ($this->case_sensitive) ?
+ $this->display_value :
+ strtolower($this->display_value);
+ $code = $this->code;
+ } else if ($this->openDatabase()) {
+ // no display_value, check the database for existing captchaId
+ $code = $this->getCodeFromDatabase();
+
+ // got back a result from the database with a valid code for captchaId
+ if (is_array($code)) {
+ $this->code = $code['code'];
+ $this->code_display = $code['code_disp'];
+ $code = $code['code'];
+ }
+ }
+ }
+
+ if ($code == '') {
+ // if the code was not set using display_value or was not found in
+ // the database, create a new code
+ $this->createCode();
+ }
+
+ if ($this->noise_level > 0) {
+ $this->drawNoise();
+ }
+
+ $this->drawWord();
+
+ if ($this->perturbation > 0 && is_readable($this->ttf_file)) {
+ $this->distortedCopy();
+ }
+
+ if ($this->num_lines > 0) {
+ $this->drawLines();
+ }
+
+ if (trim($this->image_signature) != '') {
+ $this->addSignature();
+ }
+
+ $this->output();
+ }
+
+ /**
+ * Allocate the colors to be used for the image
+ */
+ protected function allocateColors()
+ {
+ // allocate bg color first for imagecreate
+ $this->gdbgcolor = imagecolorallocate($this->im,
+ $this->image_bg_color->r,
+ $this->image_bg_color->g,
+ $this->image_bg_color->b);
+
+ $alpha = intval($this->text_transparency_percentage / 100 * 127);
+
+ if ($this->use_transparent_text == true) {
+ $this->gdtextcolor = imagecolorallocatealpha($this->im,
+ $this->text_color->r,
+ $this->text_color->g,
+ $this->text_color->b,
+ $alpha);
+ $this->gdlinecolor = imagecolorallocatealpha($this->im,
+ $this->line_color->r,
+ $this->line_color->g,
+ $this->line_color->b,
+ $alpha);
+ $this->gdnoisecolor = imagecolorallocatealpha($this->im,
+ $this->noise_color->r,
+ $this->noise_color->g,
+ $this->noise_color->b,
+ $alpha);
+ } else {
+ $this->gdtextcolor = imagecolorallocate($this->im,
+ $this->text_color->r,
+ $this->text_color->g,
+ $this->text_color->b);
+ $this->gdlinecolor = imagecolorallocate($this->im,
+ $this->line_color->r,
+ $this->line_color->g,
+ $this->line_color->b);
+ $this->gdnoisecolor = imagecolorallocate($this->im,
+ $this->noise_color->r,
+ $this->noise_color->g,
+ $this->noise_color->b);
+ }
+
+ $this->gdsignaturecolor = imagecolorallocate($this->im,
+ $this->signature_color->r,
+ $this->signature_color->g,
+ $this->signature_color->b);
+
+ }
+
+ /**
+ * The the background color, or background image to be used
+ */
+ protected function setBackground()
+ {
+ // set background color of image by drawing a rectangle since imagecreatetruecolor doesn't set a bg color
+ imagefilledrectangle($this->im, 0, 0,
+ $this->image_width, $this->image_height,
+ $this->gdbgcolor);
+ imagefilledrectangle($this->tmpimg, 0, 0,
+ $this->image_width * $this->iscale, $this->image_height * $this->iscale,
+ $this->gdbgcolor);
+
+ if ($this->bgimg == '') {
+ if ($this->background_directory != null &&
+ is_dir($this->background_directory) &&
+ is_readable($this->background_directory))
+ {
+ $img = $this->getBackgroundFromDirectory();
+ if ($img != false) {
+ $this->bgimg = $img;
+ }
+ }
+ }
+
+ if ($this->bgimg == '') {
+ return;
+ }
+
+ $dat = @getimagesize($this->bgimg);
+ if($dat == false) {
+ return;
+ }
+
+ switch($dat[2]) {
+ case 1: $newim = @imagecreatefromgif($this->bgimg); break;
+ case 2: $newim = @imagecreatefromjpeg($this->bgimg); break;
+ case 3: $newim = @imagecreatefrompng($this->bgimg); break;
+ default: return;
+ }
+
+ if(!$newim) return;
+
+ imagecopyresized($this->im, $newim, 0, 0, 0, 0,
+ $this->image_width, $this->image_height,
+ imagesx($newim), imagesy($newim));
+ }
+
+ /**
+ * Scan the directory for a background image to use
+ * @return string|bool
+ */
+ protected function getBackgroundFromDirectory()
+ {
+ $images = array();
+
+ if ( ($dh = opendir($this->background_directory)) !== false) {
+ while (($file = readdir($dh)) !== false) {
+ if (preg_match('/(jpg|gif|png)$/i', $file)) $images[] = $file;
+ }
+
+ closedir($dh);
+
+ if (sizeof($images) > 0) {
+ return rtrim($this->background_directory, '/') . '/' . $images[mt_rand(0, sizeof($images)-1)];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * This method generates a new captcha code.
+ *
+ * Generates a random captcha code based on *charset*, math problem, or captcha from the wordlist and saves the value to the session and/or database.
+ */
+ public function createCode()
+ {
+ $this->code = false;
+
+ switch($this->captcha_type) {
+ case self::SI_CAPTCHA_MATHEMATIC:
+ {
+ do {
+ $signs = array('+', '-', 'x');
+ $left = mt_rand(1, 10);
+ $right = mt_rand(1, 5);
+ $sign = $signs[mt_rand(0, 2)];
+
+ switch($sign) {
+ case 'x': $c = $left * $right; break;
+ case '-': $c = $left - $right; break;
+ default: $c = $left + $right; break;
+ }
+ } while ($c <= 0); // no negative #'s or 0
+
+ $this->code = "$c";
+ $this->code_display = "$left $sign $right";
+ break;
+ }
+
+ case self::SI_CAPTCHA_WORDS:
+ $words = $this->readCodeFromFile(2);
+ $this->code = implode(' ', $words);
+ $this->code_display = $this->code;
+ break;
+
+ default:
+ {
+ if ($this->use_wordlist && is_readable($this->wordlist_file)) {
+ $this->code = $this->readCodeFromFile();
+ }
+
+ if ($this->code == false) {
+ $this->code = $this->generateCode($this->code_length);
+ }
+
+ $this->code_display = $this->code;
+ $this->code = ($this->case_sensitive) ? $this->code : strtolower($this->code);
+ } // default
+ }
+
+ $this->saveData();
+ }
+
+ /**
+ * Draws the captcha code on the image
+ */
+ protected function drawWord()
+ {
+ $width2 = $this->image_width * $this->iscale;
+ $height2 = $this->image_height * $this->iscale;
+ $ratio = ($this->font_ratio) ? $this->font_ratio : 0.4;
+
+ if ((float)$ratio < 0.1 || (float)$ratio >= 1) {
+ $ratio = 0.4;
+ }
+
+ if (!is_readable($this->ttf_file)) {
+ imagestring($this->im, 4, 10, ($this->image_height / 2) - 5, 'Failed to load TTF font file!', $this->gdtextcolor);
+ } else {
+ if ($this->perturbation > 0) {
+ $font_size = $height2 * $ratio;
+ $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
+ $tx = $bb[4] - $bb[0];
+ $ty = $bb[5] - $bb[1];
+ $x = floor($width2 / 2 - $tx / 2 - $bb[0]);
+ $y = round($height2 / 2 - $ty / 2 - $bb[1]);
+
+ imagettftext($this->tmpimg, $font_size, 0, (int)$x, (int)$y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
+ } else {
+ $font_size = $this->image_height * $ratio;
+ $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
+ $tx = $bb[4] - $bb[0];
+ $ty = $bb[5] - $bb[1];
+ $x = floor($this->image_width / 2 - $tx / 2 - $bb[0]);
+ $y = round($this->image_height / 2 - $ty / 2 - $bb[1]);
+
+ imagettftext($this->im, $font_size, 0, (int)$x, (int)$y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
+ }
+ }
+
+ // DEBUG
+ //$this->im = $this->tmpimg;
+ //$this->output();
+
+ }
+
+ /**
+ * Copies the captcha image to the final image with distortion applied
+ */
+ protected function distortedCopy()
+ {
+ $numpoles = 3; // distortion factor
+ // make array of poles AKA attractor points
+ for ($i = 0; $i < $numpoles; ++ $i) {
+ $px[$i] = mt_rand($this->image_width * 0.2, $this->image_width * 0.8);
+ $py[$i] = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
+ $rad[$i] = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
+ $tmp = ((- $this->frand()) * 0.15) - .15;
+ $amp[$i] = $this->perturbation * $tmp;
+ }
+
+ $bgCol = imagecolorat($this->tmpimg, 0, 0);
+ $width2 = $this->iscale * $this->image_width;
+ $height2 = $this->iscale * $this->image_height;
+ imagepalettecopy($this->im, $this->tmpimg); // copy palette to final image so text colors come across
+ // loop over $img pixels, take pixels from $tmpimg with distortion field
+ for ($ix = 0; $ix < $this->image_width; ++ $ix) {
+ for ($iy = 0; $iy < $this->image_height; ++ $iy) {
+ $x = $ix;
+ $y = $iy;
+ for ($i = 0; $i < $numpoles; ++ $i) {
+ $dx = $ix - $px[$i];
+ $dy = $iy - $py[$i];
+ if ($dx == 0 && $dy == 0) {
+ continue;
+ }
+ $r = sqrt($dx * $dx + $dy * $dy);
+ if ($r > $rad[$i]) {
+ continue;
+ }
+ $rscale = $amp[$i] * sin(3.14 * $r / $rad[$i]);
+ $x += $dx * $rscale;
+ $y += $dy * $rscale;
+ }
+ $c = $bgCol;
+ $x *= $this->iscale;
+ $y *= $this->iscale;
+ if ($x >= 0 && $x < $width2 && $y >= 0 && $y < $height2) {
+ $c = imagecolorat($this->tmpimg, $x, $y);
+ }
+ if ($c != $bgCol) { // only copy pixels of letters to preserve any background image
+ imagesetpixel($this->im, $ix, $iy, $c);
+ }
+ }
+ }
+ }
+
+ /**
+ * Draws distorted lines on the image
+ */
+ protected function drawLines()
+ {
+ for ($line = 0; $line < $this->num_lines; ++ $line) {
+ $x = $this->image_width * (1 + $line) / ($this->num_lines + 1);
+ $x += (0.5 - $this->frand()) * $this->image_width / $this->num_lines;
+ $y = mt_rand($this->image_height * 0.1, $this->image_height * 0.9);
+
+ $theta = ($this->frand() - 0.5) * M_PI * 0.7;
+ $w = $this->image_width;
+ $len = mt_rand($w * 0.4, $w * 0.7);
+ $lwid = mt_rand(0, 2);
+
+ $k = $this->frand() * 0.6 + 0.2;
+ $k = $k * $k * 0.5;
+ $phi = $this->frand() * 6.28;
+ $step = 0.5;
+ $dx = $step * cos($theta);
+ $dy = $step * sin($theta);
+ $n = $len / $step;
+ $amp = 1.5 * $this->frand() / ($k + 5.0 / $len);
+ $x0 = $x - 0.5 * $len * cos($theta);
+ $y0 = $y - 0.5 * $len * sin($theta);
+
+ $ldx = round(- $dy * $lwid);
+ $ldy = round($dx * $lwid);
+
+ for ($i = 0; $i < $n; ++ $i) {
+ $x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
+ $y = $y0 + $i * $dy - $amp * $dx * sin($k * $i * $step + $phi);
+ imagefilledrectangle($this->im, $x, $y, $x + $lwid, $y + $lwid, $this->gdlinecolor);
+ }
+ }
+ }
+
+ /**
+ * Draws random noise on the image
+ */
+ protected function drawNoise()
+ {
+ if ($this->noise_level > 10) {
+ $noise_level = 10;
+ } else {
+ $noise_level = $this->noise_level;
+ }
+
+ $t0 = microtime(true);
+
+ $noise_level *= 125; // an arbitrary number that works well on a 1-10 scale
+
+ $points = $this->image_width * $this->image_height * $this->iscale;
+ $height = $this->image_height * $this->iscale;
+ $width = $this->image_width * $this->iscale;
+ for ($i = 0; $i < $noise_level; ++$i) {
+ $x = mt_rand(10, $width);
+ $y = mt_rand(10, $height);
+ $size = mt_rand(7, 10);
+ if ($x - $size <= 0 && $y - $size <= 0) continue; // dont cover 0,0 since it is used by imagedistortedcopy
+ imagefilledarc($this->tmpimg, $x, $y, $size, $size, 0, 360, $this->gdnoisecolor, IMG_ARC_PIE);
+ }
+
+ $t1 = microtime(true);
+
+ $t = $t1 - $t0;
+
+ /*
+ // DEBUG
+ imagestring($this->tmpimg, 5, 25, 30, "$t", $this->gdnoisecolor);
+ header('content-type: image/png');
+ imagepng($this->tmpimg);
+ exit;
+ */
+ }
+
+ /**
+ * Print signature text on image
+ */
+ protected function addSignature()
+ {
+ $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
+ $textlen = $bbox[2] - $bbox[0];
+ $x = $this->image_width - $textlen - 5;
+ $y = $this->image_height - 3;
+
+ imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
+ }
+
+ /**
+ * Sends the appropriate image and cache headers and outputs image to the browser
+ */
+ protected function output()
+ {
+ if ($this->canSendHeaders() || $this->send_headers == false) {
+ if ($this->send_headers) {
+ // only send the content-type headers if no headers have been output
+ // this will ease debugging on misconfigured servers where warnings
+ // may have been output which break the image and prevent easily viewing
+ // source to see the error.
+ header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+ header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
+ header("Cache-Control: no-store, no-cache, must-revalidate");
+ header("Cache-Control: post-check=0, pre-check=0", false);
+ header("Pragma: no-cache");
+ }
+
+ switch ($this->image_type) {
+ case self::SI_IMAGE_JPEG:
+ if ($this->send_headers) header("Content-Type: image/jpeg");
+ imagejpeg($this->im, null, 90);
+ break;
+ case self::SI_IMAGE_GIF:
+ if ($this->send_headers) header("Content-Type: image/gif");
+ imagegif($this->im);
+ break;
+ default:
+ if ($this->send_headers) header("Content-Type: image/png");
+ imagepng($this->im);
+ break;
+ }
+ } else {
+ echo '<hr /><strong>'
+ .'Failed to generate captcha image, content has already been '
+ .'output.<br />This is most likely due to misconfiguration or '
+ .'a PHP error was sent to the browser.</strong>';
+ }
+
+ imagedestroy($this->im);
+ restore_error_handler();
+
+ if (!$this->no_exit) exit;
+ }
+
+ /**
+ * Generates an audio captcha in WAV format
+ *
+ * @return string The audio representation of the captcha in Wav format
+ */
+ protected function getAudibleCode()
+ {
+ $letters = array();
+ $code = $this->getCode(true, true);
+
+ if (empty($code) || $code['code'] == '') {
+ if (strlen($this->display_value) > 0) {
+ $code = array('code' => $this->display_value, 'display' => $this->display_value);
+ } else {
+ $this->createCode();
+ $code = $this->getCode(true);
+ }
+ }
+
+ if (empty($code)) {
+ $error = 'Failed to get audible code (are database settings correct?). Check the error log for details';
+ trigger_error($error, E_USER_WARNING);
+ throw new Exception($error);
+ }
+
+ if (preg_match('/(\d+) (\+|-|x) (\d+)/i', $code['display'], $eq)) {
+ $math = true;
+
+ $left = $eq[1];
+ $sign = str_replace(array('+', '-', 'x'), array('plus', 'minus', 'times'), $eq[2]);
+ $right = $eq[3];
+
+ $letters = array($left, $sign, $right);
+ } else {
+ $math = false;
+
+ $length = strlen($code['display']);
+
+ for($i = 0; $i < $length; ++$i) {
+ $letter = $code['display']{$i};
+ $letters[] = $letter;
+ }
+ }
+
+ try {
+ return $this->generateWAV($letters);
+ } catch(Exception $ex) {
+ throw $ex;
+ }
+ }
+
+ /**
+ * Gets a captcha code from a file containing a list of words.
+ *
+ * Seek to a random offset in the file and reads a block of data and returns a line from the file.
+ *
+ * @param int $numWords Number of words (lines) to read from the file
+ * @return string|array|bool Returns a string if only one word is to be read, or an array of words
+ */
+ protected function readCodeFromFile($numWords = 1)
+ {
+ $strpos_func = 'strpos';
+ $strlen_func = 'strlen';
+ $substr_func = 'substr';
+ $strtolower_func = 'strtolower';
+ $mb_support = false;
+
+ if (!empty($this->wordlist_file_encoding)) {
+ if (!extension_loaded('mbstring')) {
+ trigger_error("wordlist_file_encoding option set, but PHP does not have mbstring support", E_USER_WARNING);
+ return false;
+ }
+
+ // emits PHP warning if not supported
+ $mb_support = mb_internal_encoding($this->wordlist_file_encoding);
+
+ if (!$mb_support) {
+ return false;
+ }
+
+ $strpos_func = 'mb_strpos';
+ $strlen_func = 'mb_strlen';
+ $substr_func = 'mb_substr';
+ $strtolower_func = 'mb_strtolower';
+ }
+
+ $fp = fopen($this->wordlist_file, 'rb');
+ if (!$fp) return false;
+
+ $fsize = filesize($this->wordlist_file);
+ if ($fsize < 128) return false; // too small of a list to be effective
+
+ if ((int)$numWords < 1 || (int)$numWords > 5) $numWords = 1;
+
+ $words = array();
+ $i = 0;
+ do {
+ fseek($fp, mt_rand(0, $fsize - 128), SEEK_SET); // seek to a random position of file from 0 to filesize-128
+ $data = fread($fp, 128); // read a chunk from our random position
+
+ if ($mb_support !== false) {
+ $data = mb_ereg_replace("\r?\n", "\n", $data);
+ } else {
+ $data = preg_replace("/\r?\n/", "\n", $data);
+ }
+
+ $start = @$strpos_func($data, "\n", mt_rand(0, 56)) + 1; // random start position
+ $end = @$strpos_func($data, "\n", $start); // find end of word
+
+ if ($start === false) {
+ // picked start position at end of file
+ continue;
+ } else if ($end === false) {
+ $end = $strlen_func($data);
+ }
+
+ $word = $strtolower_func($substr_func($data, $start, $end - $start)); // return a line of the file
+
+ if ($mb_support) {
+ // convert to UTF-8 for imagettftext
+ $word = mb_convert_encoding($word, 'UTF-8', $this->wordlist_file_encoding);
+ }
+
+ $words[] = $word;
+ } while (++$i < $numWords);
+
+ fclose($fp);
+
+ if ($numWords < 2) {
+ return $words[0];
+ } else {
+ return $words;
+ }
+ }
+
+ /**
+ * Generates a random captcha code from the set character set
+ *
+ * @see Securimage::$charset Charset option
+ * @return string A randomly generated CAPTCHA code
+ */
+ protected function generateCode()
+ {
+ $code = '';
+
+ if (function_exists('mb_strlen')) {
+ for($i = 1, $cslen = mb_strlen($this->charset, 'UTF-8'); $i <= $this->code_length; ++$i) {
+ $code .= mb_substr($this->charset, mt_rand(0, $cslen - 1), 1, 'UTF-8');
+ }
+ } else {
+ for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
+ $code .= substr($this->charset, mt_rand(0, $cslen - 1), 1);
+ }
+ }
+
+ return $code;
+ }
+
+ /**
+ * Validate a code supplied by the user
+ *
+ * Checks the entered code against the value stored in the session and/or database (if configured). Handles case sensitivity.
+ * Also removes the code from session/database if the code was entered correctly to prevent re-use attack.
+ *
+ * This function does not return a value.
+ *
+ * @see Securimage::$correct_code 'correct_code' property
+ */
+ protected function validate()
+ {
+ if (!is_string($this->code) || strlen($this->code) == 0) {
+ $code = $this->getCode(true);
+ // returns stored code, or an empty string if no stored code was found
+ // checks the session and database if enabled
+ } else {
+ $code = $this->code;
+ }
+
+ if (is_array($code)) {
+ if (!empty($code)) {
+ $ctime = $code['time'];
+ $code = $code['code'];
+
+ $this->_timeToSolve = time() - $ctime;
+ } else {
+ $code = '';
+ }
+ }
+
+ if ($this->case_sensitive == false && preg_match('/[A-Z]/', $code)) {
+ // case sensitive was set from securimage_show.php but not in class
+ // the code saved in the session has capitals so set case sensitive to true
+ $this->case_sensitive = true;
+ }
+
+ $code_entered = trim( (($this->case_sensitive) ? $this->code_entered
+ : strtolower($this->code_entered))
+ );
+ $this->correct_code = false;
+
+ if ($code != '') {
+ if (strpos($code, ' ') !== false) {
+ // for multi word captchas, remove more than once space from input
+ $code_entered = preg_replace('/\s+/', ' ', $code_entered);
+ $code_entered = strtolower($code_entered);
+ }
+
+ if ((string)$code === (string)$code_entered) {
+ $this->correct_code = true;
+ if ($this->no_session != true) {
+ $_SESSION['securimage_code_disp'] [$this->namespace] = '';
+ $_SESSION['securimage_code_value'][$this->namespace] = '';
+ $_SESSION['securimage_code_ctime'][$this->namespace] = '';
+ $_SESSION['securimage_code_audio'][$this->namespace] = '';
+ }
+ $this->clearCodeFromDatabase();
+ }
+ }
+ }
+
+ /**
+ * Save CAPTCHA data to session and database (if configured)
+ */
+ protected function saveData()
+ {
+ if ($this->no_session != true) {
+ if (isset($_SESSION['securimage_code_value']) && is_scalar($_SESSION['securimage_code_value'])) {
+ // fix for migration from v2 - v3
+ unset($_SESSION['securimage_code_value']);
+ unset($_SESSION['securimage_code_ctime']);
+ }
+
+ $_SESSION['securimage_code_disp'] [$this->namespace] = $this->code_display;
+ $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
+ $_SESSION['securimage_code_ctime'][$this->namespace] = time();
+ $_SESSION['securimage_code_audio'][$this->namespace] = null; // clear previous audio, if set
+ }
+
+ if ($this->use_database) {
+ $this->saveCodeToDatabase();
+ }
+ }
+
+ /**
+ * Save audio data to session and/or the configured database
+ *
+ * @param string $data The CAPTCHA audio data
+ */
+ protected function saveAudioData($data)
+ {
+ if ($this->no_session != true) {
+ $_SESSION['securimage_code_audio'][$this->namespace] = $data;
+ }
+
+ if ($this->use_database) {
+ $this->saveAudioToDatabase($data);
+ }
+ }
+
+ /**
+ * Gets audio file contents from the session or database
+ *
+ * @return string|boolean Audio contents on success, or false if no audio found in session or DB
+ */
+ protected function getAudioData()
+ {
+ if ($this->no_session != true) {
+ if (isset($_SESSION['securimage_code_audio'][$this->namespace])) {
+ return $_SESSION['securimage_code_audio'][$this->namespace];
+ }
+ }
+
+ if ($this->use_database) {
+ $this->openDatabase();
+ $code = $this->getCodeFromDatabase();
+
+ if (!empty($code['audio_data'])) {
+ return $code['audio_data'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Saves the CAPTCHA data to the configured database.
+ */
+ protected function saveCodeToDatabase()
+ {
+ $success = false;
+ $this->openDatabase();
+
+ if ($this->use_database && $this->pdo_conn) {
+ $id = $this->getCaptchaId(false);
+ $ip = $_SERVER['REMOTE_ADDR'];
+
+ if (empty($id)) {
+ $id = $ip;
+ }
+
+ $time = time();
+ $code = $this->code;
+ $code_disp = $this->code_display;
+
+ // This is somewhat expensive in PDO Sqlite3 (when there is something to delete)
+ // Clears previous captcha for this client from database so we can do a straight insert
+ // without having to do INSERT ... ON DUPLICATE KEY or a find/update
+ $this->clearCodeFromDatabase();
+
+ $query = "INSERT INTO {$this->database_table} ("
+ ."id, code, code_display, namespace, created) "
+ ."VALUES(?, ?, ?, ?, ?)";
+
+ $stmt = $this->pdo_conn->prepare($query);
+ $success = $stmt->execute(array($id, $code, $code_disp, $this->namespace, $time));
+
+ if (!$success) {
+ $err = $stmt->errorInfo();
+ $error = "Failed to insert code into database. {$err[1]}: {$err[2]}.";
+
+ if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
+ $err14 = ($err[1] == 14);
+ if ($err14) $error .= sprintf(" Ensure database directory and file are writeable by user '%s' (%d).",
+ get_current_user(), getmyuid());
+ }
+
+ trigger_error($error, E_USER_WARNING);
+ }
+ }
+
+ return $success !== false;
+ }
+
+ /**
+ * Saves CAPTCHA audio to the configured database
+ *
+ * @param string $data Audio data
+ * @return boolean true on success, false on failure
+ */
+ protected function saveAudioToDatabase($data)
+ {
+ $success = false;
+ $this->openDatabase();
+
+ if ($this->use_database && $this->pdo_conn) {
+ $id = $this->getCaptchaId(false);
+ $ip = $_SERVER['REMOTE_ADDR'];
+ $ns = $this->namespace;
+
+ if (empty($id)) {
+ $id = $ip;
+ }
+
+ $query = "UPDATE {$this->database_table} SET audio_data = :audioData WHERE id = :id AND namespace = :namespace";
+ $stmt = $this->pdo_conn->prepare($query);
+ $stmt->bindParam(':audioData', $data, PDO::PARAM_LOB);
+ $stmt->bindParam(':id', $id);
+ $stmt->bindParam(':namespace', $ns);
+ $success = $stmt->execute();
+ }
+
+ return $success !== false;
+ }
+
+ /**
+ * Opens a connection to the configured database.
+ *
+ * @see Securimage::$use_database Use database
+ * @see Securimage::$database_driver Database driver
+ * @see Securimage::$pdo_conn pdo_conn
+ * @return bool true if the database connection was successful, false if not
+ */
+ protected function openDatabase()
+ {
+ $this->pdo_conn = false;
+
+ if ($this->use_database) {
+ $pdo_extension = 'PDO_' . strtoupper($this->database_driver);
+
+ if (!extension_loaded($pdo_extension)) {
+ trigger_error("Database support is turned on in Securimage, but the chosen extension $pdo_extension is not loaded in PHP.", E_USER_WARNING);
+ return false;
+ }
+ }
+
+ if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
+ if (!file_exists($this->database_file)) {
+ $fp = fopen($this->database_file, 'w+');
+ if (!$fp) {
+ $err = error_get_last();
+ trigger_error("Securimage failed to create SQLite3 database file '{$this->database_file}'. Reason: {$err['message']}", E_USER_WARNING);
+ return false;
+ }
+ fclose($fp);
+ chmod($this->database_file, 0666);
+ } else if (!is_writeable($this->database_file)) {
+ trigger_error("Securimage does not have read/write access to database file '{$this->database_file}. Make sure permissions are 0666 and writeable by user '" . get_current_user() . "'", E_USER_WARNING);
+ return false;
+ }
+ }
+
+ try {
+ $dsn = $this->getDsn();
+
+ $options = array();
+ $this->pdo_conn = new PDO($dsn, $this->database_user, $this->database_pass, $options);
+ } catch (PDOException $pdoex) {
+ trigger_error("Database connection failed: " . $pdoex->getMessage(), E_USER_WARNING);
+ return false;
+ } catch (Exception $ex) {
+ trigger_error($ex->getMessage(), E_USER_WARNING);
+ return false;
+ }
+
+ try {
+ if (!$this->skip_table_check && !$this->checkTablesExist()) {
+ // create tables...
+ $this->createDatabaseTables();
+ }
+ } catch (Exception $ex) {
+ trigger_error($ex->getMessage(), E_USER_WARNING);
+ $this->pdo_conn = false;
+ return false;
+ }
+
+ if (mt_rand(0, 100) / 100.0 == 1.0) {
+ $this->purgeOldCodesFromDatabase();
+ }
+
+ return $this->pdo_conn;
+ }
+
+ /**
+ * Get the PDO DSN string for connecting to the database
+ *
+ * @see Securimage::$database_driver Database driver
+ * @throws Exception If database specific options are not configured
+ * @return string The DSN for connecting to the database
+ */
+ protected function getDsn()
+ {
+ $dsn = sprintf('%s:', $this->database_driver);
+
+ switch($this->database_driver) {
+ case self::SI_DRIVER_SQLITE3:
+ $dsn .= $this->database_file;
+ break;
+
+ case self::SI_DRIVER_MYSQL:
+ case self::SI_DRIVER_PGSQL:
+ if (empty($this->database_host)) {
+ throw new Exception('Securimage::database_host is not set');
+ } else if (empty($this->database_name)) {
+ throw new Exception('Securimage::database_name is not set');
+ }
+
+ $dsn .= sprintf('host=%s;dbname=%s',
+ $this->database_host,
+ $this->database_name);
+ break;
+
+ }
+
+ return $dsn;
+ }
+
+ /**
+ * Checks if the necessary database tables for storing captcha codes exist
+ *
+ * @throws Exception If the table check failed for some reason
+ * @return boolean true if the database do exist, false if not
+ */
+ protected function checkTablesExist()
+ {
+ $table = $this->pdo_conn->quote($this->database_table);
+
+ switch($this->database_driver) {
+ case self::SI_DRIVER_SQLITE3:
+ // query row count for sqlite, PRAGMA queries seem to return no
+ // rowCount using PDO even if there are rows returned
+ $query = "SELECT COUNT(id) FROM $table";
+ break;
+
+ case self::SI_DRIVER_MYSQL:
+ $query = "SHOW TABLES LIKE $table";
+ break;
+
+ case self::SI_DRIVER_PGSQL:
+ $query = "SELECT * FROM information_schema.columns WHERE table_name = $table;";
+ break;
+ }
+
+ $result = $this->pdo_conn->query($query);
+
+ if (!$result) {
+ $err = $this->pdo_conn->errorInfo();
+
+ if ($this->database_driver == self::SI_DRIVER_SQLITE3 &&
+ $err[1] === 1 && strpos($err[2], 'no such table') !== false)
+ {
+ return false;
+ }
+
+ throw new Exception("Failed to check tables: {$err[0]} - {$err[1]}: {$err[2]}");
+ } else if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
+ // successful here regardless of row count for sqlite
+ return true;
+ } else if ($result->rowCount() == 0) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Create the necessary databaes table for storing captcha codes.
+ *
+ * Based on the database adapter used, the tables will created in the existing connection.
+ *
+ * @see Securimage::$database_driver Database driver
+ * @return boolean true if the tables were created, false if not
+ */
+ protected function createDatabaseTables()
+ {
+ $queries = array();
+
+ switch($this->database_driver) {
+ case self::SI_DRIVER_SQLITE3:
+ $queries[] = "CREATE TABLE \"{$this->database_table}\" (
+ id VARCHAR(40),
+ namespace VARCHAR(32) NOT NULL,
+ code VARCHAR(32) NOT NULL,
+ code_display VARCHAR(32) NOT NULL,
+ created INTEGER NOT NULL,
+ audio_data BLOB NULL,
+ PRIMARY KEY(id, namespace)
+ )";
+
+ $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created)";
+ break;
+
+ case self::SI_DRIVER_MYSQL:
+ $queries[] = "CREATE TABLE `{$this->database_table}` (
+ `id` VARCHAR(40) NOT NULL,
+ `namespace` VARCHAR(32) NOT NULL,
+ `code` VARCHAR(32) NOT NULL,
+ `code_display` VARCHAR(32) NOT NULL,
+ `created` INT NOT NULL,
+ `audio_data` MEDIUMBLOB NULL,
+ PRIMARY KEY(id, namespace),
+ INDEX(created)
+ )";
+ break;
+
+ case self::SI_DRIVER_PGSQL:
+ $queries[] = "CREATE TABLE {$this->database_table} (
+ id character varying(40) NOT NULL,
+ namespace character varying(32) NOT NULL,
+ code character varying(32) NOT NULL,
+ code_display character varying(32) NOT NULL,
+ created integer NOT NULL,
+ audio_data bytea NULL,
+ CONSTRAINT pkey_id_namespace PRIMARY KEY (id, namespace)
+ )";
+
+ $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created);";
+ break;
+ }
+
+ $this->pdo_conn->beginTransaction();
+
+ foreach($queries as $query) {
+ $result = $this->pdo_conn->query($query);
+
+ if (!$result) {
+ $err = $this->pdo_conn->errorInfo();
+ trigger_error("Failed to create table. {$err[1]}: {$err[2]}", E_USER_WARNING);
+ $this->pdo_conn->rollBack();
+ $this->pdo_conn = false;
+ return false;
+ }
+ }
+
+ $this->pdo_conn->commit();
+
+ return true;
+ }
+
+ /**
+ * Retrieves a stored code from the database for based on the captchaId or
+ * IP address if captcha ID not used.
+ *
+ * @return string|array Empty string if no code was found or has expired,
+ * otherwise returns array of code information.
+ */
+ protected function getCodeFromDatabase()
+ {
+ $code = '';
+
+ if ($this->use_database == true && $this->pdo_conn) {
+ if (Securimage::$_captchaId !== null) {
+ $query = "SELECT * FROM {$this->database_table} WHERE id = ?";
+ $stmt = $this->pdo_conn->prepare($query);
+ $result = $stmt->execute(array(Securimage::$_captchaId));
+ } else {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ $ns = $this->namespace;
+
+ // ip is stored in id column when no captchaId
+ $query = "SELECT * FROM {$this->database_table} WHERE id = ? AND namespace = ?";
+ $stmt = $this->pdo_conn->prepare($query);
+ $result = $stmt->execute(array($ip, $ns));
+ }
+
+ if (!$result) {
+ $err = $this->pdo_conn->errorInfo();
+ trigger_error("Failed to select code from database. {$err[0]}: {$err[1]}", E_USER_WARNING);
+ } else {
+ if ( ($row = $stmt->fetch()) !== false ) {
+ if (false == $this->isCodeExpired($row['created'])) {
+ if ($this->database_driver == self::SI_DRIVER_PGSQL && is_resource($row['audio_data'])) {
+ // pg bytea data returned as stream resource
+ $data = '';
+ while (!feof($row['audio_data'])) {
+ $data .= fgets($row['audio_data']);
+ }
+ $row['audio_data'] = $data;
+ }
+ $code = array(
+ 'code' => $row['code'],
+ 'code_disp' => $row['code_display'],
+ 'time' => $row['created'],
+ 'audio_data' => $row['audio_data'],
+ );
+ }
+ }
+ }
+ }
+
+ return $code;
+ }
+
+ /**
+ * Remove a stored code from the database based on captchaId or IP address.
+ */
+ protected function clearCodeFromDatabase()
+ {
+ if ($this->pdo_conn) {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ $ns = $this->pdo_conn->quote($this->namespace);
+ $id = Securimage::$_captchaId;
+
+ if (empty($id)) {
+ $id = $ip; // if no captchaId set, IP address is captchaId.
+ }
+
+ $id = $this->pdo_conn->quote($id);
+
+ $query = sprintf("DELETE FROM %s WHERE id = %s AND namespace = %s",
+ $this->database_table, $id, $ns);
+
+ $result = $this->pdo_conn->query($query);
+ if (!$result) {
+ trigger_error("Failed to delete code from database.", E_USER_WARNING);
+ }
+ }
+ }
+
+ /**
+ * Deletes old (expired) codes from the database
+ */
+ protected function purgeOldCodesFromDatabase()
+ {
+ if ($this->use_database && $this->pdo_conn) {
+ $now = time();
+ $limit = (!is_numeric($this->expiry_time) || $this->expiry_time < 1) ? 86400 : $this->expiry_time;
+
+ $query = sprintf("DELETE FROM %s WHERE %s - created > %s",
+ $this->database_table,
+ $now,
+ $this->pdo_conn->quote("$limit", PDO::PARAM_INT));
+
+ $result = $this->pdo_conn->query($query);
+ }
+ }
+
+ /**
+ * Checks to see if the captcha code has expired and can no longer be used.
+ *
+ * @see Securimage::$expiry_time expiry_time
+ * @param int $creation_time The Unix timestamp of when the captcha code was created
+ * @return bool true if the code is expired, false if it is still valid
+ */
+ protected function isCodeExpired($creation_time)
+ {
+ $expired = true;
+
+ if (!is_numeric($this->expiry_time) || $this->expiry_time < 1) {
+ $expired = false;
+ } else if (time() - $creation_time < $this->expiry_time) {
+ $expired = false;
+ }
+
+ return $expired;
+ }
+
+ /**
+ * Generate a wav file given the $letters in the code
+ *
+ * @param array $letters The letters making up the captcha
+ * @return string The audio content in WAV format
+ */
+ protected function generateWAV($letters)
+ {
+ $wavCaptcha = new WavFile();
+ $first = true; // reading first wav file
+
+ if ($this->audio_use_sox && !is_executable($this->sox_binary_path)) {
+ throw new Exception("Path to SoX binary is incorrect or not executable");
+ }
+
+ foreach ($letters as $letter) {
+ $letter = strtoupper($letter);
+
+ try {
+ $letter_file = realpath($this->audio_path) . DIRECTORY_SEPARATOR . $letter . '.wav';
+
+ if ($this->audio_use_sox) {
+ $sox_cmd = sprintf("%s %s -t wav - %s",
+ $this->sox_binary_path,
+ $letter_file,
+ $this->getSoxEffectChain());
+
+ $data = `$sox_cmd`;
+
+ $l = new WavFile();
+ $l->setIgnoreChunkSizes(true);
+ $l->setWavData($data);
+ } else {
+ $l = new WavFile($letter_file);
+ }
+
+ if ($first) {
+ // set sample rate, bits/sample, and # of channels for file based on first letter
+ $wavCaptcha->setSampleRate($l->getSampleRate())
+ ->setBitsPerSample($l->getBitsPerSample())
+ ->setNumChannels($l->getNumChannels());
+ $first = false;
+ }
+
+ // append letter to the captcha audio
+ $wavCaptcha->appendWav($l);
+
+ // random length of silence between $audio_gap_min and $audio_gap_max
+ if ($this->audio_gap_max > 0 && $this->audio_gap_max > $this->audio_gap_min) {
+ $wavCaptcha->insertSilence( mt_rand($this->audio_gap_min, $this->audio_gap_max) / 1000.0 );
+ }
+ } catch (Exception $ex) {
+ // failed to open file, or the wav file is broken or not supported
+ // 2 wav files were not compatible, different # channels, bits/sample, or sample rate
+ throw new Exception("Error generating audio captcha on letter '$letter': " . $ex->getMessage());
+ }
+ }
+
+ /********* Set up audio filters *****************************/
+ $filters = array();
+
+ if ($this->audio_use_noise == true) {
+ // use background audio - find random file
+ $wavNoise = false;
+ $randOffset = 0;
+
+ /*
+ // uncomment to try experimental SoX noise generation.
+ // warning: sounds may be considered annoying
+ if ($this->audio_use_sox) {
+ $duration = $wavCaptcha->getDataSize() / ($wavCaptcha->getBitsPerSample() / 8) /
+ $wavCaptcha->getNumChannels() / $wavCaptcha->getSampleRate();
+ $duration = round($duration, 2);
+ $wavNoise = new WavFile();
+ $wavNoise->setIgnoreChunkSizes(true);
+ $noiseData = $this->getSoxNoiseData($duration,
+ $wavCaptcha->getNumChannels(),
+ $wavCaptcha->getSampleRate(),
+ $wavCaptcha->getBitsPerSample());
+ $wavNoise->setWavData($noiseData, true);
+
+ } else
+ */
+ if ( ($noiseFile = $this->getRandomNoiseFile()) !== false) {
+ try {
+ $wavNoise = new WavFile($noiseFile, false);
+ } catch(Exception $ex) {
+ throw $ex;
+ }
+
+ // start at a random offset from the beginning of the wavfile
+ // in order to add more randomness
+
+ $randOffset = 0;
+
+ if ($wavNoise->getNumBlocks() > 2 * $wavCaptcha->getNumBlocks()) {
+ $randBlock = mt_rand(0, $wavNoise->getNumBlocks() - $wavCaptcha->getNumBlocks());
+ $wavNoise->readWavData($randBlock * $wavNoise->getBlockAlign(), $wavCaptcha->getNumBlocks() * $wavNoise->getBlockAlign());
+ } else {
+ $wavNoise->readWavData();
+ $randOffset = mt_rand(0, $wavNoise->getNumBlocks() - 1);
+ }
+ }
+
+ if ($wavNoise !== false) {
+ $mixOpts = array('wav' => $wavNoise,
+ 'loop' => true,
+ 'blockOffset' => $randOffset);
+
+ $filters[WavFile::FILTER_MIX] = $mixOpts;
+ $filters[WavFile::FILTER_NORMALIZE] = $this->audio_mix_normalization;
+ }
+ }
+
+ if ($this->degrade_audio == true) {
+ // add random noise.
+ // any noise level below 95% is intensely distorted and not pleasant to the ear
+ $filters[WavFile::FILTER_DEGRADE] = mt_rand(95, 98) / 100.0;
+ }
+
+ if (!empty($filters)) {
+ $wavCaptcha->filter($filters); // apply filters to captcha audio
+ }
+
+ return $wavCaptcha->__toString();
+ }
+
+ /**
+ * Gets and returns the path to a random noise file from the audio noise directory.
+ *
+ * @return bool|string false if a file could not be found, or a string containing the path to the file.
+ */
+ public function getRandomNoiseFile()
+ {
+ $return = false;
+
+ if ( ($dh = opendir($this->audio_noise_path)) !== false ) {
+ $list = array();
+
+ while ( ($file = readdir($dh)) !== false ) {
+ if ($file == '.' || $file == '..') continue;
+ if (strtolower(substr($file, -4)) != '.wav') continue;
+
+ $list[] = $file;
+ }
+
+ closedir($dh);
+
+ if (sizeof($list) > 0) {
+ $file = $list[array_rand($list, 1)];
+ $return = $this->audio_noise_path . DIRECTORY_SEPARATOR . $file;
+
+ if (!is_readable($return)) $return = false;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Get a random effect or chain of effects to apply to a segment of the
+ * audio file.
+ *
+ * These effects should increase the randomness of the audio for
+ * a particular letter/number by modulating the signal. The SoX effects
+ * used are *bend*, *chorus*, *overdrive*, *pitch*, *reverb*, *tempo*, and
+ * *tremolo*.
+ *
+ * For each effect selected, random parameters are supplied to the effect.
+ *
+ * @param int $numEffects How many effects to chain together
+ * @return string A string of valid SoX effects and their respective options.
+ */
+ protected function getSoxEffectChain($numEffects = 2)
+ {
+ $effectsList = array('bend', 'chorus', 'overdrive', 'pitch', 'reverb', 'tempo', 'tremolo');
+ $effects = array_rand($effectsList, $numEffects);
+ $outEffects = array();
+
+ if (!is_array($effects)) $effects = array($effects);
+
+ foreach($effects as $effect) {
+ $effect = $effectsList[$effect];
+
+ switch($effect)
+ {
+ case 'bend':
+ $delay = mt_rand(0, 15) / 100.0;
+ $cents = mt_rand(-120, 120);
+ $dur = mt_rand(75, 400) / 100.0;
+ $outEffects[] = "$effect $delay,$cents,$dur";
+ break;
+
+ case 'chorus':
+ $gainIn = mt_rand(75, 90) / 100.0;
+ $gainOut = mt_rand(70, 95) / 100.0;
+ $chorStr = "$effect $gainIn $gainOut";
+
+ for ($i = 0; $i < mt_rand(2, 3); ++$i) {
+ $delay = mt_rand(20, 100);
+ $decay = mt_rand(10, 100) / 100.0;
+ $speed = mt_rand(20, 50) / 100.0;
+ $depth = mt_rand(150, 250) / 100.0;
+
+ $chorStr .= " $delay $decay $speed $depth -s";
+ }
+
+ $outEffects[] = $chorStr;
+ break;
+
+ case 'overdrive':
+ $gain = mt_rand(5, 25);
+ $color = mt_rand(20, 70);
+ $outEffects[] = "$effect $gain $color";
+ break;
+
+ case 'pitch':
+ $cents = mt_rand(-300, 300);
+ $outEffects[] = "$effect $cents";
+ break;
+
+ case 'reverb':
+ $reverberance = mt_rand(20, 80);
+ $damping = mt_rand(10, 80);
+ $scale = mt_rand(85, 100);
+ $depth = mt_rand(90, 100);
+ $predelay = mt_rand(0, 5);
+ $outEffects[] = "$effect $reverberance $damping $scale $depth $predelay";
+ break;
+
+ case 'tempo':
+ $factor = mt_rand(65, 135) / 100.0;
+ $outEffects[] = "$effect -s $factor";
+ break;
+
+ case 'tremolo':
+ $hz = mt_rand(10, 30);
+ $depth = mt_rand(40, 85);
+ $outEffects[] = "$effect $hz $depth";
+ break;
+ }
+ }
+
+ return implode(' ', $outEffects);
+ }
+
+ /**
+ * This function is not yet used.
+ *
+ * Generate random background noise from sweeping oscillators
+ *
+ * @param float $duration How long in seconds the generated sound will be
+ * @param int $numChannels Number of channels in output wav
+ * @param int $sampleRate Sample rate of output wav
+ * @param int $bitRate Bits per sample (8, 16, 24)
+ * @return string Audio data in wav format
+ */
+ protected function getSoxNoiseData($duration, $numChannels, $sampleRate, $bitRate)
+ {
+ $shapes = array('sine', 'square', 'triangle', 'sawtooth', 'trapezium');
+ $steps = array(':', '+', '/', '-');
+ $selShapes = array_rand($shapes, 2);
+ $selSteps = array_rand($steps, 2);
+ $sweep0 = array();
+ $sweep0[0] = mt_rand(100, 700);
+ $sweep0[1] = mt_rand(1500, 2500);
+ $sweep1 = array();
+ $sweep1[0] = mt_rand(500, 1000);
+ $sweep1[1] = mt_rand(1200, 2000);
+
+ if (mt_rand(0, 10) % 2 == 0)
+ $sweep0 = array_reverse($sweep0);
+
+ if (mt_rand(0, 10) % 2 == 0)
+ $sweep1 = array_reverse($sweep1);
+
+ $cmd = sprintf("%s -c %d -r %d -b %d -n -t wav - synth noise create vol 0.3 synth %.2f %s mix %d%s%d vol 0.3 synth %.2f %s fmod %d%s%d vol 0.3",
+ $this->sox_binary_path,
+ $numChannels,
+ $sampleRate,
+ $bitRate,
+ $duration,
+ $shapes[$selShapes[0]],
+ $sweep0[0],
+ $steps[$selSteps[0]],
+ $sweep0[1],
+ $duration,
+ $shapes[$selShapes[1]],
+ $sweep1[0],
+ $steps[$selSteps[1]],
+ $sweep1[1]
+ );
+ $data = `$cmd`;
+
+ return $data;
+ }
+
+ /**
+ * Convert WAV data to MP3 using the Lame MP3 encoder binary
+ *
+ * @param string $data Contents of the WAV file to convert
+ * @return string MP3 file data
+ */
+ protected function wavToMp3($data)
+ {
+ if (!file_exists(self::$lame_binary_path) || !is_executable(self::$lame_binary_path)) {
+ throw new Exception('Lame binary "' . $this->lame_binary_path . '" does not exist or is not executable');
+ }
+
+ // size of wav data input
+ $size = strlen($data);
+
+ // file descriptors for reading and writing to the Lame process
+ $descriptors = array(
+ 0 => array('pipe', 'r'), // stdin
+ 1 => array('pipe', 'w'), // stdout
+ 2 => array('pipe', 'a'), // stderr
+ );
+
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ // workaround for Windows conversion
+ // writing to STDIN seems to hang indefinitely after writing approximately 0xC400 bytes
+ $wavinput = tempnam(sys_get_temp_dir(), 'wav');
+ if (!$wavinput) {
+ throw new Exception('Failed to create temporary file for WAV to MP3 conversion');
+ }
+ file_put_contents($wavinput, $data);
+ $size = 0;
+ } else {
+ $wavinput = '-'; // stdin
+ }
+
+ // Mono, variable bit rate, 32 kHz sampling rate, read WAV from stdin, write MP3 to stdout
+ $cmd = sprintf("%s -m m -v -b 32 %s -", self::$lame_binary_path, $wavinput);
+ $proc = proc_open($cmd, $descriptors, $pipes);
+
+ if (!is_resource($proc)) {
+ throw new Exception('Failed to open process for MP3 encoding');
+ }
+
+ stream_set_blocking($pipes[0], 0); // set stdin to be non-blocking
+
+ for ($written = 0; $written < $size; $written += $len) {
+ // write to stdin until all WAV data is written
+ $len = fwrite($pipes[0], substr($data, $written, 0x20000));
+
+ if ($len === 0) {
+ // fwrite wrote no data, make sure process is still alive, otherwise wait for it to process
+ $status = proc_get_status($proc);
+ if ($status['running'] === false) break;
+ usleep(25000);
+ } else if ($written < $size) {
+ // couldn't write all data, small pause and try again
+ usleep(10000);
+ } else if ($len === false) {
+ // fwrite failed, should not happen
+ break;
+ }
+ }
+
+ fclose($pipes[0]);
+
+ $data = stream_get_contents($pipes[1]);
+ $err = trim(stream_get_contents($pipes[2]));
+
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+
+ $return = proc_close($proc);
+
+ if ($wavinput != '-') unlink($wavinput); // delete temp file on Windows
+
+ if ($return !== 0) {
+ throw new Exception("Failed to convert WAV to MP3. Shell returned ({$return}): {$err}");
+ } else if ($written < $size) {
+ throw new Exception('Failed to convert WAV to MP3. Failed to write all data to encoder');
+ }
+
+ return $data;
+ }
+
+ /**
+ * Return a wav file saying there was an error generating file
+ *
+ * @return string The binary audio contents
+ */
+ protected function audioError()
+ {
+ return @file_get_contents(dirname(__FILE__) . '/audio/en/error.wav');
+ }
+
+ /**
+ * Checks to see if headers can be sent and if any error has been output
+ * to the browser
+ *
+ * @return bool true if it is safe to send headers, false if not
+ */
+ protected function canSendHeaders()
+ {
+ if (headers_sent()) {
+ // output has been flushed and headers have already been sent
+ return false;
+ } else if (strlen((string)ob_get_contents()) > 0) {
+ // headers haven't been sent, but there is data in the buffer that will break image and audio data
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return a random float between 0 and 0.9999
+ *
+ * @return float Random float between 0 and 0.9999
+ */
+ function frand()
+ {
+ return 0.0001 * mt_rand(0,9999);
+ }
+
+ /**
+ * Convert an html color code to a Securimage_Color
+ * @param string $color
+ * @param Securimage_Color|string $default The defalt color to use if $color is invalid
+ */
+ protected function initColor($color, $default)
+ {
+ if ($color == null) {
+ return new Securimage_Color($default);
+ } else if (is_string($color)) {
+ try {
+ return new Securimage_Color($color);
+ } catch(Exception $e) {
+ return new Securimage_Color($default);
+ }
+ } else if (is_array($color) && sizeof($color) == 3) {
+ return new Securimage_Color($color[0], $color[1], $color[2]);
+ } else {
+ return new Securimage_Color($default);
+ }
+ }
+
+ /**
+ * The error handling function used when outputting captcha image or audio.
+ *
+ * This error handler helps determine if any errors raised would
+ * prevent captcha image or audio from displaying. If they have
+ * no effect on the output buffer or headers, true is returned so
+ * the script can continue processing.
+ *
+ * See https://github.com/dapphp/securimage/issues/15
+ *
+ * @param int $errno PHP error number
+ * @param string $errstr String description of the error
+ * @param string $errfile File error occurred in
+ * @param int $errline Line the error occurred on in file
+ * @param array $errcontext Additional context information
+ * @return boolean true if the error was handled, false if PHP should handle the error
+ */
+ public function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array())
+ {
+ // get the current error reporting level
+ $level = error_reporting();
+
+ // if error was supressed or $errno not set in current error level
+ if ($level == 0 || ($level & $errno) == 0) {
+ return true;
+ }
+
+ return false;
+ }
+}
+
+
+/**
+ * Color object for Securimage CAPTCHA
+ *
+* @since 2.0
+ * @package Securimage
+ * @subpackage classes
+ *
+ */
+class Securimage_Color
+{
+ /**
+ * Red value (0-255)
+ * @var int
+ */
+ public $r;
+
+ /**
+ * Gree value (0-255)
+ * @var int
+ */
+ public $g;
+
+ /**
+ * Blue value (0-255)
+ * @var int
+ */
+ public $b;
+
+ /**
+ * Create a new Securimage_Color object.
+ *
+ * Constructor expects 1 or 3 arguments.
+ *
+ * When passing a single argument, specify the color using HTML hex format.
+ *
+ * When passing 3 arguments, specify each RGB component (from 0-255)
+ * individually.
+ *
+ * Examples:
+ *
+ * $color = new Securimage_Color('#0080FF');
+ * $color = new Securimage_Color(0, 128, 255);
+ *
+ * @param string $color The html color code to use
+ * @throws Exception If any color value is not valid
+ */
+ public function __construct($color = '#ffffff')
+ {
+ $args = func_get_args();
+
+ if (sizeof($args) == 0) {
+ $this->r = 255;
+ $this->g = 255;
+ $this->b = 255;
+ } else if (sizeof($args) == 1) {
+ // set based on html code
+ if (substr($color, 0, 1) == '#') {
+ $color = substr($color, 1);
+ }
+
+ if (strlen($color) != 3 && strlen($color) != 6) {
+ throw new InvalidArgumentException(
+ 'Invalid HTML color code passed to Securimage_Color'
+ );
+ }
+
+ $this->constructHTML($color);
+ } else if (sizeof($args) == 3) {
+ $this->constructRGB($args[0], $args[1], $args[2]);
+ } else {
+ throw new InvalidArgumentException(
+ 'Securimage_Color constructor expects 0, 1 or 3 arguments; ' . sizeof($args) . ' given'
+ );
+ }
+ }
+
+ /**
+ * Construct from an rgb triplet
+ *
+ * @param int $red The red component, 0-255
+ * @param int $green The green component, 0-255
+ * @param int $blue The blue component, 0-255
+ */
+ protected function constructRGB($red, $green, $blue)
+ {
+ if ($red < 0) $red = 0;
+ if ($red > 255) $red = 255;
+ if ($green < 0) $green = 0;
+ if ($green > 255) $green = 255;
+ if ($blue < 0) $blue = 0;
+ if ($blue > 255) $blue = 255;
+
+ $this->r = $red;
+ $this->g = $green;
+ $this->b = $blue;
+ }
+
+ /**
+ * Construct from an html hex color code
+ *
+ * @param string $color
+ */
+ protected function constructHTML($color)
+ {
+ if (strlen($color) == 3) {
+ $red = str_repeat(substr($color, 0, 1), 2);
+ $green = str_repeat(substr($color, 1, 1), 2);
+ $blue = str_repeat(substr($color, 2, 1), 2);
+ } else {
+ $red = substr($color, 0, 2);
+ $green = substr($color, 2, 2);
+ $blue = substr($color, 4, 2);
+ }
+
+ $this->r = hexdec($red);
+ $this->g = hexdec($green);
+ $this->b = hexdec($blue);
+ }
+}
diff --git a/vendor/dapphp/securimage/securimage_play.php b/vendor/dapphp/securimage/securimage_play.php
new file mode 100644
index 0000000..b028c2b
--- /dev/null
+++ b/vendor/dapphp/securimage/securimage_play.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Project: Securimage: A PHP class for creating and managing form CAPTCHA images<br />
+ * File: securimage_play.php<br />
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or any later version.<br /><br />
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.<br /><br />
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA<br /><br />
+ *
+ * Any modifications to the library should be indicated clearly in the source code
+ * to inform users that the changes are not a part of the original software.<br /><br />
+ *
+ * If you found this script useful, please take a quick moment to rate it.<br />
+ * http://www.hotscripts.com/rate/49400.html Thanks.
+ *
+ * @link http://www.phpcaptcha.org Securimage PHP CAPTCHA
+ * @link http://www.phpcaptcha.org/latest.zip Download Latest Version
+ * @link http://www.phpcaptcha.org/Securimage_Docs/ Online Documentation
+ * @copyright 2012 Drew Phillips
+ * @author Drew Phillips <drew@drew-phillips.com>
+ * @version 3.6.6 (Nov 20 2017)
+ * @package Securimage
+ *
+ */
+
+require_once dirname(__FILE__) . '/securimage.php';
+
+// if using database, adjust these options as necessary and change $img = new Securimage(); to $img = new Securimage($options);
+// see test.mysql.php or test.sqlite.php for examples
+$options = array(
+ 'use_database' => true,
+ 'database_name' => '',
+ 'database_user' => '',
+ 'database_driver' => Securimage::SI_DRIVER_MYSQL
+);
+
+$img = new Securimage();
+
+// Other audio settings
+//$img->audio_use_sox = true;
+//$img->audio_use_noise = true;
+//$img->degrade_audio = false;
+//$img->sox_binary_path = 'sox';
+//Securimage::$lame_binary_path = '/usr/bin/lame'; // for mp3 audio support
+
+// To use an alternate language, uncomment the following and download the files from phpcaptcha.org
+// $img->audio_path = $img->securimage_path . '/audio/es/';
+
+// If you have more than one captcha on a page, one must use a custom namespace
+// $img->namespace = 'form2';
+
+// set namespace if supplied to script via HTTP GET
+if (!empty($_GET['namespace'])) $img->setNamespace($_GET['namespace']);
+
+
+// mp3 or wav format
+$format = (isset($_GET['format']) && strtolower($_GET['format']) == 'mp3') ? 'mp3' : null;
+
+$img->outputAudioFile($format);
diff --git a/vendor/dapphp/securimage/securimage_show.php b/vendor/dapphp/securimage/securimage_show.php
new file mode 100644
index 0000000..f352f76
--- /dev/null
+++ b/vendor/dapphp/securimage/securimage_show.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * Project: Securimage: A PHP class for creating and managing form CAPTCHA images<br />
+ * File: securimage_show.php<br />
+ *
+ * Copyright (c) 2013, Drew Phillips
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Any modifications to the library should be indicated clearly in the source code
+ * to inform users that the changes are not a part of the original software.<br /><br />
+ *
+ * If you found this script useful, please take a quick moment to rate it.<br />
+ * http://www.hotscripts.com/rate/49400.html Thanks.
+ *
+ * @link http://www.phpcaptcha.org Securimage PHP CAPTCHA
+ * @link http://www.phpcaptcha.org/latest.zip Download Latest Version
+ * @link http://www.phpcaptcha.org/Securimage_Docs/ Online Documentation
+ * @copyright 2013 Drew Phillips
+ * @author Drew Phillips <drew@drew-phillips.com>
+ * @version 3.6.6 (Nov 20 2017)
+ * @package Securimage
+ *
+ */
+
+// Remove the "//" from the following line for debugging problems
+// error_reporting(E_ALL); ini_set('display_errors', 1);
+
+require_once dirname(__FILE__) . '/securimage.php';
+
+$img = new Securimage();
+
+// You can customize the image by making changes below, some examples are included - remove the "//" to uncomment
+
+//$img->ttf_file = './Quiff.ttf';
+//$img->captcha_type = Securimage::SI_CAPTCHA_MATHEMATIC; // show a simple math problem instead of text
+//$img->case_sensitive = true; // true to use case sensitve codes - not recommended
+//$img->image_height = 90; // height in pixels of the image
+//$img->image_width = $img->image_height * M_E; // a good formula for image size based on the height
+//$img->perturbation = .75; // 1.0 = high distortion, higher numbers = more distortion
+//$img->image_bg_color = new Securimage_Color("#0099CC"); // image background color
+//$img->text_color = new Securimage_Color("#EAEAEA"); // captcha text color
+//$img->num_lines = 8; // how many lines to draw over the image
+//$img->line_color = new Securimage_Color("#0000CC"); // color of lines over the image
+//$img->image_type = SI_IMAGE_JPEG; // render as a jpeg image
+//$img->signature_color = new Securimage_Color(rand(0, 64),
+// rand(64, 128),
+// rand(128, 255)); // random signature color
+
+// see securimage.php for more options that can be set
+
+// set namespace if supplied to script via HTTP GET
+if (!empty($_GET['namespace'])) $img->setNamespace($_GET['namespace']);
+
+
+$img->show(); // outputs the image and content headers to the browser
+// alternate use:
+// $img->show('/path/to/background_image.jpg');
diff --git a/vendor/dapphp/securimage/words/words.txt b/vendor/dapphp/securimage/words/words.txt
new file mode 100644
index 0000000..9a444ce
--- /dev/null
+++ b/vendor/dapphp/securimage/words/words.txt
@@ -0,0 +1,15457 @@
+aahing
+aaliis
+aarrgh
+abacas
+abacus
+abakas
+abamps
+abased
+abaser
+abases
+abasia
+abated
+abater
+abates
+abatis
+abator
+abayas
+abbacy
+abbess
+abbeys
+abbots
+abduce
+abduct
+abeles
+abelia
+abhors
+abided
+abider
+abides
+abject
+abjure
+ablate
+ablaut
+ablaze
+ablest
+ablins
+abloom
+ablush
+abmhos
+aboard
+aboded
+abodes
+abohms
+abolla
+abomas
+aboral
+aborts
+abound
+aboves
+abrade
+abroad
+abrupt
+abseil
+absent
+absorb
+absurd
+abulia
+abulic
+abvolt
+abwatt
+abying
+abysms
+acacia
+acajou
+acarid
+acarus
+accede
+accent
+accept
+access
+accord
+accost
+accrue
+accuse
+acedia
+acetal
+acetic
+acetin
+acetum
+acetyl
+achene
+achier
+aching
+acidic
+acidly
+acinar
+acinic
+acinus
+ackees
+acnode
+acorns
+acquit
+across
+acting
+actins
+action
+active
+actors
+actual
+acuate
+acuity
+aculei
+acumen
+acuter
+acutes
+adages
+adagio
+adapts
+addend
+adders
+addict
+adding
+addled
+addles
+adduce
+adduct
+adeems
+adenyl
+adepts
+adhere
+adieus
+adieux
+adipic
+adjoin
+adjure
+adjust
+admass
+admire
+admits
+admixt
+adnate
+adnexa
+adnoun
+adobes
+adobos
+adonis
+adopts
+adored
+adorer
+adores
+adorns
+adrift
+adroit
+adsorb
+adults
+advect
+advent
+adverb
+advert
+advice
+advise
+adytum
+adzing
+adzuki
+aecial
+aecium
+aedile
+aedine
+aeneus
+aeonic
+aerate
+aerial
+aeried
+aerier
+aeries
+aerify
+aerily
+aerobe
+aerugo
+aether
+afeard
+affair
+affect
+affine
+affirm
+afflux
+afford
+affray
+afghan
+afield
+aflame
+afloat
+afraid
+afreet
+afresh
+afrits
+afters
+aftosa
+agamas
+agamic
+agamid
+agapae
+agapai
+agapes
+agaric
+agates
+agaves
+agedly
+ageing
+ageism
+ageist
+agency
+agenda
+agenes
+agents
+aggada
+aggers
+aggies
+aggros
+aghast
+agings
+agisms
+agists
+agitas
+aglare
+agleam
+aglets
+agnail
+agnate
+agnize
+agonal
+agones
+agonic
+agorae
+agoras
+agorot
+agouti
+agouty
+agrafe
+agreed
+agrees
+agrias
+aguish
+ahchoo
+ahimsa
+aholds
+ahorse
+aiders
+aidful
+aiding
+aidman
+aidmen
+aiglet
+aigret
+aikido
+ailing
+aimers
+aimful
+aiming
+aiolis
+airbag
+airbus
+airers
+airest
+airier
+airily
+airing
+airman
+airmen
+airted
+airths
+airway
+aisled
+aisles
+aivers
+ajivas
+ajowan
+ajugas
+akelas
+akenes
+akimbo
+alamos
+alands
+alanin
+alants
+alanyl
+alarms
+alarum
+alaska
+alated
+alates
+albata
+albedo
+albeit
+albino
+albite
+albums
+alcade
+alcaic
+alcids
+alcove
+alders
+aldols
+aldose
+aldrin
+alegar
+alephs
+alerts
+alevin
+alexia
+alexin
+alfaki
+algins
+algoid
+algors
+algums
+alibis
+alible
+alidad
+aliens
+alight
+aligns
+alined
+aliner
+alines
+aliped
+aliyah
+aliyas
+aliyos
+aliyot
+alkali
+alkane
+alkene
+alkies
+alkine
+alkoxy
+alkyds
+alkyls
+alkyne
+allays
+allees
+allege
+allele
+alleys
+allied
+allies
+allium
+allods
+allots
+allows
+alloys
+allude
+allure
+allyls
+almahs
+almehs
+almner
+almond
+almost
+almuce
+almude
+almuds
+almugs
+alnico
+alodia
+alohas
+aloins
+alpaca
+alphas
+alphyl
+alpine
+alsike
+altars
+alters
+althea
+aludel
+alulae
+alular
+alumin
+alumna
+alumni
+alvine
+always
+amadou
+amarna
+amatol
+amazed
+amazes
+amazon
+ambage
+ambari
+ambary
+ambeer
+ambers
+ambery
+ambits
+ambled
+ambler
+ambles
+ambush
+amebae
+ameban
+amebas
+amebic
+ameers
+amends
+aments
+amerce
+amices
+amicus
+amides
+amidic
+amidin
+amidol
+amidst
+amigas
+amigos
+amines
+aminic
+ammine
+ammino
+ammono
+amnion
+amnios
+amoeba
+amoles
+amoral
+amount
+amours
+ampere
+amping
+ampler
+ampule
+ampuls
+amrita
+amtrac
+amucks
+amulet
+amused
+amuser
+amuses
+amusia
+amylic
+amylum
+anabas
+anadem
+analog
+ananke
+anarch
+anatto
+anchor
+anchos
+ancone
+andros
+anears
+aneled
+aneles
+anemia
+anemic
+anenst
+anergy
+angary
+angels
+angers
+angina
+angled
+angler
+angles
+anglos
+angora
+angsts
+anilin
+animal
+animas
+animes
+animis
+animus
+anions
+anises
+anisic
+ankled
+ankles
+anklet
+ankush
+anlace
+anlage
+annals
+anneal
+annexe
+annona
+annoys
+annual
+annuli
+annuls
+anodal
+anodes
+anodic
+anoint
+anoles
+anomic
+anomie
+anonym
+anopia
+anorak
+anoxia
+anoxic
+ansate
+answer
+anteed
+anthem
+anther
+antiar
+antick
+antics
+anting
+antler
+antral
+antres
+antrum
+anural
+anuran
+anuria
+anuric
+anvils
+anyhow
+anyone
+anyons
+anyway
+aorist
+aortae
+aortal
+aortas
+aortic
+aoudad
+apache
+apathy
+apercu
+apexes
+aphids
+aphtha
+apiary
+apical
+apices
+apiece
+aplite
+aplomb
+apneal
+apneas
+apneic
+apnoea
+apodal
+apogee
+apollo
+apolog
+aporia
+appall
+appals
+appeal
+appear
+appels
+append
+apples
+applet
+appose
+aprons
+aptest
+arabic
+arable
+arames
+aramid
+arbors
+arbour
+arbute
+arcade
+arcana
+arcane
+arched
+archer
+arches
+archil
+archly
+archon
+arcing
+arcked
+arctic
+ardebs
+ardent
+ardors
+ardour
+arecas
+arenas
+arenes
+areola
+areole
+arepas
+aretes
+argala
+argali
+argals
+argent
+argils
+argled
+argles
+argols
+argons
+argosy
+argots
+argued
+arguer
+argues
+argufy
+argyle
+argyll
+arhats
+ariary
+arider
+aridly
+ariels
+aright
+ariled
+ariose
+ariosi
+arioso
+arisen
+arises
+arista
+aristo
+arkose
+armada
+armers
+armets
+armful
+armies
+arming
+armlet
+armors
+armory
+armour
+armpit
+armure
+arnica
+aroids
+aroint
+aromas
+around
+arouse
+aroynt
+arpens
+arpent
+arrack
+arrant
+arrays
+arrear
+arrest
+arriba
+arrive
+arroba
+arrows
+arrowy
+arroyo
+arseno
+arshin
+arsine
+arsino
+arsons
+artels
+artery
+artful
+artier
+artily
+artist
+asanas
+asarum
+ascend
+ascent
+ascots
+asdics
+ashcan
+ashier
+ashing
+ashlar
+ashler
+ashman
+ashmen
+ashore
+ashram
+asides
+askant
+askers
+asking
+aslant
+asleep
+aslope
+aslosh
+aspect
+aspens
+aspers
+aspics
+aspire
+aspish
+asrama
+astern
+asters
+asthma
+astony
+astral
+astray
+astute
+aswarm
+aswirl
+aswoon
+asylum
+atabal
+ataman
+atavic
+ataxia
+ataxic
+atelic
+atlatl
+atmans
+atolls
+atomic
+atonal
+atoned
+atoner
+atones
+atonia
+atonic
+atopic
+atrial
+atrium
+attach
+attack
+attain
+attars
+attend
+attent
+attest
+attics
+attire
+attorn
+attrit
+attune
+atwain
+atween
+atypic
+aubade
+auburn
+aucuba
+audads
+audial
+audile
+auding
+audios
+audits
+augend
+augers
+aughts
+augite
+augurs
+augury
+august
+auklet
+aulder
+auntie
+auntly
+aurate
+aureus
+aurist
+aurora
+aurous
+aurums
+auspex
+ausubo
+auteur
+author
+autism
+autist
+autoed
+autumn
+auxins
+avails
+avatar
+avaunt
+avenge
+avenue
+averse
+averts
+avians
+aviary
+aviate
+avidin
+avidly
+avions
+avisos
+avocet
+avoids
+avoset
+avouch
+avowal
+avowed
+avower
+avulse
+awaits
+awaked
+awaken
+awakes
+awards
+aweary
+aweigh
+aweing
+awhile
+awhirl
+awless
+awmous
+awning
+awoken
+axeman
+axemen
+axenic
+axilla
+axioms
+axions
+axised
+axises
+axites
+axlike
+axonal
+axones
+axonic
+axseed
+azalea
+azides
+azines
+azlons
+azoles
+azonal
+azonic
+azoted
+azotes
+azoths
+azotic
+azukis
+azures
+azygos
+baaing
+baalim
+baases
+babble
+babels
+babied
+babier
+babies
+babkas
+babool
+baboon
+baboos
+babuls
+baccae
+bached
+baches
+backed
+backer
+backup
+bacons
+bacula
+badass
+badder
+baddie
+badged
+badger
+badges
+badman
+badmen
+baffed
+baffle
+bagels
+bagful
+bagged
+bagger
+baggie
+bagman
+bagmen
+bagnio
+baguet
+bagwig
+bailed
+bailee
+bailer
+bailey
+bailie
+bailor
+bairns
+baited
+baiter
+baizas
+baizes
+bakers
+bakery
+baking
+balata
+balboa
+balded
+balder
+baldly
+baleen
+balers
+baling
+balked
+balker
+ballad
+ballet
+ballon
+ballot
+balsam
+balsas
+bamboo
+bammed
+banana
+bancos
+bandas
+banded
+bander
+bandit
+bandog
+banged
+banger
+bangle
+banian
+baning
+banish
+banjax
+banjos
+banked
+banker
+bankit
+banned
+banner
+bannet
+bantam
+banter
+banyan
+banzai
+baobab
+barbal
+barbed
+barbel
+barber
+barbes
+barbet
+barbie
+barbut
+barcas
+barded
+bardes
+bardic
+barege
+barely
+barest
+barfed
+barfly
+barged
+bargee
+barges
+barhop
+baring
+barite
+barium
+barked
+barker
+barley
+barlow
+barman
+barmen
+barmie
+barned
+barney
+barong
+barons
+barony
+barque
+barred
+barrel
+barren
+barres
+barret
+barrio
+barrow
+barter
+baryes
+baryon
+baryta
+baryte
+basalt
+basely
+basest
+bashaw
+bashed
+basher
+bashes
+basics
+basify
+basils
+basing
+basins
+basion
+basked
+basket
+basque
+basted
+baster
+bastes
+batboy
+bateau
+bathed
+bather
+bathes
+bathos
+batiks
+bating
+batman
+batmen
+batons
+batted
+batten
+batter
+battik
+battle
+battue
+baubee
+bauble
+baulks
+baulky
+bawbee
+bawdry
+bawled
+bawler
+bawtie
+bayamo
+bayard
+baying
+bayman
+baymen
+bayous
+bazaar
+bazars
+bazoos
+beachy
+beacon
+beaded
+beader
+beadle
+beagle
+beaked
+beaker
+beamed
+beaned
+beanie
+beanos
+beards
+bearer
+beaten
+beater
+beauts
+beauty
+bebops
+becalm
+became
+becaps
+becked
+becket
+beckon
+beclog
+become
+bedamn
+bedaub
+bedbug
+bedded
+bedder
+bedeck
+bedell
+bedels
+bedews
+bedims
+bedlam
+bedpan
+bedrid
+bedrug
+bedsit
+beduin
+bedumb
+beebee
+beechy
+beefed
+beeped
+beeper
+beetle
+beeves
+beezer
+befall
+befell
+befits
+beflag
+beflea
+befogs
+befool
+before
+befoul
+befret
+begall
+begaze
+begets
+beggar
+begged
+begins
+begird
+begirt
+beglad
+begone
+begrim
+begulf
+begums
+behalf
+behave
+behead
+beheld
+behest
+behind
+behold
+behoof
+behove
+behowl
+beiges
+beigne
+beings
+bekiss
+beknot
+belady
+belaud
+belays
+beldam
+beleap
+belfry
+belgas
+belied
+belief
+belier
+belies
+belike
+belive
+belled
+belles
+bellow
+belong
+belons
+belows
+belted
+belter
+beluga
+bemata
+bemean
+bemire
+bemist
+bemixt
+bemoan
+bemock
+bemuse
+bename
+benday
+bended
+bendee
+bender
+bendys
+benign
+bennes
+bennet
+bennis
+bentos
+benumb
+benzal
+benzin
+benzol
+benzyl
+berake
+berate
+bereft
+berets
+berime
+berlin
+bermed
+bermes
+bertha
+berths
+beryls
+beseem
+besets
+beside
+besmut
+besnow
+besoms
+besots
+bested
+bestir
+bestow
+bestud
+betake
+betels
+bethel
+betide
+betime
+betise
+betons
+betony
+betook
+betray
+bettas
+betted
+better
+bettor
+bevels
+bevies
+bevors
+bewail
+beware
+beweep
+bewept
+bewigs
+beworm
+bewrap
+bewray
+beylic
+beylik
+beyond
+bezant
+bezazz
+bezels
+bezils
+bezoar
+bhakta
+bhakti
+bhangs
+bharal
+bhoots
+bialis
+bialys
+biased
+biases
+biaxal
+bibbed
+bibber
+bibles
+bicarb
+biceps
+bicker
+bicorn
+bicron
+bidden
+bidder
+biders
+bidets
+biding
+bields
+biface
+biffed
+biffin
+biflex
+bifold
+biform
+bigamy
+bigeye
+bigger
+biggie
+biggin
+bights
+bigots
+bigwig
+bijous
+bijoux
+bikers
+bikies
+biking
+bikini
+bilboa
+bilbos
+bilged
+bilges
+bilked
+bilker
+billed
+biller
+billet
+billie
+billon
+billow
+bimahs
+bimbos
+binary
+binate
+binder
+bindis
+bindle
+biners
+binged
+binger
+binges
+bingos
+binits
+binned
+binocs
+biogas
+biogen
+biomes
+bionic
+bionts
+biopic
+biopsy
+biotas
+biotic
+biotin
+bipack
+bipeds
+bipods
+birded
+birder
+birdie
+bireme
+birkie
+birled
+birler
+birles
+birred
+birses
+births
+bisect
+bishop
+bisons
+bisque
+bister
+bistre
+bistro
+biters
+biting
+bitmap
+bitted
+bitten
+bitter
+bizone
+bizzes
+blabby
+blacks
+bladed
+blader
+blades
+blaffs
+blains
+blamed
+blamer
+blames
+blanch
+blanks
+blared
+blares
+blasts
+blasty
+blawed
+blazed
+blazer
+blazes
+blazon
+bleach
+bleaks
+blears
+bleary
+bleats
+blebby
+bleeds
+bleeps
+blench
+blende
+blends
+blenny
+blight
+blimey
+blimps
+blinds
+blinis
+blinks
+blintz
+blites
+blithe
+bloats
+blocks
+blocky
+blokes
+blonde
+blonds
+bloods
+bloody
+blooey
+blooie
+blooms
+bloomy
+bloops
+blotch
+blotto
+blotty
+blouse
+blousy
+blowby
+blowed
+blower
+blowsy
+blowup
+blowzy
+bludge
+bluely
+bluest
+bluesy
+bluets
+blueys
+bluffs
+bluing
+bluish
+blumed
+blumes
+blunge
+blunts
+blurbs
+blurry
+blurts
+blypes
+boards
+boarts
+boasts
+boated
+boatel
+boater
+bobbed
+bobber
+bobbin
+bobble
+bobcat
+bocces
+boccia
+boccie
+boccis
+boches
+bodega
+bodice
+bodied
+bodies
+bodily
+boding
+bodkin
+boffed
+boffin
+boffos
+bogans
+bogart
+bogeys
+bogged
+boggle
+bogies
+bogles
+boheas
+bohunk
+boiled
+boiler
+boings
+boinks
+boites
+bolder
+boldly
+bolero
+bolete
+boleti
+bolide
+bolled
+bolshy
+bolson
+bolted
+bolter
+bombax
+bombed
+bomber
+bombes
+bombyx
+bonaci
+bonbon
+bonded
+bonder
+bonduc
+bongos
+bonier
+boning
+bonita
+bonito
+bonnes
+bonnet
+bonnie
+bonobo
+bonsai
+bonzer
+bonzes
+boobed
+boobie
+booboo
+boocoo
+boodle
+booger
+boogey
+boogie
+boohoo
+booing
+boojum
+booked
+booker
+bookie
+bookoo
+boomed
+boomer
+boosts
+booted
+bootee
+booths
+bootie
+boozed
+boozer
+boozes
+bopeep
+bopped
+bopper
+borage
+borals
+borane
+borate
+bordel
+border
+boreal
+boreas
+boreen
+borers
+boride
+boring
+borked
+borons
+borrow
+borsch
+borsht
+borzoi
+boshes
+bosker
+bosket
+bosons
+bosque
+bossed
+bosses
+boston
+bosuns
+botany
+botchy
+botels
+botfly
+bother
+bottle
+bottom
+boubou
+boucle
+boudin
+bouffe
+boughs
+bought
+bougie
+boules
+boulle
+bounce
+bouncy
+bounds
+bounty
+bourgs
+bourne
+bourns
+bourse
+boused
+bouses
+bouton
+bovids
+bovine
+bowers
+bowery
+bowfin
+bowing
+bowled
+bowleg
+bowler
+bowman
+bowmen
+bowpot
+bowsed
+bowses
+bowwow
+bowyer
+boxcar
+boxers
+boxful
+boxier
+boxily
+boxing
+boyard
+boyars
+boyish
+boylas
+braced
+bracer
+braces
+brachs
+bracts
+braggy
+brahma
+braids
+brails
+brains
+brainy
+braise
+braize
+braked
+brakes
+branch
+brands
+brandy
+branks
+branny
+brants
+brashy
+brasil
+brassy
+bratty
+bravas
+braved
+braver
+braves
+bravos
+brawer
+brawls
+brawly
+brawns
+brawny
+brayed
+brayer
+brazas
+brazed
+brazen
+brazer
+brazes
+brazil
+breach
+breads
+bready
+breaks
+breams
+breath
+bredes
+breech
+breeds
+breeks
+breeze
+breezy
+bregma
+brents
+breves
+brevet
+brewed
+brewer
+brewis
+briard
+briars
+briary
+bribed
+bribee
+briber
+bribes
+bricks
+bricky
+bridal
+brides
+bridge
+bridle
+briefs
+briers
+briery
+bright
+brillo
+brills
+brined
+briner
+brines
+brings
+brinks
+briony
+brises
+brisks
+briths
+britts
+broach
+broads
+broche
+brocks
+brogan
+brogue
+broils
+broken
+broker
+brolly
+bromal
+bromes
+bromic
+bromid
+bromin
+bromos
+bronco
+broncs
+bronze
+bronzy
+brooch
+broods
+broody
+brooks
+brooms
+broomy
+broses
+broths
+brothy
+browed
+browns
+browny
+browse
+brucin
+brughs
+bruins
+bruise
+bruits
+brulot
+brumal
+brumby
+brumes
+brunch
+brunet
+brunts
+brushy
+brutal
+bruted
+brutes
+bruxed
+bruxes
+bryony
+bubale
+bubals
+bubbas
+bubble
+bubbly
+bubkes
+buboed
+buboes
+buccal
+bucked
+bucker
+bucket
+buckle
+buckos
+buckra
+budded
+budder
+buddha
+buddle
+budged
+budger
+budges
+budget
+budgie
+buffed
+buffer
+buffet
+buffos
+bugeye
+bugged
+bugger
+bugled
+bugler
+bugles
+bugout
+bugsha
+builds
+bulbar
+bulbed
+bulbel
+bulbil
+bulbul
+bulged
+bulger
+bulges
+bulgur
+bulked
+bullae
+bulled
+bullet
+bumble
+bumkin
+bumped
+bumper
+bumphs
+bunchy
+buncos
+bundle
+bundts
+bunged
+bungee
+bungle
+bunion
+bunked
+bunker
+bunkos
+bunkum
+bunted
+bunter
+bunyas
+buoyed
+bupkes
+bupkus
+buppie
+buqsha
+burans
+burble
+burbly
+burbot
+burden
+burdie
+bureau
+burets
+burgee
+burger
+burghs
+burgle
+burgoo
+burial
+buried
+burier
+buries
+burins
+burkas
+burked
+burker
+burkes
+burlap
+burled
+burler
+burley
+burned
+burner
+burnet
+burnie
+burped
+burqas
+burred
+burrer
+burros
+burrow
+bursae
+bursal
+bursar
+bursas
+burses
+bursts
+burton
+busbar
+busboy
+bushed
+bushel
+busher
+bushes
+bushwa
+busied
+busier
+busies
+busily
+busing
+busked
+busker
+buskin
+busman
+busmen
+bussed
+busses
+busted
+buster
+bustic
+bustle
+butane
+butene
+buteos
+butled
+butler
+butles
+butted
+butter
+buttes
+button
+bututs
+butyls
+buyers
+buying
+buyoff
+buyout
+buzuki
+buzzed
+buzzer
+buzzes
+bwanas
+byelaw
+bygone
+bylaws
+byline
+byname
+bypass
+bypast
+bypath
+byplay
+byrled
+byrnie
+byroad
+byssal
+byssus
+bytalk
+byways
+byword
+bywork
+byzant
+cabala
+cabals
+cabana
+cabbed
+cabbie
+cabers
+cabins
+cabled
+cabler
+cables
+cablet
+cabman
+cabmen
+cabobs
+cacaos
+cached
+caches
+cachet
+cachou
+cackle
+cactus
+caddie
+caddis
+cadent
+cadets
+cadged
+cadger
+cadges
+cadmic
+cadres
+caecal
+caecum
+caeoma
+caesar
+caftan
+cagers
+cagier
+cagily
+caging
+cahier
+cahoot
+cahows
+caiman
+caique
+cairds
+cairns
+cairny
+cajole
+cakier
+caking
+calami
+calash
+calcar
+calces
+calcic
+calesa
+calico
+califs
+caliph
+calked
+calker
+calkin
+callan
+callas
+called
+callee
+caller
+callet
+callow
+callus
+calmed
+calmer
+calmly
+calory
+calpac
+calque
+calved
+calves
+calxes
+camail
+camber
+cambia
+camels
+cameos
+camera
+camion
+camisa
+camise
+camlet
+cammie
+camped
+camper
+campos
+campus
+canals
+canape
+canard
+canary
+cancan
+cancel
+cancer
+cancha
+candid
+candle
+candor
+caners
+canful
+cangue
+canids
+canine
+caning
+canker
+cannas
+canned
+cannel
+canner
+cannie
+cannon
+cannot
+canoed
+canoer
+canoes
+canola
+canons
+canopy
+cansos
+cantal
+canted
+canter
+canthi
+cantic
+cantle
+canton
+cantor
+cantos
+cantus
+canula
+canvas
+canyon
+capers
+capful
+capias
+capita
+caplet
+caplin
+capons
+capote
+capped
+capper
+capric
+capris
+capsid
+captan
+captor
+carack
+carafe
+carate
+carats
+carbon
+carbos
+carboy
+carcel
+carded
+carder
+cardia
+cardio
+cardon
+careen
+career
+carers
+caress
+carets
+carful
+cargos
+carhop
+caribe
+caried
+caries
+carina
+caring
+carked
+carles
+carlin
+carman
+carmen
+carnal
+carnet
+carney
+carnie
+carobs
+caroch
+caroli
+carols
+caroms
+carpal
+carped
+carpel
+carper
+carpet
+carpus
+carrel
+carrom
+carrot
+carses
+carted
+cartel
+carter
+cartes
+carton
+cartop
+carved
+carvel
+carven
+carver
+carves
+casaba
+casava
+casbah
+casefy
+caseic
+casein
+casern
+cashaw
+cashed
+cashes
+cashew
+cashoo
+casing
+casini
+casino
+casita
+casked
+casket
+casque
+caster
+castes
+castle
+castor
+casual
+catalo
+catchy
+catena
+caters
+catgut
+cation
+catkin
+catlin
+catnap
+catnip
+catsup
+catted
+cattie
+cattle
+caucus
+caudad
+caudal
+caudex
+caudle
+caught
+caulds
+caules
+caulis
+caulks
+causal
+caused
+causer
+causes
+causey
+caveat
+cavern
+cavers
+caviar
+cavies
+cavils
+caving
+cavity
+cavort
+cawing
+cayman
+cayuse
+ceased
+ceases
+cebids
+ceboid
+cecity
+cedarn
+cedars
+cedary
+ceders
+ceding
+cedula
+ceibas
+ceiled
+ceiler
+ceilis
+celebs
+celery
+celiac
+cellae
+cellar
+celled
+cellos
+celoms
+cement
+cenote
+censed
+censer
+censes
+censor
+census
+centai
+cental
+centas
+center
+centos
+centra
+centre
+centum
+ceorls
+cerate
+cercal
+cercis
+cercus
+cereal
+cereus
+cerias
+cering
+ceriph
+cerise
+cerite
+cerium
+cermet
+cerous
+certes
+ceruse
+cervid
+cervix
+cesium
+cessed
+cesses
+cestas
+cestoi
+cestos
+cestus
+cesura
+cetane
+chabuk
+chacma
+chadar
+chador
+chadri
+chaeta
+chafed
+chafer
+chafes
+chaffs
+chaffy
+chaine
+chains
+chairs
+chaise
+chakra
+chalah
+chaleh
+chalet
+chalks
+chalky
+challa
+chally
+chalot
+chammy
+champs
+champy
+chance
+chancy
+change
+changs
+chants
+chanty
+chapel
+chapes
+charas
+chards
+chared
+chares
+charge
+charka
+charks
+charms
+charro
+charrs
+charry
+charts
+chased
+chaser
+chases
+chasms
+chasmy
+chasse
+chaste
+chatty
+chaunt
+chawed
+chawer
+chazan
+cheapo
+cheaps
+cheats
+chebec
+checks
+cheder
+cheeks
+cheeky
+cheeps
+cheero
+cheers
+cheery
+cheese
+cheesy
+chefed
+chegoe
+chelae
+chelas
+chemic
+chemos
+cheque
+cherry
+cherts
+cherty
+cherub
+chests
+chesty
+chetah
+cheths
+chevre
+chewed
+chewer
+chiasm
+chiaus
+chicas
+chicer
+chichi
+chicks
+chicle
+chicly
+chicos
+chided
+chider
+chides
+chiefs
+chield
+chiels
+chigoe
+childe
+chiles
+chilis
+chilli
+chills
+chilly
+chimar
+chimbs
+chimed
+chimer
+chimes
+chimla
+chimps
+chinas
+chinch
+chined
+chines
+chinks
+chinky
+chinos
+chints
+chintz
+chippy
+chiral
+chirks
+chirms
+chiros
+chirps
+chirpy
+chirre
+chirrs
+chirus
+chisel
+chital
+chitin
+chiton
+chitty
+chives
+chivvy
+choana
+chocks
+choice
+choirs
+choked
+choker
+chokes
+chokey
+cholas
+choler
+cholla
+cholos
+chomps
+chooks
+choose
+choosy
+chopin
+choppy
+choral
+chords
+chorea
+chored
+chores
+choric
+chorus
+chosen
+choses
+chotts
+chough
+chouse
+choush
+chowed
+chowse
+chrism
+chroma
+chrome
+chromo
+chromy
+chubby
+chucks
+chucky
+chufas
+chuffs
+chuffy
+chukar
+chukka
+chummy
+chumps
+chunks
+chunky
+chuppa
+church
+churls
+churns
+churro
+churrs
+chuted
+chutes
+chyles
+chymes
+chymic
+cibols
+cicada
+cicala
+cicale
+cicely
+cicero
+ciders
+cigars
+cilice
+cilium
+cinder
+cinema
+cineol
+cinque
+cipher
+circle
+circus
+cirque
+cirrus
+ciscos
+cisted
+cistus
+citers
+cither
+citied
+cities
+citify
+citing
+citola
+citole
+citral
+citric
+citrin
+citron
+citrus
+civets
+civics
+civies
+civism
+clachs
+clacks
+clades
+claims
+clammy
+clamor
+clamps
+clangs
+clanks
+clanky
+claque
+claret
+claros
+clasps
+claspt
+classy
+clasts
+clause
+claver
+claves
+clavus
+clawed
+clawer
+claxon
+clayed
+clayey
+cleans
+clears
+cleats
+cleave
+cleeks
+clefts
+clench
+cleome
+cleped
+clepes
+clergy
+cleric
+clerid
+clerks
+clever
+clevis
+clewed
+cliche
+clicks
+client
+cliffs
+cliffy
+clifts
+climax
+climbs
+climes
+clinal
+clinch
+clines
+clings
+clingy
+clinic
+clinks
+clique
+cliquy
+clitic
+clivia
+cloaca
+cloaks
+cloche
+clocks
+cloddy
+cloggy
+clomps
+clonal
+cloned
+cloner
+clones
+clonic
+clonks
+clonus
+cloots
+cloque
+closed
+closer
+closes
+closet
+clothe
+cloths
+clotty
+clouds
+cloudy
+clough
+clours
+clouts
+cloven
+clover
+cloves
+clowns
+cloyed
+clozes
+clubby
+clucks
+cluing
+clumps
+clumpy
+clumsy
+clunks
+clunky
+clutch
+clypei
+cnidae
+coacts
+coalas
+coaled
+coaler
+coapts
+coarse
+coasts
+coated
+coatee
+coater
+coatis
+coaxal
+coaxed
+coaxer
+coaxes
+cobalt
+cobber
+cobble
+cobias
+cobles
+cobnut
+cobras
+cobweb
+cocain
+coccal
+coccic
+coccid
+coccus
+coccyx
+cochin
+cocoas
+cocoon
+codded
+codder
+coddle
+codecs
+codeia
+codens
+coders
+codify
+coding
+codlin
+codons
+coedit
+coelom
+coempt
+coerce
+coeval
+coffee
+coffer
+coffin
+coffle
+cogent
+cogged
+cogito
+cognac
+cogons
+cogway
+cohead
+coheir
+cohere
+cohogs
+cohort
+cohosh
+cohost
+cohune
+coifed
+coiffe
+coigne
+coigns
+coiled
+coiler
+coined
+coiner
+coital
+coitus
+cojoin
+coking
+colbys
+colder
+coldly
+colead
+coleus
+colics
+colies
+colins
+collar
+collet
+collie
+collop
+colobi
+cologs
+colone
+coloni
+colons
+colony
+colors
+colour
+colter
+colugo
+column
+colure
+colzas
+comade
+comake
+comate
+combat
+combed
+comber
+combes
+combos
+comedo
+comedy
+comely
+comers
+cometh
+comets
+comfit
+comics
+coming
+comity
+commas
+commie
+commit
+commix
+common
+comose
+comous
+compas
+comped
+compel
+comply
+compos
+compts
+comtes
+concha
+concho
+conchs
+conchy
+concur
+condor
+condos
+coneys
+confab
+confer
+confit
+congas
+congee
+conger
+conges
+congii
+congos
+congou
+conics
+conies
+conine
+coning
+conins
+conium
+conked
+conker
+conned
+conner
+conoid
+consol
+consul
+contes
+contos
+contra
+convex
+convey
+convoy
+coocoo
+cooeed
+cooees
+cooers
+cooeys
+cooing
+cooked
+cooker
+cookey
+cookie
+cooled
+cooler
+coolie
+coolly
+coolth
+coombe
+coombs
+cooped
+cooper
+coopts
+cooter
+cootie
+copalm
+copals
+copays
+copeck
+copens
+copers
+copied
+copier
+copies
+coping
+coplot
+copout
+copped
+copper
+coppra
+coprah
+copras
+copses
+copter
+copula
+coquet
+corals
+corban
+corbel
+corbie
+corded
+corder
+cordon
+corers
+corgis
+coring
+corium
+corked
+corker
+cormel
+cornea
+corned
+cornel
+corner
+cornet
+cornua
+cornus
+corody
+corona
+corpse
+corpus
+corral
+corrie
+corsac
+corses
+corset
+cortex
+cortin
+corvee
+corves
+corvet
+corvid
+corymb
+coryza
+cosecs
+cosets
+coseys
+coshed
+cosher
+coshes
+cosied
+cosier
+cosies
+cosign
+cosily
+cosine
+cosmic
+cosmid
+cosmos
+cosset
+costae
+costal
+costar
+costed
+coster
+costly
+cotans
+coteau
+coting
+cottae
+cottar
+cottas
+cotter
+cotton
+cotype
+cougar
+coughs
+coulee
+coulis
+counts
+county
+couped
+coupes
+couple
+coupon
+course
+courts
+cousin
+couter
+couths
+covary
+covens
+covers
+covert
+covets
+coveys
+coving
+covins
+cowage
+coward
+cowboy
+cowers
+cowier
+cowing
+cowled
+cowman
+cowmen
+cowpat
+cowpea
+cowpie
+cowpox
+cowrie
+coxing
+coydog
+coyest
+coying
+coyish
+coyote
+coypou
+coypus
+cozens
+cozeys
+cozied
+cozier
+cozies
+cozily
+cozzes
+craals
+crabby
+cracks
+cracky
+cradle
+crafts
+crafty
+craggy
+crakes
+crambe
+crambo
+cramps
+crampy
+cranch
+craned
+cranes
+crania
+cranks
+cranky
+cranny
+crapes
+crappy
+crases
+crasis
+cratch
+crated
+crater
+crates
+craton
+cravat
+craved
+craven
+craver
+craves
+crawls
+crawly
+crayon
+crazed
+crazes
+creaks
+creaky
+creams
+creamy
+crease
+creasy
+create
+creche
+credal
+credit
+credos
+creeds
+creeks
+creels
+creeps
+creepy
+creese
+creesh
+cremes
+crenel
+creole
+creped
+crepes
+crepey
+crepon
+cresol
+cressy
+crests
+cresyl
+cretic
+cretin
+crewed
+crewel
+cricks
+criers
+crikey
+crimes
+crimps
+crimpy
+cringe
+crinum
+cripes
+crises
+crisic
+crisis
+crisps
+crispy
+crissa
+crista
+critic
+croaks
+croaky
+crocks
+crocus
+crofts
+crojik
+crones
+crooks
+croons
+crores
+crosse
+crotch
+croton
+crouch
+croupe
+croups
+croupy
+crouse
+croute
+crowds
+crowdy
+crowed
+crower
+crowns
+crozer
+crozes
+cruces
+crucks
+cruddy
+cruder
+crudes
+cruets
+cruise
+crumbs
+crumby
+crummy
+crumps
+crunch
+cruors
+crural
+cruses
+cruset
+crusts
+crusty
+crutch
+cruxes
+crwths
+crying
+crypto
+crypts
+cuatro
+cubage
+cubebs
+cubers
+cubics
+cubing
+cubism
+cubist
+cubiti
+cubits
+cuboid
+cuckoo
+cuddie
+cuddle
+cuddly
+cudgel
+cueing
+cuesta
+cuffed
+cuisse
+culets
+cullay
+culled
+culler
+cullet
+cullis
+culmed
+culpae
+cultch
+cultic
+cultus
+culver
+cumber
+cumbia
+cumins
+cummer
+cummin
+cumuli
+cundum
+cuneal
+cunner
+cupels
+cupful
+cupids
+cupola
+cuppas
+cupped
+cupper
+cupric
+cuprum
+cupula
+cupule
+curacy
+curagh
+curara
+curare
+curari
+curate
+curbed
+curber
+curded
+curdle
+curers
+curets
+curfew
+curiae
+curial
+curies
+curing
+curios
+curite
+curium
+curled
+curler
+curlew
+curran
+curred
+currie
+cursed
+curser
+curses
+cursor
+curtal
+curter
+curtly
+curtsy
+curule
+curved
+curves
+curvet
+curvey
+cuscus
+cusecs
+cushat
+cushaw
+cuspal
+cusped
+cuspid
+cuspis
+cussed
+cusser
+cusses
+cussos
+custom
+custos
+cutely
+cutest
+cutesy
+cuteys
+cuties
+cutins
+cutlas
+cutler
+cutlet
+cutoff
+cutout
+cutter
+cuttle
+cutups
+cuvees
+cyanic
+cyanid
+cyanin
+cyborg
+cycads
+cycled
+cycler
+cycles
+cyclic
+cyclin
+cyclos
+cyders
+cyeses
+cyesis
+cygnet
+cymars
+cymbal
+cymene
+cymlin
+cymoid
+cymols
+cymose
+cymous
+cynics
+cypher
+cypres
+cyprus
+cystic
+cytons
+dabbed
+dabber
+dabble
+dachas
+dacite
+dacker
+dacoit
+dacron
+dactyl
+daddle
+dadgum
+dadoed
+dadoes
+daedal
+daemon
+daffed
+dafter
+daftly
+daggas
+dagger
+daggle
+dagoba
+dagoes
+dahlia
+dahoon
+daiker
+daikon
+daimen
+daimio
+daimon
+daimyo
+dainty
+daises
+dakoit
+dalasi
+daledh
+daleth
+dalles
+dalton
+damage
+damans
+damars
+damask
+dammar
+dammed
+dammer
+dammit
+damned
+damner
+damped
+dampen
+damper
+damply
+damsel
+damson
+danced
+dancer
+dances
+dander
+dandle
+danged
+danger
+dangle
+dangly
+danios
+danish
+danker
+dankly
+daphne
+dapped
+dapper
+dapple
+darbar
+darers
+darics
+daring
+darked
+darken
+darker
+darkey
+darkie
+darkle
+darkly
+darned
+darnel
+darner
+darted
+darter
+dartle
+dashed
+dasher
+dashes
+dashis
+dassie
+datary
+datcha
+daters
+dating
+dative
+dattos
+datums
+datura
+daubed
+dauber
+daubes
+daubry
+daunts
+dauted
+dautie
+davens
+davies
+davits
+dawdle
+dawing
+dawned
+dawted
+dawtie
+daybed
+dayfly
+daylit
+dazing
+dazzle
+deacon
+deaden
+deader
+deadly
+deafen
+deafer
+deafly
+deairs
+dealer
+deaned
+dearer
+dearie
+dearly
+dearth
+deasil
+deaths
+deathy
+deaved
+deaves
+debags
+debark
+debars
+debase
+debate
+debeak
+debits
+debone
+debris
+debtor
+debugs
+debunk
+debuts
+debyes
+decade
+decafs
+decals
+decamp
+decane
+decant
+decare
+decays
+deceit
+decent
+decern
+decide
+decile
+decked
+deckel
+decker
+deckle
+declaw
+decoct
+decode
+decors
+decoys
+decree
+decury
+dedans
+deduce
+deduct
+deeded
+deejay
+deemed
+deepen
+deeper
+deeply
+deewan
+deface
+defame
+defang
+defats
+defeat
+defect
+defend
+defers
+deffer
+defied
+defier
+defies
+defile
+define
+deflea
+defoam
+defogs
+deform
+defrag
+defray
+defter
+deftly
+defuel
+defund
+defuse
+defuze
+degage
+degame
+degami
+degerm
+degree
+degums
+degust
+dehorn
+dehort
+deiced
+deicer
+deices
+deific
+deigns
+deisms
+deists
+deixis
+deject
+dekare
+deking
+dekkos
+delate
+delays
+delead
+delete
+delfts
+delict
+delime
+delish
+delist
+deltas
+deltic
+delude
+deluge
+deluxe
+delved
+delver
+delves
+demand
+demark
+demast
+demean
+dement
+demies
+demise
+demits
+demobs
+demode
+demoed
+demons
+demote
+demure
+demurs
+denari
+denars
+denary
+dengue
+denial
+denied
+denier
+denies
+denims
+denned
+denote
+denser
+dental
+dented
+dentil
+dentin
+denude
+deodar
+depart
+depend
+deperm
+depict
+deploy
+depone
+deport
+depose
+depots
+depths
+depute
+deputy
+derail
+derate
+derats
+derays
+deride
+derive
+dermal
+dermas
+dermic
+dermis
+derris
+desalt
+desand
+descry
+desert
+design
+desire
+desist
+desman
+desmid
+desorb
+desoxy
+despot
+detach
+detail
+detain
+detect
+detent
+deters
+detest
+detick
+detour
+deuced
+deuces
+devein
+devels
+devest
+device
+devils
+devise
+devoid
+devoir
+devons
+devote
+devour
+devout
+dewans
+dewars
+dewier
+dewily
+dewing
+dewlap
+dewool
+deworm
+dexies
+dexter
+dextro
+dezinc
+dharma
+dharna
+dhobis
+dholes
+dhooly
+dhoora
+dhooti
+dhotis
+dhurna
+dhutis
+diacid
+diadem
+dialed
+dialer
+dialog
+diamin
+diaper
+diapir
+diatom
+diazin
+dibbed
+dibber
+dibble
+dibbuk
+dicast
+dicers
+dicier
+dicing
+dicots
+dictum
+didact
+diddle
+diddly
+didies
+didoes
+dieing
+dienes
+dieoff
+diesel
+dieses
+diesis
+dieted
+dieter
+differ
+digamy
+digest
+digged
+digger
+dights
+digits
+diglot
+dikdik
+dikers
+diking
+diktat
+dilate
+dildoe
+dildos
+dilled
+dilute
+dimers
+dimity
+dimmed
+dimmer
+dimout
+dimple
+dimply
+dimwit
+dinars
+dindle
+dinero
+diners
+dinged
+dinger
+dinges
+dingey
+dinghy
+dingle
+dingus
+dining
+dinked
+dinkey
+dinkly
+dinkum
+dinned
+dinner
+dinted
+diobol
+diodes
+dioecy
+dioxan
+dioxid
+dioxin
+diplex
+diploe
+dipnet
+dipody
+dipole
+dipped
+dipper
+dipsas
+dipsos
+diquat
+dirams
+dirdum
+direct
+direly
+direst
+dirges
+dirham
+dirked
+dirled
+dirndl
+disarm
+disbar
+disbud
+disced
+discos
+discus
+diseur
+dished
+dishes
+disked
+dismal
+dismay
+dismes
+disown
+dispel
+dissed
+disses
+distal
+distil
+disuse
+dither
+dittos
+ditzes
+diuron
+divans
+divers
+divert
+divest
+divide
+divine
+diving
+divots
+diwans
+dixits
+dizens
+djebel
+djinni
+djinns
+djinny
+doable
+doated
+dobber
+dobbin
+dobies
+doblas
+doblon
+dobras
+dobros
+dobson
+docent
+docile
+docked
+docker
+docket
+doctor
+dodder
+dodged
+dodgem
+dodger
+dodges
+dodoes
+doffed
+doffer
+dogdom
+dogear
+dogeys
+dogged
+dogger
+doggie
+dogies
+dogleg
+dogmas
+dognap
+doiled
+doings
+doited
+doling
+dollar
+dolled
+dollop
+dolman
+dolmas
+dolmen
+dolors
+dolour
+domain
+domine
+doming
+domino
+donate
+donees
+dongle
+donjon
+donkey
+donnas
+donned
+donnee
+donors
+donsie
+donuts
+donzel
+doobie
+doodad
+doodle
+doodoo
+doofus
+doolee
+doolie
+doomed
+doowop
+doozer
+doozie
+dopant
+dopers
+dopier
+dopily
+doping
+dorado
+dorbug
+dories
+dormer
+dormie
+dormin
+dorper
+dorsad
+dorsal
+dorsel
+dorser
+dorsum
+dosage
+dosers
+dosing
+dossal
+dossed
+dossel
+dosser
+dosses
+dossil
+dotage
+dotard
+doters
+dotier
+doting
+dotted
+dottel
+dotter
+dottle
+double
+doubly
+doubts
+doughs
+dought
+doughy
+doulas
+doumas
+dourah
+douras
+dourer
+dourly
+doused
+douser
+douses
+dovens
+dovish
+dowels
+dowers
+dowery
+dowing
+downed
+downer
+dowsed
+dowser
+dowses
+doxies
+doyens
+doyley
+dozens
+dozers
+dozier
+dozily
+dozing
+drably
+drachm
+draffs
+draffy
+drafts
+drafty
+dragee
+draggy
+dragon
+drails
+drains
+drakes
+dramas
+drawee
+drawer
+drawls
+drawly
+drayed
+dreads
+dreams
+dreamt
+dreamy
+drears
+dreary
+drecks
+drecky
+dredge
+dreggy
+dreich
+dreidl
+dreigh
+drench
+dressy
+driegh
+driers
+driest
+drifts
+drifty
+drills
+drinks
+drippy
+drivel
+driven
+driver
+drives
+drogue
+droids
+droits
+drolls
+drolly
+dromon
+droned
+droner
+drones
+drongo
+drools
+drooly
+droops
+droopy
+dropsy
+drosky
+drossy
+drouks
+drouth
+droved
+drover
+droves
+drownd
+drowns
+drowse
+drowsy
+drudge
+druggy
+druids
+drumly
+drunks
+drupes
+druses
+dryads
+dryers
+dryest
+drying
+dryish
+drylot
+dually
+dubbed
+dubber
+dubbin
+ducats
+ducked
+ducker
+duckie
+ductal
+ducted
+duddie
+dudeen
+duding
+dudish
+dueled
+dueler
+duelli
+duello
+duende
+duenna
+dueted
+duffel
+duffer
+duffle
+dugong
+dugout
+duiker
+duking
+dulcet
+dulias
+dulled
+duller
+dulses
+dumbed
+dumber
+dumbly
+dumbos
+dumdum
+dumped
+dumper
+dunams
+dunces
+dunged
+dunite
+dunked
+dunker
+dunlin
+dunned
+dunner
+dunted
+duolog
+duomos
+dupers
+dupery
+duping
+duplex
+dupped
+durbar
+duress
+durian
+during
+durion
+durned
+durocs
+durras
+durrie
+durums
+dusked
+dusted
+duster
+dustup
+duties
+duvets
+dwarfs
+dweebs
+dweeby
+dwells
+dwined
+dwines
+dyable
+dyadic
+dybbuk
+dyeing
+dyings
+dyking
+dynamo
+dynast
+dynein
+dynels
+dynode
+dyvour
+eagers
+eagled
+eagles
+eaglet
+eagres
+earbud
+earful
+earing
+earlap
+earned
+earner
+earths
+earthy
+earwax
+earwig
+easels
+easier
+easies
+easily
+easing
+easter
+eaters
+eatery
+eating
+ebbets
+ebbing
+ebooks
+ecarte
+ecesic
+ecesis
+echard
+eching
+echini
+echoed
+echoer
+echoes
+echoey
+echoic
+eclair
+eclats
+ectype
+eczema
+eddied
+eddies
+eddoes
+edemas
+edenic
+edgers
+edgier
+edgily
+edging
+edible
+edicts
+ediles
+edited
+editor
+educed
+educes
+educts
+eelier
+eerier
+eerily
+efface
+effect
+effete
+effigy
+efflux
+effort
+effuse
+egesta
+egests
+eggars
+eggcup
+eggers
+egging
+eggnog
+egises
+egoism
+egoist
+egress
+egrets
+eiders
+eidola
+eighth
+eights
+eighty
+eikons
+either
+ejecta
+ejects
+ekuele
+elains
+elands
+elapid
+elapse
+elated
+elater
+elates
+elbows
+elders
+eldest
+elects
+elegit
+elemis
+eleven
+elevon
+elfins
+elfish
+elicit
+elided
+elides
+elints
+elites
+elixir
+elmier
+elodea
+eloign
+eloins
+eloped
+eloper
+elopes
+eluant
+eluate
+eluded
+eluder
+eludes
+eluent
+eluted
+elutes
+eluvia
+elvers
+elvish
+elytra
+emails
+embalm
+embank
+embark
+embars
+embays
+embeds
+embers
+emblem
+embody
+emboli
+emboly
+embosk
+emboss
+embows
+embrue
+embryo
+emceed
+emcees
+emdash
+emeers
+emends
+emerge
+emerod
+emeses
+emesis
+emetic
+emetin
+emeute
+emigre
+emmers
+emmets
+emodin
+emoted
+emoter
+emotes
+empale
+empery
+empire
+employ
+emydes
+enable
+enacts
+enamel
+enamor
+enates
+enatic
+encage
+encamp
+encase
+encash
+encina
+encode
+encore
+encyst
+endash
+endear
+enders
+ending
+endite
+endive
+endows
+endrin
+endued
+endues
+endure
+enduro
+energy
+enface
+enfold
+engage
+engild
+engine
+engird
+engirt
+englut
+engram
+engulf
+enhalo
+enigma
+enisle
+enjoin
+enjoys
+enlace
+enlist
+enmesh
+enmity
+ennead
+ennuis
+ennuye
+enokis
+enolic
+enosis
+enough
+enrage
+enrapt
+enrich
+enrobe
+enroll
+enrols
+enroot
+enserf
+ensign
+ensile
+ensoul
+ensued
+ensues
+ensure
+entail
+entera
+enters
+entice
+entire
+entity
+entoil
+entomb
+entrap
+entree
+enured
+enures
+envied
+envier
+envies
+enviro
+envois
+envoys
+enwind
+enwomb
+enwrap
+enzyme
+enzyms
+eocene
+eolian
+eolith
+eonian
+eonism
+eosine
+eosins
+epacts
+eparch
+ephahs
+ephebe
+ephebi
+ephods
+ephori
+ephors
+epical
+epigon
+epilog
+epimer
+epizoa
+epochs
+epodes
+eponym
+epopee
+eposes
+equals
+equate
+equids
+equine
+equips
+equity
+erased
+eraser
+erases
+erbium
+erects
+erenow
+ergate
+ergots
+ericas
+eringo
+ermine
+eroded
+erodes
+eroses
+erotic
+errand
+errant
+errata
+erring
+errors
+ersatz
+eructs
+erugos
+erupts
+ervils
+eryngo
+escape
+escarp
+escars
+eschar
+eschew
+escort
+escots
+escrow
+escudo
+eskars
+eskers
+espial
+espied
+espies
+esprit
+essays
+essoin
+estate
+esteem
+esters
+estops
+estral
+estray
+estrin
+estrum
+estrus
+etalon
+etamin
+etapes
+etched
+etcher
+etches
+eterne
+ethane
+ethene
+ethers
+ethics
+ethion
+ethnic
+ethnos
+ethoxy
+ethyls
+ethyne
+etoile
+etudes
+etwees
+etymon
+euchre
+eulogy
+eunuch
+eupnea
+eureka
+euripi
+euroky
+eutaxy
+evaded
+evader
+evades
+evened
+evener
+evenly
+events
+everts
+evicts
+eviler
+evilly
+evince
+evited
+evites
+evoked
+evoker
+evokes
+evolve
+evulse
+evzone
+exacta
+exacts
+exalts
+examen
+exarch
+exceed
+excels
+except
+excess
+excide
+excise
+excite
+excuse
+exedra
+exempt
+exequy
+exerts
+exeunt
+exhale
+exhort
+exhume
+exiled
+exiler
+exiles
+exilic
+exines
+exists
+exited
+exodoi
+exodos
+exodus
+exogen
+exonic
+exonym
+exotic
+expand
+expats
+expect
+expels
+expend
+expert
+expire
+expiry
+export
+expose
+exsect
+exsert
+extant
+extend
+extent
+extern
+extoll
+extols
+extort
+extras
+exuded
+exudes
+exults
+exurbs
+exuvia
+eyases
+eyebar
+eyecup
+eyeful
+eyeing
+eyelet
+eyelid
+eyries
+fabber
+fabled
+fabler
+fables
+fabric
+facade
+facers
+facete
+facets
+faceup
+facial
+facile
+facing
+factor
+facula
+fadein
+faders
+fading
+faenas
+faerie
+failed
+faille
+fainer
+faints
+faired
+fairer
+fairly
+faiths
+fajita
+fakeer
+fakers
+fakery
+faking
+fakirs
+falces
+falcon
+fallal
+fallen
+faller
+fallow
+falser
+falsie
+falter
+family
+famine
+faming
+famish
+famous
+famuli
+fandom
+fanega
+fanfic
+fangas
+fanged
+fanion
+fanjet
+fanned
+fanner
+fanons
+fantod
+fantom
+fanums
+faqirs
+faquir
+farads
+farced
+farcer
+farces
+farcie
+farded
+fardel
+farers
+farfal
+farfel
+farina
+faring
+farles
+farmed
+farmer
+farrow
+farted
+fasces
+fascia
+fashed
+fashes
+fasted
+fasten
+faster
+father
+fathom
+fating
+fatwas
+faucal
+fauces
+faucet
+faulds
+faults
+faulty
+faunae
+faunal
+faunas
+fauves
+favela
+favism
+favors
+favour
+fawned
+fawner
+faxing
+faying
+fazing
+fealty
+feared
+fearer
+feased
+feases
+feasts
+feater
+featly
+feazed
+feazes
+feckly
+fecund
+fedora
+feeble
+feebly
+feeder
+feeing
+feeler
+feezed
+feezes
+feigns
+feijoa
+feints
+feirie
+feists
+feisty
+felids
+feline
+fellah
+fellas
+felled
+feller
+felloe
+fellow
+felons
+felony
+felsic
+felted
+female
+femmes
+femora
+femurs
+fenced
+fencer
+fences
+fended
+fender
+fennec
+fennel
+feoffs
+ferals
+ferbam
+feriae
+ferial
+ferias
+ferine
+ferity
+ferlie
+fermis
+ferrel
+ferret
+ferric
+ferrum
+ferula
+ferule
+fervid
+fervor
+fescue
+fessed
+fesses
+festal
+fester
+fetial
+fetich
+feting
+fetish
+fetors
+fetted
+fetter
+fettle
+feuars
+feudal
+feuded
+feuing
+fevers
+fewest
+feyest
+fezzed
+fezzes
+fiacre
+fiance
+fiasco
+fibbed
+fibber
+fibers
+fibres
+fibril
+fibrin
+fibula
+fiches
+fichus
+ficins
+fickle
+fickly
+ficoes
+fiddle
+fiddly
+fidged
+fidges
+fidget
+fields
+fiends
+fierce
+fiesta
+fifers
+fifing
+fifths
+figged
+fights
+figure
+filers
+filets
+filial
+filing
+filled
+filler
+filles
+fillet
+fillip
+fillos
+filmed
+filmer
+filmic
+filmis
+filose
+filter
+filths
+filthy
+fimble
+finale
+finals
+fincas
+finder
+finely
+finery
+finest
+finger
+finial
+fining
+finish
+finite
+finito
+finked
+finned
+fiords
+fipple
+fiques
+firers
+firing
+firkin
+firman
+firmed
+firmer
+firmly
+firsts
+firths
+fiscal
+fished
+fisher
+fishes
+fistic
+fitchy
+fitful
+fitted
+fitter
+fivers
+fixate
+fixers
+fixing
+fixity
+fixure
+fizgig
+fizzed
+fizzer
+fizzes
+fizzle
+fjelds
+fjords
+flabby
+flacks
+flacon
+flaggy
+flagon
+flails
+flairs
+flaked
+flaker
+flakes
+flakey
+flambe
+flamed
+flamen
+flamer
+flames
+flanes
+flanks
+flappy
+flared
+flares
+flashy
+flasks
+flatly
+flatus
+flaunt
+flauta
+flavin
+flavor
+flawed
+flaxen
+flaxes
+flayed
+flayer
+fleams
+fleche
+flecks
+flecky
+fledge
+fledgy
+fleece
+fleech
+fleecy
+fleers
+fleets
+flench
+flense
+fleshy
+fletch
+fleury
+flexed
+flexes
+flexor
+fleyed
+flicks
+fliers
+fliest
+flight
+flimsy
+flinch
+flings
+flints
+flinty
+flippy
+flirts
+flirty
+flitch
+flited
+flites
+floats
+floaty
+flocci
+flocks
+flocky
+flongs
+floods
+flooey
+flooie
+floors
+floosy
+floozy
+floppy
+florae
+floral
+floras
+floret
+florid
+florin
+flossy
+flotas
+flours
+floury
+flouts
+flowed
+flower
+fluent
+fluffs
+fluffy
+fluids
+fluish
+fluked
+flukes
+flukey
+flumed
+flumes
+flumps
+flunks
+flunky
+fluors
+flurry
+fluted
+fluter
+flutes
+flutey
+fluxed
+fluxes
+fluyts
+flyboy
+flybys
+flyers
+flying
+flyman
+flymen
+flyoff
+flysch
+flyted
+flytes
+flyway
+foaled
+foamed
+foamer
+fobbed
+fodder
+fodgel
+foehns
+foeman
+foemen
+foetal
+foetid
+foetor
+foetus
+fogbow
+fogdog
+fogeys
+fogged
+fogger
+fogies
+foible
+foiled
+foined
+foison
+foists
+folate
+folded
+folder
+foldup
+foleys
+foliar
+folios
+folium
+folkie
+folksy
+folles
+follis
+follow
+foment
+fomite
+fonded
+fonder
+fondle
+fondly
+fondue
+fondus
+fontal
+foodie
+fooled
+footed
+footer
+footie
+footle
+footsy
+foozle
+fopped
+forage
+forams
+forays
+forbad
+forbid
+forbye
+forced
+forcer
+forces
+forded
+fordid
+foreby
+foredo
+forego
+forest
+forgat
+forged
+forger
+forges
+forget
+forgot
+forint
+forked
+forker
+formal
+format
+formed
+formee
+former
+formes
+formic
+formol
+formyl
+fornix
+forrit
+fortes
+fortis
+forums
+forwhy
+fossae
+fossas
+fosses
+fossil
+foster
+fought
+fouled
+fouler
+foully
+founds
+founts
+fourth
+foveae
+foveal
+foveas
+fowled
+fowler
+foxier
+foxily
+foxing
+foyers
+fozier
+fracas
+fracti
+fraena
+frails
+fraise
+framed
+framer
+frames
+francs
+franks
+frappe
+frater
+frauds
+frayed
+frazil
+freaks
+freaky
+freely
+freers
+freest
+freeze
+french
+frenum
+frenzy
+freres
+fresco
+fretty
+friars
+friary
+fridge
+friend
+friers
+frieze
+friges
+fright
+frigid
+frijol
+frills
+frilly
+fringe
+fringy
+frisee
+frises
+frisks
+frisky
+frites
+friths
+fritts
+frivol
+frized
+frizer
+frizes
+frizzy
+frocks
+froggy
+frolic
+fronds
+fronts
+frosts
+frosty
+froths
+frothy
+frouzy
+frowns
+frowst
+frowsy
+frowzy
+frozen
+frugal
+fruits
+fruity
+frumps
+frumpy
+frusta
+fryers
+frying
+frypan
+fubbed
+fucoid
+fucose
+fucous
+fuddle
+fudged
+fudges
+fueled
+fueler
+fugato
+fugged
+fugios
+fugled
+fugles
+fugued
+fugues
+fuhrer
+fulcra
+fulfil
+fulgid
+fulham
+fullam
+fulled
+fuller
+fulmar
+fumble
+fumers
+fumets
+fumier
+fuming
+fumuli
+funded
+funder
+fundus
+funest
+fungal
+fungic
+fungus
+funked
+funker
+funkia
+funned
+funnel
+funner
+furane
+furans
+furfur
+furies
+furled
+furler
+furore
+furors
+furred
+furrow
+furzes
+fusain
+fusees
+fusels
+fusile
+fusils
+fusing
+fusion
+fussed
+fusser
+fusses
+fustic
+fusuma
+futile
+futons
+future
+futzed
+futzes
+fuzees
+fuzils
+fuzing
+fuzzed
+fuzzes
+fylfot
+fynbos
+fyttes
+gabbed
+gabber
+gabble
+gabbro
+gabies
+gabion
+gabled
+gables
+gaboon
+gadded
+gadder
+gaddis
+gadfly
+gadget
+gadids
+gadoid
+gaeing
+gaffed
+gaffer
+gaffes
+gagaku
+gagers
+gagged
+gagger
+gaggle
+gaging
+gagman
+gagmen
+gaiety
+gaijin
+gained
+gainer
+gainly
+gainst
+gaited
+gaiter
+galago
+galahs
+galaxy
+galeae
+galeas
+galena
+galere
+galiot
+galled
+gallet
+galley
+gallic
+gallon
+gallop
+gallus
+galoot
+galops
+galore
+galosh
+galyac
+galyak
+gamays
+gambas
+gambes
+gambia
+gambir
+gambit
+gamble
+gambol
+gamely
+gamers
+gamest
+gamete
+gamier
+gamily
+gamine
+gaming
+gamins
+gammas
+gammed
+gammer
+gammon
+gamuts
+gander
+ganefs
+ganevs
+ganged
+ganger
+gangly
+gangue
+ganjah
+ganjas
+gannet
+ganofs
+ganoid
+gantry
+gaoled
+gaoler
+gapers
+gaping
+gapped
+garage
+garbed
+garble
+garcon
+gardai
+garden
+garget
+gargle
+garish
+garlic
+garner
+garnet
+garote
+garred
+garret
+garron
+garter
+garths
+garvey
+gasbag
+gascon
+gashed
+gasher
+gashes
+gasify
+gasket
+gaskin
+gaslit
+gasman
+gasmen
+gasped
+gasper
+gassed
+gasser
+gasses
+gasted
+gaster
+gateau
+gaters
+gather
+gating
+gators
+gauche
+gaucho
+gauged
+gauger
+gauges
+gaults
+gaumed
+gauzes
+gavage
+gavels
+gavial
+gavots
+gawked
+gawker
+gawped
+gawper
+gawsie
+gayals
+gazabo
+gazars
+gazebo
+gazers
+gazing
+gazoos
+gazump
+geared
+gecked
+geckos
+geegaw
+geeing
+geeked
+geests
+geezer
+geisha
+gelada
+gelant
+gelate
+gelati
+gelato
+gelcap
+gelded
+gelder
+gelees
+gelled
+gemmae
+gemmed
+gemote
+gemots
+gender
+genera
+genets
+geneva
+genial
+genies
+genips
+genius
+genoas
+genome
+genoms
+genres
+genros
+gentes
+gentil
+gentle
+gently
+gentoo
+gentry
+geodes
+geodic
+geoids
+gerahs
+gerbil
+gerent
+german
+germen
+gerund
+gestes
+gestic
+getter
+getups
+gewgaw
+geyser
+gharri
+gharry
+ghauts
+ghazis
+gherao
+ghetto
+ghibli
+ghosts
+ghosty
+ghouls
+ghylls
+giants
+giaour
+gibbed
+gibber
+gibbet
+gibbon
+gibers
+gibing
+giblet
+gibson
+giddap
+gieing
+gifted
+giftee
+gigged
+giggle
+giggly
+giglet
+giglot
+gigolo
+gigots
+gigues
+gilded
+gilder
+gilled
+giller
+gillie
+gimbal
+gimels
+gimlet
+gimmal
+gimmes
+gimmie
+gimped
+gingal
+ginger
+gingko
+ginkgo
+ginned
+ginner
+gipons
+gipped
+gipper
+girded
+girder
+girdle
+girlie
+girned
+girons
+girted
+girths
+gismos
+gitano
+gitted
+gittin
+givens
+givers
+giving
+gizmos
+glaces
+glacis
+glades
+gladly
+glaire
+glairs
+glairy
+glaive
+glamor
+glance
+glands
+glared
+glares
+glassy
+glazed
+glazer
+glazes
+gleams
+gleamy
+gleans
+glebae
+glebes
+gledes
+gleeds
+gleeks
+gleets
+gleety
+glegly
+gleyed
+glibly
+glided
+glider
+glides
+gliffs
+glimed
+glimes
+glints
+glinty
+glioma
+glitch
+glitzy
+gloams
+gloats
+global
+globby
+globed
+globes
+globin
+gloggs
+glomus
+glooms
+gloomy
+gloppy
+gloria
+glossa
+glossy
+glosts
+glouts
+gloved
+glover
+gloves
+glowed
+glower
+glozed
+glozes
+glucan
+gluers
+gluier
+gluily
+gluing
+glumes
+glumly
+glumpy
+glunch
+gluons
+glutei
+gluten
+glutes
+glycan
+glycin
+glycol
+glycyl
+glyphs
+gnarls
+gnarly
+gnarrs
+gnatty
+gnawed
+gnawer
+gneiss
+gnomes
+gnomic
+gnomon
+gnoses
+gnosis
+goaded
+goaled
+goalie
+goanna
+goatee
+gobang
+gobans
+gobbed
+gobbet
+gobble
+gobies
+goblet
+goblin
+goboes
+gobony
+godets
+godown
+godson
+gofers
+goffer
+goggle
+goggly
+goglet
+goings
+golden
+golder
+golems
+golfed
+golfer
+golosh
+gombos
+gomers
+gomuti
+gonefs
+goners
+gonged
+goniff
+gonifs
+gonion
+gonium
+gonofs
+gonoph
+goodby
+goodie
+goodly
+goofed
+googly
+googol
+gooier
+gooney
+goonie
+gooral
+goosed
+gooses
+goosey
+gopher
+gorals
+gorged
+gorger
+gorges
+gorget
+gorgon
+gorhen
+gorier
+gorily
+goring
+gormed
+gorses
+gospel
+gossan
+gossip
+gotcha
+gothic
+gotten
+gouged
+gouger
+gouges
+gourde
+gourds
+govern
+gowans
+gowany
+gowned
+goyish
+graals
+grabby
+graben
+graced
+graces
+graded
+grader
+grades
+gradin
+gradus
+grafts
+graham
+grails
+grains
+grainy
+gramas
+gramma
+gramme
+grampa
+gramps
+grands
+grange
+granny
+grants
+granum
+grapes
+grapey
+graphs
+grappa
+grasps
+grassy
+grated
+grater
+grates
+gratin
+gratis
+graved
+gravel
+graven
+graver
+graves
+gravid
+grayed
+grayer
+grayly
+grazed
+grazer
+grazes
+grease
+greasy
+greats
+greave
+grebes
+greeds
+greedy
+greens
+greeny
+greets
+gregos
+greige
+gremmy
+greyed
+greyer
+greyly
+grided
+grides
+griefs
+grieve
+griffe
+griffs
+grifts
+grigri
+grille
+grills
+grilse
+grimed
+grimes
+grimly
+grinch
+grinds
+gringa
+gringo
+griots
+griped
+griper
+gripes
+gripey
+grippe
+grippy
+grisly
+grison
+grists
+griths
+gritty
+grivet
+groans
+groats
+grocer
+groggy
+grooms
+groove
+groovy
+groped
+groper
+gropes
+grosze
+groszy
+grotto
+grotty
+grouch
+ground
+groups
+grouse
+grouts
+grouty
+groved
+grovel
+groves
+grower
+growls
+growly
+growth
+groyne
+grubby
+grudge
+gruels
+gruffs
+gruffy
+grugru
+grumes
+grumps
+grumpy
+grunge
+grungy
+grunts
+grutch
+guacos
+guaiac
+guanay
+guanin
+guanos
+guards
+guavas
+guenon
+guests
+guffaw
+guggle
+guglet
+guided
+guider
+guides
+guidon
+guilds
+guiled
+guiles
+guilts
+guilty
+guimpe
+guinea
+guiros
+guised
+guises
+guitar
+gulags
+gulden
+gulfed
+gulled
+gullet
+gulley
+gulped
+gulper
+gumbos
+gummas
+gummed
+gummer
+gundog
+gunite
+gunman
+gunmen
+gunned
+gunnel
+gunnen
+gunner
+gunsel
+gurged
+gurges
+gurgle
+gurnet
+gurney
+gushed
+gusher
+gushes
+gusset
+gussie
+gusted
+guttae
+gutted
+gutter
+guttle
+guying
+guyots
+guzzle
+gweduc
+gybing
+gyozas
+gypped
+gypper
+gypsum
+gyrase
+gyrate
+gyrene
+gyring
+gyrons
+gyrose
+gyttja
+gyving
+habile
+habits
+haboob
+haceks
+hacked
+hackee
+hackie
+hackle
+hackly
+hading
+hadith
+hadjee
+hadjes
+hadjis
+hadron
+haeing
+haemal
+haemic
+haemin
+haeres
+haffet
+haffit
+hafted
+hafter
+hagbut
+hagdon
+hagged
+haggis
+haggle
+haikus
+hailed
+hailer
+haints
+hairdo
+haired
+hajjes
+hajjis
+hakeem
+hakims
+halala
+halals
+halers
+haleru
+halest
+halide
+halids
+haling
+halite
+hallah
+hallal
+hallel
+halloa
+halloo
+hallos
+hallot
+hallow
+hallux
+halmas
+haloed
+haloes
+haloid
+halons
+halted
+halter
+halutz
+halvah
+halvas
+halved
+halves
+hamada
+hamals
+hamate
+hamaul
+hamlet
+hammal
+hammam
+hammed
+hammer
+hamper
+hamuli
+hamzah
+hamzas
+hances
+handax
+handed
+hander
+handle
+hangar
+hanger
+hangul
+hangup
+haniwa
+hanked
+hanker
+hankie
+hansas
+hansel
+hanses
+hansom
+hanted
+hantle
+haoles
+happed
+happen
+hapten
+haptic
+harbor
+harden
+harder
+hardly
+hareem
+harems
+haring
+harked
+harken
+harlot
+harmed
+harmer
+harmin
+harped
+harper
+harpin
+harrow
+hartal
+hashed
+hashes
+haslet
+hasped
+hassel
+hassle
+hasted
+hasten
+hastes
+hatbox
+haters
+hatful
+hating
+hatpin
+hatred
+hatted
+hatter
+haughs
+hauled
+hauler
+haulms
+haulmy
+haunch
+haunts
+hausen
+havens
+havers
+having
+havior
+havocs
+hawala
+hawing
+hawked
+hawker
+hawkey
+hawkie
+hawser
+hawses
+hayers
+haying
+haymow
+hazans
+hazard
+hazels
+hazers
+hazier
+hazily
+hazing
+hazmat
+hazzan
+headed
+header
+healed
+healer
+health
+heaped
+heaper
+hearer
+hearse
+hearth
+hearts
+hearty
+heated
+heater
+heaths
+heathy
+heaume
+heaved
+heaven
+heaver
+heaves
+heckle
+hectic
+hector
+heddle
+heders
+hedged
+hedger
+hedges
+heeded
+heeder
+heehaw
+heeled
+heeler
+heezed
+heezes
+hefted
+hefter
+hegari
+hegira
+heifer
+height
+heiled
+heinie
+heired
+heishi
+heists
+hejira
+heliac
+helios
+helium
+helled
+heller
+hellos
+helmed
+helmet
+helots
+helped
+helper
+helved
+helves
+hemins
+hemmed
+hemmer
+hemoid
+hempen
+hempie
+henbit
+henges
+henley
+hennas
+henrys
+hented
+hepcat
+hepper
+heptad
+herald
+herbal
+herbed
+herded
+herder
+herdic
+hereat
+hereby
+herein
+hereof
+hereon
+heresy
+hereto
+heriot
+hermae
+hermai
+hermit
+hernia
+heroes
+heroic
+herons
+herpes
+hetero
+hetman
+heuchs
+heughs
+hewers
+hewing
+hexade
+hexads
+hexane
+hexers
+hexing
+hexone
+hexose
+hexyls
+heyday
+heydey
+hiatal
+hiatus
+hiccup
+hickey
+hickie
+hidden
+hiders
+hiding
+hieing
+hiemal
+higgle
+higher
+highly
+highth
+hights
+hijabs
+hijack
+hijrah
+hijras
+hikers
+hiking
+hilled
+hiller
+hilloa
+hillos
+hilted
+hinder
+hinged
+hinger
+hinges
+hinted
+hinter
+hipped
+hipper
+hippos
+hirees
+hirers
+hiring
+hirple
+hirsel
+hirsle
+hispid
+hissed
+hisser
+hisses
+histed
+hither
+hitman
+hitmen
+hitter
+hiving
+hoagie
+hoards
+hoarse
+hoaxed
+hoaxer
+hoaxes
+hobbed
+hobber
+hobbit
+hobble
+hobnob
+hoboed
+hoboes
+hocked
+hocker
+hockey
+hodads
+hodden
+hoddin
+hoeing
+hogans
+hogged
+hogger
+hogget
+hognut
+hogtie
+hoicks
+hoiden
+hoised
+hoises
+hoists
+hokier
+hokily
+hoking
+hokums
+holard
+holden
+holder
+holdup
+holier
+holies
+holily
+holing
+holism
+holist
+holked
+hollas
+holler
+holloa
+holloo
+hollos
+hollow
+holmic
+holpen
+homage
+hombre
+homely
+homers
+homily
+homing
+hominy
+hommos
+honans
+honcho
+hondas
+hondle
+honers
+honest
+honied
+honing
+honked
+honker
+honors
+honour
+hooded
+hoodoo
+hooeys
+hoofed
+hoofer
+hooked
+hookey
+hookup
+hoolie
+hooped
+hooper
+hoopla
+hoopoe
+hoopoo
+hoorah
+hooray
+hootch
+hooted
+hooter
+hooved
+hoover
+hooves
+hopers
+hoping
+hopped
+hopper
+hopple
+horahs
+horary
+horded
+hordes
+horned
+hornet
+horrid
+horror
+horsed
+horses
+horsey
+horste
+horsts
+hosels
+hosers
+hoseys
+hosier
+hosing
+hostas
+hosted
+hostel
+hostly
+hotbed
+hotbox
+hotdog
+hotels
+hotrod
+hotted
+hotter
+hottie
+houdah
+hounds
+houris
+hourly
+housed
+housel
+houser
+houses
+hovels
+hovers
+howdah
+howdie
+howffs
+howked
+howled
+howler
+howlet
+hoyden
+hoyles
+hryvna
+hubbly
+hubbub
+hubcap
+hubris
+huckle
+huddle
+huffed
+hugely
+hugest
+hugged
+hugger
+huipil
+hulked
+hulled
+huller
+hulloa
+hulloo
+hullos
+humane
+humans
+humate
+humble
+humbly
+humbug
+humeri
+hummed
+hummer
+hummus
+humors
+humour
+humped
+humper
+humphs
+humvee
+hunger
+hungry
+hunker
+hunkey
+hunkie
+hunted
+hunter
+huppah
+hurdle
+hurled
+hurler
+hurley
+hurrah
+hurray
+hursts
+hurter
+hurtle
+hushed
+hushes
+husked
+husker
+hussar
+hustle
+hutted
+hutzpa
+huzzah
+huzzas
+hyaena
+hyalin
+hybrid
+hybris
+hydrae
+hydras
+hydria
+hydric
+hydrid
+hydros
+hyenas
+hyenic
+hyetal
+hymens
+hymnal
+hymned
+hyoids
+hypers
+hyphae
+hyphal
+hyphen
+hyping
+hypnic
+hypoed
+hysons
+hyssop
+iambic
+iambus
+iatric
+ibexes
+ibices
+ibidem
+ibises
+icebox
+icecap
+iceman
+icemen
+ichors
+icicle
+iciest
+icings
+ickers
+ickier
+ickily
+icones
+iconic
+ideals
+ideate
+idiocy
+idioms
+idiots
+idlers
+idlest
+idling
+idylls
+iffier
+igging
+igloos
+ignify
+ignite
+ignore
+iguana
+ihrams
+ilexes
+iliads
+illest
+illite
+illude
+illume
+imaged
+imager
+images
+imagos
+imaret
+imaums
+imbalm
+imbark
+imbeds
+imbibe
+imbody
+imbrue
+imbued
+imbues
+imides
+imidic
+imines
+immane
+immesh
+immies
+immune
+immure
+impact
+impair
+impala
+impale
+impark
+impart
+impawn
+impede
+impels
+impend
+imphee
+imping
+impish
+impled
+impone
+import
+impose
+impost
+improv
+impugn
+impure
+impute
+inaner
+inanes
+inarch
+inarms
+inborn
+inbred
+incage
+incant
+incase
+incent
+incept
+incest
+inched
+incher
+inches
+incise
+incite
+inclip
+incogs
+income
+incony
+incubi
+incult
+incurs
+incuse
+indaba
+indeed
+indene
+indent
+indict
+indies
+indign
+indigo
+indite
+indium
+indole
+indols
+indoor
+indows
+indris
+induce
+induct
+indued
+indues
+indult
+inerts
+infall
+infamy
+infant
+infare
+infect
+infers
+infest
+infill
+infirm
+inflow
+influx
+infold
+inform
+infuse
+ingate
+ingest
+ingles
+ingots
+ingulf
+inhale
+inhaul
+inhere
+inhume
+inions
+inject
+injure
+injury
+inkers
+inkier
+inking
+inkjet
+inkles
+inkpot
+inlace
+inlaid
+inland
+inlays
+inlets
+inlier
+inmate
+inmesh
+inmost
+innage
+innate
+inners
+inning
+inpour
+inputs
+inroad
+inruns
+inrush
+insane
+inseam
+insect
+insert
+insets
+inside
+insist
+insole
+insoul
+inspan
+instal
+instar
+instep
+instil
+insult
+insure
+intact
+intake
+intend
+intent
+intern
+inters
+intima
+intime
+intine
+intomb
+intone
+intort
+intown
+intron
+intros
+intuit
+inturn
+inulin
+inured
+inures
+inurns
+invade
+invars
+invent
+invert
+invest
+invite
+invoke
+inwall
+inward
+inwind
+inwove
+inwrap
+iodate
+iodide
+iodids
+iodine
+iodins
+iodise
+iodism
+iodize
+iodous
+iolite
+ionics
+ionise
+ionium
+ionize
+ionone
+ipecac
+irades
+irater
+ireful
+irenic
+irides
+iridic
+irised
+irises
+iritic
+iritis
+irking
+irokos
+ironed
+ironer
+irones
+ironic
+irreal
+irrupt
+isatin
+ischia
+island
+islets
+isling
+isobar
+isogon
+isohel
+isolog
+isomer
+isopod
+isseis
+issued
+issuer
+issues
+isthmi
+istles
+italic
+itched
+itches
+itemed
+iterum
+itself
+ixodid
+ixoras
+ixtles
+izzard
+jabbed
+jabber
+jabiru
+jabots
+jacals
+jacana
+jackal
+jacked
+jacker
+jacket
+jading
+jadish
+jaeger
+jagers
+jagged
+jagger
+jagras
+jaguar
+jailed
+jailer
+jailor
+jalaps
+jalops
+jalopy
+jambed
+jambes
+jammed
+jammer
+jangle
+jangly
+japans
+japers
+japery
+japing
+jarful
+jargon
+jarina
+jarrah
+jarred
+jarvey
+jasmin
+jasper
+jassid
+jauked
+jaunce
+jaunts
+jaunty
+jauped
+jawans
+jawing
+jaygee
+jayvee
+jazzbo
+jazzed
+jazzer
+jazzes
+jeaned
+jebels
+jeeing
+jeeped
+jeered
+jeerer
+jehads
+jejuna
+jejune
+jelled
+jellos
+jennet
+jerboa
+jereed
+jerids
+jerked
+jerrid
+jersey
+jessed
+jesses
+jested
+jester
+jesuit
+jetlag
+jetons
+jetsam
+jetsom
+jetted
+jetton
+jetway
+jewels
+jezail
+jibbed
+jibber
+jibers
+jibing
+jicama
+jigged
+jigger
+jiggle
+jiggly
+jigsaw
+jihads
+jilted
+jilter
+jiminy
+jimmie
+jimper
+jimply
+jingal
+jingko
+jingle
+jingly
+jinked
+jinker
+jinnee
+jinnis
+jitney
+jitter
+jivers
+jivier
+jiving
+jnanas
+jobbed
+jobber
+jockey
+jockos
+jocose
+jocund
+jogged
+jogger
+joggle
+johnny
+joined
+joiner
+joints
+joists
+jojoba
+jokers
+jokier
+jokily
+joking
+jolted
+jolter
+jorams
+jordan
+jorums
+joseph
+joshed
+josher
+joshes
+josses
+jostle
+jotted
+jotter
+jouals
+jouked
+joules
+jounce
+jouncy
+journo
+jousts
+jovial
+jowars
+jowing
+jowled
+joyful
+joying
+joyous
+joypop
+jubbah
+jubhah
+jubile
+judder
+judged
+judger
+judges
+judoka
+jugate
+jugful
+jugged
+juggle
+jugula
+jugums
+juiced
+juicer
+juices
+jujube
+juking
+juleps
+jumbal
+jumble
+jumbos
+jumped
+jumper
+juncos
+jungle
+jungly
+junior
+junked
+junker
+junket
+juntas
+juntos
+jupons
+jurant
+jurats
+jurels
+juried
+juries
+jurist
+jurors
+justed
+juster
+justle
+justly
+jutted
+kababs
+kabaka
+kabala
+kabars
+kabaya
+kabiki
+kabobs
+kabuki
+kaffir
+kafirs
+kaftan
+kahuna
+kaiaks
+kainit
+kaiser
+kakapo
+kalams
+kalian
+kalifs
+kaliph
+kalium
+kalmia
+kalong
+kalpac
+kalpak
+kalpas
+kamala
+kamiks
+kamsin
+kanaka
+kanban
+kanjis
+kantar
+kanzus
+kaolin
+kaonic
+kapoks
+kappas
+kaputt
+karate
+karats
+karmas
+karmic
+karoos
+kaross
+karroo
+karsts
+kasbah
+kashas
+kasher
+kation
+kauris
+kavass
+kayaks
+kayles
+kayoed
+kayoes
+kazoos
+kebabs
+kebars
+kebbie
+keblah
+kebobs
+kecked
+keckle
+keddah
+kedged
+kedges
+keeked
+keeled
+keened
+keener
+keenly
+keeper
+keeves
+kefirs
+kegged
+kegger
+kegler
+keleps
+kelims
+keloid
+kelped
+kelpie
+kelson
+kelter
+kelvin
+kenafs
+kendos
+kenned
+kennel
+kentes
+kepped
+keppen
+kerbed
+kerfed
+kermes
+kermis
+kerned
+kernel
+kernes
+kerria
+kersey
+ketene
+ketols
+ketone
+ketose
+kettle
+kevels
+kevils
+kewpie
+keying
+keypad
+keypal
+keyset
+keyway
+khadis
+khakis
+khalif
+khaphs
+khazen
+khedah
+khedas
+kheths
+khoums
+kiangs
+kiaugh
+kibbeh
+kibbes
+kibbis
+kibble
+kibeis
+kibitz
+kiblah
+kiblas
+kibosh
+kicked
+kicker
+kickup
+kidded
+kidder
+kiddie
+kiddos
+kidnap
+kidney
+kidvid
+kilims
+killed
+killer
+killie
+kilned
+kilted
+kilter
+kiltie
+kimchi
+kimono
+kinara
+kinase
+kinder
+kindle
+kindly
+kinema
+kinged
+kingly
+kinins
+kinked
+kiosks
+kipped
+kippen
+kipper
+kirned
+kirsch
+kirtle
+kishka
+kishke
+kismat
+kismet
+kissed
+kisser
+kisses
+kitbag
+kiters
+kithed
+kithes
+kiting
+kitsch
+kitted
+kittel
+kitten
+kittle
+klatch
+klaxon
+klepht
+klepto
+klicks
+klongs
+kloofs
+kludge
+kludgy
+kluged
+kluges
+klutzy
+knacks
+knarry
+knaurs
+knaves
+knawel
+knawes
+kneads
+kneels
+knells
+knifed
+knifer
+knifes
+knight
+knives
+knobby
+knocks
+knolls
+knolly
+knosps
+knotty
+knouts
+knower
+knowns
+knubby
+knurls
+knurly
+koalas
+kobold
+koines
+kolhoz
+kolkoz
+kombus
+konked
+koodoo
+kookie
+kopeck
+kopeks
+kopjes
+koppas
+koppie
+korats
+kormas
+koruna
+koruny
+kosher
+kotows
+koumis
+koumys
+kouroi
+kouros
+kousso
+kowtow
+kraals
+krafts
+kraits
+kraken
+krater
+krauts
+kreeps
+krewes
+krills
+krises
+kronen
+kroner
+kronor
+kronur
+krooni
+kroons
+krubis
+krubut
+kuchen
+kudzus
+kugels
+kukris
+kulaki
+kulaks
+kultur
+kumiss
+kummel
+kurgan
+kurtas
+kussos
+kuvasz
+kvases
+kvells
+kvetch
+kwacha
+kwanza
+kyacks
+kybosh
+kyries
+kythed
+kythes
+laager
+labara
+labels
+labile
+labium
+labors
+labour
+labret
+labrum
+lacers
+laches
+lacier
+lacily
+lacing
+lacked
+lacker
+lackey
+lactam
+lactic
+lacuna
+lacune
+ladder
+laddie
+ladens
+laders
+ladies
+lading
+ladino
+ladled
+ladler
+ladles
+ladron
+lagans
+lagend
+lagers
+lagged
+lagger
+lagoon
+laguna
+lagune
+lahars
+laical
+laichs
+laighs
+lairds
+laired
+lakers
+lakier
+laking
+lallan
+lalled
+lambda
+lambed
+lamber
+lambie
+lamedh
+lameds
+lamely
+lament
+lamest
+lamiae
+lamias
+lamina
+laming
+lammed
+lampad
+lampas
+lamped
+lanais
+lanate
+lanced
+lancer
+lances
+lancet
+landau
+landed
+lander
+lanely
+langue
+langur
+lanker
+lankly
+lanner
+lanose
+lanugo
+laogai
+lapdog
+lapels
+lapful
+lapins
+lapped
+lapper
+lappet
+lapsed
+lapser
+lapses
+lapsus
+laptop
+larded
+larder
+lardon
+larees
+larger
+larges
+largos
+lariat
+larine
+larked
+larker
+larrup
+larums
+larvae
+larval
+larvas
+larynx
+lascar
+lasers
+lashed
+lasher
+lashes
+lasing
+lasses
+lassie
+lassis
+lassos
+lasted
+laster
+lastly
+lateen
+lately
+latens
+latent
+latest
+lathed
+lather
+lathes
+lathis
+latigo
+latina
+latino
+latish
+latkes
+latria
+latten
+latter
+lattes
+lattin
+lauans
+lauded
+lauder
+laughs
+launce
+launch
+laurae
+lauras
+laurel
+lavabo
+lavage
+lavash
+laveer
+lavers
+laving
+lavish
+lawful
+lawine
+lawing
+lawman
+lawmen
+lawyer
+laxest
+laxity
+layers
+laying
+layins
+layman
+laymen
+layoff
+layout
+layups
+lazars
+lazied
+lazier
+lazies
+lazily
+lazing
+lazuli
+leachy
+leaded
+leaden
+leader
+leafed
+league
+leaked
+leaker
+leally
+lealty
+leaned
+leaner
+leanly
+leaped
+leaper
+learns
+learnt
+leased
+leaser
+leases
+leasts
+leaved
+leaven
+leaver
+leaves
+lebens
+leched
+lecher
+leches
+lechwe
+lectin
+lector
+ledger
+ledges
+leered
+leeway
+lefter
+legacy
+legals
+legate
+legato
+legend
+legers
+legged
+leggin
+legion
+legist
+legits
+legman
+legmen
+legong
+legume
+lehuas
+lekked
+lekvar
+lemans
+lemmas
+lemons
+lemony
+lemurs
+lender
+length
+lenite
+lenity
+lensed
+lenses
+lenten
+lentic
+lentil
+lentos
+leones
+lepers
+leptin
+lepton
+lesion
+lessee
+lessen
+lesser
+lesson
+lessor
+lethal
+lethes
+letted
+letter
+letups
+leucin
+leudes
+leukon
+levant
+leveed
+levees
+levels
+levers
+levied
+levier
+levies
+levins
+levity
+lewder
+lewdly
+lexeme
+lexica
+lezzes
+lezzie
+liable
+liaise
+lianas
+lianes
+liangs
+liards
+libber
+libels
+libers
+libido
+liblab
+librae
+libras
+lichee
+lichen
+liches
+lichis
+lichts
+licked
+licker
+lictor
+lidars
+lidded
+lieder
+liefer
+liefly
+lieges
+lienal
+lierne
+liever
+lifers
+lifted
+lifter
+ligand
+ligans
+ligase
+ligate
+ligers
+lights
+lignan
+lignin
+ligula
+ligule
+ligure
+likely
+likens
+likers
+likest
+liking
+likuta
+lilacs
+lilied
+lilies
+lilted
+limans
+limbas
+limbed
+limber
+limbic
+limbos
+limbus
+limens
+limeys
+limier
+limina
+liming
+limits
+limmer
+limned
+limner
+limnic
+limpas
+limped
+limper
+limpet
+limpid
+limply
+limpsy
+limuli
+linacs
+linage
+linden
+lineal
+linear
+linens
+lineny
+liners
+lineup
+lingam
+lingas
+linger
+lingua
+linier
+lining
+linins
+linked
+linker
+linkup
+linnet
+linsey
+linted
+lintel
+linter
+lintol
+linums
+lipase
+lipide
+lipids
+lipins
+lipoid
+lipoma
+lipped
+lippen
+lipper
+liquid
+liquor
+liroth
+lisles
+lisped
+lisper
+lissom
+listed
+listee
+listel
+listen
+lister
+litany
+litchi
+liters
+lither
+lithia
+lithic
+lithos
+litmus
+litres
+litten
+litter
+little
+lively
+livens
+livers
+livery
+livest
+livier
+living
+livres
+livyer
+lizard
+llamas
+llanos
+loaded
+loader
+loafed
+loafer
+loamed
+loaned
+loaner
+loathe
+loaves
+lobate
+lobbed
+lobber
+lobule
+locale
+locals
+locate
+lochan
+lochia
+locked
+locker
+locket
+lockup
+locoed
+locoes
+locule
+loculi
+locums
+locust
+lodens
+lodged
+lodger
+lodges
+lofted
+lofter
+logans
+logged
+logger
+loggia
+loggie
+logics
+logier
+logily
+logins
+logion
+logjam
+logons
+logway
+loided
+loiter
+lolled
+loller
+lollop
+lomein
+loment
+lonely
+loners
+longan
+longed
+longer
+longes
+longly
+looeys
+loofah
+loofas
+looies
+looing
+looked
+looker
+lookup
+loomed
+looped
+looper
+loosed
+loosen
+looser
+looses
+looted
+looter
+lopers
+loping
+lopped
+lopper
+loquat
+lorans
+lorded
+lordly
+loreal
+lorica
+lories
+losels
+losers
+losing
+losses
+lotahs
+lotion
+lotted
+lotter
+lottes
+lottos
+louche
+louden
+louder
+loudly
+loughs
+louies
+loumas
+lounge
+loungy
+louped
+loupen
+loupes
+loured
+loused
+louses
+louted
+louver
+louvre
+lovage
+lovats
+lovely
+lovers
+loving
+lowboy
+lowers
+lowery
+lowest
+lowing
+lowish
+loxing
+lubber
+lubing
+lubric
+lucent
+lucern
+lucite
+lucked
+luckie
+lucres
+luetic
+luffas
+luffed
+lugers
+lugged
+lugger
+luggie
+luging
+lulled
+luller
+lumbar
+lumber
+lumens
+lumina
+lummox
+lumped
+lumpen
+lumper
+lunacy
+lunars
+lunate
+lunets
+lungan
+lunged
+lungee
+lunger
+lunges
+lungis
+lungyi
+lunier
+lunies
+lunker
+lunted
+lunula
+lunule
+lupine
+lupins
+lupous
+lurdan
+lurers
+luring
+lurked
+lurker
+lushed
+lusher
+lushes
+lushly
+lusted
+luster
+lustra
+lustre
+luteal
+lutein
+luteum
+luting
+lutist
+lutzes
+luxate
+luxury
+lyases
+lycees
+lyceum
+lychee
+lyches
+lycras
+lyings
+lymphs
+lynxes
+lyrate
+lyrics
+lyrism
+lyrist
+lysate
+lysine
+lysing
+lysins
+lyssas
+lyttae
+lyttas
+macaco
+macaws
+macers
+maches
+machos
+macing
+mackle
+macled
+macles
+macons
+macron
+macros
+macula
+macule
+madame
+madams
+madcap
+madded
+madden
+madder
+madras
+madres
+madtom
+maduro
+maenad
+maffia
+mafias
+maftir
+maggot
+magian
+magics
+magilp
+maglev
+magmas
+magnet
+magnum
+magots
+magpie
+maguey
+mahoes
+mahout
+mahzor
+maiden
+maigre
+maihem
+mailed
+mailer
+mailes
+maills
+maimed
+maimer
+mainly
+maists
+maizes
+majors
+makars
+makers
+makeup
+making
+makuta
+malady
+malars
+malate
+malfed
+malgre
+malice
+malign
+maline
+malkin
+malled
+mallee
+mallei
+mallet
+mallow
+maloti
+malted
+maltha
+maltol
+mambas
+mambos
+mameys
+mamies
+mamluk
+mammae
+mammal
+mammas
+mammee
+mammer
+mammet
+mammey
+mammie
+mammon
+mamzer
+manage
+manana
+manats
+manche
+manege
+manful
+mangas
+mangel
+manger
+manges
+mangey
+mangle
+mangos
+maniac
+manias
+manics
+manila
+manioc
+manito
+manitu
+mannan
+mannas
+manned
+manner
+manors
+manque
+manses
+mantas
+mantel
+mantes
+mantic
+mantid
+mantis
+mantle
+mantra
+mantua
+manual
+manure
+maples
+mapped
+mapper
+maquis
+maraca
+maraud
+marble
+marbly
+marcel
+margay
+marges
+margin
+marina
+marine
+marish
+markas
+marked
+marker
+market
+markka
+markup
+marled
+marlin
+marmot
+maroon
+marque
+marram
+marred
+marrer
+marron
+marrow
+marses
+marshy
+marted
+marten
+martin
+martyr
+marvel
+masala
+mascon
+mascot
+masers
+mashed
+masher
+mashes
+mashie
+masjid
+masked
+maskeg
+masker
+masons
+masque
+massif
+masted
+master
+mastic
+mastix
+maters
+mateys
+matier
+mating
+matins
+matres
+matrix
+matron
+matsah
+matted
+matter
+mattes
+mattin
+mature
+matzah
+matzas
+matzoh
+matzos
+matzot
+mauger
+maugre
+mauled
+mauler
+maumet
+maunds
+maundy
+mauves
+mavens
+mavies
+mavins
+mawing
+maxima
+maxims
+maxing
+maxixe
+maybes
+mayday
+mayest
+mayfly
+mayhap
+mayhem
+maying
+mayors
+maypop
+mayvin
+mazard
+mazers
+mazier
+mazily
+mazing
+mazuma
+mbiras
+meadow
+meager
+meagre
+mealie
+meaner
+meanie
+meanly
+measle
+measly
+meatal
+meated
+meatus
+meccas
+medaka
+medals
+meddle
+medfly
+mediad
+mediae
+medial
+median
+medias
+medick
+medico
+medics
+medina
+medium
+medius
+medlar
+medley
+medusa
+meeker
+meekly
+meeter
+meetly
+megara
+megilp
+megohm
+megrim
+mehndi
+meikle
+meinie
+melded
+melder
+melees
+melena
+melled
+mellow
+melody
+meloid
+melons
+melted
+melter
+melton
+member
+memoir
+memory
+menace
+menads
+menage
+mended
+mender
+menhir
+menial
+meninx
+mensae
+mensal
+mensas
+mensch
+mensed
+menses
+mental
+mentee
+mentor
+mentum
+menudo
+meoued
+meowed
+mercer
+merces
+merdes
+merely
+merest
+merged
+mergee
+merger
+merges
+merino
+merits
+merles
+merlin
+merlon
+merlot
+merman
+mermen
+mescal
+meshed
+meshes
+mesial
+mesian
+mesnes
+mesons
+messan
+messed
+messes
+mestee
+metage
+metals
+metate
+meteor
+metepa
+meters
+method
+methyl
+metier
+meting
+metols
+metope
+metred
+metres
+metric
+metros
+mettle
+metump
+mewing
+mewled
+mewler
+mezcal
+mezuza
+mezzos
+miaous
+miaows
+miasma
+miasms
+miauls
+micell
+miched
+miches
+mickey
+mickle
+micron
+micros
+midair
+midcap
+midday
+midden
+middle
+midges
+midget
+midgut
+midleg
+midrib
+midsts
+midway
+miffed
+miggle
+mights
+mighty
+mignon
+mihrab
+mikado
+miking
+mikron
+mikvah
+mikveh
+mikvos
+mikvot
+miladi
+milady
+milage
+milded
+milden
+milder
+mildew
+mildly
+milers
+milieu
+milium
+milked
+milker
+milled
+miller
+milles
+millet
+milneb
+milord
+milpas
+milted
+milter
+mimbar
+mimeos
+mimers
+mimics
+miming
+mimosa
+minced
+minder
+miners
+mingle
+minify
+minima
+minims
+mining
+minion
+minish
+minium
+minkes
+minnow
+minors
+minted
+minter
+minuet
+minute
+minxes
+minyan
+mioses
+miosis
+miotic
+mirage
+mirier
+miring
+mirins
+mirker
+mirror
+mirths
+mirzas
+misact
+misadd
+misaim
+misate
+miscue
+miscut
+misdid
+miseat
+misers
+misery
+misfed
+misfit
+mishap
+miskal
+mislay
+misled
+mislie
+mislit
+mismet
+mispen
+missal
+missay
+missed
+missel
+misses
+misset
+missis
+missus
+misted
+mister
+misuse
+miters
+mither
+mitier
+mitral
+mitred
+mitres
+mitten
+mixers
+mixing
+mixups
+mizens
+mizuna
+mizzen
+mizzle
+mizzly
+moaned
+moaner
+moated
+mobbed
+mobber
+mobcap
+mobile
+mobled
+mochas
+mocked
+mocker
+mockup
+modals
+models
+modems
+modern
+modest
+modica
+modify
+modish
+module
+moduli
+modulo
+mogged
+moggie
+moghul
+moguls
+mohair
+mohawk
+mohels
+mohurs
+moiety
+moiled
+moiler
+moirai
+moires
+mojoes
+molars
+molded
+molder
+molies
+moline
+mollah
+mollie
+moloch
+molted
+molten
+molter
+moment
+mommas
+momser
+momzer
+monads
+mondes
+mondos
+moneys
+monger
+mongoe
+mongol
+mongos
+mongst
+monied
+monies
+monish
+monism
+monist
+monkey
+monody
+montes
+months
+mooing
+moolah
+moolas
+mooley
+mooned
+mooner
+moored
+mooted
+mooter
+mopeds
+mopers
+mopery
+mopier
+moping
+mopish
+mopoke
+mopped
+mopper
+moppet
+morale
+morals
+morays
+morbid
+moreen
+morels
+morgan
+morgen
+morgue
+morion
+morose
+morpho
+morphs
+morris
+morros
+morrow
+morsel
+mortal
+mortar
+morula
+mosaic
+moseys
+moshav
+moshed
+mosher
+moshes
+mosque
+mossed
+mosser
+mosses
+mostly
+motels
+motets
+mother
+motifs
+motile
+motion
+motive
+motley
+motmot
+motors
+mottes
+mottle
+mottos
+moujik
+moulds
+mouldy
+moulin
+moults
+mounds
+mounts
+mourns
+moused
+mouser
+mouses
+mousey
+mousse
+mouths
+mouthy
+mouton
+movers
+movies
+moving
+mowers
+mowing
+moxies
+muches
+muchly
+mucins
+mucked
+mucker
+muckle
+mucluc
+mucoid
+mucors
+mucosa
+mucose
+mucous
+mudbug
+mudcap
+mudcat
+mudded
+mudder
+muddle
+muddly
+mudhen
+mudras
+muesli
+muffed
+muffin
+muffle
+muftis
+mugful
+muggar
+mugged
+muggee
+mugger
+muggur
+mughal
+mujiks
+mukluk
+muktuk
+mulcts
+muleta
+muleys
+muling
+mulish
+mullah
+mullas
+mulled
+mullen
+muller
+mullet
+mulley
+mumble
+mumbly
+mummed
+mummer
+mumped
+mumper
+mungos
+muntin
+muonic
+murals
+murder
+murein
+murids
+murine
+muring
+murker
+murkly
+murmur
+murphy
+murras
+murres
+murrey
+murrha
+muscae
+muscat
+muscid
+muscle
+muscly
+musers
+museum
+mushed
+musher
+mushes
+musick
+musics
+musing
+musjid
+muskeg
+musket
+muskie
+muskit
+muskox
+muslin
+mussed
+mussel
+musses
+musted
+mustee
+muster
+musths
+mutant
+mutase
+mutate
+mutely
+mutest
+mutine
+muting
+mutiny
+mutism
+mutons
+mutter
+mutton
+mutual
+mutuel
+mutule
+muumuu
+muzhik
+muzjik
+muzzle
+myases
+myasis
+mycele
+myelin
+mylars
+mynahs
+myomas
+myopes
+myopia
+myopic
+myoses
+myosin
+myosis
+myotic
+myriad
+myrica
+myrrhs
+myrtle
+myself
+mysids
+mysost
+mystic
+mythic
+mythoi
+mythos
+myxoid
+myxoma
+nabbed
+nabber
+nabobs
+nachas
+naches
+nachos
+nacred
+nacres
+nadirs
+naevus
+naffed
+nagana
+nagged
+nagger
+naiads
+nailed
+nailer
+nairas
+nairus
+naiver
+naives
+nakfas
+naleds
+namely
+namers
+naming
+nances
+nandin
+nanism
+nankin
+nannie
+napalm
+napery
+napkin
+nappas
+napped
+napper
+nappes
+nappie
+narcos
+narial
+narine
+narked
+narrow
+narwal
+nasals
+nasial
+nasion
+nastic
+natant
+nation
+native
+natron
+natter
+nature
+naught
+nausea
+nautch
+navaid
+navars
+navels
+navies
+nawabs
+naysay
+nazify
+nearby
+neared
+nearer
+nearly
+neaten
+neater
+neatly
+nebula
+nebule
+nebuly
+necked
+necker
+nectar
+needed
+needer
+needle
+negate
+neighs
+nekton
+nellie
+nelson
+neocon
+neoned
+nepeta
+nephew
+nereid
+nereis
+neroli
+nerols
+nerved
+nerves
+nesses
+nested
+nester
+nestle
+nestor
+nether
+netops
+netted
+netter
+nettle
+nettly
+neumes
+neumic
+neural
+neuron
+neuter
+nevoid
+newbie
+newels
+newest
+newies
+newish
+newsie
+newton
+niacin
+nibbed
+nibble
+nicads
+nicely
+nicest
+nicety
+niched
+niches
+nicked
+nickel
+nicker
+nickle
+nicols
+nidate
+nidget
+nidify
+niding
+nieces
+nielli
+niello
+nieves
+niffer
+niggle
+niggly
+nighed
+nigher
+nights
+nighty
+nihils
+nilgai
+nilgau
+nilled
+nimble
+nimbly
+nimbus
+nimmed
+nimrod
+ninety
+ninjas
+ninons
+ninths
+niobic
+nipped
+nipper
+niseis
+niters
+nitery
+nitons
+nitres
+nitric
+nitrid
+nitril
+nitros
+nitwit
+nixies
+nixing
+nizams
+nobble
+nobler
+nobles
+nobody
+nocent
+nocked
+nodded
+nodder
+noddle
+nodose
+nodous
+nodule
+noesis
+noetic
+nogged
+noggin
+noised
+noises
+nomads
+nomina
+nomism
+nonage
+nonart
+nonces
+noncom
+nonego
+nonets
+nonfan
+nonfat
+nongay
+nonman
+nonmen
+nonpar
+nontax
+nonuse
+nonwar
+nonyls
+noodge
+noodle
+noogie
+nookie
+noosed
+nooser
+nooses
+nopals
+nordic
+norias
+norite
+normal
+normed
+norths
+noshed
+nosher
+noshes
+nosier
+nosily
+nosing
+nostoc
+notary
+notate
+noters
+nother
+notice
+notify
+noting
+notion
+nougat
+nought
+nounal
+nouses
+novels
+novena
+novice
+noways
+nowise
+noyade
+nozzle
+nuance
+nubbin
+nubble
+nubbly
+nubias
+nubile
+nubuck
+nuchae
+nuchal
+nuclei
+nudely
+nudest
+nudged
+nudger
+nudges
+nudies
+nudism
+nudist
+nudity
+nudnik
+nugget
+nuking
+nullah
+nulled
+numbat
+numbed
+number
+numbly
+numina
+nuncio
+nuncle
+nurled
+nursed
+nurser
+nurses
+nutant
+nutate
+nutlet
+nutmeg
+nutria
+nuzzle
+nyalas
+oafish
+oakier
+oakums
+oaring
+oaters
+obeahs
+obelia
+obelus
+obento
+obeyed
+obeyer
+obiism
+object
+objets
+oblast
+oblate
+oblige
+oblong
+oboist
+oboles
+obolus
+obsess
+obtain
+obtect
+obtest
+obtund
+obtuse
+obvert
+occult
+occupy
+occurs
+oceans
+ocelli
+ocelot
+ochers
+ochery
+ochone
+ochrea
+ochred
+ochres
+ocicat
+ockers
+ocreae
+octads
+octane
+octans
+octant
+octave
+octavo
+octets
+octopi
+octroi
+octyls
+ocular
+oculus
+oddest
+oddish
+oddity
+odeons
+odeums
+odious
+odists
+odiums
+odored
+odours
+odyles
+oedema
+oeuvre
+offals
+offcut
+offend
+offers
+office
+offing
+offish
+offkey
+offset
+oftest
+ogdoad
+oghams
+ogival
+ogives
+oglers
+ogling
+ogress
+ogrish
+ogrism
+ohmage
+oidium
+oilcan
+oilcup
+oilers
+oilier
+oilily
+oiling
+oilman
+oilmen
+oilway
+oinked
+okapis
+okayed
+oldest
+oldies
+oldish
+oleate
+olefin
+oleine
+oleins
+oleums
+olingo
+olives
+omasum
+ombers
+ombres
+omegas
+omelet
+omened
+omenta
+onager
+onagri
+onions
+oniony
+onlays
+online
+onload
+onrush
+onsets
+onside
+onuses
+onward
+onyxes
+oocyst
+oocyte
+oodles
+oogamy
+oogeny
+oohing
+oolite
+oolith
+oology
+oolong
+oomiac
+oomiak
+oompah
+oomphs
+oorali
+ootids
+oozier
+oozily
+oozing
+opaque
+opened
+opener
+openly
+operas
+operon
+ophite
+opiate
+opined
+opines
+opioid
+opiums
+oppose
+oppugn
+opsins
+optics
+optima
+optime
+opting
+option
+opuses
+orache
+oracle
+orally
+orange
+orangs
+orangy
+orated
+orates
+orator
+orbier
+orbing
+orbits
+orcein
+orchid
+orchil
+orchis
+orcins
+ordain
+ordeal
+orders
+ordure
+oreads
+oreide
+orfray
+organs
+orgone
+oribis
+oriels
+orient
+origan
+origin
+oriole
+orisha
+orison
+orlons
+orlops
+ormers
+ormolu
+ornate
+ornery
+oroide
+orphan
+orphic
+orpine
+orpins
+orrery
+orrice
+oryxes
+oscine
+oscula
+oscule
+osetra
+osiers
+osmics
+osmium
+osmole
+osmols
+osmose
+osmous
+osmund
+osprey
+ossein
+ossify
+osteal
+ostium
+ostler
+ostomy
+otalgy
+others
+otiose
+otitic
+otitis
+ottars
+ottava
+otters
+ouched
+ouches
+oughts
+ounces
+ouphes
+ourang
+ourari
+ourebi
+ousels
+ousted
+ouster
+outact
+outadd
+outage
+outask
+outate
+outbeg
+outbid
+outbox
+outbuy
+outbye
+outcry
+outdid
+outeat
+outers
+outfit
+outfly
+outfox
+outgas
+outgun
+outhit
+outing
+outjut
+outlaw
+outlay
+outled
+outlet
+outlie
+outman
+output
+outran
+outrig
+outrow
+outrun
+outsat
+outsaw
+outsay
+outsee
+outset
+outsin
+outsit
+outvie
+outwar
+outwit
+ouzels
+ovally
+overdo
+overed
+overly
+ovibos
+ovines
+ovisac
+ovoids
+ovolos
+ovonic
+ovular
+ovules
+owlets
+owlish
+owners
+owning
+oxalic
+oxalis
+oxbows
+oxcart
+oxeyes
+oxford
+oxides
+oxidic
+oximes
+oxlike
+oxlips
+oxtail
+oxters
+oxygen
+oyezes
+oyster
+ozalid
+ozones
+ozonic
+pablum
+pacers
+pachas
+pacier
+pacify
+pacing
+packed
+packer
+packet
+packly
+padauk
+padded
+padder
+paddle
+padles
+padnag
+padouk
+padres
+paeans
+paella
+paeons
+paesan
+pagans
+pagers
+paging
+pagoda
+pagods
+paiked
+painch
+pained
+paints
+painty
+paired
+paisan
+paisas
+pajama
+pakeha
+pakora
+palace
+palais
+palapa
+palate
+paleae
+paleal
+palely
+palest
+palets
+palier
+paling
+palish
+palled
+pallet
+pallia
+pallid
+pallor
+palmar
+palmed
+palmer
+palpal
+palped
+palpus
+palter
+paltry
+pampas
+pamper
+panada
+panama
+pandas
+pander
+pandit
+panels
+panfry
+panful
+pangas
+panged
+pangen
+panics
+panier
+panini
+panino
+panned
+panner
+pannes
+panted
+pantie
+pantos
+pantry
+panzer
+papacy
+papain
+papaws
+papaya
+papers
+papery
+papism
+papist
+pappus
+papula
+papule
+papyri
+parade
+paramo
+parang
+paraph
+parcel
+pardah
+pardee
+pardie
+pardon
+parent
+pareos
+parers
+pareus
+pareve
+parged
+parges
+parget
+pargos
+pariah
+parian
+paries
+paring
+parish
+parity
+parkas
+parked
+parker
+parlay
+parled
+parles
+parley
+parlor
+parody
+parole
+parols
+parous
+parral
+parred
+parrel
+parrot
+parsec
+parsed
+parser
+parses
+parson
+partan
+parted
+partly
+parton
+parura
+parure
+parvis
+parvos
+pascal
+paseos
+pashas
+pashed
+pashes
+pastas
+pasted
+pastel
+paster
+pastes
+pastie
+pastil
+pastis
+pastor
+pastry
+pataca
+patchy
+patens
+patent
+paters
+pathos
+patina
+patine
+patins
+patios
+patois
+patrol
+patron
+patted
+pattee
+patten
+patter
+pattie
+patzer
+paulin
+paunch
+pauper
+pausal
+paused
+pauser
+pauses
+pavane
+pavans
+paveed
+pavers
+paving
+pavins
+pavior
+pavise
+pawers
+pawing
+pawned
+pawnee
+pawner
+pawnor
+pawpaw
+paxwax
+payday
+payees
+payers
+paying
+paynim
+payoff
+payola
+payors
+payout
+pazazz
+peaced
+peaces
+peachy
+peages
+peahen
+peaked
+pealed
+peanut
+pearls
+pearly
+peasen
+peases
+peavey
+pebble
+pebbly
+pecans
+pechan
+peched
+pecked
+pecten
+pectic
+pectin
+pedalo
+pedals
+pedant
+pedate
+peddle
+pedlar
+pedler
+pedros
+peeing
+peeked
+peeled
+peeler
+peened
+peered
+peerie
+pegged
+peined
+peised
+peises
+pekans
+pekins
+pekoes
+pelage
+pelite
+pellet
+pelmet
+pelota
+pelted
+pelter
+peltry
+pelves
+pelvic
+pelvis
+penang
+pencel
+pencil
+pended
+pengos
+penman
+penmen
+pennae
+penned
+penner
+pennon
+pensee
+pensil
+pentad
+pentyl
+penult
+penury
+peones
+people
+pepino
+peplos
+peplum
+peplus
+pepped
+pepper
+pepsin
+peptic
+peptid
+perdie
+perdue
+perdus
+pereia
+pereon
+perils
+period
+perish
+periti
+perked
+permed
+permit
+pernio
+pernod
+peroxy
+perron
+perses
+person
+perter
+pertly
+peruke
+peruse
+pesade
+peseta
+pesewa
+pester
+pestle
+pestos
+petals
+petard
+peters
+petite
+petnap
+petrel
+petrol
+petsai
+petted
+petter
+pettle
+pewees
+pewits
+pewter
+phages
+pharos
+phased
+phases
+phasic
+phasis
+phatic
+phenix
+phenol
+phenom
+phenyl
+phials
+phizes
+phlegm
+phloem
+phobia
+phobic
+phoebe
+phonal
+phoned
+phones
+phoney
+phonic
+phonon
+phonos
+phooey
+photic
+photog
+photon
+photos
+phrase
+phreak
+phylae
+phylar
+phylic
+phyllo
+phylon
+phylum
+physed
+physes
+physic
+physis
+phytin
+phytol
+phyton
+piaffe
+pianic
+pianos
+piazza
+piazze
+pibals
+picara
+picaro
+pickax
+picked
+picker
+picket
+pickle
+pickup
+picnic
+picots
+picric
+piculs
+piddle
+piddly
+pidgin
+pieced
+piecer
+pieces
+pieing
+pierce
+pietas
+piffle
+pigeon
+pigged
+piggie
+piggin
+piglet
+pignus
+pignut
+pigout
+pigpen
+pigsty
+pikake
+pikers
+piking
+pilaff
+pilafs
+pilaus
+pilaws
+pileum
+pileup
+pileus
+pilfer
+piling
+pillar
+pilled
+pillow
+pilose
+pilots
+pilous
+pilule
+pimped
+pimple
+pimply
+pinang
+pinata
+pincer
+pinder
+pineal
+pinene
+pinery
+pineta
+pinged
+pinger
+pingos
+pinier
+pining
+pinion
+pinite
+pinked
+pinken
+pinker
+pinkey
+pinkie
+pinkly
+pinkos
+pinnae
+pinnal
+pinnas
+pinned
+pinner
+pinole
+pinons
+pinots
+pintas
+pintle
+pintos
+pinups
+pinyin
+pinyon
+piolet
+pionic
+pipage
+pipals
+pipers
+pipets
+pipier
+piping
+pipits
+pipkin
+pipped
+pippin
+piqued
+piques
+piquet
+piracy
+pirana
+pirate
+piraya
+pirogi
+piscos
+pistil
+pistol
+piston
+pistou
+pitaya
+pitchy
+pithed
+pitied
+pitier
+pities
+pitman
+pitmen
+pitons
+pitsaw
+pittas
+pitted
+pivots
+pixels
+pixies
+pizazz
+pizzas
+pizzaz
+pizzle
+placed
+placer
+places
+placet
+placid
+placks
+plagal
+plages
+plague
+plaguy
+plaice
+plaids
+plains
+plaint
+plaits
+planar
+planch
+planed
+planer
+planes
+planet
+planks
+plants
+plaque
+plashy
+plasma
+plasms
+platan
+plated
+platen
+plater
+plates
+platys
+playas
+played
+player
+plazas
+pleach
+pleads
+please
+pleats
+plebes
+pledge
+pleiad
+plench
+plenty
+plenum
+pleons
+pleura
+plexal
+plexes
+plexor
+plexus
+pliant
+plicae
+plical
+pliers
+plight
+plinks
+plinth
+plisky
+plisse
+ploidy
+plonks
+plotty
+plough
+plover
+plowed
+plower
+ployed
+plucks
+plucky
+plumbs
+plumed
+plumes
+plummy
+plumps
+plunge
+plunks
+plunky
+plural
+pluses
+plushy
+plutei
+pluton
+plyers
+plying
+pneuma
+poachy
+poboys
+pocked
+pocket
+podded
+podite
+podium
+podsol
+podzol
+poetic
+poetry
+pogeys
+pogies
+pogrom
+poilus
+poinds
+pointe
+points
+pointy
+poised
+poiser
+poises
+poisha
+poison
+pokers
+pokeys
+pokier
+pokies
+pokily
+poking
+polars
+polder
+poleax
+poleis
+polers
+poleyn
+police
+policy
+polies
+poling
+polios
+polish
+polite
+polity
+polkas
+polled
+pollee
+pollen
+poller
+pollex
+polyol
+polypi
+polyps
+pomace
+pomade
+pomelo
+pommee
+pommel
+pommie
+pompom
+pompon
+ponced
+ponces
+poncho
+ponded
+ponder
+ponent
+ponged
+pongee
+pongid
+ponied
+ponies
+pontes
+pontil
+ponton
+popery
+popgun
+popish
+poplar
+poplin
+poppas
+popped
+popper
+poppet
+popple
+popsie
+poring
+porism
+porked
+porker
+pornos
+porose
+porous
+portal
+ported
+porter
+portly
+posada
+posers
+poseur
+posher
+poshly
+posies
+posing
+posits
+posole
+posses
+posset
+possum
+postal
+posted
+poster
+postie
+postin
+postop
+potage
+potash
+potato
+potboy
+poteen
+potent
+potful
+pother
+pothos
+potion
+potman
+potmen
+potpie
+potsie
+potted
+potter
+pottle
+pottos
+potzer
+pouchy
+poufed
+pouffe
+pouffs
+pouffy
+poults
+pounce
+pounds
+poured
+pourer
+pouted
+pouter
+powder
+powers
+powter
+powwow
+poxier
+poxing
+poyous
+pozole
+praams
+prahus
+praise
+prajna
+prance
+prangs
+pranks
+prases
+prated
+prater
+prates
+prawns
+praxes
+praxis
+prayed
+prayer
+preach
+preact
+preamp
+prearm
+prebid
+prebuy
+precis
+precut
+predry
+preens
+prefab
+prefer
+prefix
+prelaw
+prelim
+preman
+premed
+premen
+premie
+premix
+preops
+prepay
+preppy
+preset
+presto
+prests
+pretax
+pretor
+pretty
+prevue
+prewar
+prexes
+preyed
+preyer
+prezes
+priapi
+priced
+pricer
+prices
+pricey
+prided
+prides
+priers
+priest
+prills
+primal
+primas
+primed
+primer
+primes
+primly
+primos
+primps
+primus
+prince
+prinks
+prints
+prions
+priors
+priory
+prised
+prises
+prisms
+prison
+prissy
+privet
+prized
+prizer
+prizes
+probed
+prober
+probes
+probit
+proems
+profit
+progun
+projet
+prolan
+proleg
+proles
+prolix
+prolog
+promos
+prompt
+prongs
+pronto
+proofs
+propel
+proper
+propyl
+prosed
+proser
+proses
+prosit
+prosos
+protea
+protei
+proton
+protyl
+proved
+proven
+prover
+proves
+prowar
+prower
+prowls
+prudes
+pruned
+pruner
+prunes
+prunus
+prutah
+prutot
+pryers
+prying
+psalms
+pseudo
+pseuds
+pshaws
+psocid
+psyche
+psycho
+psychs
+psylla
+psyops
+psywar
+pterin
+ptisan
+ptooey
+ptoses
+ptosis
+ptotic
+public
+pucker
+puddle
+puddly
+pueblo
+puffed
+puffer
+puffin
+pugged
+puggry
+pugree
+puisne
+pujahs
+puking
+pulers
+puling
+pulled
+puller
+pullet
+pulley
+pullup
+pulpal
+pulped
+pulper
+pulpit
+pulque
+pulsar
+pulsed
+pulser
+pulses
+pumelo
+pumice
+pummel
+pumped
+pumper
+punchy
+pundit
+pungle
+punier
+punily
+punish
+punjis
+punkah
+punkas
+punker
+punkey
+punkie
+punkin
+punned
+punner
+punnet
+punted
+punter
+puntos
+pupate
+pupils
+pupped
+puppet
+purana
+purdah
+purdas
+pureed
+purees
+purely
+purest
+purfle
+purged
+purger
+purges
+purify
+purine
+purins
+purism
+purist
+purity
+purled
+purlin
+purple
+purply
+purred
+pursed
+purser
+purses
+pursue
+purvey
+pushed
+pushes
+pushup
+pusley
+putlog
+putoff
+putons
+putout
+putrid
+putsch
+putted
+puttee
+putter
+puttie
+putzed
+putzes
+puzzle
+pyemia
+pyemic
+pyjama
+pyknic
+pylons
+pylori
+pyoses
+pyosis
+pyrans
+pyrene
+pyrite
+pyrola
+pyrone
+pyrope
+pyrrol
+python
+pyuria
+pyxies
+qabala
+qanats
+qindar
+qintar
+qiviut
+quacks
+quacky
+quaere
+quaffs
+quagga
+quaggy
+quahog
+quaich
+quaigh
+quails
+quaint
+quaked
+quaker
+quakes
+qualia
+qualms
+qualmy
+quango
+quanta
+quants
+quarks
+quarry
+quarte
+quarto
+quarts
+quartz
+quasar
+quatre
+quaver
+qubits
+qubyte
+queans
+queasy
+queazy
+queens
+queers
+quelea
+quells
+quench
+querns
+quests
+queued
+queuer
+queues
+quezal
+quiche
+quicks
+quiets
+quiffs
+quills
+quilts
+quince
+quinic
+quinin
+quinoa
+quinol
+quinsy
+quinta
+quinte
+quints
+quippu
+quippy
+quipus
+quired
+quires
+quirks
+quirky
+quirts
+quitch
+quiver
+quohog
+quoins
+quoits
+quokka
+quolls
+quorum
+quotas
+quoted
+quoter
+quotes
+quotha
+qurush
+qwerty
+rabato
+rabats
+rabbet
+rabbin
+rabbis
+rabbit
+rabble
+rabies
+raceme
+racers
+rachet
+rachis
+racier
+racily
+racing
+racked
+racker
+racket
+rackle
+racons
+racoon
+radars
+radded
+raddle
+radial
+radian
+radios
+radish
+radium
+radius
+radome
+radons
+radula
+raffia
+raffle
+rafted
+rafter
+ragbag
+ragees
+ragged
+raggee
+raggle
+raging
+raglan
+ragman
+ragmen
+ragout
+ragtag
+ragtop
+raided
+raider
+railed
+railer
+rained
+raised
+raiser
+raises
+raisin
+raitas
+rajahs
+rakees
+rakers
+raking
+rakish
+rallye
+ralphs
+ramada
+ramate
+rambla
+ramble
+ramees
+ramets
+ramies
+ramify
+ramjet
+rammed
+rammer
+ramona
+ramose
+ramous
+ramped
+ramrod
+ramson
+ramtil
+rances
+rancho
+rancid
+rancor
+randan
+random
+ranees
+ranged
+ranger
+ranges
+ranids
+ranked
+ranker
+rankle
+rankly
+ransom
+ranted
+ranter
+ranula
+rarefy
+rarely
+rarest
+rarify
+raring
+rarity
+rascal
+rasers
+rasher
+rashes
+rashly
+rasing
+rasped
+rasper
+rassle
+raster
+rasure
+ratals
+ratans
+ratany
+ratbag
+ratels
+raters
+rather
+ratify
+ratine
+rating
+ration
+ratios
+ratite
+ratlin
+ratoon
+rattan
+ratted
+ratten
+ratter
+rattle
+rattly
+ratton
+raunch
+ravage
+ravels
+ravens
+ravers
+ravine
+raving
+ravins
+ravish
+rawest
+rawins
+rawish
+raxing
+rayahs
+raying
+rayons
+razeed
+razees
+razers
+razing
+razors
+razzed
+razzes
+reacts
+readds
+reader
+reagin
+realer
+reales
+realia
+really
+realms
+realty
+reamed
+reamer
+reaped
+reaper
+reared
+rearer
+rearms
+reason
+reatas
+reaved
+reaver
+reaves
+reavow
+rebait
+rebars
+rebate
+rebato
+rebbes
+rebeck
+rebecs
+rebels
+rebids
+rebill
+rebind
+rebody
+reboil
+rebook
+reboot
+rebops
+rebore
+reborn
+rebozo
+rebred
+rebuff
+rebuke
+rebury
+rebuts
+rebuys
+recall
+recane
+recant
+recaps
+recast
+recces
+recede
+recent
+recept
+recess
+rechew
+recipe
+recite
+recits
+recked
+reckon
+reclad
+recoal
+recoat
+recode
+recoil
+recoin
+recomb
+recons
+recook
+recopy
+record
+recork
+recoup
+rectal
+rector
+rectos
+recurs
+recuse
+recuts
+redact
+redans
+redate
+redbay
+redbud
+redbug
+redcap
+redded
+redden
+redder
+reddle
+redear
+redeem
+redefy
+redeny
+redeye
+redfin
+rediae
+redial
+redias
+reding
+redips
+redipt
+redleg
+redock
+redoes
+redone
+redons
+redout
+redowa
+redraw
+redrew
+redtop
+redubs
+reduce
+redyed
+redyes
+reearn
+reecho
+reechy
+reeded
+reedit
+reefed
+reeled
+reeler
+reemit
+reests
+reeved
+reeves
+reface
+refall
+refect
+refeed
+refeel
+refell
+refels
+refelt
+refers
+reffed
+refile
+refill
+refilm
+refind
+refine
+refire
+refits
+reflag
+reflet
+reflew
+reflex
+reflow
+reflux
+refold
+reform
+refuel
+refuge
+refund
+refuse
+refute
+regain
+regale
+regard
+regave
+regear
+regent
+reggae
+regild
+regilt
+regime
+regina
+region
+regius
+regive
+reglet
+reglow
+reglue
+regnal
+regnum
+regret
+regrew
+regrow
+reguli
+rehabs
+rehang
+rehash
+rehear
+reheat
+reheel
+rehems
+rehire
+rehung
+reigns
+reined
+reinks
+reived
+reiver
+reives
+reject
+rejigs
+rejoin
+rekeys
+reknit
+reknot
+relace
+relaid
+reland
+relate
+relays
+relend
+relent
+relets
+releve
+relics
+relict
+relied
+relief
+relier
+relies
+reline
+relink
+relish
+relist
+relive
+reload
+reloan
+relock
+relook
+reluct
+relume
+remade
+remail
+remain
+remake
+remand
+remans
+remaps
+remark
+remate
+remedy
+remeet
+remelt
+remend
+remind
+remint
+remise
+remiss
+remits
+remixt
+remold
+remora
+remote
+remove
+remuda
+renail
+rename
+rended
+render
+renege
+renest
+renews
+renigs
+renins
+rennet
+rennin
+renown
+rental
+rented
+renter
+rentes
+renvoi
+reoils
+reopen
+repack
+repaid
+repair
+repand
+repark
+repass
+repast
+repave
+repays
+repeal
+repeat
+repegs
+repels
+repent
+reperk
+repine
+repins
+replan
+replay
+repled
+replot
+replow
+repoll
+report
+repose
+repots
+repour
+repped
+repros
+repugn
+repump
+repute
+requin
+rerack
+reread
+rerent
+rerigs
+rerise
+reroll
+reroof
+rerose
+reruns
+resaid
+resail
+resale
+resawn
+resaws
+resays
+rescue
+reseal
+reseat
+reseau
+resect
+reseda
+reseed
+reseek
+reseen
+resees
+resell
+resend
+resent
+resets
+resewn
+resews
+reshes
+reship
+reshod
+reshoe
+reshot
+reshow
+reside
+resids
+resift
+resign
+resile
+resins
+resiny
+resist
+resite
+resits
+resize
+resoak
+resods
+resold
+resole
+resorb
+resort
+resown
+resows
+respot
+rested
+rester
+result
+resume
+retack
+retags
+retail
+retain
+retake
+retape
+reteam
+retear
+retell
+retems
+retene
+retest
+retial
+retied
+reties
+retile
+retime
+retina
+retine
+retint
+retire
+retold
+retook
+retool
+retore
+retorn
+retort
+retral
+retrim
+retros
+retted
+retune
+return
+retuse
+retype
+reused
+reuses
+revamp
+reveal
+revels
+reverb
+revere
+revers
+revert
+revery
+revest
+revets
+review
+revile
+revise
+revive
+revoke
+revolt
+revote
+revues
+revved
+rewake
+reward
+rewarm
+rewash
+rewear
+reweds
+reweld
+rewets
+rewind
+rewins
+rewire
+rewoke
+reword
+rewore
+rework
+reworn
+rewove
+rewrap
+rexine
+rezero
+rezone
+rhaphe
+rhebok
+rhemes
+rhesus
+rhetor
+rheums
+rheumy
+rhinal
+rhinos
+rhodic
+rhombi
+rhombs
+rhotic
+rhumba
+rhumbs
+rhuses
+rhymed
+rhymer
+rhymes
+rhythm
+rhyton
+rialto
+riatas
+ribald
+riband
+ribbed
+ribber
+ribbon
+ribier
+riblet
+ribose
+ricers
+richen
+richer
+riches
+richly
+ricing
+ricins
+ricked
+rickey
+ricrac
+rictal
+rictus
+ridded
+ridden
+ridder
+riddle
+rident
+riders
+ridged
+ridgel
+ridges
+ridgil
+riding
+ridley
+riever
+rifely
+rifest
+riffed
+riffle
+rifled
+rifler
+rifles
+riflip
+rifted
+rigged
+rigger
+righto
+rights
+righty
+rigors
+rigour
+riling
+rilled
+rilles
+rillet
+rimers
+rimier
+rimose
+rimous
+rimple
+rinded
+ringed
+ringer
+rinsed
+rinser
+rinses
+riojas
+rioted
+rioter
+ripely
+ripens
+ripest
+riping
+ripoff
+ripost
+ripped
+ripper
+ripple
+ripply
+riprap
+ripsaw
+risers
+rishis
+rising
+risked
+risker
+risque
+ristra
+ritard
+ritter
+ritual
+ritzes
+rivage
+rivals
+rivers
+rivets
+riving
+riyals
+roadeo
+roadie
+roamed
+roamer
+roared
+roarer
+roasts
+robalo
+roband
+robbed
+robber
+robbin
+robing
+robins
+robles
+robots
+robust
+rochet
+rocked
+rocker
+rocket
+rococo
+rodded
+rodent
+rodeos
+rodman
+rodmen
+rogers
+rogued
+rogues
+roiled
+rolfed
+rolfer
+rolled
+roller
+romaji
+romano
+romans
+romeos
+rondel
+rondos
+ronion
+ronnel
+ronyon
+roofed
+roofer
+roofie
+rooked
+rookie
+roomed
+roomer
+roomie
+roosed
+rooser
+rooses
+roosts
+rooted
+rooter
+rootle
+ropers
+ropery
+ropier
+ropily
+roping
+roques
+roquet
+rosary
+roscoe
+rosery
+rosets
+roshis
+rosier
+rosily
+rosing
+rosins
+rosiny
+roster
+rostra
+rotary
+rotate
+rotche
+rotgut
+rotors
+rotund
+rouble
+rouche
+rouens
+rouged
+rouges
+roughs
+roughy
+rounds
+rouped
+roupet
+roused
+rouser
+rouses
+rousts
+routed
+router
+routes
+rouths
+rovers
+roving
+rowans
+rowels
+rowens
+rowers
+rowing
+rowths
+royals
+rozzer
+ruanas
+rubace
+rubati
+rubato
+rubbed
+rubber
+rubble
+rubbly
+rubels
+rubied
+rubier
+rubies
+rubigo
+rubles
+ruboff
+rubout
+rubric
+ruched
+ruches
+rucked
+ruckle
+ruckus
+rudder
+ruddle
+rudely
+rudery
+rudest
+rueful
+ruffed
+ruffes
+ruffle
+ruffly
+rufous
+rugate
+rugged
+rugger
+rugola
+rugosa
+rugose
+rugous
+ruined
+ruiner
+rulers
+rulier
+ruling
+rumaki
+rumbas
+rumble
+rumbly
+rumens
+rumina
+rummer
+rumors
+rumour
+rumple
+rumply
+rumpus
+rundle
+runkle
+runlet
+runnel
+runner
+runoff
+runout
+runway
+rupees
+rupiah
+rurban
+rushed
+rushee
+rusher
+rushes
+rusine
+russet
+rusted
+rustic
+rustle
+rutile
+rutins
+rutted
+ryking
+ryokan
+sabals
+sabbat
+sabbed
+sabers
+sabine
+sabins
+sabirs
+sables
+sabots
+sabras
+sabred
+sabres
+sacbut
+sachem
+sachet
+sacked
+sacker
+sacque
+sacral
+sacred
+sacrum
+sadden
+sadder
+saddhu
+saddle
+sadhes
+sadhus
+safari
+safely
+safest
+safety
+safrol
+sagbut
+sagely
+sagest
+saggar
+sagged
+sagger
+sagier
+sahibs
+saices
+saigas
+sailed
+sailer
+sailor
+saimin
+sained
+saints
+saithe
+saiyid
+sajous
+sakers
+salaam
+salads
+salals
+salami
+salary
+saleps
+salify
+salina
+saline
+saliva
+sallet
+sallow
+salmis
+salmon
+salols
+salons
+saloon
+saloop
+salpae
+salpas
+salpid
+salsas
+salted
+salter
+saltie
+saluki
+salute
+salved
+salver
+salves
+salvia
+salvor
+salvos
+samara
+sambal
+sambar
+sambas
+sambos
+sambur
+samech
+samekh
+sameks
+samiel
+samite
+samlet
+samosa
+sampan
+sample
+samshu
+sancta
+sandal
+sanded
+sander
+sandhi
+sanely
+sanest
+sangar
+sangas
+sanger
+sanghs
+sanies
+saning
+sanity
+sanjak
+sannop
+sannup
+sansar
+sansei
+santir
+santol
+santos
+santur
+sapors
+sapota
+sapote
+sapour
+sapped
+sapper
+sarans
+sarape
+sardar
+sarees
+sarges
+sargos
+sarins
+sarode
+sarods
+sarong
+sarsar
+sarsen
+sartor
+sashay
+sashed
+sashes
+sasins
+sassed
+sasses
+satang
+satara
+satays
+sateen
+sating
+satins
+satiny
+satire
+satori
+satrap
+satyrs
+sauced
+saucer
+sauces
+sauchs
+sauger
+saughs
+saughy
+saults
+saunas
+saurel
+sauted
+sautes
+savage
+savant
+savate
+savers
+savine
+saving
+savins
+savior
+savors
+savory
+savour
+savoys
+sawers
+sawfly
+sawing
+sawlog
+sawney
+sawyer
+saxony
+sayeds
+sayers
+sayest
+sayids
+saying
+sayyid
+scabby
+scalar
+scalds
+scaled
+scaler
+scales
+scalls
+scalps
+scampi
+scamps
+scants
+scanty
+scaped
+scapes
+scarab
+scarce
+scared
+scarer
+scares
+scarey
+scarfs
+scarph
+scarps
+scarry
+scarts
+scathe
+scatts
+scatty
+scaups
+scaurs
+scenas
+scends
+scenes
+scenic
+scents
+schavs
+schema
+scheme
+schism
+schist
+schlep
+schlub
+schmoe
+schmos
+schnoz
+school
+schorl
+schrik
+schrod
+schtik
+schuit
+schuln
+schuls
+schuss
+schwas
+scilla
+scions
+sclaff
+sclera
+scoffs
+scolds
+scolex
+sconce
+scones
+scooch
+scoops
+scoots
+scoped
+scopes
+scorch
+scored
+scorer
+scores
+scoria
+scorns
+scotch
+scoter
+scotia
+scours
+scouse
+scouth
+scouts
+scowed
+scowls
+scrags
+scrams
+scrape
+scraps
+scrawl
+screak
+scream
+screed
+screen
+screes
+screws
+screwy
+scribe
+scried
+scries
+scrimp
+scrims
+scrips
+script
+scrive
+scrods
+scroll
+scroop
+scrota
+scrubs
+scruff
+scrums
+scubas
+scuffs
+sculch
+sculks
+sculls
+sculps
+sculpt
+scurfs
+scurfy
+scurry
+scurvy
+scutch
+scutes
+scutum
+scyphi
+scythe
+seabag
+seabed
+seadog
+sealed
+sealer
+seaman
+seamed
+seamer
+seance
+search
+seared
+searer
+season
+seated
+seater
+seawan
+seaway
+sebums
+secant
+seccos
+secede
+secern
+second
+secpar
+secret
+sector
+secund
+secure
+sedans
+sedate
+seders
+sedges
+sedile
+seduce
+sedums
+seeded
+seeder
+seeing
+seeker
+seeled
+seemed
+seemer
+seemly
+seeped
+seesaw
+seethe
+seggar
+segnos
+segued
+segues
+seiche
+seidel
+seined
+seiner
+seines
+seised
+seiser
+seises
+seisin
+seisms
+seisor
+seitan
+seized
+seizer
+seizes
+seizin
+seizor
+sejant
+selahs
+seldom
+select
+selfed
+selkie
+seller
+selles
+selsyn
+selvas
+selves
+sememe
+semple
+sempre
+senary
+senate
+sendal
+sended
+sender
+sendup
+seneca
+senega
+senhor
+senile
+senior
+seniti
+sennas
+sennet
+sennit
+senora
+senors
+senryu
+sensed
+sensei
+senses
+sensor
+sensum
+sentry
+sepals
+sepias
+sepoys
+sepses
+sepsis
+septal
+septet
+septic
+septum
+sequel
+sequin
+seracs
+serail
+serais
+serape
+seraph
+serdab
+serein
+serene
+serest
+serged
+serger
+serges
+serial
+series
+serifs
+serine
+sering
+serins
+sermon
+serosa
+serous
+serows
+serums
+serval
+served
+server
+serves
+servos
+sesame
+sestet
+setoff
+setons
+setose
+setous
+setout
+settee
+setter
+settle
+setups
+sevens
+severe
+severs
+sewage
+sewans
+sewars
+sewers
+sewing
+shabby
+shacko
+shacks
+shaded
+shader
+shades
+shadow
+shaduf
+shafts
+shaggy
+shaird
+shairn
+shaken
+shaker
+shakes
+shakos
+shaled
+shales
+shaley
+shalom
+shaman
+shamas
+shamed
+shames
+shammy
+shamos
+shamoy
+shamus
+shandy
+shanks
+shanny
+shanti
+shanty
+shaped
+shapen
+shaper
+shapes
+shards
+shared
+sharer
+shares
+sharia
+sharif
+sharks
+sharns
+sharny
+sharps
+sharpy
+shaugh
+shauls
+shaved
+shaven
+shaver
+shaves
+shavie
+shawed
+shawls
+shawms
+shazam
+sheafs
+sheals
+shears
+sheath
+sheave
+sheens
+sheeny
+sheers
+sheesh
+sheets
+sheeve
+sheikh
+sheiks
+sheila
+shekel
+shells
+shelly
+shelta
+shelty
+shelve
+shelvy
+shends
+sheols
+sheqel
+sherds
+sherif
+sherpa
+sherry
+sheuch
+sheugh
+shewed
+shewer
+shibah
+shield
+shiels
+shiers
+shiest
+shifts
+shifty
+shikar
+shiksa
+shikse
+shills
+shimmy
+shindy
+shined
+shiner
+shines
+shinny
+shires
+shirks
+shirrs
+shirts
+shirty
+shists
+shivah
+shivas
+shiver
+shives
+shlepp
+shleps
+shlock
+shlubs
+shlump
+shmear
+shmoes
+shmuck
+shnaps
+shnook
+shoals
+shoaly
+shoats
+shocks
+shoddy
+shoers
+shofar
+shogis
+shogun
+shojis
+sholom
+shooed
+shooks
+shools
+shoots
+shoppe
+shoran
+shored
+shores
+shorls
+shorts
+shorty
+shotes
+shotts
+should
+shouts
+shoved
+shovel
+shover
+shoves
+showed
+shower
+shoyus
+shrank
+shreds
+shrewd
+shrews
+shriek
+shrift
+shrike
+shrill
+shrimp
+shrine
+shrink
+shrive
+shroff
+shroud
+shrove
+shrubs
+shrugs
+shrunk
+shtetl
+shtick
+shtiks
+shucks
+shunts
+shuted
+shutes
+shyers
+shyest
+shying
+sialic
+sialid
+sibyls
+siccan
+sicced
+sicked
+sickee
+sicken
+sicker
+sickie
+sickle
+sickly
+sickos
+siddur
+siding
+sidled
+sidler
+sidles
+sieged
+sieges
+sienna
+sierra
+siesta
+sieurs
+sieved
+sieves
+sifaka
+sifted
+sifter
+sighed
+sigher
+sights
+sigils
+sigloi
+siglos
+siglum
+sigmas
+signal
+signed
+signee
+signer
+signet
+signor
+silage
+silane
+sileni
+silent
+silica
+silked
+silken
+silkie
+siller
+siloed
+silted
+silvae
+silvan
+silvas
+silver
+silvex
+simars
+simian
+simile
+simlin
+simmer
+simnel
+simony
+simoom
+simoon
+simper
+simple
+simply
+sinews
+sinewy
+sinful
+singed
+singer
+singes
+single
+singly
+sinker
+sinned
+sinner
+sinter
+siphon
+siping
+sipped
+sipper
+sippet
+sirdar
+sirees
+sirens
+siring
+sirrah
+sirras
+sirree
+sirups
+sirupy
+sisals
+siskin
+sisses
+sister
+sistra
+sitars
+sitcom
+siting
+sitten
+sitter
+situps
+sivers
+sixmos
+sixtes
+sixths
+sizars
+sizers
+sizier
+sizing
+sizzle
+skalds
+skated
+skater
+skates
+skatol
+skeane
+skeans
+skeens
+skeets
+skeigh
+skeins
+skells
+skelms
+skelps
+skenes
+skerry
+sketch
+skewed
+skewer
+skibob
+skiddy
+skidoo
+skiers
+skiffs
+skiing
+skills
+skimos
+skimps
+skimpy
+skinks
+skinny
+skirls
+skirrs
+skirts
+skited
+skites
+skived
+skiver
+skives
+skivvy
+sklent
+skoals
+skorts
+skulks
+skulls
+skunks
+skunky
+skybox
+skycap
+skying
+skylit
+skyman
+skymen
+skyway
+slacks
+slaggy
+slaked
+slaker
+slakes
+slalom
+slangs
+slangy
+slants
+slanty
+slatch
+slated
+slater
+slates
+slatey
+slaved
+slaver
+slaves
+slavey
+slayed
+slayer
+sleave
+sleaze
+sleazo
+sleazy
+sledge
+sleeks
+sleeky
+sleeps
+sleepy
+sleets
+sleety
+sleeve
+sleigh
+sleuth
+slewed
+sliced
+slicer
+slices
+slicks
+slider
+slides
+sliest
+slieve
+slight
+slimed
+slimes
+slimly
+slimsy
+slings
+slinks
+slinky
+sliped
+slipes
+slippy
+slipup
+slitty
+sliver
+slobby
+slogan
+sloids
+slojds
+sloops
+sloped
+sloper
+slopes
+sloppy
+sloshy
+sloths
+slouch
+slough
+sloven
+slowed
+slower
+slowly
+sloyds
+sludge
+sludgy
+sluffs
+sluice
+sluicy
+sluing
+slummy
+slumps
+slurbs
+slurps
+slurry
+slushy
+slyest
+slypes
+smacks
+smalls
+smalti
+smalto
+smalts
+smarms
+smarmy
+smarts
+smarty
+smazes
+smears
+smeary
+smeeks
+smegma
+smells
+smelly
+smelts
+smerks
+smidge
+smilax
+smiled
+smiler
+smiles
+smiley
+smirch
+smirks
+smirky
+smiter
+smites
+smiths
+smithy
+smocks
+smoggy
+smoked
+smoker
+smokes
+smokey
+smolts
+smooch
+smoosh
+smooth
+smudge
+smudgy
+smugly
+smutch
+snacks
+snafus
+snaggy
+snails
+snaked
+snakes
+snakey
+snappy
+snared
+snarer
+snares
+snarfs
+snarks
+snarky
+snarls
+snarly
+snatch
+snathe
+snaths
+snawed
+snazzy
+sneaks
+sneaky
+sneaps
+snecks
+sneers
+sneery
+sneesh
+sneeze
+sneezy
+snells
+snicks
+snider
+sniffs
+sniffy
+sniped
+sniper
+snipes
+snippy
+snitch
+snivel
+snobby
+snoods
+snooks
+snools
+snoops
+snoopy
+snoots
+snooty
+snooze
+snoozy
+snored
+snorer
+snores
+snorts
+snotty
+snouts
+snouty
+snowed
+snubby
+snuffs
+snuffy
+snugly
+soaked
+soaker
+soaped
+soaper
+soared
+soarer
+soaves
+sobbed
+sobber
+sobeit
+sobers
+sobful
+socage
+soccer
+social
+socked
+socket
+socles
+socman
+socmen
+sodded
+sodden
+sodium
+soever
+sofars
+soffit
+softas
+soften
+softer
+softie
+softly
+sogged
+soigne
+soiled
+soiree
+sokols
+solace
+soland
+solano
+solans
+solate
+soldan
+solder
+solely
+solemn
+soleus
+solgel
+solidi
+solids
+soling
+solion
+soloed
+solons
+solums
+solute
+solved
+solver
+solves
+somans
+somata
+somber
+sombre
+somite
+somoni
+sonant
+sonars
+sonata
+sonder
+sondes
+sonics
+sonnet
+sonsie
+sooner
+sooted
+soothe
+sooths
+sopite
+sopors
+sopped
+sorbed
+sorbet
+sorbic
+sordid
+sordor
+sorels
+sorely
+sorest
+sorgho
+sorgos
+soring
+sorned
+sorner
+sorrel
+sorrow
+sorted
+sorter
+sortie
+sotols
+sotted
+souari
+soucar
+soudan
+soughs
+sought
+souled
+sounds
+souped
+source
+soured
+sourer
+sourly
+soused
+souses
+souter
+souths
+soviet
+sovran
+sowans
+sowars
+sowcar
+sowens
+sowers
+sowing
+sozine
+sozins
+spaced
+spacer
+spaces
+spacey
+spaded
+spader
+spades
+spadix
+spahee
+spahis
+spails
+spaits
+spales
+spalls
+spanks
+spared
+sparer
+spares
+sparge
+sparid
+sparks
+sparky
+sparry
+sparse
+spasms
+spates
+spathe
+spavie
+spavin
+spawns
+spayed
+speaks
+speans
+spears
+specie
+specks
+speech
+speedo
+speeds
+speedy
+speels
+speers
+speils
+speirs
+speise
+speiss
+spells
+spelts
+speltz
+spence
+spends
+spendy
+spense
+spewed
+spewer
+sphene
+sphere
+sphery
+sphinx
+sphynx
+spicae
+spicas
+spiced
+spicer
+spices
+spicey
+spicks
+spider
+spiels
+spiers
+spiffs
+spiffy
+spigot
+spiked
+spiker
+spikes
+spikey
+spiled
+spiles
+spills
+spilth
+spinal
+spined
+spinel
+spines
+spinet
+spinny
+spinor
+spinto
+spiral
+spirea
+spired
+spirem
+spires
+spirit
+spirts
+spital
+spited
+spites
+spivvy
+splake
+splash
+splats
+splays
+spleen
+splent
+splice
+spline
+splint
+splits
+splore
+splosh
+spodes
+spoils
+spoilt
+spoked
+spoken
+spokes
+sponge
+spongy
+spoofs
+spoofy
+spooks
+spooky
+spools
+spoons
+spoony
+spoors
+sporal
+spored
+spores
+sports
+sporty
+spotty
+spouse
+spouts
+sprags
+sprain
+sprang
+sprats
+sprawl
+sprays
+spread
+sprees
+sprent
+sprier
+sprigs
+spring
+sprint
+sprite
+sprits
+spritz
+sprout
+spruce
+sprucy
+sprues
+sprugs
+sprung
+spryer
+spryly
+spuing
+spumed
+spumes
+spurns
+spurry
+spying
+squabs
+squads
+squall
+squama
+square
+squark
+squash
+squats
+squawk
+squaws
+squeak
+squeal
+squegs
+squibs
+squids
+squill
+squint
+squire
+squirm
+squirt
+squish
+squush
+sradha
+stable
+stably
+stacks
+stacte
+stades
+stadia
+staffs
+staged
+stager
+stages
+stagey
+staggy
+staigs
+stains
+stairs
+staked
+stakes
+stalag
+staled
+staler
+stales
+stalks
+stalky
+stalls
+stamen
+stamps
+stance
+stanch
+stands
+staned
+stanes
+stangs
+stanks
+stanol
+stanza
+stapes
+staphs
+staple
+starch
+stared
+starer
+stares
+starry
+starts
+starve
+stases
+stasis
+statal
+stated
+stater
+states
+static
+statin
+stator
+statue
+status
+staved
+staves
+stayed
+stayer
+steads
+steady
+steaks
+steals
+steams
+steamy
+steeds
+steeks
+steels
+steely
+steeps
+steers
+steeve
+steins
+stelae
+stelai
+stelar
+steles
+stelic
+stella
+stemma
+stemmy
+stench
+stenos
+stents
+steppe
+stereo
+steres
+steric
+sterna
+sterns
+sterol
+stewed
+stichs
+sticks
+sticky
+stiffs
+stifle
+stigma
+stiles
+stills
+stilly
+stilts
+stimes
+stingo
+stings
+stints
+stiped
+stipel
+stipes
+stirks
+stirps
+stitch
+stithy
+stiver
+stoats
+stocks
+stocky
+stodge
+stodgy
+stogey
+stogie
+stoics
+stoked
+stoker
+stokes
+stoled
+stolen
+stoles
+stolid
+stolon
+stomal
+stomas
+stomps
+stoned
+stoner
+stones
+stoney
+stooge
+stooks
+stoops
+stoped
+stoper
+stopes
+storax
+stored
+storer
+stores
+storey
+storks
+storms
+stormy
+stotin
+stotts
+stound
+stoups
+stoure
+stours
+stoury
+stouts
+stover
+stoves
+stowed
+stowps
+strafe
+strain
+strait
+strake
+strand
+strang
+straps
+strass
+strata
+strath
+strati
+straws
+strawy
+strays
+streak
+stream
+streek
+streel
+street
+streps
+stress
+strewn
+strews
+striae
+strick
+strict
+stride
+strife
+strike
+string
+stripe
+strips
+stript
+stripy
+strive
+strobe
+strode
+stroke
+stroll
+stroma
+strong
+strook
+strops
+stroud
+strove
+strown
+strows
+stroys
+struck
+struma
+strums
+strung
+strunt
+struts
+stubby
+stucco
+studio
+studly
+stuffs
+stuffy
+stulls
+stumps
+stumpy
+stunts
+stupas
+stupes
+stupor
+sturdy
+sturts
+stying
+stylar
+styled
+styler
+styles
+stylet
+stylus
+stymie
+styrax
+suable
+suably
+suaver
+subahs
+subbed
+subdeb
+subdue
+subers
+subfix
+subgum
+subito
+sublet
+sublot
+submit
+subnet
+suborn
+subpar
+subsea
+subset
+subtle
+subtly
+suburb
+subway
+succah
+succor
+sucres
+sudary
+sudden
+sudors
+sudsed
+sudser
+sudses
+sueded
+suedes
+suffer
+suffix
+sugars
+sugary
+sughed
+suints
+suited
+suiter
+suites
+suitor
+sukkah
+sukkot
+sulcal
+sulcus
+suldan
+sulfas
+sulfid
+sulfur
+sulked
+sulker
+sullen
+sulpha
+sultan
+sultry
+sumach
+sumacs
+summae
+summas
+summed
+summer
+summit
+summon
+sunbow
+sundae
+sunder
+sundew
+sundog
+sundry
+sunken
+sunket
+sunlit
+sunnah
+sunnas
+sunned
+sunray
+sunset
+suntan
+sunups
+superb
+supers
+supine
+supped
+supper
+supple
+supply
+surahs
+surely
+surest
+surety
+surfed
+surfer
+surged
+surger
+surges
+surimi
+surras
+surrey
+surtax
+survey
+sushis
+suslik
+sussed
+susses
+sutler
+sutras
+suttas
+suttee
+suture
+svaraj
+svelte
+swabby
+swaged
+swager
+swages
+swails
+swains
+swales
+swamis
+swamps
+swampy
+swanks
+swanky
+swanny
+swaraj
+swards
+swarfs
+swarms
+swarth
+swarty
+swatch
+swathe
+swaths
+swayed
+swayer
+swears
+sweats
+sweaty
+swedes
+sweeny
+sweeps
+sweepy
+sweets
+swells
+swerve
+sweven
+swifts
+swills
+swimmy
+swinge
+swings
+swingy
+swinks
+swiped
+swipes
+swiple
+swirls
+swirly
+swishy
+switch
+swithe
+swived
+swivel
+swives
+swivet
+swoons
+swoony
+swoops
+swoopy
+swoosh
+swords
+swound
+swouns
+syboes
+sycees
+sylphs
+sylphy
+sylvae
+sylvan
+sylvas
+sylvin
+symbol
+synced
+synchs
+syncom
+syndet
+syndic
+syngas
+synods
+syntax
+synths
+synura
+sypher
+syphon
+syrens
+syrinx
+syrups
+syrupy
+sysops
+system
+syzygy
+tabard
+tabbed
+tabbis
+tabers
+tablas
+tabled
+tables
+tablet
+taboos
+tabors
+tabour
+tabued
+tabuli
+tabuns
+taches
+tacked
+tacker
+tacket
+tackey
+tackle
+tactic
+taenia
+taffia
+tafias
+tagged
+tagger
+tagrag
+tahini
+tahsil
+taigas
+tailed
+tailer
+taille
+tailor
+taints
+taipan
+takahe
+takers
+takeup
+taking
+takins
+talars
+talced
+talcky
+talcum
+talent
+talers
+talion
+talked
+talker
+talkie
+taller
+tallis
+tallit
+tallol
+tallow
+talons
+taluka
+taluks
+tamale
+tamals
+tamari
+tambac
+tambak
+tambur
+tamein
+tamely
+tamers
+tamest
+taming
+tammie
+tampan
+tamped
+tamper
+tampon
+tandem
+tanged
+tangle
+tangly
+tangos
+tanist
+tankas
+tanked
+tanker
+tanned
+tanner
+tannic
+tannin
+tannoy
+tanrec
+tantra
+tanuki
+tapalo
+tapers
+tapeta
+taping
+tapirs
+tapped
+tapper
+tappet
+tarama
+targes
+target
+tariff
+taring
+tarmac
+tarnal
+tarocs
+taroks
+tarots
+tarpan
+tarpon
+tarred
+tarres
+tarsal
+tarsia
+tarsus
+tartan
+tartar
+tarted
+tarter
+tartly
+tarzan
+tasked
+tassel
+tasses
+tasset
+tassie
+tasted
+taster
+tastes
+tatami
+tatars
+taters
+tatsoi
+tatted
+tatter
+tattie
+tattle
+tattoo
+taught
+taunts
+tauons
+taupes
+tauted
+tauten
+tauter
+tautly
+tautog
+tavern
+tawdry
+tawers
+tawing
+tawney
+tawpie
+tawsed
+tawses
+taxeme
+taxers
+taxied
+taxies
+taxing
+taxite
+taxman
+taxmen
+taxols
+taxons
+tazzas
+teabox
+teacup
+teamed
+teapot
+teapoy
+teared
+tearer
+teased
+teasel
+teaser
+teases
+teated
+teazel
+teazle
+teched
+techie
+techno
+tectal
+tectum
+tedded
+tedder
+tedium
+teeing
+teemed
+teemer
+teener
+teensy
+teepee
+teeter
+teethe
+teflon
+tegmen
+teguas
+teiids
+teinds
+tekkie
+telcos
+teledu
+telega
+telfer
+telial
+telium
+teller
+tellys
+telnet
+telome
+telson
+temped
+tempeh
+temper
+temple
+tempos
+tempts
+tenace
+tenail
+tenant
+tended
+tender
+tendon
+tendus
+tenets
+teniae
+tenias
+tenner
+tennis
+tenons
+tenors
+tenour
+tenpin
+tenrec
+tensed
+tenser
+tenses
+tensor
+tented
+tenter
+tenths
+tentie
+tenues
+tenuis
+tenure
+tenuti
+tenuto
+teopan
+tepals
+tepees
+tepefy
+tephra
+tepoys
+terais
+teraph
+terbia
+terbic
+tercel
+terces
+tercet
+teredo
+terete
+tergal
+tergum
+termed
+termer
+termly
+termor
+ternes
+terrae
+terras
+terret
+territ
+terror
+terser
+teslas
+testae
+tested
+testee
+tester
+teston
+tetany
+tetchy
+tether
+tetrad
+tetras
+tetris
+tetryl
+tetter
+tewing
+thacks
+thairm
+thaler
+thalli
+thanes
+thanks
+tharms
+thatch
+thawed
+thawer
+thebes
+thecae
+thecal
+thefts
+thegns
+theine
+theins
+theirs
+theism
+theist
+themed
+themes
+thenal
+thenar
+thence
+theory
+theres
+therme
+therms
+theses
+thesis
+thesps
+thetas
+thetic
+thicks
+thieve
+thighs
+thills
+things
+thinks
+thinly
+thiols
+thiram
+thirds
+thirls
+thirst
+thirty
+tholed
+tholes
+tholoi
+tholos
+thongs
+thorax
+thoria
+thoric
+thorns
+thorny
+thoron
+thorpe
+thorps
+thoued
+though
+thrall
+thrash
+thrave
+thrawn
+thraws
+thread
+threap
+threat
+threep
+threes
+thresh
+thrice
+thrift
+thrill
+thrips
+thrive
+throat
+throbs
+throes
+throne
+throng
+throve
+thrown
+throws
+thrums
+thrust
+thujas
+thulia
+thumbs
+thumps
+thunks
+thurls
+thusly
+thuyas
+thwack
+thwart
+thymes
+thymey
+thymic
+thymol
+thymus
+thyrse
+thyrsi
+tiaras
+tibiae
+tibial
+tibias
+ticals
+ticced
+ticked
+ticker
+ticket
+tickle
+tictac
+tictoc
+tidbit
+tiddly
+tidied
+tidier
+tidies
+tidily
+tiding
+tieing
+tiepin
+tierce
+tiered
+tiffed
+tiffin
+tigers
+tights
+tiglon
+tigons
+tikkas
+tilaks
+tildes
+tilers
+tiling
+tilled
+tiller
+tilted
+tilter
+tilths
+timbal
+timber
+timbre
+timely
+timers
+timing
+tincal
+tincts
+tinder
+tineal
+tineas
+tineid
+tinful
+tinged
+tinges
+tingle
+tingly
+tinier
+tinily
+tining
+tinker
+tinkle
+tinkly
+tinman
+tinmen
+tinned
+tinner
+tinpot
+tinsel
+tinted
+tinter
+tipcat
+tipoff
+tipped
+tipper
+tippet
+tipple
+tiptoe
+tiptop
+tirade
+tiring
+tirled
+tisane
+tissue
+titans
+tmeses
+tmesis
+toasts
+toasty
+tobies
+tocher
+tocsin
+todays
+toddle
+todies
+toecap
+toeing
+toffee
+togaed
+togate
+togged
+toggle
+togues
+toiled
+toiler
+toiles
+toited
+tokays
+tokens
+tolane
+tolans
+tolars
+toledo
+toling
+tolled
+toller
+toluic
+toluid
+toluol
+toluyl
+tolyls
+tomans
+tomato
+tombac
+tombak
+tombal
+tombed
+tomboy
+tomcat
+tomcod
+tommed
+tomtit
+tondos
+toneme
+toners
+tongas
+tonged
+tonger
+tongue
+tonics
+tonier
+toning
+tonish
+tonlet
+tonner
+tonnes
+tonsil
+tooled
+tooler
+toonie
+tooted
+tooter
+tooths
+toothy
+tootle
+tootsy
+topees
+topers
+topful
+tophes
+tophus
+topics
+toping
+topped
+topper
+topple
+toques
+toquet
+torahs
+torchy
+torero
+torics
+tories
+toroid
+torose
+toroth
+torous
+torpid
+torpor
+torque
+torrid
+torses
+torsks
+torsos
+tortas
+torten
+tortes
+torula
+toshes
+tossed
+tosses
+tossup
+totals
+totems
+toters
+tother
+toting
+totted
+totter
+toucan
+touche
+touchy
+toughs
+toughy
+toupee
+toured
+tourer
+toused
+touses
+tousle
+touted
+touter
+touzle
+towage
+toward
+towels
+towers
+towery
+towhee
+towies
+towing
+townee
+townie
+toxics
+toxine
+toxins
+toxoid
+toyers
+toying
+toyish
+toyons
+traced
+tracer
+traces
+tracks
+tracts
+traded
+trader
+trades
+tragic
+tragus
+traiks
+trails
+trains
+traits
+tramel
+tramps
+trampy
+trance
+tranks
+tranny
+tranqs
+trapan
+trapes
+trashy
+trauma
+travel
+traves
+trawls
+treads
+treats
+treaty
+treble
+trebly
+treens
+trefah
+tremor
+trench
+trends
+trendy
+trepan
+trepid
+tressy
+trevet
+triacs
+triads
+triage
+trials
+tribal
+tribes
+triced
+tricep
+trices
+tricks
+tricky
+tricot
+triene
+triens
+triers
+trifid
+trifle
+trigly
+trigon
+trigos
+trijet
+trikes
+trilby
+trills
+trimer
+trimly
+trinal
+trined
+trines
+triode
+triols
+triose
+tripes
+triple
+triply
+tripod
+tripos
+trippy
+triste
+triter
+triton
+triune
+trivet
+trivia
+troaks
+trocar
+troche
+trocks
+trogon
+troika
+troked
+trokes
+trolls
+trolly
+trompe
+tromps
+tronas
+trones
+troops
+tropes
+trophy
+tropic
+tropin
+troths
+trotyl
+trough
+troupe
+trouts
+trouty
+trover
+troves
+trowed
+trowel
+trowth
+truant
+truced
+truces
+trucks
+trudge
+truest
+truffe
+truing
+truism
+trulls
+trumps
+trunks
+trusts
+trusty
+truths
+trying
+tryout
+tryste
+trysts
+tsades
+tsadis
+tsetse
+tsking
+tsktsk
+tsores
+tsoris
+tsuris
+tubate
+tubbed
+tubber
+tubers
+tubful
+tubing
+tubist
+tubule
+tuchun
+tucked
+tucker
+tucket
+tuffet
+tufoli
+tufted
+tufter
+tugged
+tugger
+tugrik
+tuille
+tuladi
+tulips
+tulles
+tumble
+tumefy
+tumors
+tumour
+tumped
+tumuli
+tumult
+tundra
+tuners
+tuneup
+tunica
+tunics
+tuning
+tunned
+tunnel
+tupelo
+tupiks
+tupped
+tuques
+turaco
+turban
+turbid
+turbit
+turbos
+turbot
+tureen
+turfed
+turgid
+turgor
+turion
+turkey
+turned
+turner
+turnip
+turnon
+turnup
+turret
+turtle
+turves
+tusche
+tushed
+tushes
+tushie
+tusked
+tusker
+tussal
+tusseh
+tusser
+tusses
+tussis
+tussle
+tussor
+tussur
+tutees
+tutors
+tutted
+tuttis
+tutued
+tuxedo
+tuyere
+tuyers
+twains
+twanky
+tweaks
+tweaky
+tweeds
+tweedy
+tweens
+tweeny
+tweets
+tweeze
+twelve
+twenty
+twerps
+twibil
+twiers
+twiggy
+twilit
+twills
+twined
+twiner
+twines
+twinge
+twirls
+twirly
+twirps
+twists
+twisty
+twitch
+twofer
+twyers
+tycoon
+tymbal
+tympan
+tyning
+typhon
+typhus
+typier
+typify
+typing
+typist
+tyrant
+tyring
+tythed
+tythes
+tzetze
+tzuris
+uakari
+ubiety
+ubique
+udders
+uglier
+uglies
+uglify
+uglily
+ugsome
+uhlans
+ukases
+ulamas
+ulcers
+ulemas
+ullage
+ulster
+ultima
+ultimo
+ultras
+umamis
+umbels
+umbers
+umbles
+umbrae
+umbral
+umbras
+umiack
+umiacs
+umiaks
+umiaqs
+umlaut
+umping
+umpire
+unable
+unaged
+unakin
+unarms
+unawed
+unaxed
+unbale
+unbans
+unbars
+unbear
+unbelt
+unbend
+unbent
+unbind
+unbolt
+unborn
+unbred
+unbusy
+uncage
+uncake
+uncaps
+uncase
+uncast
+unchic
+unciae
+uncial
+uncini
+unclad
+uncles
+unclip
+unclog
+uncoil
+uncool
+uncork
+uncuff
+uncurb
+uncurl
+uncute
+undead
+undies
+undine
+undock
+undoer
+undoes
+undone
+undraw
+undrew
+unduly
+undyed
+unease
+uneasy
+uneven
+unfair
+unfelt
+unfits
+unfixt
+unfold
+unfond
+unfree
+unfurl
+ungird
+ungirt
+unglue
+ungual
+ungues
+unguis
+ungula
+unhair
+unhand
+unhang
+unhats
+unhelm
+unhewn
+unholy
+unhood
+unhook
+unhung
+unhurt
+unhusk
+unific
+unions
+unipod
+unique
+unisex
+unison
+united
+uniter
+unites
+unjams
+unjust
+unkend
+unkent
+unkept
+unkind
+unkink
+unknit
+unknot
+unlace
+unlade
+unlaid
+unlash
+unlays
+unlead
+unless
+unlike
+unlink
+unlive
+unload
+unlock
+unmade
+unmake
+unmans
+unmask
+unmeet
+unmesh
+unmews
+unmixt
+unmold
+unmoor
+unmown
+unnail
+unopen
+unpack
+unpaid
+unpegs
+unpens
+unpent
+unpick
+unpile
+unpins
+unplug
+unpure
+unread
+unreal
+unreel
+unrent
+unrest
+unrigs
+unripe
+unrips
+unrobe
+unroll
+unroof
+unroot
+unrove
+unruly
+unsafe
+unsaid
+unsawn
+unsays
+unseal
+unseam
+unseat
+unseen
+unsell
+unsent
+unsets
+unsewn
+unsews
+unsexy
+unshed
+unship
+unshod
+unshut
+unsnag
+unsnap
+unsold
+unsown
+unspun
+unstep
+unstop
+unsung
+unsunk
+unsure
+untack
+untame
+untidy
+untied
+unties
+untold
+untorn
+untrim
+untrod
+untrue
+untuck
+untune
+unused
+unveil
+unvext
+unwary
+unwell
+unwept
+unwind
+unwise
+unwish
+unwits
+unworn
+unwove
+unwrap
+unyoke
+unzips
+upases
+upbear
+upbeat
+upbind
+upboil
+upbore
+upbows
+upcast
+upcoil
+upcurl
+updart
+update
+updive
+updove
+upends
+upflow
+upfold
+upgaze
+upgird
+upgirt
+upgrew
+upgrow
+upheap
+upheld
+uphill
+uphold
+uphove
+uphroe
+upkeep
+upland
+upleap
+uplift
+uplink
+upload
+upmost
+uppers
+uppile
+upping
+uppish
+uppity
+upprop
+uprate
+uprear
+uprise
+uproar
+uproot
+uprose
+uprush
+upsend
+upsent
+upsets
+upshot
+upside
+upsize
+upsoar
+upstep
+upstir
+uptake
+uptalk
+uptear
+uptick
+uptilt
+uptime
+uptore
+uptorn
+uptoss
+uptown
+upturn
+upwaft
+upward
+upwell
+upwind
+uracil
+uraeus
+urania
+uranic
+uranyl
+urares
+uraris
+urases
+urates
+uratic
+urbane
+urbias
+urchin
+urease
+uredia
+uredos
+ureide
+uremia
+uremic
+urgent
+urgers
+urging
+urials
+uropod
+urping
+ursids
+ursine
+urtext
+uruses
+usable
+usably
+usages
+usance
+useful
+ushers
+usneas
+usques
+usuals
+usurer
+usurps
+uterus
+utmost
+utopia
+utters
+uveous
+uvulae
+uvular
+uvulas
+vacant
+vacate
+vacuum
+vadose
+vagary
+vagile
+vagrom
+vaguer
+vahine
+vailed
+vainer
+vainly
+vakeel
+vakils
+valets
+valgus
+valine
+valise
+valkyr
+valley
+valors
+valour
+valses
+valued
+valuer
+values
+valuta
+valval
+valvar
+valved
+valves
+vamose
+vamped
+vamper
+vandal
+vandas
+vanish
+vanity
+vanman
+vanmen
+vanned
+vanner
+vapors
+vapory
+vapour
+varias
+varied
+varier
+varies
+varlet
+varnas
+varoom
+varved
+varves
+vassal
+vaster
+vastly
+vatful
+vatted
+vaults
+vaulty
+vaunts
+vaunty
+vaward
+vealed
+vealer
+vector
+veejay
+veenas
+veepee
+veered
+vegans
+vegete
+vegged
+veggie
+vegies
+veiled
+veiler
+veinal
+veined
+veiner
+velars
+velate
+velcro
+veldts
+vellum
+veloce
+velour
+velure
+velvet
+vended
+vendee
+vender
+vendor
+vendue
+veneer
+venene
+venery
+venged
+venges
+venial
+venine
+venins
+venire
+venoms
+venose
+venous
+vented
+venter
+venues
+venule
+verbal
+verbid
+verdin
+verged
+verger
+verges
+verier
+verify
+verily
+verism
+verist
+verite
+verity
+vermes
+vermin
+vermis
+vernal
+vernix
+versal
+versed
+verser
+verses
+verset
+versos
+verste
+versts
+versus
+vertex
+vertus
+verves
+vervet
+vesica
+vesper
+vespid
+vessel
+vestal
+vestas
+vested
+vestee
+vestry
+vetoed
+vetoer
+vetoes
+vetted
+vetter
+vexers
+vexils
+vexing
+viable
+viably
+vialed
+viands
+viatic
+viator
+vibist
+vibrio
+vicars
+vicing
+victim
+victor
+vicuna
+videos
+viewed
+viewer
+vigias
+vigils
+vigors
+vigour
+viking
+vilely
+vilest
+vilify
+villae
+villas
+villus
+vimina
+vinals
+vincas
+vineal
+vinery
+vinier
+vinify
+vining
+vinous
+vinyls
+violas
+violet
+violin
+vipers
+virago
+vireos
+virgas
+virgin
+virile
+virion
+viroid
+virtue
+virtus
+visaed
+visage
+visard
+viscid
+viscus
+viseed
+vising
+vision
+visits
+visive
+visors
+vistas
+visual
+vitals
+vitric
+vittae
+vittle
+vivace
+vivary
+vivers
+vivify
+vixens
+vizard
+vizier
+vizirs
+vizors
+vizsla
+vocabs
+vocals
+vodkas
+vodoun
+vodous
+voduns
+vogued
+voguer
+vogues
+voiced
+voicer
+voices
+voided
+voider
+voiles
+volant
+volery
+voling
+volley
+volost
+voltes
+volume
+volute
+volvas
+volvox
+vomers
+vomica
+voodoo
+vortex
+votary
+voters
+voting
+votive
+voudon
+vowels
+vowers
+vowing
+voyage
+voyeur
+vrooms
+vrouws
+wabble
+wabbly
+wadded
+wadder
+waddie
+waddle
+waddly
+waders
+wadies
+wading
+wadmal
+wadmel
+wadmol
+wadset
+waeful
+wafers
+wafery
+waffed
+waffie
+waffle
+waffly
+wafted
+wafter
+wagers
+wagged
+wagger
+waggle
+waggly
+waggon
+waging
+wagons
+wahine
+wahoos
+waifed
+wailed
+wailer
+waired
+waists
+waited
+waiter
+waived
+waiver
+waives
+wakame
+wakens
+wakers
+wakiki
+waking
+walers
+walies
+waling
+walked
+walker
+walkup
+wallah
+wallas
+walled
+wallet
+wallie
+wallop
+wallow
+walnut
+walrus
+wamble
+wambly
+wammus
+wampum
+wampus
+wander
+wandle
+wanier
+waning
+wanion
+wanned
+wanner
+wanted
+wanter
+wanton
+wapiti
+wapped
+warble
+warded
+warden
+warder
+warier
+warily
+waring
+warked
+warmed
+warmer
+warmly
+warmth
+warmup
+warned
+warner
+warped
+warper
+warred
+warren
+warsaw
+warsle
+warted
+wasabi
+washed
+washer
+washes
+washup
+wastes
+wastry
+watape
+wataps
+waters
+watery
+watter
+wattle
+waucht
+waught
+wauked
+wauled
+wavers
+wavery
+waveys
+wavier
+wavies
+wavily
+waving
+wawled
+waxers
+waxier
+waxily
+waxing
+waylay
+wazoos
+weaken
+weaker
+weakly
+weakon
+wealds
+wealth
+weaned
+weaner
+weapon
+wearer
+weasel
+weason
+weaved
+weaver
+weaves
+webbed
+webcam
+webers
+webfed
+weblog
+wechts
+wedded
+wedder
+wedeln
+wedels
+wedged
+wedges
+wedgie
+weeded
+weeder
+weekly
+weened
+weenie
+weensy
+weeper
+weepie
+weeted
+weever
+weevil
+weewee
+weighs
+weight
+weirds
+weirdy
+welded
+welder
+weldor
+welkin
+welled
+wellie
+welted
+welter
+wended
+weskit
+wester
+wether
+wetted
+whacks
+whacky
+whaled
+whaler
+whales
+whammo
+whammy
+whangs
+wharfs
+wharve
+whaups
+wheals
+wheats
+wheels
+wheens
+wheeps
+wheeze
+wheezy
+whelks
+whelky
+whelms
+whelps
+whenas
+whence
+wheres
+wherry
+wherve
+wheyey
+whidah
+whiffs
+whiled
+whiles
+whilom
+whilst
+whimsy
+whined
+whiner
+whines
+whiney
+whinge
+whinny
+whippy
+whirls
+whirly
+whirrs
+whirry
+whisht
+whisks
+whisky
+whists
+whited
+whiten
+whiter
+whites
+whitey
+whizzy
+wholes
+wholly
+whomps
+whomso
+whoofs
+whoops
+whoosh
+whorls
+whorts
+whosis
+whumps
+whydah
+wiccan
+wiccas
+wiches
+wicked
+wicker
+wicket
+wicopy
+widder
+widdie
+widdle
+widely
+widens
+widest
+widget
+widish
+widows
+widths
+wields
+wieldy
+wiener
+wienie
+wifely
+wifeys
+wifing
+wigans
+wigeon
+wigged
+wiggle
+wiggly
+wights
+wiglet
+wigwag
+wigwam
+wikiup
+wilded
+wilder
+wildly
+wilful
+wilier
+wilily
+wiling
+willed
+willer
+willet
+willie
+willow
+wilted
+wimble
+wimmin
+wimped
+wimple
+winced
+wincer
+winces
+wincey
+winded
+winder
+windle
+window
+windup
+winery
+winged
+winger
+winier
+wining
+winish
+winked
+winker
+winkle
+winned
+winner
+winnow
+winoes
+winter
+wintle
+wintry
+winzes
+wipers
+wiping
+wirers
+wirier
+wirily
+wiring
+wisdom
+wisely
+wisent
+wisest
+wished
+wisher
+wishes
+wising
+wisped
+wissed
+wisses
+wisted
+witans
+witchy
+withal
+withed
+wither
+withes
+within
+witing
+witney
+witted
+wittol
+wivern
+wivers
+wiving
+wizard
+wizens
+wizzen
+wizzes
+woaded
+woalds
+wobble
+wobbly
+wodges
+woeful
+wolfed
+wolfer
+wolver
+wolves
+womans
+wombat
+wombed
+womera
+wonder
+wonned
+wonner
+wonted
+wonton
+wooded
+wooden
+woodie
+woodsy
+wooers
+woofed
+woofer
+wooing
+wooled
+woolen
+wooler
+woolie
+woolly
+worded
+worked
+worker
+workup
+worlds
+wormed
+wormer
+wormil
+worrit
+worsen
+worser
+worses
+worset
+worsts
+worths
+worthy
+wotted
+wounds
+wovens
+wowing
+wowser
+wracks
+wraith
+wrangs
+wrasse
+wraths
+wrathy
+wreaks
+wreath
+wrecks
+wrench
+wrests
+wretch
+wricks
+wriest
+wright
+wrings
+wrists
+wristy
+writer
+writes
+writhe
+wrongs
+wryest
+wrying
+wursts
+wurzel
+wusses
+wuther
+wyches
+wyling
+wyting
+wyvern
+xebecs
+xenial
+xenias
+xenons
+xylans
+xylems
+xylene
+xyloid
+xylols
+xylose
+xylyls
+xyster
+xystoi
+xystos
+xystus
+yabber
+yabbie
+yachts
+yacked
+yaffed
+yagers
+yahoos
+yairds
+yakked
+yakker
+yakuza
+yamens
+yammer
+yamuns
+yanked
+yanqui
+yantra
+yapock
+yapoks
+yapons
+yapped
+yapper
+yarded
+yarder
+yarely
+yarest
+yarned
+yarner
+yarrow
+yasmak
+yatter
+yauped
+yauper
+yaupon
+yautia
+yawing
+yawled
+yawned
+yawner
+yawped
+yawper
+yclept
+yeaned
+yearly
+yearns
+yeasts
+yeasty
+yecchs
+yeelin
+yelled
+yeller
+yellow
+yelped
+yelper
+yenned
+yentas
+yentes
+yeoman
+yeomen
+yerbas
+yerked
+yessed
+yesses
+yester
+yeuked
+yields
+yipped
+yippee
+yippie
+yirred
+yirths
+yobbos
+yocked
+yodels
+yodled
+yodler
+yodles
+yogees
+yogini
+yogins
+yogurt
+yoicks
+yokels
+yoking
+yolked
+yonder
+yonker
+youngs
+youpon
+youths
+yowies
+yowing
+yowled
+yowler
+yttria
+yttric
+yuccas
+yucked
+yukked
+yulans
+yupons
+yuppie
+yutzes
+zaddik
+zaffar
+zaffer
+zaffir
+zaffre
+zaftig
+zagged
+zaikai
+zaires
+zamias
+zanana
+zander
+zanier
+zanies
+zanily
+zanzas
+zapped
+zapper
+zareba
+zariba
+zayins
+zazens
+zealot
+zeatin
+zebeck
+zebecs
+zebras
+zechin
+zenana
+zenith
+zephyr
+zeroed
+zeroes
+zeroth
+zested
+zester
+zeugma
+zibeth
+zibets
+zigged
+zigzag
+zillah
+zinced
+zincic
+zincky
+zinebs
+zinged
+zinger
+zinnia
+zipped
+zipper
+zirams
+zircon
+zither
+zizith
+zizzle
+zlotys
+zoaria
+zocalo
+zodiac
+zoecia
+zoftig
+zombie
+zombis
+zonary
+zonate
+zoners
+zoning
+zonked
+zonula
+zonule
+zooids
+zooier
+zoomed
+zoonal
+zooned
+zorils
+zoster
+zouave
+zounds
+zoysia
+zydeco
+zygoid
+zygoma
+zygose
+zygote
+zymase