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.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/LICENSE +1 -1
  5. data/Rakefile +1 -1
  6. data/VERSION +1 -1
  7. data/lib/hexapdf.rb +1 -1
  8. data/lib/hexapdf/cli.rb +19 -52
  9. data/lib/hexapdf/cli/command.rb +251 -0
  10. data/lib/hexapdf/cli/{extract.rb → files.rb} +19 -23
  11. data/lib/hexapdf/cli/images.rb +147 -0
  12. data/lib/hexapdf/cli/info.rb +5 -5
  13. data/lib/hexapdf/cli/inspect.rb +13 -12
  14. data/lib/hexapdf/cli/merge.rb +200 -0
  15. data/lib/hexapdf/cli/modify.rb +39 -242
  16. data/lib/hexapdf/cli/optimize.rb +104 -0
  17. data/lib/hexapdf/configuration.rb +1 -1
  18. data/lib/hexapdf/content.rb +1 -1
  19. data/lib/hexapdf/content/canvas.rb +1 -1
  20. data/lib/hexapdf/content/color_space.rb +1 -1
  21. data/lib/hexapdf/content/graphic_object.rb +1 -1
  22. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  23. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  24. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  25. data/lib/hexapdf/content/graphics_state.rb +1 -1
  26. data/lib/hexapdf/content/operator.rb +1 -1
  27. data/lib/hexapdf/content/parser.rb +16 -15
  28. data/lib/hexapdf/content/processor.rb +1 -1
  29. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  30. data/lib/hexapdf/data_dir.rb +1 -1
  31. data/lib/hexapdf/dictionary.rb +1 -1
  32. data/lib/hexapdf/dictionary_fields.rb +1 -1
  33. data/lib/hexapdf/document.rb +1 -1
  34. data/lib/hexapdf/document/files.rb +1 -1
  35. data/lib/hexapdf/document/fonts.rb +1 -1
  36. data/lib/hexapdf/document/images.rb +1 -1
  37. data/lib/hexapdf/document/pages.rb +1 -1
  38. data/lib/hexapdf/encryption.rb +1 -1
  39. data/lib/hexapdf/encryption/aes.rb +1 -1
  40. data/lib/hexapdf/encryption/arc4.rb +1 -1
  41. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  42. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  43. data/lib/hexapdf/encryption/identity.rb +1 -1
  44. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  45. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  46. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  47. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  48. data/lib/hexapdf/error.rb +1 -1
  49. data/lib/hexapdf/filter.rb +1 -1
  50. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  51. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  52. data/lib/hexapdf/filter/dct_decode.rb +1 -1
  53. data/lib/hexapdf/filter/encryption.rb +1 -1
  54. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  55. data/lib/hexapdf/filter/jpx_decode.rb +1 -1
  56. data/lib/hexapdf/filter/lzw_decode.rb +2 -3
  57. data/lib/hexapdf/filter/predictor.rb +11 -11
  58. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  59. data/lib/hexapdf/font/cmap.rb +1 -1
  60. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  61. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  62. data/lib/hexapdf/font/encoding.rb +1 -1
  63. data/lib/hexapdf/font/encoding/base.rb +1 -1
  64. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  65. data/lib/hexapdf/font/encoding/glyph_list.rb +1 -1
  66. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  67. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  68. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  69. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  70. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  71. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  72. data/lib/hexapdf/font/true_type.rb +2 -1
  73. data/lib/hexapdf/font/true_type/font.rb +1 -1
  74. data/lib/hexapdf/font/true_type/subsetter.rb +186 -0
  75. data/lib/hexapdf/font/true_type/table.rb +8 -4
  76. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  77. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  78. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  79. data/lib/hexapdf/font/true_type/table/glyf.rb +6 -2
  80. data/lib/hexapdf/font/true_type/table/head.rb +2 -2
  81. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  82. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  83. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  84. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  85. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  86. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  87. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  88. data/lib/hexapdf/font/true_type_wrapper.rb +56 -8
  89. data/lib/hexapdf/font/type1.rb +1 -1
  90. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  91. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  92. data/lib/hexapdf/font/type1/font.rb +1 -1
  93. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  94. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  95. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  96. data/lib/hexapdf/font_loader.rb +1 -1
  97. data/lib/hexapdf/font_loader/from_configuration.rb +6 -3
  98. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  99. data/lib/hexapdf/image_loader.rb +1 -1
  100. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  101. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  102. data/lib/hexapdf/image_loader/png.rb +1 -1
  103. data/lib/hexapdf/importer.rb +1 -1
  104. data/lib/hexapdf/name_tree_node.rb +1 -1
  105. data/lib/hexapdf/number_tree_node.rb +1 -1
  106. data/lib/hexapdf/object.rb +1 -1
  107. data/lib/hexapdf/parser.rb +1 -1
  108. data/lib/hexapdf/rectangle.rb +1 -1
  109. data/lib/hexapdf/reference.rb +1 -1
  110. data/lib/hexapdf/revision.rb +1 -1
  111. data/lib/hexapdf/revisions.rb +13 -15
  112. data/lib/hexapdf/serializer.rb +7 -3
  113. data/lib/hexapdf/stream.rb +1 -1
  114. data/lib/hexapdf/task.rb +1 -1
  115. data/lib/hexapdf/task/dereference.rb +1 -1
  116. data/lib/hexapdf/task/optimize.rb +1 -1
  117. data/lib/hexapdf/tokenizer.rb +12 -12
  118. data/lib/hexapdf/type.rb +1 -1
  119. data/lib/hexapdf/type/catalog.rb +1 -1
  120. data/lib/hexapdf/type/embedded_file.rb +1 -1
  121. data/lib/hexapdf/type/file_specification.rb +1 -1
  122. data/lib/hexapdf/type/font.rb +1 -1
  123. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  124. data/lib/hexapdf/type/font_simple.rb +1 -1
  125. data/lib/hexapdf/type/font_true_type.rb +1 -1
  126. data/lib/hexapdf/type/font_type1.rb +1 -1
  127. data/lib/hexapdf/type/form.rb +1 -1
  128. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  129. data/lib/hexapdf/type/image.rb +187 -1
  130. data/lib/hexapdf/type/info.rb +1 -1
  131. data/lib/hexapdf/type/names.rb +1 -1
  132. data/lib/hexapdf/type/object_stream.rb +1 -1
  133. data/lib/hexapdf/type/page.rb +1 -1
  134. data/lib/hexapdf/type/page_tree_node.rb +6 -1
  135. data/lib/hexapdf/type/resources.rb +1 -1
  136. data/lib/hexapdf/type/trailer.rb +2 -2
  137. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  138. data/lib/hexapdf/type/xref_stream.rb +22 -18
  139. data/lib/hexapdf/utils/bit_field.rb +1 -1
  140. data/lib/hexapdf/utils/bit_stream.rb +16 -32
  141. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  142. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  143. data/lib/hexapdf/utils/object_hash.rb +1 -1
  144. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  145. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  146. data/lib/hexapdf/version.rb +2 -2
  147. data/lib/hexapdf/writer.rb +2 -1
  148. data/lib/hexapdf/xref_section.rb +6 -1
  149. data/man/man1/hexapdf.1 +194 -115
  150. data/test/data/images/greyscale-1bit.png +0 -0
  151. data/test/data/images/greyscale-2bit.png +0 -0
  152. data/test/data/images/greyscale-8bit.png +0 -0
  153. data/test/data/images/indexed-alpha-4bit.png +0 -0
  154. data/test/data/images/truecolour-8bit.png +0 -0
  155. data/test/hexapdf/content/test_operator.rb +8 -8
  156. data/test/hexapdf/content/test_processor.rb +1 -1
  157. data/test/hexapdf/encryption/test_security_handler.rb +1 -1
  158. data/test/hexapdf/font/test_true_type_wrapper.rb +89 -48
  159. data/test/hexapdf/font/true_type/table/test_glyf.rb +1 -0
  160. data/test/hexapdf/font/true_type/test_subsetter.rb +70 -0
  161. data/test/hexapdf/font/true_type/test_table.rb +16 -0
  162. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -0
  163. data/test/hexapdf/test_document.rb +1 -1
  164. data/test/hexapdf/test_object.rb +1 -1
  165. data/test/hexapdf/test_revisions.rb +34 -8
  166. data/test/hexapdf/test_serializer.rb +3 -0
  167. data/test/hexapdf/test_writer.rb +11 -2
  168. data/test/hexapdf/test_xref_section.rb +15 -0
  169. data/test/hexapdf/type/test_image.rb +234 -0
  170. data/test/hexapdf/type/test_object_stream.rb +2 -2
  171. data/test/hexapdf/type/test_trailer.rb +4 -0
  172. data/test/hexapdf/utils/test_bit_stream.rb +69 -0
  173. metadata +14 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 874a5d2e5a2408ceed91a9bc61df138248fbbcd2
4
- data.tar.gz: 894e67110daae93c1479a5ced992a84dabc66cd1
3
+ metadata.gz: ecec6549dbb8ccc811ea329857ff768bb248d9ca
4
+ data.tar.gz: 2e04be0ab4a81fc11491314b18ad95411dc2a0cc
5
5
  SHA512:
6
- metadata.gz: 9d7c18191ea0d64992fc01b05904c3324cc1f32e98fe848bc931970b53f42d9ab6878d4e8c3730a570c607f677dfdaa8a8adebe7ab4c8e279cc13383124dee1b
7
- data.tar.gz: 1e080dd694ad58c3256a647a98a9b03c439ae340311b037fad3b1bdbe436ffbed74fec0c00af3df68540ca32aa75336c328a317a74233428dcf16d8920cc8e75
6
+ metadata.gz: 4a447429d78977fb78336e4f000d4b53d13bb40fd9e27a4d5a02b9a984d6e2fad76dbbe4d8a069236e24d1295b49ec996eb09d416cbe2668018b3331109e4df1
7
+ data.tar.gz: aba92a4c25811ab09146a3dafb544194c4aa42373098137ffe339061c1b779c5239afd092521cfe683d4bc8b9e11e609e108213e94375894c854278dac98be41
@@ -1,4 +1,36 @@
1
- ## 0.2.0 - unreleased
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
 
@@ -1,3 +1,3 @@
1
1
  Count Name
2
2
  ======= ====
3
- 647 Thomas Leitner <t_leitner@gmx.at>
3
+ 682 Thomas Leitner <t_leitner@gmx.at>
data/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
1
  HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
2
- Copyright (C) 2016 Thomas Leitner
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.1')
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.2.0
1
+ 0.3.0
@@ -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) 2016 Thomas Leitner
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
@@ -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) 2016 Thomas Leitner
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/extract'
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 "An error occurred: #{e.message}"
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::Extract.new)
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