pdfbeads 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 = ''
|