Получение изображения с капчей в GeckoFx [C#]
Бывают случаи, когда нужно получить капчу и показать ее пользователю для ввода, либо отправить в какой-то сервис для автоматического распознавания. В этой статье я опишу, как получить изображение капчи (или любого другого) в GeckoFx.
GeckoFx имеет класс ImageCreator, который позволяет создавать скриншоты указанного размера текущей открытой страницы. Т.е. мы просто передаем отступы (можно обойтись и без них, тогда начальная точка будет 0,0) по X и Y и размер изображения — на выходе получаем массив байт, который легко «трансформируется» в Image:
private static Image ByteArrayToImage( byte[] byteArrayIn )
{
var ms = new MemoryStream( byteArrayIn );
var returnImage = Image.FromStream( ms );
return returnImage;
}
...
var ic = new ImageCreator( browser );
Image image = ByteArrayToImage( ic.CanvasGetPngImage( xOffset, yOffset, imgWidth, imgHeight ) );
В качестве примера напишем приложение, которое будет заходить на этот сайт, брать оттуда капчу, отображать пользователя для ввода и проверять корректность ввода капчи.
Создаем новое WinForms приложение, называем его GeckoFxImage, размеры формы 195; 181, имя формы — FrmMain.
Список компонентов на форме:
- PictureBox (Name = pbImage, Size = 175; 60, Location = 0;0);
- TextBox (Name = tbInput, Size = 175; 20, Location = 0; 66);
- Button (Name = btnEnter, Size = 175; 23, Location = 0; 92, Text = Enter);
- Button (Name = btnGetNew, Size = 175; 23, Location = 0; 121, Text = Get new);
Устанавливаем Async Targeting Pack (нужен для возможности использования «асинхронных фич» в .NET 4).
Теперь приступим к написанию кода.
Добавим новый класс GeckoExtensionsMethods:
#region Using
using System.Drawing;
using System.IO;
using System.Xml;
using Gecko;
#endregion
namespace GeckoFxImage
{
public static class GeckoExtensionsMethods
{
public class JsImage
{
public int Left { get; set; }
public int Top { get; set; }
public int Right { get; set; }
public int Bottom { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public Image Image { get; set; }
}
/// <summary>
/// Конвертирует массив байт в изображение
/// </summary>
/// <param name="byteArrayIn">Массив байт изображения</param>
/// <returns></returns>
private static Image ByteArrayToImage( byte[] byteArrayIn )
{
var ms = new MemoryStream( byteArrayIn );
var returnImage = Image.FromStream( ms );
return returnImage;
}
/// <summary>
/// Создает скриншот страницы
/// </summary>
/// <param name="browser">Ссылка на браузер</param>
/// <param name="imageInfo">Ссылка на данные об изображении</param>
/// <returns></returns>
public static Image MakeImage( GeckoWebBrowser browser, JsImage imageInfo )
{
var ic = new ImageCreator( browser );
return
ByteArrayToImage( ic.CanvasGetPngImage( (uint)imageInfo.Left, (uint)imageInfo.Top, (uint)imageInfo.Width,
(uint)imageInfo.Height ) );
}
/// <summary>
/// Возвразает информацию о указанном изображении
/// </summary>
/// <param name="browser">Ссылка на браузер</param>
/// <param name="elementId">Id элемента на странице</param>
/// <returns></returns>
public static JsImage GetJsImage( this GeckoWebBrowser browser, string elementId )
{
var jsImage = new JsImage();
using ( var context = new AutoJSContext( browser.Window.JSContext ) )
{
string js = @"
function getElementInfo( elementId )
{
var resArr = new Array();
var element = document.getElementById( elementId );
var rect = element.getBoundingClientRect();
resArr.push( rect.top );
resArr.push( rect.right );
resArr.push( rect.bottom );
resArr.push( rect.left );
resArr.push( element.offsetWidth );
resArr.push( element.offsetHeight );
return resArr;
}
getElementInfo(""" + elementId + @""");";
string result;
// выполняем простой js, который возвращает массив с нужными для нас данными
context.EvaluateScript( js, (nsISupports)browser.Document.DomObject, out result );
if ( result == "undefined" )
{
return null;
}
string[] dataArr = result.Split( ',' );
jsImage.Top = (int)XmlConvert.ToDouble( dataArr[ 0 ] );
jsImage.Right = (int)XmlConvert.ToDouble( dataArr[ 1 ] );
jsImage.Bottom = (int)XmlConvert.ToDouble( dataArr[ 2 ] );
jsImage.Left = (int)XmlConvert.ToDouble( dataArr[ 3 ] );
jsImage.Width = (int)XmlConvert.ToDouble( dataArr[ 4 ] );
jsImage.Height = (int)XmlConvert.ToDouble( dataArr[ 5 ] );
jsImage.Image = MakeImage( browser, jsImage );
}
return jsImage;
}
}
}
Класс содержит статические методы, среди которых метод расширения GetJsImage для GeckoWebBrowser, который упрощает получение изображения с указанным Id:
GeckoExtensionsMethods.JsImage jsImage = browser.GetJsImage( "si_image" );
Теперь добавим класс CaptchaEnter. Он будет отвечать за загрузку страницы, получения и возвращения изображения капчи в форму и ввод капчи, которую ввел пользователь.
#region Using
using System;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using Gecko;
#endregion
namespace GeckoFxImage
{
public class CaptchaEnter
{
private readonly GeckoWebBrowser _browser;
private readonly Timer _loadingTimer; // таймер загрузки
private bool _loading; // указывает на статус загрузки
public CaptchaEnter( GeckoWebBrowser browser )
{
this._browser = browser;
this._loadingTimer = new Timer { Interval = 4000 };
this._loadingTimer.Tick += this._loadingTimer_Tick;
}
private void _loadingTimer_Tick( object sender, EventArgs e )
{
this._loadingTimer.Stop();
this._loading = false;
}
/// <summary>
/// Ожидает загрузки страницы
/// </summary>
private async Task WaitForLoading()
{
while ( this._loading )
{
await TaskEx.Delay( 200 );
}
}
/// <summary>
/// Загружает указанную страницу и ждет завершения загрузки
/// </summary>
/// <param name="url">Url страницы</param>
private async Task Navigate( string url )
{
this._loading = true;
this._browser.Navigate( url );
this._loadingTimer.Start();
await this.WaitForLoading();
}
/// <summary>
/// Возвращает изображение с капчей
/// </summary>
/// <returns></returns>
public async Task<Image> GetCaptcha()
{
await this.Navigate( "http://www.unitedway-pdx.org/contact-files/captcha/test/captcha_test.php" );
GeckoExtensionsMethods.JsImage jsImage = this._browser.GetJsImage( "si_image" );
return jsImage == null ? null : jsImage.Image;
}
/// <summary>
/// Вводит капчу и проверяет корректность ввода
/// </summary>
/// <param name="input">Текст капчи</param>
/// <returns>Возвращает значение, указывающее на корректность ввода</returns>
public async Task<bool> InputCaptcha( string input )
{
// получаем элемент (input) с указанным id
var inputEl = this._browser.Document.GetElementById( "code" );
if ( inputEl == null )
{
return false;
}
// устанавливаем input'у текст, который ввел пользователь
inputEl.SetAttribute( "value", input );
// выбираем первый input, значение (value) которого равно submit
var btnEl =
this._browser.Document.GetElementsByTagName( "input" )
.FirstOrDefault( b => b.GetAttribute( "value" ) == "submit" );
if ( btnEl == null )
{
return false;
}
btnEl.Click();
await TaskEx.Delay( 2500 );
// выбираем первый p, который содержить указанный текст
var pEl =
this._browser.Document.GetElementsByTagName( "p" )
.FirstOrDefault( p => p.InnerHtml.Contains( "Test Passed." ) );
// если такой элемент получен - тест пройден успешно
return pEl != null;
}
}
}
Теперь перейдем к коду главной формы.
Добавим следующие переменные:
private readonly GeckoWebBrowser _webBrowser;
private readonly CaptchaEnter _captcha;
Добавим инициализацию Gecko в конструктор главной формы:
public FrmMain()
{
// инициализация Xulrunner
Xpcom.Initialize( Application.StartupPath + "\\xulrunner\\" );
this.InitializeComponent();
this._webBrowser = new GeckoWebBrowser
{
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom,
Width = this.Width,
Height = this.Height - 75,
Top = 0,
AutoSize = false
};
// маскируемся под Firefox 22
GeckoPreferences.User[ "general.useragent.override" ] =
"Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0";
var frm = new Form();
frm.Controls.Add( this._webBrowser );
this._captcha = new CaptchaEnter( this._webBrowser );
}
Напишем метод, который будет изменять активность (свойство enabled) двух кнопок:
private void DisEnObjs()
{
this.btnEnter.Enabled = !this.btnEnter.Enabled;
this.btnGetNew.Enabled = !this.btnGetNew.Enabled;
}
И напоследок, создадим два обработчика нажатия для кнопок btnEnter и btnGetNew:
private async void btnEnter_Click( object sender, EventArgs e )
{
this.DisEnObjs();
if ( await this._captcha.InputCaptcha( this.tbInput.Text ) )
{
MessageBox.Show( "Капча введена верно!", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information );
}
else
{
MessageBox.Show( "Капча введена неверно!", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error );
}
this.DisEnObjs();
}
private async void btnGetNew_Click( object sender, EventArgs e )
{
this.DisEnObjs();
Image image = await this._captcha.GetCaptcha();
this.pbImage.Image = image;
this.pbImage.Refresh();
this.DisEnObjs();
this.tbInput.Focus();
}
Первая кнопка будет передавать введенную капчу пользователем, а вторая — получать новое изображение капчи.
Исходники: