hexapdf 0.2.0 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +33 -1
- data/CONTRIBUTERS +1 -1
- data/LICENSE +1 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/hexapdf.rb +1 -1
- data/lib/hexapdf/cli.rb +19 -52
- data/lib/hexapdf/cli/command.rb +251 -0
- data/lib/hexapdf/cli/{extract.rb → files.rb} +19 -23
- data/lib/hexapdf/cli/images.rb +147 -0
- data/lib/hexapdf/cli/info.rb +5 -5
- data/lib/hexapdf/cli/inspect.rb +13 -12
- data/lib/hexapdf/cli/merge.rb +200 -0
- data/lib/hexapdf/cli/modify.rb +39 -242
- data/lib/hexapdf/cli/optimize.rb +104 -0
- data/lib/hexapdf/configuration.rb +1 -1
- data/lib/hexapdf/content.rb +1 -1
- data/lib/hexapdf/content/canvas.rb +1 -1
- data/lib/hexapdf/content/color_space.rb +1 -1
- data/lib/hexapdf/content/graphic_object.rb +1 -1
- data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
- data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
- data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
- data/lib/hexapdf/content/graphics_state.rb +1 -1
- data/lib/hexapdf/content/operator.rb +1 -1
- data/lib/hexapdf/content/parser.rb +16 -15
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/content/transformation_matrix.rb +1 -1
- data/lib/hexapdf/data_dir.rb +1 -1
- data/lib/hexapdf/dictionary.rb +1 -1
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/document.rb +1 -1
- data/lib/hexapdf/document/files.rb +1 -1
- data/lib/hexapdf/document/fonts.rb +1 -1
- data/lib/hexapdf/document/images.rb +1 -1
- data/lib/hexapdf/document/pages.rb +1 -1
- data/lib/hexapdf/encryption.rb +1 -1
- data/lib/hexapdf/encryption/aes.rb +1 -1
- data/lib/hexapdf/encryption/arc4.rb +1 -1
- data/lib/hexapdf/encryption/fast_aes.rb +1 -1
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/identity.rb +1 -1
- data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
- data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +1 -1
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
- data/lib/hexapdf/error.rb +1 -1
- data/lib/hexapdf/filter.rb +1 -1
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
- data/lib/hexapdf/filter/dct_decode.rb +1 -1
- data/lib/hexapdf/filter/encryption.rb +1 -1
- data/lib/hexapdf/filter/flate_decode.rb +1 -1
- data/lib/hexapdf/filter/jpx_decode.rb +1 -1
- data/lib/hexapdf/filter/lzw_decode.rb +2 -3
- data/lib/hexapdf/filter/predictor.rb +11 -11
- data/lib/hexapdf/filter/run_length_decode.rb +1 -1
- data/lib/hexapdf/font/cmap.rb +1 -1
- data/lib/hexapdf/font/cmap/parser.rb +1 -1
- data/lib/hexapdf/font/cmap/writer.rb +1 -1
- data/lib/hexapdf/font/encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/base.rb +1 -1
- data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/glyph_list.rb +1 -1
- data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
- data/lib/hexapdf/font/true_type.rb +2 -1
- data/lib/hexapdf/font/true_type/font.rb +1 -1
- data/lib/hexapdf/font/true_type/subsetter.rb +186 -0
- data/lib/hexapdf/font/true_type/table.rb +8 -4
- data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
- data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
- data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
- data/lib/hexapdf/font/true_type/table/glyf.rb +6 -2
- data/lib/hexapdf/font/true_type/table/head.rb +2 -2
- data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
- data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
- data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
- data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
- data/lib/hexapdf/font/true_type/table/name.rb +1 -1
- data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
- data/lib/hexapdf/font/true_type/table/post.rb +1 -1
- data/lib/hexapdf/font/true_type_wrapper.rb +56 -8
- data/lib/hexapdf/font/type1.rb +1 -1
- data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
- data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
- data/lib/hexapdf/font/type1/font.rb +1 -1
- data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
- data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
- data/lib/hexapdf/font/type1_wrapper.rb +1 -1
- data/lib/hexapdf/font_loader.rb +1 -1
- data/lib/hexapdf/font_loader/from_configuration.rb +6 -3
- data/lib/hexapdf/font_loader/standard14.rb +1 -1
- data/lib/hexapdf/image_loader.rb +1 -1
- data/lib/hexapdf/image_loader/jpeg.rb +1 -1
- data/lib/hexapdf/image_loader/pdf.rb +1 -1
- data/lib/hexapdf/image_loader/png.rb +1 -1
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/name_tree_node.rb +1 -1
- data/lib/hexapdf/number_tree_node.rb +1 -1
- data/lib/hexapdf/object.rb +1 -1
- data/lib/hexapdf/parser.rb +1 -1
- data/lib/hexapdf/rectangle.rb +1 -1
- data/lib/hexapdf/reference.rb +1 -1
- data/lib/hexapdf/revision.rb +1 -1
- data/lib/hexapdf/revisions.rb +13 -15
- data/lib/hexapdf/serializer.rb +7 -3
- data/lib/hexapdf/stream.rb +1 -1
- data/lib/hexapdf/task.rb +1 -1
- data/lib/hexapdf/task/dereference.rb +1 -1
- data/lib/hexapdf/task/optimize.rb +1 -1
- data/lib/hexapdf/tokenizer.rb +12 -12
- data/lib/hexapdf/type.rb +1 -1
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/embedded_file.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_descriptor.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +1 -1
- data/lib/hexapdf/type/font_type1.rb +1 -1
- data/lib/hexapdf/type/form.rb +1 -1
- data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
- data/lib/hexapdf/type/image.rb +187 -1
- data/lib/hexapdf/type/info.rb +1 -1
- data/lib/hexapdf/type/names.rb +1 -1
- data/lib/hexapdf/type/object_stream.rb +1 -1
- data/lib/hexapdf/type/page.rb +1 -1
- data/lib/hexapdf/type/page_tree_node.rb +6 -1
- data/lib/hexapdf/type/resources.rb +1 -1
- data/lib/hexapdf/type/trailer.rb +2 -2
- data/lib/hexapdf/type/viewer_preferences.rb +1 -1
- data/lib/hexapdf/type/xref_stream.rb +22 -18
- data/lib/hexapdf/utils/bit_field.rb +1 -1
- data/lib/hexapdf/utils/bit_stream.rb +16 -32
- data/lib/hexapdf/utils/lru_cache.rb +1 -1
- data/lib/hexapdf/utils/math_helpers.rb +1 -1
- data/lib/hexapdf/utils/object_hash.rb +1 -1
- data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
- data/lib/hexapdf/version.rb +2 -2
- data/lib/hexapdf/writer.rb +2 -1
- data/lib/hexapdf/xref_section.rb +6 -1
- data/man/man1/hexapdf.1 +194 -115
- data/test/data/images/greyscale-1bit.png +0 -0
- data/test/data/images/greyscale-2bit.png +0 -0
- data/test/data/images/greyscale-8bit.png +0 -0
- data/test/data/images/indexed-alpha-4bit.png +0 -0
- data/test/data/images/truecolour-8bit.png +0 -0
- data/test/hexapdf/content/test_operator.rb +8 -8
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/encryption/test_security_handler.rb +1 -1
- data/test/hexapdf/font/test_true_type_wrapper.rb +89 -48
- data/test/hexapdf/font/true_type/table/test_glyf.rb +1 -0
- data/test/hexapdf/font/true_type/test_subsetter.rb +70 -0
- data/test/hexapdf/font/true_type/test_table.rb +16 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +7 -0
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_revisions.rb +34 -8
- data/test/hexapdf/test_serializer.rb +3 -0
- data/test/hexapdf/test_writer.rb +11 -2
- data/test/hexapdf/test_xref_section.rb +15 -0
- data/test/hexapdf/type/test_image.rb +234 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -2
- data/test/hexapdf/type/test_trailer.rb +4 -0
- data/test/hexapdf/utils/test_bit_stream.rb +69 -0
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecec6549dbb8ccc811ea329857ff768bb248d9ca
|
|
4
|
+
data.tar.gz: 2e04be0ab4a81fc11491314b18ad95411dc2a0cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a447429d78977fb78336e4f000d4b53d13bb40fd9e27a4d5a02b9a984d6e2fad76dbbe4d8a069236e24d1295b49ec996eb09d416cbe2668018b3331109e4df1
|
|
7
|
+
data.tar.gz: aba92a4c25811ab09146a3dafb544194c4aa42373098137ffe339061c1b779c5239afd092521cfe683d4bc8b9e11e609e108213e94375894c854278dac98be41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
## 0.
|
|
1
|
+
## 0.3.0 - 2017-01-25
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* TrueType font subsetting support
|
|
6
|
+
* Image extraction ability to CLI via `hexapdf images` command
|
|
7
|
+
* [HexaPDF::Type::Image#write] for writing an image XObject to an IO stream or
|
|
8
|
+
file
|
|
9
|
+
* [HexaPDF::Type::Image#info] for getting image properties of an image XObject
|
|
10
|
+
* CLI option `--[no-]force` to force overwriting existing files
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
* Refactor `hexapdf modify` command into three individual commands `modify`,
|
|
15
|
+
`merge` and `optimize`
|
|
16
|
+
* Rename `hexapdf extract` to `hexapdf files` and the option `--indices` to
|
|
17
|
+
`--extract`
|
|
18
|
+
* Show PDF trailer by default with `hexapdf inspect`
|
|
19
|
+
* Refactor CLI command classes to use specialized superclass
|
|
20
|
+
[HexaPDF::CLI::Command]
|
|
21
|
+
* Optimize parsing of PDF files for better performance and memory efficiency
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* Writing of hybrid-reference PDF files - they are written as standard PDF files
|
|
26
|
+
since all current applications should be able to handle PDF 1.5
|
|
27
|
+
* Serialization of self-referential, indirect PDF objects
|
|
28
|
+
* Performance problem for `hexapdf inspect --pages` when inspecting huge files
|
|
29
|
+
* TrueType compound glyph component offset calculation
|
|
30
|
+
* Parsing of TrueType data type 'fixed'
|
|
31
|
+
* Updating a PDF trailer's ID field when it isn't an array
|
|
32
|
+
|
|
33
|
+
## 0.2.0 - 2016-11-28
|
|
2
34
|
|
|
3
35
|
### Added
|
|
4
36
|
|
data/CONTRIBUTERS
CHANGED
data/LICENSE
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
2
|
-
Copyright (C)
|
|
2
|
+
Copyright (C) 2014-2017 Thomas Leitner
|
|
3
3
|
|
|
4
4
|
HexaPDF is free software: you can redistribute it and/or modify it
|
|
5
5
|
under the terms of the GNU Affero General Public License version 3 as
|
data/Rakefile
CHANGED
|
@@ -64,7 +64,7 @@ namespace :dev do
|
|
|
64
64
|
s.require_path = 'lib'
|
|
65
65
|
s.executables = ['hexapdf']
|
|
66
66
|
s.default_executable = 'hexapdf'
|
|
67
|
-
s.add_dependency('cmdparse', '~> 3.0', '>= 3.0.
|
|
67
|
+
s.add_dependency('cmdparse', '~> 3.0', '>= 3.0.3')
|
|
68
68
|
s.add_development_dependency('kramdown', '~> 1.0', '>= 1.13.0')
|
|
69
69
|
|
|
70
70
|
s.author = 'Thomas Leitner'
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
data/lib/hexapdf.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# This file is part of HexaPDF.
|
|
5
5
|
#
|
|
6
6
|
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
-
# Copyright (C)
|
|
7
|
+
# Copyright (C) 2014-2017 Thomas Leitner
|
|
8
8
|
#
|
|
9
9
|
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
10
|
# under the terms of the GNU Affero General Public License version 3 as
|
data/lib/hexapdf/cli.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# This file is part of HexaPDF.
|
|
5
5
|
#
|
|
6
6
|
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
-
# Copyright (C)
|
|
7
|
+
# Copyright (C) 2014-2017 Thomas Leitner
|
|
8
8
|
#
|
|
9
9
|
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
10
|
# under the terms of the GNU Affero General Public License version 3 as
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
# is created or manipulated using HexaPDF.
|
|
32
32
|
#++
|
|
33
33
|
|
|
34
|
-
require 'io/console'
|
|
35
34
|
require 'cmdparse'
|
|
36
35
|
require 'hexapdf/cli/info'
|
|
37
|
-
require 'hexapdf/cli/
|
|
36
|
+
require 'hexapdf/cli/files'
|
|
38
37
|
require 'hexapdf/cli/inspect'
|
|
39
38
|
require 'hexapdf/cli/modify'
|
|
39
|
+
require 'hexapdf/cli/merge'
|
|
40
|
+
require 'hexapdf/cli/optimize'
|
|
41
|
+
require 'hexapdf/cli/images'
|
|
40
42
|
require 'hexapdf/version'
|
|
41
43
|
require 'hexapdf/document'
|
|
42
44
|
|
|
@@ -50,23 +52,35 @@ module HexaPDF
|
|
|
50
52
|
def self.run(args = ARGV)
|
|
51
53
|
Application.new.parse(args)
|
|
52
54
|
rescue => e
|
|
53
|
-
$stderr.puts "
|
|
55
|
+
$stderr.puts "Problem encountered: #{e.message}"
|
|
54
56
|
exit(1)
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
# The CmdParse::CommandParser class that is used for running the CLI application.
|
|
58
60
|
class Application < CmdParse::CommandParser
|
|
59
61
|
|
|
62
|
+
# Specifies whether an operation should be forced. For example, if an existing file should be
|
|
63
|
+
# overwritten.
|
|
64
|
+
attr_reader :force
|
|
65
|
+
|
|
60
66
|
def initialize #:nodoc:
|
|
61
67
|
super(handle_exceptions: :no_help)
|
|
62
68
|
main_command.options.program_name = "hexapdf"
|
|
63
69
|
main_command.options.version = HexaPDF::VERSION
|
|
64
70
|
add_command(HexaPDF::CLI::Info.new)
|
|
65
|
-
add_command(HexaPDF::CLI::
|
|
71
|
+
add_command(HexaPDF::CLI::Files.new)
|
|
72
|
+
add_command(HexaPDF::CLI::Images.new)
|
|
66
73
|
add_command(HexaPDF::CLI::Inspect.new)
|
|
67
74
|
add_command(HexaPDF::CLI::Modify.new)
|
|
75
|
+
add_command(HexaPDF::CLI::Optimize.new)
|
|
76
|
+
add_command(HexaPDF::CLI::Merge.new)
|
|
68
77
|
add_command(CmdParse::HelpCommand.new)
|
|
69
78
|
add_command(CmdParse::VersionCommand.new)
|
|
79
|
+
|
|
80
|
+
@force = false
|
|
81
|
+
global_options.on("--[no-]force", "Force overwriting existing files. Default: false") do |f|
|
|
82
|
+
@force = f
|
|
83
|
+
end
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
def parse(argv = ARGV) #:nodoc:
|
|
@@ -74,53 +88,6 @@ module HexaPDF
|
|
|
74
88
|
super
|
|
75
89
|
end
|
|
76
90
|
|
|
77
|
-
PAGE_NUMBER_SPEC = "([1-9]\\d*|e)".freeze #:nodoc:
|
|
78
|
-
ROTATE_MAP = {'l' => -90, 'r' => 90, 'd' => 180, 'n' => :none}.freeze #:nodoc:
|
|
79
|
-
|
|
80
|
-
# Parses the pages specification string and returns an array of tuples containing a page
|
|
81
|
-
# number and a rotation value (either -90, 90, 180 or :none).
|
|
82
|
-
#
|
|
83
|
-
# The parameter +count+ needs to be the total number of pages in the document.
|
|
84
|
-
def parse_pages_specification(range, count)
|
|
85
|
-
range.split(',').each_with_object([]) do |str, arr|
|
|
86
|
-
case str
|
|
87
|
-
when /\A#{PAGE_NUMBER_SPEC}(l|r|d|n)?\z/o
|
|
88
|
-
arr << [($1 == 'e' ? count : str.to_i) - 1, ROTATE_MAP[$2]]
|
|
89
|
-
when /\A#{PAGE_NUMBER_SPEC}-#{PAGE_NUMBER_SPEC}(?:\/([1-9]\d*))?(l|r|d|n)?\z/
|
|
90
|
-
start_nr = ($1 == 'e' ? count : $1.to_i) - 1
|
|
91
|
-
end_nr = ($2 == 'e' ? count : $2.to_i) - 1
|
|
92
|
-
step = ($3 ? $3.to_i : 1) * (start_nr > end_nr ? -1 : 1)
|
|
93
|
-
rotation = ROTATE_MAP[$4]
|
|
94
|
-
start_nr.step(to: end_nr, by: step) {|n| arr << [n, rotation]}
|
|
95
|
-
else
|
|
96
|
-
raise OptionParser::InvalidArgument, "invalid page range format: #{str}"
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Reads a password from the standard input and falls back to the console if needed.
|
|
102
|
-
#
|
|
103
|
-
# The optional argument +prompt+ can be used to customize the prompt when reading from the
|
|
104
|
-
# console.
|
|
105
|
-
def read_password(prompt = "Password")
|
|
106
|
-
if $stdin.tty?
|
|
107
|
-
read_from_console(prompt)
|
|
108
|
-
else
|
|
109
|
-
pwd = $stdin.gets
|
|
110
|
-
pwd = read_from_console(prompt) unless pwd
|
|
111
|
-
pwd.chomp
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
# Displays the given prompt, reads from the console without echo and returns the read string.
|
|
118
|
-
def read_from_console(prompt)
|
|
119
|
-
IO.console.write("#{prompt}: ")
|
|
120
|
-
str = IO.console.noecho {|io| io.gets.chomp}
|
|
121
|
-
puts
|
|
122
|
-
str
|
|
123
|
-
end
|
|
124
91
|
end
|
|
125
92
|
|
|
126
93
|
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2017 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#++
|
|
33
|
+
|
|
34
|
+
require 'io/console'
|
|
35
|
+
require 'ostruct'
|
|
36
|
+
require 'cmdparse'
|
|
37
|
+
require 'hexapdf/document'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module CLI
|
|
41
|
+
|
|
42
|
+
# Base class for all hexapdf commands. It provides utility methods needed by the individual
|
|
43
|
+
# commands.
|
|
44
|
+
class Command < CmdParse::Command
|
|
45
|
+
|
|
46
|
+
def initialize(*args, &block) #:nodoc:
|
|
47
|
+
super
|
|
48
|
+
@out_options = OpenStruct.new
|
|
49
|
+
@out_options.compact = true
|
|
50
|
+
@out_options.compress_pages = false
|
|
51
|
+
@out_options.object_streams = :preserve
|
|
52
|
+
@out_options.xref_streams = :preserve
|
|
53
|
+
@out_options.streams = :preserve
|
|
54
|
+
|
|
55
|
+
@out_options.encryption = :preserve
|
|
56
|
+
@out_options.enc_user_pwd = @out_options.enc_owner_pwd = nil
|
|
57
|
+
@out_options.enc_key_length = 128
|
|
58
|
+
@out_options.enc_algorithm = :aes
|
|
59
|
+
@out_options.enc_force_v4 = false
|
|
60
|
+
@out_options.enc_permissions = []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
protected
|
|
64
|
+
|
|
65
|
+
# Checks whether the given output file exists and raises an error if it does and
|
|
66
|
+
# HexaPDF::CLI#force is not set.
|
|
67
|
+
def maybe_raise_on_existing_file(filename)
|
|
68
|
+
if !command_parser.force && File.exist?(filename)
|
|
69
|
+
raise "Output file '#{filename}' already exists, not overwriting. Use --force to " \
|
|
70
|
+
"force writing"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Defines the optimization options.
|
|
75
|
+
#
|
|
76
|
+
# See: #out_options, #apply_optimization_options
|
|
77
|
+
def define_optimization_options
|
|
78
|
+
options.on("--[no-]compact", "Delete unnecessary PDF objects (default: " \
|
|
79
|
+
"#{@out_options.compact})") do |c|
|
|
80
|
+
@out_options.compact = c
|
|
81
|
+
end
|
|
82
|
+
options.on("--object-streams MODE", [:generate, :preserve, :delete],
|
|
83
|
+
"Handling of object streams (either generate, preserve or delete; " \
|
|
84
|
+
"default: #{@out_options.object_streams})") do |os|
|
|
85
|
+
@out_options.object_streams = os
|
|
86
|
+
end
|
|
87
|
+
options.on("--xref-streams MODE", [:generate, :preserve, :delete],
|
|
88
|
+
"Handling of cross-reference streams (either generate, preserve or delete; " \
|
|
89
|
+
"default: #{@out_options.xref_streams})") do |x|
|
|
90
|
+
@out_options.xref_streams = x
|
|
91
|
+
end
|
|
92
|
+
options.on("--streams MODE", [:compress, :preserve, :uncompress],
|
|
93
|
+
"Handling of stream data (either compress, preserve or uncompress; default: " \
|
|
94
|
+
"#{@out_options.streams})") do |streams|
|
|
95
|
+
@out_options.streams = streams
|
|
96
|
+
end
|
|
97
|
+
options.on("--[no-]compress-pages", "Recompress page content streams (may take a long " \
|
|
98
|
+
"time; default: #{@out_options.compress_pages})") do |c|
|
|
99
|
+
@out_options.compress_pages = c
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Defines the encryption options.
|
|
104
|
+
#
|
|
105
|
+
# See: #out_options, #apply_encryption_options
|
|
106
|
+
def define_encryption_options
|
|
107
|
+
options.on("--decrypt", "Remove any encryption") do
|
|
108
|
+
@out_options.encryption = :remove
|
|
109
|
+
end
|
|
110
|
+
options.on("--encrypt", "Encrypt the output file") do
|
|
111
|
+
@out_options.encryption = :add
|
|
112
|
+
end
|
|
113
|
+
options.on("--owner-password PASSWORD", String, "The owner password to be set on the " \
|
|
114
|
+
"output file (use - for reading from standard input)") do |pwd|
|
|
115
|
+
@out_options.encryption = :add
|
|
116
|
+
@out_options.enc_owner_pwd = (pwd == '-' ? read_password("Owner password") : pwd)
|
|
117
|
+
end
|
|
118
|
+
options.on("--user-password PASSWORD", String, "The user password to be set on the " \
|
|
119
|
+
"output file (use - for reading from standard input)") do |pwd|
|
|
120
|
+
@out_options.encryption = :add
|
|
121
|
+
@out_options.enc_user_pwd = (pwd == '-' ? read_password("User password") : pwd)
|
|
122
|
+
end
|
|
123
|
+
options.on("--algorithm ALGORITHM", [:aes, :arc4],
|
|
124
|
+
"The encryption algorithm: aes or arc4 (default: " \
|
|
125
|
+
"#{@out_options.enc_algorithm})") do |a|
|
|
126
|
+
@out_options.encryption = :add
|
|
127
|
+
@out_options.enc_algorithm = a
|
|
128
|
+
end
|
|
129
|
+
options.on("--key-length BITS", Integer,
|
|
130
|
+
"The encryption key length in bits (default: " \
|
|
131
|
+
"#{@out_options.enc_key_length})") do |i|
|
|
132
|
+
@out_options.encryption = :add
|
|
133
|
+
@out_options.enc_key_length = i
|
|
134
|
+
end
|
|
135
|
+
options.on("--force-V4",
|
|
136
|
+
"Force use of encryption version 4 if key length=128 and algorithm=arc4") do
|
|
137
|
+
@out_options.encryption = :add
|
|
138
|
+
@out_options.enc_force_v4 = true
|
|
139
|
+
end
|
|
140
|
+
syms = HexaPDF::Encryption::StandardSecurityHandler::Permissions::SYMBOL_TO_PERMISSION.keys
|
|
141
|
+
options.on("--permissions PERMS", Array,
|
|
142
|
+
"Comma separated list of permissions to be set on the output file. Possible " \
|
|
143
|
+
"values: #{syms.join(', ')}") do |perms|
|
|
144
|
+
perms.map! do |perm|
|
|
145
|
+
unless syms.include?(perm.to_sym)
|
|
146
|
+
raise OptionParser::InvalidArgument, "#{perm} (invalid permission name)"
|
|
147
|
+
end
|
|
148
|
+
perm.to_sym
|
|
149
|
+
end
|
|
150
|
+
@out_options.encryption = :add
|
|
151
|
+
@out_options.enc_permissions = perms
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Applies the optimization options to the given HexaPDF::Document instance.
|
|
156
|
+
#
|
|
157
|
+
# See: #define_optimization_options
|
|
158
|
+
def apply_optimization_options(doc)
|
|
159
|
+
doc.task(:optimize, compact: @out_options.compact,
|
|
160
|
+
object_streams: @out_options.object_streams,
|
|
161
|
+
xref_streams: @out_options.xref_streams,
|
|
162
|
+
compress_pages: @out_options.compress_pages)
|
|
163
|
+
handle_streams(doc) unless @out_options.streams == :preserve
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
IGNORED_FILTERS = { #:nodoc:
|
|
167
|
+
CCITTFaxDecode: true, JBIG2Decode: true, DCTDecode: true, JPXDecode: true, Crypt: true
|
|
168
|
+
}.freeze
|
|
169
|
+
|
|
170
|
+
# Applies the chosen stream mode to all streams.
|
|
171
|
+
def handle_streams(doc)
|
|
172
|
+
doc.each(current: false) do |obj|
|
|
173
|
+
next if !obj.respond_to?(:set_filter) || Array(obj[:Filter]).any? {|f| IGNORED_FILTERS[f]}
|
|
174
|
+
if @out_options.streams == :compress
|
|
175
|
+
obj.set_filter(:FlateDecode)
|
|
176
|
+
else
|
|
177
|
+
obj.set_filter(nil)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Applies the encryption related options to the given HexaPDF::Document instance.
|
|
183
|
+
#
|
|
184
|
+
# See: #define_encryption_options
|
|
185
|
+
def apply_encryption_options(doc)
|
|
186
|
+
if @out_options.encryption == :add
|
|
187
|
+
doc.encrypt(algorithm: @out_options.enc_algorithm,
|
|
188
|
+
key_length: @out_options.enc_key_length,
|
|
189
|
+
force_V4: @out_options.enc_force_v4,
|
|
190
|
+
permissions: @out_options.enc_permissions,
|
|
191
|
+
owner_password: @out_options.enc_owner_pwd,
|
|
192
|
+
user_password: @out_options.enc_user_pwd)
|
|
193
|
+
elsif @out_options.encryption == :remove
|
|
194
|
+
doc.encrypt(name: nil)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
PAGE_NUMBER_SPEC = "([1-9]\\d*|e)".freeze #:nodoc:
|
|
199
|
+
ROTATE_MAP = {'l' => -90, 'r' => 90, 'd' => 180, 'n' => :none}.freeze #:nodoc:
|
|
200
|
+
|
|
201
|
+
# Parses the pages specification string and returns an array of tuples containing a page
|
|
202
|
+
# number and a rotation value (either -90, 90, 180 or :none).
|
|
203
|
+
#
|
|
204
|
+
# The parameter +count+ needs to be the total number of pages in the document.
|
|
205
|
+
#
|
|
206
|
+
# For details on the pages specification see the hexapdf(1) manual page.
|
|
207
|
+
def parse_pages_specification(range, count)
|
|
208
|
+
range.split(',').each_with_object([]) do |str, arr|
|
|
209
|
+
case str
|
|
210
|
+
when /\A#{PAGE_NUMBER_SPEC}(l|r|d|n)?\z/o
|
|
211
|
+
arr << [($1 == 'e' ? count : str.to_i) - 1, ROTATE_MAP[$2]]
|
|
212
|
+
when /\A#{PAGE_NUMBER_SPEC}-#{PAGE_NUMBER_SPEC}(?:\/([1-9]\d*))?(l|r|d|n)?\z/
|
|
213
|
+
start_nr = ($1 == 'e' ? count : $1.to_i) - 1
|
|
214
|
+
end_nr = ($2 == 'e' ? count : $2.to_i) - 1
|
|
215
|
+
step = ($3 ? $3.to_i : 1) * (start_nr > end_nr ? -1 : 1)
|
|
216
|
+
rotation = ROTATE_MAP[$4]
|
|
217
|
+
start_nr.step(to: end_nr, by: step) {|n| arr << [n, rotation]}
|
|
218
|
+
else
|
|
219
|
+
raise OptionParser::InvalidArgument, "invalid page range format: #{str}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Reads a password from the standard input and falls back to the console if needed.
|
|
225
|
+
#
|
|
226
|
+
# The optional argument +prompt+ can be used to customize the prompt when reading from the
|
|
227
|
+
# console.
|
|
228
|
+
def read_password(prompt = "Password")
|
|
229
|
+
if $stdin.tty?
|
|
230
|
+
read_from_console(prompt)
|
|
231
|
+
else
|
|
232
|
+
pwd = $stdin.gets
|
|
233
|
+
pwd = read_from_console(prompt) unless pwd
|
|
234
|
+
pwd.chomp
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
# Displays the given prompt, reads from the console without echo and returns the read string.
|
|
241
|
+
def read_from_console(prompt)
|
|
242
|
+
IO.console.write("#{prompt}: ")
|
|
243
|
+
str = IO.console.noecho {|io| io.gets.chomp}
|
|
244
|
+
puts
|
|
245
|
+
str
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
end
|
|
251
|
+
end
|