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.
- checksums.yaml +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
data/lib/prawnx/pdf_a.rb
ADDED
|
@@ -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
|
data/lib/prawnx/stamp.rb
ADDED
|
@@ -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
|