Конвертируем защищенные и редкие форматы документов на Delphi

Наверняка каждый из нас сталкивался с проблемой, когда позарез нужно достать текст из PDF-документа, а он защищен от копирования. Или вьювер djvu не поддерживает экспорт отдельных страниц. Или вам принесли текст в редком формате, который воспроизводит только специальная программа, и вы оттуда тоже ничего не можете скопировать или экспортировать. Сегодня мы напишем утилиту, которая будет спасать нас в таких ситуациях.

А идея очень простая – если нам не дают скопировать информацию непосредственно, мы сделаем это сами с помощью механизма PrintScreen. Насколько я знаю, ни одна программа для просмотра не защищает от «снимков» экрана. Единственным ограничением будет разрешение полученной картинки – оно будет равно разрешению вашего монитора. Конечно, делать «снимки» можно и вручную, но мы напишем программу, которая будет делать это автоматически, прокручивая документ от начала и до конца. Таким образом, вам останется только запустить программу и идти пить чай, пока «отснятый» документ не будет лежать у вас на винчестере в виде привычных картинок. Потом их можно будет импортировать в FineReader и распознать, получив тем самым желанный текст.

Итак, приступаем к реализации. Для начала самое простое – алгоритм скроллинга и снятие «снимков».

var
Form1: TForm1;
i, speed : integer;
bmp,bmp2 : tbitmap; r1,r2:trect

procedure TForm1.Timer1Timer(Sender: TObject);
var
point:Tpoint;
begin
inc(i); //счетчик добавляет номер снапшота в название файла
getcursorpos(point); //получаем информацию о том, где сейчас находится курсор
bmp.width := screen.width; bmp.height := screen.height;
bmp2.width := screen.width-(cleft+cright); bmp2.height := screen.height-(ctop+cbottom);
r1.Left:=0; r1.Top:=0; r1.Right:=bmp2.Width; r1.Bottom:=bmp2.Height; //задаем области копирования
bitblt(bmp.canvas.handle, 0, 0, screen.width, screen.height,
getdc(getdesktopwindow), 0, 0, srccopy); //копируем содержимое окна.
bmp2.Canvas.CopyRect(r1,bmp.Canvas,r2); //делаем обрезку краев
bmp2.SaveToFile(‘snapshot '+inttostr(i)+'.bmp'); //сохраняем полученный результат
Image1.Picture.Bitmap:=bmp2;
SendMessage(WindowFromPoint(point), messages.WM_MOUSEWHEEL,
(-speed shl 16) or 0,DWORD(SmallPoint(point.X,point.Y))); //посылаем сообщение о прокрутке
// (эмуляция скролла мыши)
end;

Весь процесс будет работать на таймере. Мы будем посылать окну сообщение о прокрутке колеса мыши и копировать содержимое активного окна функцией bitblt. Замечу, что мы не используем эмуляцию нажатия непосредственно самой кнопки PrintScreen, т.к. некоторые приложения могут перехватывать такое сообщение. Перехватить функцию из GDI32 намного сложнее. Ссылку на графический контекст мы получаем при помощи getdc(getdesktopwindow), либо же это можно сделать, получив информацию о курсоре функцией getcursorpos, а затем вытянуть хэндл окна под курсором с помощью WindowFromPoint. В переменной speed у нас хранится скорость прокрутки. Один поворот колеса мыши оценивается в 120 единиц, поэтому значение speed должно быть кратно этому числу. Если разрешение экрана довольно большое, то имеет смысл ставить большое значение скорости, чтобы «сфотографировать» документ за как можно меньшее число шагов. Но в то же время учтите, что процессор должен будет успевать отрисовывать содержимое документа, поэтому на медленных процессорах придется жертвовать скоростью. Кроме скорости, нам нужны и некоторые другие константы. Мы инициализируем их на FormCreate. Когда мы делаем «снимок», то вместе с полезной информацией – текстом – в него попадает интерфейс программы, который нам совершенно ни к чему, поэтому зададим заранее отступы. Они будут храниться в переменных cleft, cright, ctop и cbottom.

procedure TForm1.FormCreate(Sender: TObject);
begin
i:=0; //ставим счетчик числа «снимков» на 0
bmp := tbitmap.create; //создаем битмапы
bmp2:=tbitmap.Create;
cleft:=strtoint(edit1.text); //задаем границы копирования
cright:=strtoint(edit2.Text);
ctop:=strtoint(edit3.Text);
cbottom:=strtoint(edit4.Text);
speed:=strtoint(edit5.Text);
r1.Left:=0; r1.Top:=0; r1.Right:=bmp2.Width; r1.Bottom:=bmp2.Height;
r2.Left:=cleft; r2.Top:=ctop; r2.Right:=screen.Width-cright; r2.Bottom:=screen.Height-cbottom;
end;

В данном случае чтобы начать «снимать», нужно нажать кнопку и быстро перейти на нужное окно. Чтобы остановить процесс, нужно также нажать на кнопку. Обработчики нажатия этих кнопок представлены ниже:

procedure TForm1.Button7Click(Sender: TObject);
begin
cleft:=strtoint(edit1.text);
cright:=strtoint(edit2.Text);
ctop:=strtoint(edit3.Text);
cbottom:=strtoint(edit4.Text);
speed:=strtoint(edit5.Text);
r2.Left:=cleft; r2.Top:=ctop; r2.Right:=screen.Width-cright; r2.Bottom:=screen.Height-cbottom;
Timer1.Enabled:=true; //Запускаем процесс по нажатию кнопки
end;

procedure TForm1.Button6Click(Sender: TObject);
begin
timer1.Enabled:=false; //останавливаем процесс по требованию.
end;

Как вы уже догадались, страницы не влезают в экран целиком, кроме того, на двух соседних «снимках» присутствует небольшой общий кусочек изображения. Поэтому вначале мы будем накладывать соседние «снимки» для совмещения до тех пор, пока не получим одну большую картинку, содержащую все страницы. Программа будет использовать простую идею о том, что число не белых пикселей в изображении с текстом на двух картинках с одинаковыми частями должно быть одинаковым. Это самый простой алгоритм, т.к. он не учитывает артефактов и рассчитан только на белый фон, но он был написан за два часа, и в то же время он очень эффективен. Что касается алгоритмов совмещения цветных изображений, то здесь может быть использована похожая идея, только нужно вычислять число пикселей определенного цвета либо число пикселей {255,0,0},{0,255,0},{0,0,255}. В профессиональных программах (вроде Panorama Tools) для склейки используются гораздо более сложные алгоритмы на основе геометрических образов, но они нам здесь ни к чему – ведь у нас нет разных условий «съемки» или цифрового шума. Функция, которая будет считать число не белых пикселей в строке, выглядит довольно просто:

function count(canvas:tCanvas;n,width:integer):integer; //передаем в параметрах графический объект, номер строки и ширину
var c,i:integer;
begin
c:=0;
for i:=0 to width do
if (canvas.Pixels[i,n] mod 256)<<<255 then
begin
inc(c); //считаем число не белых пикселей
end;
result:=c; //возвращаем количество не белых пикселей
end;

Далее по нажатию кнопки запускаем алгоритм слияния одинаковых изображений. После того как count подсчитает количество не белых пикселей в каждой строке, мы получим обыкновенный массив чисел, и задача сведется к сравнению двух числовых массивов. Т.к. размер картинок различен, то и массивы у нас будут динамическими. Мы будем сравнивать две картинки по четырем строкам. Как показали эксперименты во время написания программы, этого вполне достаточно. Программа также должна отступить все белые строки сверху и начинать с черных. В результате мы получаем файл Out.bmp, в котором содержится весь документ, как будто его не делили на страницы, а раскатали в один большой рулон. Если вы хотите «вытянуть» таким образом целую книгу, то будьте готовы к тому, что Out.bmp будет занимать очень много места как на жестком диске, так и в оперативной памяти. Кроме того, каждый «снимок» занимает по около 3 МБ при разрешении 1280х1024, а их может быть и несколько тысяч. Здесь мы вводим две переменные otG и doG для того, чтобы отбросить плохие снимки, которые могут быть в начале или в конце «съемки». Например, вы не успели быстро переключиться на окно с документом или по окончании процесса не успели вовремя его выключить. С помощью этих переменных мы сообщим программе, с каких файлов нужно начинать. В битмапы bm1 и bm2 мы будем загружать пары «снимков», а bm3 будет нашим рабочим битмапом, и в конце работы алгоритма в нем будет содержаться итоговая картинка. Переносить куски изображений будем обычным CopyRect.

procedure TForm1.Button3Click(Sender: TObject);
var i,n,h,ot,otG,doG,t:integer; m1,m2:array of integer;bm1,bm2,bm3:tBitMap;
begin
bm1:=tBitMap.Create;
bm2:=tBitMap.Create;
bm3:=tBitMap.Create; //создаем битмапы
bm3.LoadFromFile('snapshot .bmp'); //загружаем в рабочий битмап первый файл

h:=0; //отступ с начала
t:=0;
otG:=SpinEdit1.Value; // переменные для прогресс-бара
doG:=SpinEdit2.Value; // цифры- имена файлов
for n:=otG to dog do
begin
Gauge1.Progress:=round((n-otG)*100/(doG-otG)); // выводим процент выполненной задачи
application.ProcessMessages; // Делаем наше приложение отзывчивым
bm1.LoadFromFile('snapshot '+inttostr(n)+'.bmp'); // загружаем два соседних «снимка» из файла
bm2.LoadFromFile('snapshot '+inttostr(n+1)+'.bmp');
setlength(m1,bm1.Height+1); // массив динамический, устанавливаем длину
setlength(m2,bm2.Height+1);
for i:=0 to bm1.Height do
begin
// Заполняем массивы.
m1[i]:=count(bm1.Canvas,i,bm1.Width);
m2[i]:=count(bm2.Canvas,i,bm2.Width);
end;
ot:=0;
while (m2[ot]<<<4)and(ot<<inc(ot);
for i:=ot to bm1.Height-1 do
begin
application.ProcessMessages; // Делаем наше приложение отзывчивым
if m2[ot]=m1[i] then
if m2[ot+1]=m1[i+1] then
if m2[ot+2]=m1[i+2] then
if m2[ot+3]=m1[i+3] then //сравнение двух картинок по четырем строкам.
begin
// склеивание
form1.memo1.Lines.Add(inttostr(i));
bm3.Height:=bm3.Height+i;
bm3.Canvas.CopyRect(rect(0,i+h,bm3.Width,i+h+bm2.Height),
bm2.Canvas,rect(0,0,bm2.Width,bm2.Height));
h:=h+i; t:=1;
break;
end;
end;
end;
bm3.SaveToFile('Out.bmp'); //вывод результата
end;

merge.bmp (результат работы склеивающего алгоритма

Теперь Out.bmp нужно вновь разрезать на страницы и перекодировать их во что-то более приличное, чем bmp. Отрезаем при помощи CopyRect целое количество страниц, сохраняем каждую страницу в файл, а потом сохраняем остаток. Эта довольно простая операция выглядит так:

procedure TForm1.Button2Click(Sender: TObject);
var bm1,bm2:tBitMap;H,W,i,s,ots:integer;
begin
// Разрезание на страницы
H:=SpinEdit4.Value; // Высота страницы
ots:=SpinEdit3.Value; // отступ сверху
W:= SpinEdit5.Value; // Ширина страницы
bm1:=tBitMap.Create;
bm2:=tBitMap.Create; // bm2 будет нашим рабочим битмапом
bm1.LoadFromFile('Out.bmp');
bm2.LoadFromFile('Out.bmp');
bm2.Height:=H;
for i:=1 to ((bm1.Height-ots) div H) do
begin // сначала копируем целое количество страниц
bm2.Canvas.CopyRect(rect(0,0,bm2.Width,bm2.Height),bm1.Canvas,
rect(0,(i-1)*H+ots,bm2.Width,i*H+ots));
bm2.SaveToFile('Out '+inttostr(i)+'.bmp');
end;
s:=(bm1.Height-ots) mod H; // вывод последнего куска
bm2.Height:=s;
bm2.Canvas.CopyRect(rect(0,0,bm2.Width,s),bm1.Canvas,rect(0,(i-1)*H+ots,bm2.Width,(i-1)*H+s));
bm2.SaveToFile('Out '+inttostr((bm1.Height div H)+1)+'.bmp');
end;

Конвертирование в формат jpeg можно выполнить при помощи такой простой процедуры:

procedure TForm1.Button5Click(Sender: TObject);
var
j:TjpegImage;
b:TBitmap;
i:integer;
begin
j:=TJpegImage.Create; // создаем графические объекты
b:= Tbitmap.Create;
for i:=1 to SpinEdit1.Value do begin // количество файлов для сжатия в jpeg
b.LoadFromFile('Out '+inttostr(i)+'.bmp');
j.Assign(b); // назначаем переменной j наш битмап
j.Grayscale:=true; // изображение в оттенках серого
j.Compress; // сжимаем изображение
j.SaveToFile(inttostr(i)+'reaaady'+'.jpg'); // сохраняем
end;
end;

Класс TJpegImage находится в модуле jpeg. Он реализует все основные операции с этим форматом. Более подробно можно узнать в переменной j через точку. Можно также объединить операцию разрезания и сохранения в jpeg в одну процедуру.

Заключение

Вот так за вечер можно решить проблемы с конвертированием любого редкого/неудобного/защищенного формата в обычные картинки. Для лучшего результата нужно использовать максимальное разрешение своего монитора. При определенной сноровке и доработке можно конвертировать сразу PDF в PDF, запускать просмотр автоматически, блокировать мышку и клавиатуру на время «фотографирования» или добавить дополнительных функций. Но и эта заготовка вполне рабочая и уже несколько раз выручала меня и моих друзей.

Алексей Голованов AlekseyGolovanov@mail.ru


Компьютерная газета. Статья была опубликована в номере 29 за 2009 год в рубрике программирование

©1997-2022 Компьютерная газета