prawnx 0.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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module PrawnX
6
+ module Errors
7
+ PdfAViolation = Class.new(StandardError)
8
+ end
9
+
10
+ # PDF/A-3b (ISO 19005-3), switched on with Prawn::Document.new(pdf_a: true): XMP metadata matching the
11
+ # document information, an sRGB output intent, a file identifier, and errors for what PDF/A forbids.
12
+ # Factur-X invoices (pdf.factur_x) embed their CII XML into such a document.
13
+ module PdfA
14
+ FACTUR_X_NAMESPACE = "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"
15
+ FACTUR_X_FILENAME = "factur-x.xml"
16
+ # Profile names of the Factur-X specification, and the relationship it gives the XML of each (§ 6.2.2).
17
+ FACTUR_X_LEVEL_BY_PROFILE = {
18
+ minimum: "MINIMUM", basic_wl: "BASIC WL", basic: "BASIC", en16931: "EN 16931", extended: "EXTENDED",
19
+ }.freeze
20
+ RELATIONSHIP_BY_PROFILE = {
21
+ minimum: :Data, basic_wl: :Data, basic: :Alternative, en16931: :Alternative, extended: :Alternative,
22
+ }.freeze
23
+ # Annotation flag 3, Print: PDF/A requires printing every annotation (ISO 19005-2 §6.3.2).
24
+ PRINT_FLAG = 4
25
+ XMP_PROPERTY_BY_INFO_KEY = {
26
+ Title: "dc:title", Author: "dc:creator", Subject: "dc:description", Keywords: "pdf:Keywords",
27
+ Creator: "xmp:CreatorTool", Producer: "pdf:Producer", CreationDate: "xmp:CreateDate", ModDate: "xmp:ModifyDate",
28
+ }.freeze
29
+
30
+ def pdf_a?
31
+ @pdf_a == true
32
+ end
33
+
34
+ def factur_x(xml, profile: :en16931, filename: FACTUR_X_FILENAME, description: "Factur-X invoice")
35
+ raise Errors::PdfAViolation, "Factur-X invoices are PDF/A-3 documents: create this one with pdf_a: true" unless pdf_a?
36
+
37
+ level = FACTUR_X_LEVEL_BY_PROFILE.fetch(profile) { raise ArgumentError, "Unknown Factur-X profile #{profile.inspect}: #{FACTUR_X_LEVEL_BY_PROFILE.keys.join(", ")}" }
38
+ @factur_x = {xml: xml.b, level:, relationship: RELATIONSHIP_BY_PROFILE.fetch(profile), filename:, description:}
39
+ end
40
+
41
+ private
42
+
43
+ def finalize_pdf_a
44
+ min_version(1.7)
45
+ icc_profile = ref!({N: 3}, PdfA::SrgbProfile.bytes)
46
+ @root.data[:OutputIntents] = [{
47
+ Type: :OutputIntent, S: :GTS_PDFA1, OutputConditionIdentifier: PDF::LiteralString.new(SrgbProfile::DESCRIPTION),
48
+ Info: PDF::LiteralString.new(SrgbProfile::DESCRIPTION), DestOutputProfile: icc_profile,
49
+ }]
50
+ embed_factur_x if @factur_x
51
+ metadata = ref!({Type: :Metadata, Subtype: :XML}, xmp_packet)
52
+ metadata.uncompressed = true
53
+ @root.data[:Metadata] = metadata
54
+ end
55
+
56
+ def embed_factur_x
57
+ xml = @factur_x[:xml]
58
+ modification_date = @info.data[:ModDate] || @info.data[:CreationDate]
59
+ params = {Size: xml.bytesize}
60
+ params[:ModDate] = modification_date if modification_date
61
+ embedded_file = ref!({Type: :EmbeddedFile, Subtype: :"text/xml", Params: params}, xml)
62
+ file_specification = ref!(
63
+ Type: :Filespec, F: PDF::LiteralString.new(@factur_x[:filename]), UF: @factur_x[:filename],
64
+ Desc: @factur_x[:description], AFRelationship: @factur_x[:relationship], EF: {F: embedded_file, UF: embedded_file},
65
+ )
66
+ @root.data[:AF] = [file_specification]
67
+ (names.data[:EmbeddedFiles] ||= ref!(PDF::NameTree.new)).data.add(@factur_x[:filename], file_specification)
68
+ end
69
+
70
+ def xmp_packet
71
+ properties = XMP_PROPERTY_BY_INFO_KEY.filter_map do |key, property|
72
+ value = @info.data[key]
73
+ next unless value
74
+
75
+ text = value.is_a?(Time) ? value.iso8601 : escape_xml(value.to_s)
76
+ case property
77
+ when "dc:title", "dc:description" then "<#{property}><rdf:Alt><rdf:li xml:lang=\"x-default\">#{text}</rdf:li></rdf:Alt></#{property}>"
78
+ when "dc:creator" then "<#{property}><rdf:Seq><rdf:li>#{text}</rdf:li></rdf:Seq></#{property}>"
79
+ else "<#{property}>#{text}</#{property}>"
80
+ end
81
+ end
82
+ <<~XMP
83
+ <?xpacket begin="\u{FEFF}" id="W5M0MpCehiHzreSzNTczkc9d"?>
84
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
85
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
86
+ <rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
87
+ <pdfaid:part>3</pdfaid:part>
88
+ <pdfaid:conformance>B</pdfaid:conformance>
89
+ #{properties.join("\n")}
90
+ </rdf:Description>
91
+ #{factur_x_xmp}
92
+ </rdf:RDF>
93
+ </x:xmpmeta>
94
+ <?xpacket end="w"?>
95
+ XMP
96
+ end
97
+
98
+ # The Factur-X properties, and the PDF/A extension schema declaring them (Factur-X specification § 6.2.1).
99
+ def factur_x_xmp
100
+ return "" unless @factur_x
101
+
102
+ fields = {DocumentFileName: "name of the embedded XML invoice file", DocumentType: "INVOICE",
103
+ Version: "The actual version of the Factur-X XML schema", ConformanceLevel: "The conformance level of the embedded Factur-X data"}
104
+ described = fields.map do |name, description|
105
+ "<rdf:li rdf:parseType=\"Resource\"><pdfaProperty:name>#{name}</pdfaProperty:name><pdfaProperty:valueType>Text</pdfaProperty:valueType>" \
106
+ "<pdfaProperty:category>external</pdfaProperty:category><pdfaProperty:description>#{description}</pdfaProperty:description></rdf:li>"
107
+ end
108
+ <<~XMP
109
+ <rdf:Description rdf:about="" xmlns:fx="#{FACTUR_X_NAMESPACE}">
110
+ <fx:DocumentType>INVOICE</fx:DocumentType>
111
+ <fx:DocumentFileName>#{escape_xml(@factur_x[:filename])}</fx:DocumentFileName>
112
+ <fx:Version>1.0</fx:Version>
113
+ <fx:ConformanceLevel>#{@factur_x[:level]}</fx:ConformanceLevel>
114
+ </rdf:Description>
115
+ <rdf:Description rdf:about="" xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
116
+ <pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">
117
+ <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
118
+ <pdfaSchema:namespaceURI>#{FACTUR_X_NAMESPACE}</pdfaSchema:namespaceURI>
119
+ <pdfaSchema:prefix>fx</pdfaSchema:prefix>
120
+ <pdfaSchema:property><rdf:Seq>#{described.join}</rdf:Seq></pdfaSchema:property>
121
+ </rdf:li></rdf:Bag></pdfaExtension:schemas>
122
+ </rdf:Description>
123
+ XMP
124
+ end
125
+
126
+ def escape_xml(text)
127
+ text.encode(xml: :text)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The few PDF::Core helpers and constants code written for Prawn reaches for directly, prawn-svg among it.
4
+ module PDF
5
+ module Core
6
+ PageGeometry = PrawnX::PageGeometry
7
+ LiteralString = PrawnX::PDF::LiteralString
8
+ ByteString = PrawnX::PDF::ByteString
9
+ Reference = PrawnX::PDF::Reference
10
+
11
+ module_function
12
+
13
+ def real(number)
14
+ PrawnX::PDF.real(number)
15
+ end
16
+
17
+ def real_params(numbers)
18
+ PrawnX::PDF.real_params(numbers)
19
+ end
20
+
21
+ def pdf_object(object, in_content_stream = false)
22
+ PrawnX::PDF.serialize(object, in_content_stream)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ class Document
5
+ def repeat(page_filter, options = {}, &block)
6
+ repeaters << Repeater.new(self, page_filter, options.fetch(:dynamic, false), &block)
7
+ end
8
+
9
+ def number_pages(string, options = {})
10
+ options = options.dup
11
+ start_count_at = options.delete(:start_count_at)
12
+ page_filter = options.key?(:page_filter) ? options.delete(:page_filter) : :all
13
+ total_pages = options.delete(:total_pages)
14
+ text_color = options.delete(:color)
15
+ options[:height] = 50 unless options.key?(:height)
16
+
17
+ counting = false
18
+ pseudo_page = 0
19
+ (1..page_count).each do |page_number|
20
+ unless counting
21
+ pseudo_page =
22
+ case start_count_at
23
+ when String then Integer(start_count_at, 10)
24
+ when (1..) then Integer(start_count_at)
25
+ else 1
26
+ end
27
+ end
28
+ if page_match?(page_filter, page_number)
29
+ go_to_page(page_number)
30
+ fill_color(text_color) unless text_color.nil?
31
+ total_pages = page_count if total_pages.nil?
32
+ text_box(string.gsub("<page>", pseudo_page.to_s).gsub("<total>", total_pages.to_s), options)
33
+ counting = true
34
+ end
35
+ pseudo_page += 1 if counting
36
+ end
37
+ end
38
+ end
39
+
40
+ # Content drawn on every page matching a filter when the document renders: stamped once, or drawn again on
41
+ # each page when dynamic.
42
+ class Repeater
43
+ class << self
44
+ attr_writer :count
45
+
46
+ def count
47
+ @count ||= 0
48
+ end
49
+ end
50
+
51
+ attr_reader :name
52
+
53
+ def initialize(document, page_filter, dynamic = false, &block)
54
+ @document = document
55
+ @page_filter = page_filter
56
+ @dynamic = dynamic
57
+ @stamp_name = "prawn_repeater(#{document.repeaters.size})"
58
+ @document.create_stamp(@stamp_name, &block) unless dynamic
59
+ @block = block if dynamic
60
+ @graphic_state = document.page.graphic_state.dup
61
+ Repeater.count += 1
62
+ end
63
+
64
+ def match?(page_number)
65
+ @document.page_match?(@page_filter, page_number)
66
+ end
67
+
68
+ def run(page_number)
69
+ return unless match?(page_number)
70
+ return @document.stamp(@stamp_name) unless @dynamic
71
+ return unless @block
72
+
73
+ @document.save_graphics_state(@graphic_state) do
74
+ @document.__send__(:freeze_stamp_graphics)
75
+ @block.call
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module PrawnX
6
+ class Document
7
+ # The standard security handler, revision 2: 40-bit RC4 (ISO 32000-1 §7.6.3).
8
+ module Security
9
+ PERMISSIONS_BITS = {print_document: 3, modify_contents: 4, copy_contents: 5, modify_annotations: 6}.freeze
10
+ FULL_PERMISSIONS = 0xFFFFFFFF
11
+ PASSWORD_PADDING = ["28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A"].pack("H*").freeze
12
+ KEY_LENGTH_BYTES = 5
13
+ private_constant :PERMISSIONS_BITS, :FULL_PERMISSIONS
14
+
15
+ def encrypt_document(options = {})
16
+ raise Errors::PdfAViolation, "PDF/A forbids encryption" if pdf_a?
17
+
18
+ PrawnX.verify_options(%i[user_password owner_password permissions], options)
19
+ @user_password = options.delete(:user_password) || ""
20
+ @owner_password = options.delete(:owner_password) || @user_password
21
+ @owner_password = Random.bytes(32) if @owner_password == :random
22
+ self.permissions = options.delete(:permissions) || {}
23
+ @encryption = Encryption.new(user_password: @user_password, owner_password: @owner_password, permissions: permissions_value)
24
+ end
25
+
26
+ def self.encrypt_string(string, key, identifier, generation)
27
+ Arcfour.new(object_key(key, identifier, generation)).encrypt(string)
28
+ end
29
+
30
+ # The key of one object: the document key extended with its number and generation (algorithm 1).
31
+ def self.object_key(key, identifier, generation)
32
+ Digest::MD5.digest(key + [identifier].pack("V")[0, 3] + [generation].pack("V")[0, 2])[0, key.bytesize + 5]
33
+ end
34
+
35
+ def self.pad_password(password)
36
+ password = password.b[0, 32]
37
+ password + PASSWORD_PADDING[0, 32 - password.bytesize]
38
+ end
39
+
40
+ private
41
+
42
+ def permissions=(permissions = {})
43
+ @permissions ||= FULL_PERMISSIONS
44
+ permissions.each do |key, value|
45
+ raise ArgumentError, "Unknown permission :#{key}. Valid options: #{PERMISSIONS_BITS.keys.map(&:inspect).join(", ")}" unless PERMISSIONS_BITS[key]
46
+
47
+ bit = 1 << (PERMISSIONS_BITS[key] - 1)
48
+ @permissions = value ? @permissions | bit : @permissions & ~bit
49
+ end
50
+ end
51
+
52
+ def permissions_value
53
+ @permissions || FULL_PERMISSIONS
54
+ end
55
+ end
56
+
57
+ # The keys and dictionary of an encrypted document, which depend on its file identifier.
58
+ class Encryption
59
+ def initialize(user_password:, owner_password:, permissions:)
60
+ @user_password = user_password
61
+ @owner_password = owner_password
62
+ @permissions = permissions
63
+ end
64
+
65
+ def owner_password_hash
66
+ @owner_password_hash ||= begin
67
+ key = Digest::MD5.digest(Security.pad_password(@owner_password))[0, Security::KEY_LENGTH_BYTES]
68
+ Arcfour.new(key).encrypt(Security.pad_password(@user_password))
69
+ end
70
+ end
71
+
72
+ def key(file_identifier)
73
+ Digest::MD5.digest(Security.pad_password(@user_password) + owner_password_hash + [@permissions].pack("V") + file_identifier)[0, Security::KEY_LENGTH_BYTES]
74
+ end
75
+
76
+ def dictionary(file_identifier)
77
+ user_password_hash = Arcfour.new(key(file_identifier)).encrypt(Security::PASSWORD_PADDING)
78
+ {Filter: :Standard, V: 1, R: 2, O: PDF::ByteString.new(owner_password_hash), U: PDF::ByteString.new(user_password_hash),
79
+ P: [@permissions].pack("L").unpack1("l")}
80
+ end
81
+ end
82
+
83
+ # RC4, the stream cipher of the standard security handler's first revisions.
84
+ class Arcfour
85
+ def initialize(key)
86
+ @state = (0..255).to_a
87
+ j = 0
88
+ key_bytes = key.bytes
89
+ 256.times do |i|
90
+ j = (j + @state[i] + key_bytes[i % key_bytes.size]) & 0xFF
91
+ @state[i], @state[j] = @state[j], @state[i]
92
+ end
93
+ end
94
+
95
+ def encrypt(string)
96
+ state = @state.dup
97
+ i = 0
98
+ j = 0
99
+ string.b.bytes.map do |byte|
100
+ i = (i + 1) & 0xFF
101
+ j = (j + state[i]) & 0xFF
102
+ state[i], state[j] = state[j], state[i]
103
+ byte ^ state[(state[i] + state[j]) & 0xFF]
104
+ end.pack("C*")
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ # Content drawn once into a form XObject, then placed on any page by name.
5
+ module Stamp
6
+ def stamp(name)
7
+ dictionary_name, dictionary = stamp_dictionary(name)
8
+ add_content("/#{dictionary_name} Do")
9
+ annotations = dictionary.data[:Annots]
10
+ page.dictionary.data[:Annots] = (page.dictionary.data[:Annots] || []) | annotations if annotations&.any?
11
+ page.xobjects[dictionary_name] = dictionary
12
+ end
13
+
14
+ def stamp_at(name, point, &)
15
+ translate(point[0], point[1]) { stamp(name) }
16
+ end
17
+
18
+ def create_stamp(name, &)
19
+ page.stamp_stream(create_stamp_dictionary(name), &)
20
+ end
21
+
22
+ private
23
+
24
+ def stamp_dictionary_registry
25
+ @stamp_dictionary_registry ||= {}
26
+ end
27
+
28
+ def stamp_dictionary(name)
29
+ raise Errors::InvalidName if name.empty?
30
+ raise Errors::UndefinedObjectName unless stamp_dictionary_registry.key?(name)
31
+
32
+ stamp_dictionary_registry[name].values_at(:stamp_dictionary_name, :stamp_dictionary)
33
+ end
34
+
35
+ def create_stamp_dictionary(name)
36
+ raise Errors::InvalidName if name.empty?
37
+ raise Errors::NameTaken if stamp_dictionary_registry.key?(name)
38
+
39
+ _left, _bottom, width, height = page.dimensions
40
+ dictionary = ref!({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height]}, +"")
41
+ stamp_dictionary_registry[name] = {stamp_dictionary_name: "Stamp#{stamp_dictionary_registry.length + 1}", stamp_dictionary: dictionary}
42
+ dictionary
43
+ end
44
+
45
+ def freeze_stamp_graphics
46
+ update_colors
47
+ write_line_width
48
+ write_stroke_cap_style
49
+ write_stroke_join_style
50
+ write_stroke_dash
51
+ end
52
+ end
53
+
54
+ module SoftMask
55
+ def soft_mask(&)
56
+ min_version(1.4)
57
+ group_attributes = ref!(Type: :Group, S: :Transparency, CS: :DeviceRGB, I: false, K: false)
58
+ group = ref!({Type: :XObject, Subtype: :Form, BBox: page.dimensions, Group: group_attributes}, +"")
59
+ page.stamp_stream(group, &)
60
+ mask = ref!(Type: :Mask, S: :Luminosity, G: group)
61
+ graphic_state_dictionary = ref!(Type: :ExtGState, SMask: mask, AIS: false, BM: :Normal, OP: false, op: false, OPM: 1, SA: true)
62
+
63
+ registry_key = [page.dimensions, group.stream, page_count].hash
64
+ @soft_mask_registry ||= {}
65
+ unless @soft_mask_registry[registry_key]
66
+ masks = page.resources[:ExtGState] ||= {}
67
+ name = masks.empty? ? "GS1" : masks.keys.max.succ
68
+ masks[name] = graphic_state_dictionary
69
+ @soft_mask_registry[registry_key] = name
70
+ end
71
+ add_content("/#{@soft_mask_registry[registry_key]} gs")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ class Table
5
+ class Cell
6
+ class Text < Cell
7
+ TextOptions = %i[inline_format kerning size align valign rotate rotate_around leading single_line skip_encoding overflow min_font_size].freeze
8
+
9
+ TextOptions.each do |option|
10
+ define_method(:"#{option}=") { |value| @text_options[option] = value }
11
+ define_method(option) { @text_options[option] }
12
+ end
13
+
14
+ attr_writer :font, :text_color
15
+
16
+ def initialize(pdf, point, options = {})
17
+ @text_options = {}
18
+ super
19
+ end
20
+
21
+ def font
22
+ with_font { @pdf.font }
23
+ end
24
+
25
+ def font_style=(style)
26
+ @text_options[:style] = style
27
+ end
28
+
29
+ def natural_content_width
30
+ @natural_content_width ||= [styled_width_of(@content), @pdf.bounds.width].min
31
+ end
32
+
33
+ def natural_content_height
34
+ with_font do
35
+ @laid_out_box = text_box(width: spanned_content_width + FPTolerance)
36
+ @laid_out_box.render(dry_run: true, keep_layout: true)
37
+ @layout_inputs = layout_inputs
38
+ @laid_out_box.height + @laid_out_box.line_gap
39
+ end
40
+ end
41
+
42
+ def draw_content
43
+ with_font do
44
+ @pdf.move_down((@pdf.font.line_gap + @pdf.font.descender) / 2)
45
+ with_text_color do
46
+ if laid_out_box_reusable?
47
+ @laid_out_box.render_laid_out([0, @pdf.cursor])
48
+ else
49
+ text_box(width: spanned_content_width + FPTolerance, height: spanned_content_height + FPTolerance, at: [0, @pdf.cursor]).render
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def set_width_constraints
56
+ unless @min_width
57
+ @min_width = padding_left + padding_right + [natural_content_width, styled_width_of("M")].min
58
+ super
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def with_font
65
+ @pdf.save_font do
66
+ options = {}
67
+ options[:style] = @text_options[:style] if @text_options[:style]
68
+ options[:style] ||= @pdf.font.options[:style] if @pdf.font.options[:style]
69
+ @pdf.font(@font || @pdf.font.family, options)
70
+ yield
71
+ end
72
+ end
73
+
74
+ def with_text_color
75
+ return yield unless @text_color
76
+
77
+ begin
78
+ old_color = @pdf.fill_color || "000000"
79
+ @pdf.fill_color(@text_color)
80
+ yield
81
+ ensure
82
+ @pdf.fill_color(old_color)
83
+ end
84
+ end
85
+
86
+ def text_box(extra_options = {})
87
+ inline_format = @text_options[:inline_format]
88
+ if inline_format
89
+ options = @text_options.except(:inline_format).merge(extra_options).merge(document: @pdf)
90
+ array = @pdf.text_formatter.format(@content, *(inline_format.is_a?(Array) ? inline_format : []))
91
+ PrawnX::Text::Formatted::Box.new(array, options)
92
+ else
93
+ PrawnX::Text::Box.new(@content, @text_options.merge(extra_options).merge(document: @pdf))
94
+ end
95
+ end
96
+
97
+ def styled_width_of(text)
98
+ @pdf.width_of(text, @text_options)
99
+ end
100
+
101
+ private
102
+
103
+ # A cell is laid out while the table is sized, then drawn: the layout is drawn again unless
104
+ # before_rendering_page, the document or a truncating height changed what the cell would wrap.
105
+ def laid_out_box_reusable?
106
+ @laid_out_box&.layout_reusable? && @laid_out_box.height <= spanned_content_height + FPTolerance &&
107
+ @layout_inputs == layout_inputs
108
+ end
109
+
110
+ def layout_inputs
111
+ [@content.dup, spanned_content_width, @text_options.dup, @pdf.font, @pdf.font_size, @pdf.default_leading,
112
+ @pdf.character_spacing, @pdf.text_direction, @pdf.fallback_fonts, @pdf.default_kerning?]
113
+ end
114
+ end
115
+
116
+ class Subtable < Cell
117
+ attr_reader :subtable
118
+
119
+ def initialize(pdf, point, options = {})
120
+ super
121
+ @subtable = options[:content]
122
+ @padding = [0, 0, 0, 0]
123
+ end
124
+
125
+ def text_color=(color)
126
+ @subtable.cells.text_color = color
127
+ end
128
+
129
+ def natural_content_width
130
+ @subtable.cells.width
131
+ end
132
+
133
+ def min_width
134
+ @subtable.cells.min_width
135
+ end
136
+
137
+ def max_width
138
+ @subtable.cells.max_width
139
+ end
140
+
141
+ def natural_content_height
142
+ @subtable.cells.height
143
+ end
144
+
145
+ def draw_content
146
+ @subtable.draw
147
+ end
148
+ end
149
+
150
+ class Image < Cell
151
+ def initialize(pdf, point, options = {})
152
+ @image_options = {}
153
+ super
154
+ @pdf_object, @image_info = @pdf.build_image_object(@file)
155
+ @natural_width, @natural_height = @image_info.calc_image_dimensions(@image_options)
156
+ end
157
+
158
+ def image=(file)
159
+ @file = file
160
+ end
161
+
162
+ {scale: :scale, fit: :fit, image_height: :height, image_width: :width, position: :position, vposition: :vposition}.each do |setter, option|
163
+ define_method(:"#{setter}=") { |value| @image_options[option] = value }
164
+ end
165
+
166
+ def natural_content_width
167
+ @natural_width
168
+ end
169
+
170
+ def natural_content_height
171
+ @natural_height
172
+ end
173
+
174
+ def draw_content
175
+ @pdf.embed_image(@pdf_object, @image_info, @image_options)
176
+ end
177
+ end
178
+
179
+ # Stands in the place a spanning cell covers beyond its first row and column, passing on the styles of
180
+ # its right and bottom edges.
181
+ class SpanDummy < Cell
182
+ def initialize(pdf, master_cell)
183
+ super(pdf, [0, pdf.cursor])
184
+ @master_cell = master_cell
185
+ @padding = [0, 0, 0, 0]
186
+ end
187
+
188
+ def natural_content_height
189
+ 0
190
+ end
191
+
192
+ def natural_content_width
193
+ 0
194
+ end
195
+
196
+ def avg_spanned_min_width
197
+ @master_cell.avg_spanned_min_width
198
+ end
199
+
200
+ def draw_borders(_point)
201
+ end
202
+
203
+ def draw_bounded_content(_point)
204
+ end
205
+
206
+ def padding_right=(value)
207
+ @master_cell.padding_right = value if rightmost?
208
+ end
209
+
210
+ def padding_bottom=(value)
211
+ @master_cell.padding_bottom = value if bottommost?
212
+ end
213
+
214
+ def border_right_color=(value)
215
+ @master_cell.border_right_color = value if rightmost?
216
+ end
217
+
218
+ def border_bottom_color=(value)
219
+ @master_cell.border_bottom_color = value if bottommost?
220
+ end
221
+
222
+ def border_right_width=(value)
223
+ @master_cell.border_right_width = value if rightmost?
224
+ end
225
+
226
+ def border_bottom_width=(value)
227
+ @master_cell.border_bottom_width = value if bottommost?
228
+ end
229
+
230
+ def background_color
231
+ @master_cell.background_color
232
+ end
233
+
234
+ private
235
+
236
+ def rightmost?
237
+ @column == @master_cell.column + @master_cell.colspan - 1
238
+ end
239
+
240
+ def bottommost?
241
+ @row == @master_cell.row + @master_cell.rowspan - 1
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end