pdfbeads 1.0.9 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/ChangeLog +23 -0
- data/bin/pdfbeads +28 -3
- data/doc/pdfbeads.en.html +552 -0
- data/doc/pdfbeads.ru.html +74 -34
- data/lib/pdfbeads.rb +17 -6
- data/lib/pdfbeads/pdfbuilder.rb +254 -74
- data/lib/pdfbeads/pdfpage.rb +8 -8
- data/lib/pdfbeads/pdftoc.rb +7 -3
- metadata +80 -48
data/doc/pdfbeads.ru.html
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
<meta name="Generator" content="Written directly in html">
|
10
10
|
|
11
|
-
<meta name="Description" content="Руководство пользователя
|
11
|
+
<meta name="Description" content="Руководство пользователя pdfbeads версии 1.1">
|
12
12
|
|
13
13
|
<style type="text/css">
|
14
14
|
body {
|
@@ -30,22 +30,22 @@
|
|
30
30
|
h1 {
|
31
31
|
font-size: 36px;
|
32
32
|
font-family: Times New Roman, Times, serif;
|
33
|
-
text-align: center;
|
34
|
-
font-style: normal;
|
33
|
+
text-align: center;
|
34
|
+
font-style: normal;
|
35
35
|
font-weight: bold
|
36
36
|
}
|
37
37
|
h2 {
|
38
38
|
font-size: 20px;
|
39
39
|
font-family: Arial, Helvetica, sans-serif;
|
40
|
-
text-align: center;
|
41
|
-
font-style: normal;
|
40
|
+
text-align: center;
|
41
|
+
font-style: normal;
|
42
42
|
font-weight: bold;
|
43
43
|
}
|
44
44
|
h3 {
|
45
45
|
font-size: 16px;
|
46
46
|
font-family: Arial, Helvetica, sans-serif;
|
47
|
-
text-align: left;
|
48
|
-
font-style: italic;
|
47
|
+
text-align: left;
|
48
|
+
font-style: italic;
|
49
49
|
font-weight: bold;
|
50
50
|
}
|
51
51
|
dt {
|
@@ -57,9 +57,9 @@
|
|
57
57
|
|
58
58
|
<body>
|
59
59
|
|
60
|
-
<h1>Руководство пользователя
|
60
|
+
<h1>Руководство пользователя pdfbeads версии 1.1</h1>
|
61
61
|
|
62
|
-
<p>(c) Алексей Крюков,
|
62
|
+
<p>(c) Алексей Крюков, 2013</p>
|
63
63
|
|
64
64
|
<p>Утилита pdfbeads предназначена для создания электронных книг в формате
|
65
65
|
PDF из предварительно обработанных отсканированных страниц. В отличие от
|
@@ -90,7 +90,8 @@ JPEG2000;</p></li>
|
|
90
90
|
<li><p>создание PDF-файлов с оглавлением и метаданными;</p></li>
|
91
91
|
|
92
92
|
<li><p>добавление скрытого текстового слоя из документов в формате hOCR
|
93
|
-
с корректной обработкой символов
|
93
|
+
с корректной обработкой символов кириллицы либо перенос текста из другого
|
94
|
+
PDF-файла.</p></li>
|
94
95
|
|
95
96
|
</ul>
|
96
97
|
|
@@ -103,23 +104,19 @@ JPEG2000;</p></li>
|
|
103
104
|
<h2>Требования</h2>
|
104
105
|
|
105
106
|
<p>Для запуска программы требуется прежде всего интерпретатор языка Ruby
|
106
|
-
версии 1.8 или
|
107
|
+
версии 1.8 или выше, доступный в дистрибутивах большинства Unix-подобных
|
107
108
|
систем. Версия для Windows может быть загружена с сайта <a
|
108
109
|
href="http://www.rubyinstaller.org/">RubyInstaller</a>. Для корректной
|
109
110
|
установки pdfbeads необходимо также загрузить пакетный менеджер RubyGems,
|
110
111
|
представляющий собой стандартный интерфейс языка Ruby для работы с
|
111
112
|
расширениями. Кроме того, в дополнение к основному дистрибутиву Ruby
|
112
|
-
понадобятся расширения RMagick
|
113
|
-
|
113
|
+
понадобятся расширения RMagick, Nokogiri (для обработки распознанного
|
114
|
+
текста в формате hOCR) и PDF::Reader (для считывания распознанного
|
115
|
+
текста из другого PDF-файла).</p>
|
114
116
|
|
115
117
|
<p>Если вы хотите создавать PDF-файлы с использованием формата сжатия
|
116
118
|
данных JBIG2, то в системе также должна быть установлена утилита jbig2
|
117
|
-
из пакета <a href="http://github.com/agl/jbig2enc">jbig2enc</a
|
118
|
-
по состоянию на октябрь 2010 г., когда пишется этот файл,
|
119
|
-
настоятельно рекомендуется использовать версию jbig2, самостоятельно
|
120
|
-
собранную из исходников, доступных в репозитории git, поскольку более
|
121
|
-
ранние версии не обеспечивают корректного сохранения информации о разрешении
|
122
|
-
изображений.</p>
|
119
|
+
из пакета <a href="http://github.com/agl/jbig2enc">jbig2enc</a>.</p>
|
123
120
|
|
124
121
|
<h2>Установка</h2>
|
125
122
|
|
@@ -132,10 +129,10 @@ gem install pdfbeads
|
|
132
129
|
</pre>
|
133
130
|
|
134
131
|
<p>Перед запуском программы необходимо удостовериться, что расширение
|
135
|
-
RMagick установлено и доступно интерпретатору Ruby.
|
136
|
-
зависимость нельзя отследить
|
132
|
+
RMagick установлено и доступно интерпретатору Ruby. <strong>К сожалению, эту
|
133
|
+
зависимость нельзя отследить автоматически</strong>, поскольку в некоторых
|
137
134
|
дистрибутивах Linux (в частности, Ubuntu) пакет RMagick устанавливается в
|
138
|
-
обход механизма RubyGems, так что утилите gem о нем ничего не известно.</p>
|
135
|
+
обход механизма RubyGems, так что утилите <tt>gem</tt> о нем ничего не известно.</p>
|
139
136
|
|
140
137
|
<p>Пользователям Ubuntu также следует иметь в виду, что в этом дистрибутиве
|
141
138
|
исполняемые файлы из пакетов gem по умолчанию распаковываются в каталоги
|
@@ -195,7 +192,7 @@ HTM(L) или HOCR, содержащих распознанный текст в
|
|
195
192
|
pdfbeads, иногда занимает довольно много времени, эти файлы в дальнейшем
|
196
193
|
не удаляются с диска и могут быть повторно использованы при последующих
|
197
194
|
прогонах в целях экономии времени. Для того, чтобы заставить pdfbeads
|
198
|
-
заменить такие файлы заново созданными версиями, можно запустить его с
|
195
|
+
заменить такие файлы заново созданными версиями, можно запустить его с ключом
|
199
196
|
<tt>-f</tt> или <tt>--force-update</tt>.</p>
|
200
197
|
|
201
198
|
<p>pdfbeads предназначен для сборки PDF из предварительно обработанных
|
@@ -239,7 +236,7 @@ pdfbeads [options] [files to process] [> output_file.pdf]
|
|
239
236
|
специальное назначение.</p>
|
240
237
|
|
241
238
|
<p>Вместо записи PDF-файла на стандартное устройство вывода можно использовать
|
242
|
-
|
239
|
+
ключ <tt>-o</tt> или <tt>--output</tt>, сопроводив ее указанием имени файла.</p>
|
243
240
|
|
244
241
|
<h2>Обработка бинаризованных изображений</h2>
|
245
242
|
|
@@ -276,12 +273,12 @@ ImageMagick, что существенно увеличивает скорост
|
|
276
273
|
<p>По умолчанию передний план страницы упаковывается с помощью технологии
|
277
274
|
сжатия JBIG2, для чего pdfbeads использует утилиту <a
|
278
275
|
href="http://github.com/agl/jbig2enc">jbig2enc</a> (автор — Адам
|
279
|
-
Лэнгли). При этом можно задать
|
276
|
+
Лэнгли). При этом можно задать ключ <tt>-p</tt> (<tt>--pages-per-dict</tt>),
|
280
277
|
чтобы указать желательное количество страниц, использующих общий словарь
|
281
278
|
разделенных символов (по умолчанию — 15).</p>
|
282
279
|
|
283
|
-
<p>Если утилита jbig2enc недоступна, либо при запуске pdfbeads
|
284
|
-
|
280
|
+
<p>Если утилита jbig2enc недоступна, либо при запуске pdfbeads был указан
|
281
|
+
ключ <tt>-m</tt> (<tt>--mask-compression</tt>) с аргументом `G4' (синонимы —
|
285
282
|
`Group4', `CCITTFax'), то вместо JBIG2-сжатия будет использоваться формат
|
286
283
|
CCITT Group 4 fax.</p>
|
287
284
|
|
@@ -303,7 +300,7 @@ CCITT Group 4 fax.</p>
|
|
303
300
|
`JP2' или `JPX'), `JPEG' (с синонимом `JPG'), а также `LOSSLESS'
|
304
301
|
(синонимы — `DEFLATE', `PNG'). Если используемая сборка библиотеки
|
305
302
|
ImageMagick поддерживает формат JPEG2000, по умолчанию используется именно
|
306
|
-
он; в противном случае — JPEG. Если
|
303
|
+
он; в противном случае — JPEG. Если выбрано значение LOSSLESS,
|
307
304
|
то pdfbeads будет использовать для сжатия изображений технологию deflate.
|
308
305
|
Следует иметь в виду, что это может привести к значительному возрастанию
|
309
306
|
объема данных по сравнению с форматами JPEG2000 или JPEG.</p></dd>
|
@@ -314,7 +311,7 @@ ImageMagick поддерживает формат JPEG2000, по умолчан
|
|
314
311
|
|
315
312
|
<dt>-g, --grayscale</dt>
|
316
313
|
<dd><p>Заставляет pdfbeads принудительно конвертировать цветные картинки в
|
317
|
-
оттенки серого.
|
314
|
+
оттенки серого. Данный ключ может быть полезен в том случае, если исходные
|
318
315
|
сканы были выполнены в цвете, но фактически содержали только черно-белые
|
319
316
|
картинки, причем преобразование в оттенки серого не было выполнено на этапе
|
320
317
|
первичной сканобработки. Такая ситуация часто возникает, в частности, при
|
@@ -354,7 +351,7 @@ pdfbeads, необходимо подготовить два графическ
|
|
354
351
|
<tt>*.bg.*</tt>) будет содержать фон, освобожденный от текстовых данных,
|
355
352
|
а на втором (с суффиксом <tt>*.fg.*</tt>) останутся только элементы маски
|
356
353
|
с присущей им текстурой. Данная процедура по смыслу напоминает операцию,
|
357
|
-
осуществляемую утилитой <tt>djvumake</tt> при указании
|
354
|
+
осуществляемую утилитой <tt>djvumake</tt> при указании ключа <tt>PPM</tt>,
|
358
355
|
и имеет ту же самую цель: создание трехслойной страницы, где один из
|
359
356
|
полноцветных слоев отвечает за отображение фона, а другой —
|
360
357
|
за раскраску наложенной на этот фон маски.</p>
|
@@ -379,9 +376,9 @@ pdfbeads, необходимо подготовить два графическ
|
|
379
376
|
<a href="http://www.imagemagick.org/discourse-server/viewtopic.php?p=41498#p41498">дискуссии
|
380
377
|
на форуме ImageMagick</a>, где обсуждались возможные способы удаления текста
|
381
378
|
с картинки с последующим заполнением образовавшихся «дырок»
|
382
|
-
исходя из значений соседних
|
379
|
+
исходя из значений соседних пикселей.</p>
|
383
380
|
|
384
|
-
|
381
|
+
<h2>Дополнительные возможности</h2>
|
385
382
|
|
386
383
|
<h3>Добавление метаданных</h3>
|
387
384
|
|
@@ -397,7 +394,7 @@ pdfbeads, необходимо подготовить два графическ
|
|
397
394
|
<tt>Author</tt>, <tt>Subject</tt> и <tt>Keywords</tt>. Строки, начинающиеся
|
398
395
|
с символа `#', считаются комментариями и игнорируются.</p>
|
399
396
|
|
400
|
-
<p>Ссылку на созданный файл можно передать pdfbeads с помощью
|
397
|
+
<p>Ссылку на созданный файл можно передать pdfbeads с помощью ключа
|
401
398
|
<tt>-M</tt> (или <tt>--meta</tt>).</p>
|
402
399
|
|
403
400
|
<h3>Метки страниц</h3>
|
@@ -481,11 +478,54 @@ PDF-файлу. Для этого используется параметр <tt>
|
|
481
478
|
параметр указывает, должен ли данный пункт оглавления отображаться
|
482
479
|
развернутым по умолчанию (символы `+' и `1' означают «да»).</p>
|
483
480
|
|
484
|
-
<p
|
481
|
+
<p>Ключ <tt>--toc</tt> целесообразно использовать в сочетании с ключом
|
485
482
|
<tt>--labels</tt>. В этом случае в файле оглавления можно использовать
|
486
483
|
те же номера страниц, что и в бумажной книге, не задумываясь о сдвигах
|
487
484
|
нумерации.</p>
|
488
485
|
|
486
|
+
<h3>Добавление текстового слоя</h3>
|
487
|
+
|
488
|
+
<p>pdfbeads позволяет создавать документы PDF со скрытым текстовым
|
489
|
+
слоем. Последний может быть либо получен из файлов в формате
|
490
|
+
<a href="http://docs.google.com/View?docid=dfxcv4vc_67g844kf">hOCR</a>
|
491
|
+
(расширение языка HTML, позволяющее сохранять в документе информацию
|
492
|
+
о положении символов и элементов разметки текста на странице), либо
|
493
|
+
импортирован из другого PDF-файла.</p>
|
494
|
+
|
495
|
+
<p>Для создания файлов в формате hOCR необходимо воспользоваться программой
|
496
|
+
оптического распознавания символов, поддерживающей этот формат, например
|
497
|
+
<a href="https://launchpad.net/cuneiform-linux/">Cuneiform</a> или
|
498
|
+
<a href="http://code.google.com/p/tesseract-ocr/">Tesseract</a>.
|
499
|
+
Распознанный текст следует сохранить в той же директории, что и остальные
|
500
|
+
файлы, относящиеся к проекту. При этом каждой распознанной странице должен
|
501
|
+
соответствовать отдельный файл с тем же базовым именем, что и у исходного
|
502
|
+
изображения, при расширении HTM(L) или HOCR. Обработка файлов hOCR
|
503
|
+
осуществляется автоматически при условии, что интерпретатору Ruby доступно
|
504
|
+
расширение Nokogiri.</p>
|
505
|
+
|
506
|
+
<p>Иное возможное решение заключается в том, чтобы импортировать текстовый
|
507
|
+
слой из другого PDF-файла (естественно, последний должен быть получен путем
|
508
|
+
распознавания тех же самых изображений, которые предполагается затем обработать
|
509
|
+
с помощью pdfbeads). Имя полученного файла следует передать pdfbeads с помощью
|
510
|
+
ключа <tt>-T</tt> (полная форма — <tt>-text-pdf</tt>). Эта
|
511
|
+
возможность особенно важна в тех случаях, когда приходится использовать для
|
512
|
+
распознавания текста коммерческое приложение (например,
|
513
|
+
<a href="http://www.abbyy.ru/finereader/">ABBYY Finereader</a>), в котором
|
514
|
+
не предусмотрена поддержка формата hOCR. <strong>Внимание:</strong> возможно,
|
515
|
+
вам придется поэкспериментировать с настройками экспорта PDF в OCR-приложении
|
516
|
+
для того, чтобы получить наилучшее соответствие между размещением распознанного
|
517
|
+
текста на странице и исходным изображением. В частности, в ABBYY Finereader
|
518
|
+
11-й версии желаемый результат достигается только при сохранении файла в
|
519
|
+
режиме «текст под изображением».</p>
|
520
|
+
|
521
|
+
<h3>Обработка документов с направлением текста справа налево</h3>
|
522
|
+
|
523
|
+
<p>Ключ <tt>-R</tt> (или <tt>--right-to-left</tt> позволяет сохранить
|
524
|
+
в создаваемом файле пометку, указывающую на то, что основной язык
|
525
|
+
данного документа предполагает направление чтения справа налево. Данный
|
526
|
+
флажок используется Acrobat Reader при выборе порядка следования страниц в
|
527
|
+
режиме их попарного отображения.</p>
|
528
|
+
|
489
529
|
<h2>Лицензия</h2>
|
490
530
|
|
491
531
|
<p>Данная программа является свободным программным обеспечением. Вы
|
data/lib/pdfbeads.rb
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
# Unlike other PDF creation tools, this utility attempts to implement
|
9
9
|
# the approach typically used for DjVu books. Its key feature is
|
10
10
|
# separating scanned text (typically black, but indexed images with
|
11
|
-
# a small number of colors are also accepted) from halftone images
|
11
|
+
# a small number of colors are also accepted) from halftone images
|
12
12
|
# placed into a background layer.
|
13
13
|
#
|
14
14
|
# Copyright (C) 2010 Alexey Kryukov (amkryukov@gmail.com).
|
@@ -30,19 +30,25 @@
|
|
30
30
|
#
|
31
31
|
#######################################################################
|
32
32
|
|
33
|
-
require 'iconv'
|
34
33
|
require 'zlib'
|
35
34
|
|
36
35
|
require 'RMagick'
|
37
36
|
include Magick
|
38
37
|
|
39
38
|
begin
|
40
|
-
require '
|
41
|
-
$
|
39
|
+
require 'nokogiri'
|
40
|
+
$has_nokogiri = true
|
42
41
|
rescue LoadError
|
43
|
-
$stderr.puts( "Warning: the
|
42
|
+
$stderr.puts( "Warning: the nokogiri extension is not available. I'll not be able" )
|
44
43
|
$stderr.puts( "\tto create hidden text layer from hOCR files." )
|
45
|
-
$
|
44
|
+
$has_nokogiri = false
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
require 'pdf/reader'
|
49
|
+
$has_pdfreader = true
|
50
|
+
rescue LoadError
|
51
|
+
$has_pdfreader = false
|
46
52
|
end
|
47
53
|
|
48
54
|
unless ''.respond_to? :ord
|
@@ -50,6 +56,11 @@ unless ''.respond_to? :ord
|
|
50
56
|
require 'jcode'
|
51
57
|
end
|
52
58
|
|
59
|
+
# Require iconv for Ruby version less than 1.9.3
|
60
|
+
unless ''.respond_to? :encode
|
61
|
+
require 'iconv'
|
62
|
+
end
|
63
|
+
|
53
64
|
class String
|
54
65
|
# Protect strings which are supposed be treated as a raw sequence of bytes.
|
55
66
|
# This is important for Ruby 1.9. For earlier versions the method just
|
data/lib/pdfbeads/pdfbuilder.rb
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
# Unlike other PDF creation tools, this utility attempts to implement
|
10
10
|
# the approach typically used for DjVu books. Its key feature is
|
11
11
|
# separating scanned text (typically black, but indexed images with
|
12
|
-
# a small number of colors are also accepted) from halftone images
|
12
|
+
# a small number of colors are also accepted) from halftone images
|
13
13
|
# placed into a background layer.
|
14
14
|
#
|
15
15
|
# Copyright (C) 2010 Alexey Kryukov (amkryukov@gmail.com).
|
@@ -69,6 +69,7 @@ class PDFBeads::PDFBuilder
|
|
69
69
|
labels = PDFLabels.new( @pdfargs[:labels] ) unless @pdfargs[:labels].nil?
|
70
70
|
toc = PDFTOC.new( @pdfargs[:toc] ) unless @pdfargs[:toc].nil?
|
71
71
|
meta = parseMeta( @pdfargs[:meta] )
|
72
|
+
reader = getPDFReader( @pdfargs[:textpdf] )
|
72
73
|
|
73
74
|
cat = XObj.new(Hash[
|
74
75
|
'Type' => '/Catalog',
|
@@ -98,12 +99,12 @@ class PDFBeads::PDFBuilder
|
|
98
99
|
info.addToDict(key, "(\xFE\xFF#{meta[key].to_text})")
|
99
100
|
end
|
100
101
|
|
101
|
-
|
102
|
-
|
103
|
-
'
|
104
|
-
|
105
|
-
|
106
|
-
|
102
|
+
if ( toc != nil and toc.length > 0 ) or @pdfargs[:rtl]
|
103
|
+
vpref = XObj.new(Hash.new())
|
104
|
+
vpref.addToDict('Direction', "/R2L") if @pdfargs[:rtl]
|
105
|
+
@doc.addObject(vpref)
|
106
|
+
cat.addToDict('ViewerPreferences', ref(vpref.getID))
|
107
|
+
end
|
107
108
|
|
108
109
|
pages = XObj.new(Hash[
|
109
110
|
'Type' => '/Pages'
|
@@ -132,7 +133,7 @@ class PDFBeads::PDFBuilder
|
|
132
133
|
'Intent' => '[/View/Design]'
|
133
134
|
})
|
134
135
|
@doc.addObject(ocBack)
|
135
|
-
cat.addToDict('OCProperties',
|
136
|
+
cat.addToDict('OCProperties',
|
136
137
|
sprintf("<< /OCGs[%s %s] /D<< /Intent /View /BaseState (ON) /Order[%s %s] >>>>",
|
137
138
|
ref(ocFore.getID), ref(ocBack.getID), ref(ocFore.getID), ref(ocBack.getID)))
|
138
139
|
|
@@ -150,9 +151,14 @@ class PDFBeads::PDFBuilder
|
|
150
151
|
begin
|
151
152
|
# If possible, use iso8859-1 (aka PDFDocEncoding) for page labels:
|
152
153
|
# it is at least guaranteed to be safe
|
153
|
-
|
154
|
-
|
155
|
-
|
154
|
+
if rng[:prefix].respond_to? :encode
|
155
|
+
ltitl = rng[:prefix].encode( "iso8859-1", "utf-8" )
|
156
|
+
else
|
157
|
+
ltitl = Iconv.iconv( "iso8859-1", "utf-8", rng[:prefix] ).first
|
158
|
+
end
|
159
|
+
nTree << "/P (#{ltitl.to_text}) "
|
160
|
+
# Iconv::InvalidCharacter, Iconv::IllegalSequence, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
161
|
+
rescue
|
156
162
|
ltitl = Iconv.iconv( "utf-16be", "utf-8", rng[:prefix] ).first
|
157
163
|
# If there is no number (just prefix) then put a zero character after the prefix:
|
158
164
|
# this makes acroread happy, but prevents displaying the number in evince
|
@@ -176,27 +182,31 @@ class PDFBeads::PDFBuilder
|
|
176
182
|
|
177
183
|
needs_font = false
|
178
184
|
fonts = encodings = nil
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
185
|
+
unless reader.nil?
|
186
|
+
fdict = importPDFFonts( reader,@pdfargs[:textpdf] )
|
187
|
+
else
|
188
|
+
pagefiles.each do |p|
|
189
|
+
unless p.hocr_path.nil?
|
190
|
+
needs_font = true
|
191
|
+
break
|
192
|
+
end
|
183
193
|
end
|
184
|
-
end
|
185
194
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
195
|
+
if needs_font
|
196
|
+
fonts = Array.new()
|
197
|
+
encodings = [ [' '] ]
|
198
|
+
fdict = XObj.new( Hash[] )
|
199
|
+
@doc.addObject( fdict )
|
200
|
+
|
201
|
+
descr = XObj.new( Hash[
|
202
|
+
'Type' => '/FontDescriptor',
|
203
|
+
'BaseFont' => '/Times-Roman',
|
204
|
+
] )
|
205
|
+
@fdata.header.each_key do |key|
|
206
|
+
descr.addToDict( key,@fdata.header[key] )
|
207
|
+
end
|
208
|
+
@doc.addObject( descr )
|
198
209
|
end
|
199
|
-
@doc.addObject( descr )
|
200
210
|
end
|
201
211
|
|
202
212
|
pagefiles.each do |p|
|
@@ -261,17 +271,24 @@ class PDFBeads::PDFBuilder
|
|
261
271
|
doc_objs.concat( [contents, resobj, resources] )
|
262
272
|
|
263
273
|
hocr = nil
|
264
|
-
|
265
|
-
hocr = open( p.hocr_path ) { |f| Hpricot.parse( f ) }
|
274
|
+
if not reader.nil?
|
266
275
|
procSet << '/Text'
|
267
|
-
c_str << getPDFText(
|
276
|
+
c_str << getPDFText( reader,pidx,@pdfargs[:debug] )
|
277
|
+
elsif not p.hocr_path.nil?
|
278
|
+
hocr = open( p.hocr_path ) { |f| Nokogiri::HTML( f ) }
|
279
|
+
procSet << '/Text'
|
280
|
+
c_str << getHOCRText( hocr,pheight,72.0/xres,72.0/yres,encodings )
|
268
281
|
end
|
269
282
|
|
270
|
-
|
271
|
-
|
272
|
-
|
283
|
+
unless @pdfargs[:debug]
|
284
|
+
contents.reinit( Hash[
|
285
|
+
'Filter' => '/FlateDecode'
|
286
|
+
], Zlib::Deflate.deflate( c_str,9 ) )
|
287
|
+
else
|
288
|
+
contents.reinit( Hash[], c_str )
|
289
|
+
end
|
273
290
|
resources.addToDict( 'ProcSet', "[ #{procSet.join(' ')} ]" )
|
274
|
-
resources.addToDict( 'Font', ref( fdict.getID ) ) unless hocr.nil?
|
291
|
+
resources.addToDict( 'Font', ref( fdict.getID ) ) unless hocr.nil? and reader.nil?
|
275
292
|
|
276
293
|
page = XObj.new(Hash[
|
277
294
|
'Type' => '/Page',
|
@@ -325,6 +342,7 @@ class PDFBeads::PDFBuilder
|
|
325
342
|
getOutlineObjs( toc,pages_by_num,page_objs[0].getID )
|
326
343
|
cat.addToDict('Outlines', ref(toc[0][:pdfobj].getID))
|
327
344
|
cat.addToDict('PageMode', "/UseOutlines")
|
345
|
+
vpref.addToDict('NonFullScreenPageMode', "/UseOutlines")
|
328
346
|
end
|
329
347
|
|
330
348
|
if @pdfargs[:delfiles]
|
@@ -381,7 +399,11 @@ class PDFBeads::PDFBuilder
|
|
381
399
|
key = $1
|
382
400
|
if keys.include? key
|
383
401
|
begin
|
384
|
-
|
402
|
+
if $2.respond_to? :encode
|
403
|
+
ret[key] = $2.encode( "utf-16be", "utf-8" )
|
404
|
+
else
|
405
|
+
ret[key] = Iconv.iconv( "utf-16be", "utf-8", $2 ).first
|
406
|
+
end
|
385
407
|
rescue
|
386
408
|
$stderr.puts("Error: metadata should be specified in utf-8")
|
387
409
|
end
|
@@ -392,6 +414,171 @@ class PDFBeads::PDFBuilder
|
|
392
414
|
ret
|
393
415
|
end
|
394
416
|
|
417
|
+
def getPDFReader( path )
|
418
|
+
return nil if path.nil? or path.eql? ''
|
419
|
+
return nil unless File.file? path
|
420
|
+
|
421
|
+
PDF::Reader.new( path )
|
422
|
+
end
|
423
|
+
|
424
|
+
def encodePDFArray( in_a )
|
425
|
+
out_a = Array.new()
|
426
|
+
out_a << '['
|
427
|
+
in_a.each do |item|
|
428
|
+
if item.is_a? String
|
429
|
+
out_a << ( '(' << item.to_s << ')' )
|
430
|
+
elsif item.is_a? Symbol
|
431
|
+
out_a << ( '/' << item.to_s )
|
432
|
+
elsif item.is_a? Array
|
433
|
+
out_a << encodePDFArray( item )
|
434
|
+
else
|
435
|
+
out_a << item.to_s
|
436
|
+
end
|
437
|
+
end
|
438
|
+
out_a << ']'
|
439
|
+
out_a.join( ' ' )
|
440
|
+
end
|
441
|
+
|
442
|
+
def encodePDFObjEntry( inhash,outobj,label )
|
443
|
+
if inhash[label].is_a? String
|
444
|
+
outobj.addToDict( label,"(#{inhash[label]})" )
|
445
|
+
|
446
|
+
elsif inhash[label].is_a? Symbol
|
447
|
+
outobj.addToDict( label,"/#{inhash[label]}" )
|
448
|
+
|
449
|
+
elsif inhash[label].is_a? Integer
|
450
|
+
outobj.addToDict( label,"#{inhash[label]}" )
|
451
|
+
|
452
|
+
elsif inhash[label].is_a? Array
|
453
|
+
outobj.addToDict( label,encodePDFArray( inhash[label] ) )
|
454
|
+
|
455
|
+
elsif inhash[label].is_a? Hash
|
456
|
+
newobj = XObj.new( Hash.new() )
|
457
|
+
@doc.addObject( newobj )
|
458
|
+
outobj.addToDict( label,ref(newobj.getID) )
|
459
|
+
inhash[label].keys.each do |newlabel|
|
460
|
+
encodePDFObjEntry( inhash[label],newobj,newlabel )
|
461
|
+
end
|
462
|
+
|
463
|
+
elsif inhash[label].is_a? PDF::Reader::Stream
|
464
|
+
newobj = XObj.new( Hash.new(),inhash[label].data )
|
465
|
+
@doc.addObject( newobj )
|
466
|
+
outobj.addToDict( label,ref(newobj.getID) )
|
467
|
+
inhash[label].hash.keys.each do |newlabel|
|
468
|
+
encodePDFObjEntry( inhash[label].hash,newobj,newlabel ) unless newlabel.eql? :Length
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def importPDFFont( label,font )
|
474
|
+
fontobj = XObj.new( Hash.new() )
|
475
|
+
fontobj.addToDict( 'Name',"/#{label}" ) unless label.nil?
|
476
|
+
@doc.addObject( fontobj )
|
477
|
+
|
478
|
+
if font.has_key? :DescendantFonts
|
479
|
+
dfonts = Array.new()
|
480
|
+
font[:DescendantFonts].each {|dfont| dfonts << importPDFFont( nil,dfont ) }
|
481
|
+
fontobj.addToDict( "DescendantFonts",'[ ' << dfonts.map{|dfont| ref(dfont.getID)}.join(' ') << ' ]' )
|
482
|
+
end
|
483
|
+
|
484
|
+
[ :BaseFont, :Type, :Subtype, :FirstChar, :LastChar, :Widths, :FontDescriptor,
|
485
|
+
:Encoding, :ToUnicode, :DW, :W, :CIDSystemInfo, :CIDToGIDMap ].each do |fontkey|
|
486
|
+
encodePDFObjEntry( font,fontobj,fontkey ) if font.has_key? fontkey
|
487
|
+
end
|
488
|
+
fontobj
|
489
|
+
end
|
490
|
+
|
491
|
+
def importPDFFonts( reader,path )
|
492
|
+
fonts = Hash.new()
|
493
|
+
reader.pages.each_index do |i|
|
494
|
+
$stderr.puts("Reading font data from #{path}: page #{i}\n")
|
495
|
+
page = reader.pages[i]
|
496
|
+
page.fonts.each do |label,font|
|
497
|
+
fonts[label] = page.objects.deref( font ) unless fonts.has_key? label
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
fdict = XObj.new( Hash[] )
|
502
|
+
@doc.addObject( fdict )
|
503
|
+
fonts.keys.sort_by {|sym| sym.to_s}.each do |label|
|
504
|
+
fontobj = importPDFFont( label,fonts[label] )
|
505
|
+
fdict.addToDict( label,ref(fontobj.getID) )
|
506
|
+
end
|
507
|
+
fdict
|
508
|
+
end
|
509
|
+
|
510
|
+
def getPDFText( reader,pidx,debug )
|
511
|
+
return "" unless reader.pages.length > pidx
|
512
|
+
|
513
|
+
page = reader.pages[pidx]
|
514
|
+
pcont = page.raw_content.to_binary()
|
515
|
+
cidx = 0
|
516
|
+
in_t = false
|
517
|
+
pstack = 0
|
518
|
+
prevc = "\0"
|
519
|
+
ch_start = -1
|
520
|
+
ret = ""
|
521
|
+
tr_val = debug ? 0 : 3
|
522
|
+
|
523
|
+
pcont.each_byte do |char|
|
524
|
+
if char.chr.eql? '('
|
525
|
+
ctx = pcont[0,cidx].match( /\\+$/ )
|
526
|
+
pstack += 1 if ( ctx.nil? or ctx[0].length % 2 == 0 )
|
527
|
+
elsif char.chr.eql? ')'
|
528
|
+
ctx = pcont[0,cidx].match( /\\+$/ )
|
529
|
+
pstack -= 1 if ( ctx.nil? or ctx[0].length % 2 == 0 )
|
530
|
+
end
|
531
|
+
|
532
|
+
unless pstack > 0
|
533
|
+
# Text state operators may occur outside text objects. We have to take care of this
|
534
|
+
if not in_t and prevc.eql? 'T'
|
535
|
+
case char.chr
|
536
|
+
when 'c'
|
537
|
+
if pcont[0,cidx-1] =~ /([-+]?\d*\.?\d+)\s+$/
|
538
|
+
ret << " #{$1} Tc"
|
539
|
+
end
|
540
|
+
when 'w'
|
541
|
+
if pcont[0,cidx-1] =~ /([-+]?\d*\.?\d+)\s+$/
|
542
|
+
ret << " #{$1} Tw"
|
543
|
+
end
|
544
|
+
when 'z'
|
545
|
+
if pcont[0,cidx-1] =~ /([-+]?\d*\.?\d+)\s+$/
|
546
|
+
ret << " #{$1} Tz"
|
547
|
+
end
|
548
|
+
when 'L'
|
549
|
+
if pcont[0,cidx-1] =~ /([-+]?\d*\.?\d+)\s+$/
|
550
|
+
ret << " #{$1} TL"
|
551
|
+
end
|
552
|
+
when 'f'
|
553
|
+
if pcont[0,cidx-1] =~ /\/([A-Za-z0-9]+)\s+([-+]?\d*\.?\d+)\s+$/
|
554
|
+
ret << " /#{$1} #{$2} Tf"
|
555
|
+
end
|
556
|
+
# Tr operators are ignored, since we always need either a hidden text (3 Tr)
|
557
|
+
# or (for debugging purposes) a visible text without special effects (0 Tr)
|
558
|
+
when 's'
|
559
|
+
if pcont[0,cidx-1] =~ /([-+]?\d*\.?\d+)\s+$/
|
560
|
+
chunks << " #{$1} Ts"
|
561
|
+
end
|
562
|
+
end
|
563
|
+
elsif not in_t and ( prevc + char.chr ).eql? 'BT'
|
564
|
+
ch_start = cidx -1
|
565
|
+
in_t = true
|
566
|
+
elsif in_t and ( prevc + char.chr ).eql? 'ET'
|
567
|
+
chunk = pcont.slice( ch_start,cidx - ch_start + 1 )
|
568
|
+
chunk.gsub!( /\d{1}\s+Tr/,"#{tr_val} Tr" )
|
569
|
+
ret << "\n" << chunk
|
570
|
+
ch_start = -1
|
571
|
+
in_t = false
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
prevc = char.chr
|
576
|
+
cidx += 1
|
577
|
+
end
|
578
|
+
return "\nq #{tr_val} Tr" << ret << " Q" if ret.length > 0
|
579
|
+
return ""
|
580
|
+
end
|
581
|
+
|
395
582
|
def getOutlineObjs( toc,page_ids,fp_id )
|
396
583
|
root = toc[0]
|
397
584
|
root[:pdfobj] = XObj.new( Hash[
|
@@ -453,8 +640,8 @@ class PDFBeads::PDFBuilder
|
|
453
640
|
def elementCoordinates( element,xscale,yscale )
|
454
641
|
out = [0,0,0,0]
|
455
642
|
|
456
|
-
if element.attributes.
|
457
|
-
if /bbox((\s+\d+){4})/.match(element.attributes
|
643
|
+
if element.attributes.has_key? 'title'
|
644
|
+
if /bbox((\s+\d+){4})/.match(element.attributes['title'].content)
|
458
645
|
coords = $1.strip.split(/\s+/)
|
459
646
|
out = [ (coords[0].to_i*xscale).to_f,(coords[1].to_i*xscale).to_f,
|
460
647
|
(coords[2].to_i*yscale).to_f,(coords[3].to_i*yscale).to_f ]
|
@@ -463,23 +650,16 @@ class PDFBeads::PDFBuilder
|
|
463
650
|
return out
|
464
651
|
end
|
465
652
|
|
466
|
-
def elementText( elem
|
467
|
-
|
468
|
-
|
469
|
-
txt = elem.to_plain_text.strip
|
470
|
-
txt = Iconv.iconv( 'utf-8',charset,txt ).first unless charset.downcase.eql? 'utf-8'
|
471
|
-
rescue
|
472
|
-
end
|
473
|
-
|
474
|
-
txt.force_encoding( 'utf-8' ) if txt.respond_to? :force_encoding
|
475
|
-
return txt
|
653
|
+
def elementText( elem )
|
654
|
+
# used to put some Iconv stuff here, but nokogiri makes this conversion itself
|
655
|
+
return elem.inner_text.strip
|
476
656
|
end
|
477
657
|
|
478
|
-
def getOCRUnits( ocr_line,lbbox,fsize,
|
658
|
+
def getOCRUnits( ocr_line,lbbox,fsize,xscale,yscale )
|
479
659
|
units = Array.new()
|
480
|
-
ocr_words = ocr_line.
|
660
|
+
ocr_words = ocr_line.xpath(".//span[@class='ocrx_word']")
|
481
661
|
ocr_chars = nil
|
482
|
-
ocr_chars = ocr_line.
|
662
|
+
ocr_chars = ocr_line.at_xpath(".//span[@class='ocr_cinfo']") if ocr_words.length == 0
|
483
663
|
|
484
664
|
# If 'ocrx_word' elements are available (as in Tesseract owtput), split the line
|
485
665
|
# into individual words
|
@@ -487,16 +667,16 @@ class PDFBeads::PDFBuilder
|
|
487
667
|
ocr_words.each do |word|
|
488
668
|
bbox = elementCoordinates( word,xscale,yscale )
|
489
669
|
next if bbox == [0,0,0,0]
|
490
|
-
txt = elementText( word
|
670
|
+
txt = elementText( word )
|
491
671
|
units << [txt,bbox]
|
492
672
|
end
|
493
673
|
|
494
|
-
# If 'ocrx_cinfo' data is available (as in Cuneiform) owtput, then split it
|
674
|
+
# If 'ocrx_cinfo' data is available (as in Cuneiform) owtput, then split it
|
495
675
|
# into individual characters and then combine them into words
|
496
|
-
elsif not ocr_chars.nil? and ocr_chars.attributes.
|
497
|
-
if /x_bboxes([-\s\d]+)/.match( ocr_chars.attributes
|
676
|
+
elsif not ocr_chars.nil? and ocr_chars.attributes.has_key? 'title'
|
677
|
+
if /x_bboxes([-\s\d]+)/.match( ocr_chars.attributes['title'].content )
|
498
678
|
coords = $1.strip.split(/\s+/)
|
499
|
-
ltxt = elementText( ocr_line
|
679
|
+
ltxt = elementText( ocr_line )
|
500
680
|
charcnt = 0
|
501
681
|
ltxt.each_char { |uc| charcnt += 1 }
|
502
682
|
|
@@ -521,10 +701,11 @@ class PDFBeads::PDFBuilder
|
|
521
701
|
if /^\s+$/.match( uc )
|
522
702
|
wtxt = ''
|
523
703
|
|
524
|
-
# A workaround for probable hpricot bug
|
525
|
-
# characters from inside a string
|
526
|
-
# a bounding box with negative values
|
527
|
-
# character here, even if not
|
704
|
+
# A workaround for probable hpricot bug (TODO: is Nokogiri affected?),
|
705
|
+
# which sometimes causes whitespace characters from inside a string
|
706
|
+
# to be stripped. So if we find a bounding box with negative values
|
707
|
+
# we assume there was a whitespace character here, even if not
|
708
|
+
# preserved in the string itself
|
528
709
|
else
|
529
710
|
wtxt = uc
|
530
711
|
i += 1
|
@@ -541,7 +722,7 @@ class PDFBeads::PDFBuilder
|
|
541
722
|
|
542
723
|
# If neither word nor character bounding boxes are available, then store the line as a whole
|
543
724
|
if units.length == 0
|
544
|
-
ltxt = elementText( ocr_line
|
725
|
+
ltxt = elementText( ocr_line )
|
545
726
|
units << [ltxt,lbbox] unless ltxt.eql? ''
|
546
727
|
end
|
547
728
|
|
@@ -549,22 +730,15 @@ class PDFBeads::PDFBuilder
|
|
549
730
|
return units
|
550
731
|
end
|
551
732
|
|
552
|
-
def
|
733
|
+
def getHOCRText( hocr,pheight,xscale,yscale,encodings )
|
553
734
|
fsize = 10
|
554
735
|
cur_enc = nil
|
555
736
|
ret = " BT 3 Tr "
|
556
737
|
|
557
|
-
|
558
|
-
hocr.search("//meta[@http-equiv='Content-Type']").each do |el|
|
559
|
-
attrs = el.attributes.to_hash
|
560
|
-
charset = $1 if attrs.has_key? 'content' and
|
561
|
-
/\Atext\/html;charset=([A-Za-z0-9-]+)\Z/i.match( attrs['content'] )
|
562
|
-
end
|
563
|
-
|
564
|
-
hocr.search("//span[@class='ocr_line']").each do |line|
|
738
|
+
hocr.xpath("//span[@class='ocr_line']").each do |line|
|
565
739
|
lbbox = elementCoordinates( line,xscale,yscale )
|
566
740
|
next if lbbox[2] - lbbox[0] <= 0 or lbbox[3] - lbbox[1] <= 0
|
567
|
-
units = getOCRUnits( line,lbbox,fsize,
|
741
|
+
units = getOCRUnits( line,lbbox,fsize,xscale,yscale )
|
568
742
|
next if units.length == 0
|
569
743
|
|
570
744
|
wwidth = 0
|
@@ -573,7 +747,9 @@ class PDFBeads::PDFBuilder
|
|
573
747
|
ltxt << unit[0]
|
574
748
|
wwidth += ( unit[1][2] - unit[1][0] )
|
575
749
|
end
|
576
|
-
|
750
|
+
lw = @fdata.getLineWidth( ltxt,fsize )
|
751
|
+
ratio = 1
|
752
|
+
ratio = wwidth / lw unless lw == 0
|
577
753
|
pos = lbbox[0]
|
578
754
|
posdiff = 0
|
579
755
|
|
@@ -592,7 +768,11 @@ class PDFBeads::PDFBuilder
|
|
592
768
|
txt8 = ''
|
593
769
|
wtxt.each_char do |char|
|
594
770
|
begin
|
595
|
-
|
771
|
+
if char.respond_to? :encode
|
772
|
+
char.encode!( "utf-16be", "utf-8" )
|
773
|
+
else
|
774
|
+
Iconv.iconv( "utf-16be","utf-8",char )
|
775
|
+
end
|
596
776
|
rescue
|
597
777
|
rawbytes = char.unpack( 'C*' )
|
598
778
|
bs = ''
|