zugpferd 0.2.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37973184a46c78a112635cd899a6184312d90d1a7a9a29e6ea1a8cb860fdce61
4
- data.tar.gz: 00acee1d503c4d39c667e602ec76865d80b3b12eabf0e0f4f95efb6cd0478bab
3
+ metadata.gz: 668190b295d1286b2f8efd2f2896054e9c102c48da549d70486ef13677d042ad
4
+ data.tar.gz: '0274834d75a8c12690e178b5e075a8bed9792180b627c19a1fd3313534d9914c'
5
5
  SHA512:
6
- metadata.gz: f906e49f621cc8141b7a155740e5f0317e95eab9ed5102594960c36be9d3da5818b0a3826456130c51189bbeaa91c576f5cea14d20b7b14b02b59f8dc358d2b8
7
- data.tar.gz: e461043ec18836b92d9a40f532dbf4bca9444efcd1e038e0e576cdf955ef9964f8824f71cf1f1e0291f21476575611e4bee304afb6b28ed23de0b7f90c16d303
6
+ metadata.gz: 74be6147cd6ef40e90a23548e0e15c0b65e7c5e402535c2575f1dc41926b66b981f7c1e555d297f006ae69988ff9470c4d8b2eb2e8a4c7c83aa8791cc8bde1f5
7
+ data.tar.gz: 2cb11d5ef99e1d3341006fd37d2fa8e9ba3703fbebc7c642577f269aaa9aae3ddf666c2ed892a6ca5e92df17945d1d2b916b9b542c16f283b909ec862ddd8cdd
@@ -23,6 +23,10 @@ module Zugpferd
23
23
  profile_id: "#{CONTEXT}/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID",
24
24
  }.freeze
25
25
 
26
+ # Delivery (BG-13)
27
+ DELIVERY = "#{TRANSACTION}/ram:ApplicableHeaderTradeDelivery"
28
+ DELIVERY_DATE = "ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString"
29
+
26
30
  # Settlement (contains currency, payment, tax, totals)
27
31
  SETTLEMENT = "#{TRANSACTION}/ram:ApplicableHeaderTradeSettlement"
28
32
  AGREEMENT = "#{TRANSACTION}/ram:ApplicableHeaderTradeAgreement"
@@ -35,6 +35,7 @@ module Zugpferd
35
35
 
36
36
  def build_invoice(root)
37
37
  settlement = root.at_xpath(SETTLEMENT, NS)
38
+ delivery = root.at_xpath(DELIVERY, NS)
38
39
  type_code = text(root, INVOICE[:type_code])
39
40
  model_class = TYPE_CODE_MAP.fetch(type_code, Model::Invoice)
40
41
 
@@ -42,6 +43,7 @@ module Zugpferd
42
43
  number: text(root, INVOICE[:number]),
43
44
  issue_date: parse_cii_date(text(root, INVOICE[:issue_date])),
44
45
  due_date: parse_cii_date(settlement ? text(settlement, PAYMENT_TERMS_DUE_DATE) : nil),
46
+ delivery_date: parse_cii_date(delivery ? text(delivery, DELIVERY_DATE) : nil),
45
47
  type_code: text(root, INVOICE[:type_code]),
46
48
  currency_code: text(root, INVOICE_SETTLEMENT[:currency_code]),
47
49
  buyer_reference: text(root, INVOICE_SETTLEMENT[:buyer_reference]),
@@ -66,7 +66,7 @@ module Zugpferd
66
66
  xml["rsm"].SupplyChainTradeTransaction do
67
67
  doc.line_items.each { |li| build_line_item(xml, li) }
68
68
  build_agreement(xml, doc)
69
- xml["ram"].ApplicableHeaderTradeDelivery
69
+ build_delivery(xml, doc)
70
70
  build_settlement(xml, doc)
71
71
  end
72
72
  end
@@ -141,6 +141,18 @@ module Zugpferd
141
141
  end
142
142
  end
143
143
 
144
+ def build_delivery(xml, doc)
145
+ xml["ram"].ApplicableHeaderTradeDelivery do
146
+ if doc.delivery_date
147
+ xml["ram"].ActualDeliverySupplyChainEvent do
148
+ xml["ram"].OccurrenceDateTime do
149
+ xml["udt"].DateTimeString(format_cii_date(doc.delivery_date), format: "102")
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+
144
156
  def build_settlement(xml, doc)
145
157
  xml["ram"].ApplicableHeaderTradeSettlement do
146
158
  if doc.payment_instructions&.creditor_reference_id
@@ -18,6 +18,7 @@ module Zugpferd
18
18
  # @return [Date, nil] BT-9 Payment due date
19
19
  # @return [String] BT-3 Invoice type code
20
20
  # @return [String] BT-5 Document currency code (default: "EUR")
21
+ # @return [Date, nil] BT-72 Actual delivery date
21
22
  # @return [String, nil] BT-10 Buyer reference
22
23
  # @return [String, nil] BT-24 Specification identifier
23
24
  # @return [String, nil] BT-23 Business process type
@@ -30,10 +31,10 @@ module Zugpferd
30
31
  # @return [PaymentInstructions, nil] BG-16 Payment information
31
32
  # @return [Array<AllowanceCharge>] BG-20/BG-21 Document-level allowances and charges
32
33
  attr_accessor :number, :issue_date, :due_date, :type_code,
33
- :currency_code, :buyer_reference, :customization_id,
34
- :profile_id, :note, :seller, :buyer, :line_items,
35
- :tax_breakdown, :monetary_totals, :payment_instructions,
36
- :allowance_charges
34
+ :currency_code, :delivery_date, :buyer_reference,
35
+ :customization_id, :profile_id, :note, :seller, :buyer,
36
+ :line_items, :tax_breakdown, :monetary_totals,
37
+ :payment_instructions, :allowance_charges
37
38
 
38
39
  # @param number [String] BT-1 Invoice number
39
40
  # @param issue_date [Date] BT-2 Issue date
@@ -0,0 +1,126 @@
1
+ require "open3"
2
+ require "tempfile"
3
+
4
+ module Zugpferd
5
+ module PDF
6
+ class Embedder
7
+ class GhostscriptNotFound < Zugpferd::Error; end
8
+ class EmbedError < Zugpferd::Error; end
9
+
10
+ VERSIONS = %w[rc 1p0 2p0 2p1].freeze
11
+
12
+ CONFORMANCE_LEVELS = {
13
+ "2p1" => ["MINIMUM", "BASIC WL", "BASIC", "EN 16931", "EXTENDED", "XRECHNUNG"],
14
+ "2p0" => ["MINIMUM", "BASIC WL", "BASIC", "EN 16931", "EXTENDED", "XRECHNUNG"],
15
+ "1p0" => ["BASIC", "COMFORT", "EXTENDED"],
16
+ }.freeze
17
+
18
+ # @param pdf_path [String] Pfad zum Input-PDF
19
+ # @param xml [String] XML-String (UBL oder CII)
20
+ # @param output_path [String] Pfad zur Ausgabe-PDF
21
+ # @param version [String] "2p1" (default), "2p0", "1p0", "rc"
22
+ # @param conformance_level [String] "EN 16931" (default), "BASIC", etc.
23
+ # @return [String] Pfad zur erzeugten PDF-Datei
24
+ def embed(pdf_path:, xml:, output_path:, version: "2p1", conformance_level: "EN 16931")
25
+ validate_params!(pdf_path, version, conformance_level)
26
+ ensure_ghostscript!
27
+
28
+ xml_file = write_xml_tempfile(xml)
29
+ begin
30
+ run_ghostscript(pdf_path, xml_file.path, output_path, version, conformance_level)
31
+ ensure
32
+ xml_file.close!
33
+ end
34
+
35
+ output_path
36
+ end
37
+
38
+ private
39
+
40
+ def validate_params!(pdf_path, version, conformance_level)
41
+ raise ArgumentError, "PDF file not found: #{pdf_path}" unless File.exist?(pdf_path)
42
+ raise ArgumentError, "Unknown version: #{version}. Valid: #{VERSIONS.join(", ")}" unless VERSIONS.include?(version)
43
+
44
+ if version != "rc"
45
+ valid_levels = CONFORMANCE_LEVELS[version]
46
+ unless valid_levels&.include?(conformance_level)
47
+ raise ArgumentError,
48
+ "Unknown conformance level '#{conformance_level}' for version #{version}. " \
49
+ "Valid: #{valid_levels.join(", ")}"
50
+ end
51
+ end
52
+ end
53
+
54
+ def ensure_ghostscript!
55
+ _, _, status = Open3.capture3("gs", "--version")
56
+ raise GhostscriptNotFound, "Ghostscript (gs) not found in PATH" unless status.success?
57
+ rescue Errno::ENOENT
58
+ raise GhostscriptNotFound, "Ghostscript (gs) not found in PATH"
59
+ end
60
+
61
+ def vendor_dir
62
+ File.expand_path("../../../vendor/zugferd", __dir__)
63
+ end
64
+
65
+ def zugferd_ps_path
66
+ File.join(vendor_dir, "zugferd.ps")
67
+ end
68
+
69
+ def icc_profile_path
70
+ File.join(vendor_dir, "default_rgb.icc")
71
+ end
72
+
73
+ def write_xml_tempfile(xml)
74
+ tmpfile = Tempfile.new(["zugferd", ".xml"])
75
+ tmpfile.binmode
76
+ tmpfile.write(xml)
77
+ tmpfile.flush
78
+ tmpfile
79
+ end
80
+
81
+ def run_ghostscript(pdf_path, xml_path, output_path, version, conformance_level)
82
+ ps_path = zugferd_ps_path
83
+ icc_path = icc_profile_path
84
+
85
+ unless File.exist?(ps_path)
86
+ raise EmbedError, "zugferd.ps not found at #{ps_path}. Run bin/setup-schemas."
87
+ end
88
+
89
+ unless File.exist?(icc_path)
90
+ raise EmbedError, "ICC profile not found at #{icc_path}. Run bin/setup-schemas."
91
+ end
92
+
93
+ cmd = build_command(pdf_path, xml_path, output_path, version, conformance_level, ps_path, icc_path)
94
+ stdout, stderr, status = Open3.capture3(*cmd)
95
+
96
+ unless status.success?
97
+ raise EmbedError, "Ghostscript failed (exit #{status.exitstatus}):\n#{stderr}#{stdout}"
98
+ end
99
+
100
+ unless File.exist?(output_path)
101
+ raise EmbedError, "Ghostscript did not produce output file: #{output_path}"
102
+ end
103
+ end
104
+
105
+ def build_command(pdf_path, xml_path, output_path, version, conformance_level, ps_path, icc_path)
106
+ cmd = %w[gs]
107
+ cmd += %w[-dBATCH -dNOPAUSE -dNOOUTERSAVE]
108
+ cmd += %w[-sDEVICE=pdfwrite]
109
+ cmd += %w[-dPDFA=3 -dPDFACompatibilityPolicy=1]
110
+ cmd += %w[-sColorConversionStrategy=RGB -sProcessColorModel=DeviceRGB]
111
+ cmd << "--permit-file-read=#{xml_path}"
112
+ cmd << "--permit-file-read=#{icc_path}"
113
+ cmd << "--permit-file-read=#{pdf_path}"
114
+ cmd << "-sZUGFeRDXMLFile=#{xml_path}"
115
+ cmd << "-sZUGFeRDProfile=#{icc_path}"
116
+ cmd << "-sZUGFeRDVersion=#{version}"
117
+ cmd << "-sZUGFeRDConformanceLevel=#{conformance_level}"
118
+ cmd << "-o"
119
+ cmd << output_path
120
+ cmd << ps_path
121
+ cmd << pdf_path
122
+ cmd
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1 @@
1
+ require_relative "pdf/embedder"
@@ -26,6 +26,10 @@ module Zugpferd
26
26
  note: "cbc:Note",
27
27
  }.freeze
28
28
 
29
+ # Delivery (BG-13)
30
+ DELIVERY = "cac:Delivery"
31
+ DELIVERY_DATE = "cbc:ActualDeliveryDate"
32
+
29
33
  # Seller (BG-4)
30
34
  SELLER = "cac:AccountingSupplierParty/cac:Party"
31
35
  # Buyer (BG-7)
@@ -33,10 +33,13 @@ module Zugpferd
33
33
 
34
34
  model_class = @credit_note ? Model::CreditNote : Model::Invoice
35
35
 
36
+ delivery_node = root.at_xpath(DELIVERY, @ns)
37
+
36
38
  model_class.new(
37
39
  number: text(root, INVOICE[:number]),
38
40
  issue_date: parse_date(text(root, INVOICE[:issue_date])),
39
41
  due_date: parse_date(text(root, INVOICE[:due_date])),
42
+ delivery_date: delivery_node ? parse_date(text(delivery_node, DELIVERY_DATE)) : nil,
40
43
  type_code: text(root, type_code_element),
41
44
  currency_code: text(root, INVOICE[:currency_code]),
42
45
  buyer_reference: text(root, INVOICE[:buyer_reference]),
@@ -53,6 +53,7 @@ module Zugpferd
53
53
 
54
54
  build_supplier(xml, doc.seller, doc.payment_instructions) if doc.seller
55
55
  build_customer(xml, doc.buyer) if doc.buyer
56
+ build_delivery(xml, doc) if doc.delivery_date
56
57
  build_payment_means(xml, doc.payment_instructions) if doc.payment_instructions
57
58
  build_payment_terms(xml, doc.payment_instructions) if doc.payment_instructions&.note
58
59
  doc.allowance_charges.each { |ac| build_allowance_charge(xml, ac, doc.currency_code) }
@@ -140,6 +141,12 @@ module Zugpferd
140
141
  end
141
142
  end
142
143
 
144
+ def build_delivery(xml, doc)
145
+ xml["cac"].Delivery do
146
+ xml["cbc"].ActualDeliveryDate doc.delivery_date.to_s
147
+ end
148
+ end
149
+
143
150
  def build_payment_means(xml, payment)
144
151
  xml["cac"].PaymentMeans do
145
152
  xml["cbc"].PaymentMeansCode payment.payment_means_code
@@ -0,0 +1,41 @@
1
+ require "open3"
2
+
3
+ module Zugpferd
4
+ module Validation
5
+ class MustangValidator
6
+ Result = Struct.new(:valid, :output, keyword_init: true)
7
+
8
+ CONTAINER_NAME = "zugpferd-mustang"
9
+ IMAGE = "zugpferd-mustang:latest"
10
+
11
+ # @param pdf_path [String] Pfad zur PDF-Datei
12
+ # @return [Result]
13
+ def validate(pdf_path)
14
+ pdf_path = File.expand_path(pdf_path)
15
+ dir = File.dirname(pdf_path)
16
+ filename = File.basename(pdf_path)
17
+
18
+ cmd = [
19
+ "docker", "run", "--rm",
20
+ "-v", "#{dir}:/data:ro",
21
+ IMAGE,
22
+ "--action", "validate",
23
+ "--source", "/data/#{filename}",
24
+ ]
25
+
26
+ stdout, stderr, status = Open3.capture3(*cmd)
27
+ output = "#{stdout}#{stderr}".strip
28
+
29
+ Result.new(valid: status.success?, output: output)
30
+ end
31
+
32
+ # @return [Boolean] true wenn das Docker-Image vorhanden ist
33
+ def available?
34
+ _, _, status = Open3.capture3("docker", "image", "inspect", IMAGE)
35
+ status.success?
36
+ rescue Errno::ENOENT
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,93 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Zugpferd
6
+ module Validation
7
+ class PdfValidator
8
+ Result = Struct.new(:compliant, :failures, keyword_init: true)
9
+
10
+ def initialize(host: "localhost", port: 8080)
11
+ @base_uri = URI("http://#{host}:#{port}")
12
+ end
13
+
14
+ # @param pdf_path [String] Pfad zur PDF-Datei
15
+ # @param profile [String] veraPDF-Profil ("3b", "3a", "3u")
16
+ # @return [Result]
17
+ def validate(pdf_path, profile: "3b")
18
+ uri = URI("#{@base_uri}/api/validate/#{profile}")
19
+
20
+ boundary = "----ZugpferdBoundary#{SecureRandom.hex(16)}"
21
+ body = build_multipart_body(pdf_path, boundary)
22
+
23
+ request = Net::HTTP::Post.new(uri)
24
+ request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
25
+ request["Accept"] = "application/json"
26
+ request.body = body
27
+
28
+ response = Net::HTTP.start(uri.hostname, uri.port) do |http|
29
+ http.read_timeout = 120
30
+ http.request(request)
31
+ end
32
+
33
+ parse_response(response)
34
+ end
35
+
36
+ # @return [Boolean] true wenn veraPDF erreichbar ist
37
+ def available?
38
+ uri = URI("#{@base_uri}/api/info")
39
+ response = Net::HTTP.get_response(uri)
40
+ response.code == "200"
41
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError, Net::OpenTimeout
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ def build_multipart_body(pdf_path, boundary)
48
+ filename = File.basename(pdf_path)
49
+ content = File.binread(pdf_path)
50
+
51
+ body = +""
52
+ body << "--#{boundary}\r\n"
53
+ body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
54
+ body << "Content-Type: application/pdf\r\n"
55
+ body << "\r\n"
56
+ body << content
57
+ body << "\r\n"
58
+ body << "--#{boundary}--\r\n"
59
+ body
60
+ end
61
+
62
+ def parse_response(response)
63
+ unless response.code == "200"
64
+ raise "veraPDF returned HTTP #{response.code}: #{response.body}"
65
+ end
66
+
67
+ data = JSON.parse(response.body)
68
+ job = data.dig("report", "jobs", 0) || data.dig("report", "batchSummary") || {}
69
+ validation = job["validationResult"] || job
70
+
71
+ compliant = validation["compliant"] == true
72
+ failures = extract_failures(validation)
73
+
74
+ Result.new(compliant: compliant, failures: failures)
75
+ end
76
+
77
+ def extract_failures(validation)
78
+ details = validation["details"] || validation["rulesSummary"] || {}
79
+ rules = details["rules"] || []
80
+
81
+ rules.select { |r| r["status"] == "failed" }.map do |rule|
82
+ {
83
+ clause: rule["clause"],
84
+ test_number: rule["testNumber"],
85
+ description: rule["description"],
86
+ object: rule["object"],
87
+ checks: rule["checks"]&.select { |c| c["status"] == "failed" },
88
+ }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,46 @@
1
+ require "nokogiri"
2
+
3
+ module Zugpferd
4
+ module Validation
5
+ # Validates XML against XSD schemas using Nokogiri.
6
+ #
7
+ # @example
8
+ # validator = SchemaValidator.new(schemas_path: "vendor/schemas")
9
+ # errors = validator.validate(xml, schema_key: :ubl_invoice)
10
+ class SchemaValidator
11
+ XSD_PATHS = {
12
+ ubl_invoice: "ubl/xsd/maindoc/UBL-Invoice-2.1.xsd",
13
+ ubl_credit_note: "ubl/xsd/maindoc/UBL-CreditNote-2.1.xsd",
14
+ cii: "cii/CrossIndustryInvoice_100pD16B.xsd",
15
+ }.freeze
16
+
17
+ # @param schemas_path [String] path to the schemas directory
18
+ def initialize(schemas_path:)
19
+ @schemas_path = schemas_path
20
+ @schema_cache = {}
21
+ end
22
+
23
+ # Validates XML against an XSD schema.
24
+ #
25
+ # @param xml_string [String] XML to validate
26
+ # @param schema_key [Symbol] one of +:ubl_invoice+, +:ubl_credit_note+, +:cii+
27
+ # @return [Array<String>] error messages (empty if valid)
28
+ def validate(xml_string, schema_key:)
29
+ doc = Nokogiri::XML(xml_string)
30
+ schema = load_schema(schema_key)
31
+ schema.validate(doc).map(&:message)
32
+ end
33
+
34
+ private
35
+
36
+ def load_schema(key)
37
+ @schema_cache[key] ||= begin
38
+ path = File.join(@schemas_path, XSD_PATHS.fetch(key))
39
+ Dir.chdir(File.dirname(path)) do
40
+ Nokogiri::XML::Schema(File.read(File.basename(path)))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,114 @@
1
+ require "nokogiri"
2
+ require "open3"
3
+ require "tempfile"
4
+
5
+ module Zugpferd
6
+ module Validation
7
+ # Validates XML against Schematron business rules using Saxon HE.
8
+ #
9
+ # Supports CEN EN 16931 and XRechnung rule sets.
10
+ #
11
+ # @example
12
+ # validator = SchematronValidator.new(schemas_path: "vendor/schemas")
13
+ # errors = validator.validate(xml, rule_set: :xrechnung_ubl)
14
+ # fatals = errors.select { |e| e.flag == "fatal" }
15
+ class SchematronValidator
16
+ SVRL_NS = "http://purl.oclc.org/dsdl/svrl"
17
+
18
+ XSLT_PATHS = {
19
+ cen_ubl: "schematron/cen/ubl/xslt/EN16931-UBL-validation.xslt",
20
+ cen_cii: "schematron/cen/cii/xslt/EN16931-CII-validation.xslt",
21
+ xrechnung_ubl: "schematron/xrechnung/schematron/ubl/XRechnung-UBL-validation.xsl",
22
+ xrechnung_cii: "schematron/xrechnung/schematron/cii/XRechnung-CII-validation.xsl",
23
+ }.freeze
24
+
25
+ SAXON_JARS = [
26
+ "saxon/saxon-he-12.5.jar",
27
+ "saxon/xmlresolver-5.2.2.jar",
28
+ ].freeze
29
+
30
+ class TransformError < StandardError; end
31
+
32
+ Result = Struct.new(:id, :location, :text, :flag, keyword_init: true)
33
+
34
+ # @param schemas_path [String] path to the schemas directory
35
+ def initialize(schemas_path:)
36
+ @schemas_path = schemas_path
37
+ end
38
+
39
+ # Validates XML against a single Schematron rule set.
40
+ #
41
+ # @param xml_string [String] XML to validate
42
+ # @param rule_set [Symbol] one of +:cen_ubl+, +:cen_cii+, +:xrechnung_ubl+, +:xrechnung_cii+
43
+ # @return [Array<Result>] validation errors
44
+ # @raise [TransformError] if Saxon fails
45
+ def validate(xml_string, rule_set:)
46
+ xslt_path = resolve_xslt(rule_set)
47
+ svrl_xml = run_saxon(xml_string, xslt_path)
48
+ svrl_doc = Nokogiri::XML(svrl_xml)
49
+ parse_failed_asserts(svrl_doc)
50
+ end
51
+
52
+ # Validates XML against multiple rule sets.
53
+ #
54
+ # @param xml_string [String] XML to validate
55
+ # @param rule_sets [Array<Symbol>] rule sets to apply
56
+ # @return [Array<Result>] merged validation errors
57
+ def validate_all(xml_string, rule_sets:)
58
+ rule_sets.flat_map { |rs| validate(xml_string, rule_set: rs) }
59
+ end
60
+
61
+ private
62
+
63
+ def resolve_xslt(rule_set)
64
+ path = File.join(@schemas_path, XSLT_PATHS.fetch(rule_set))
65
+ raise ArgumentError,
66
+ "XSLT not found: #{path} – run bin/setup-schemas" \
67
+ unless File.exist?(path)
68
+ path
69
+ end
70
+
71
+ def saxon_classpath
72
+ SAXON_JARS.map do |jar|
73
+ path = File.join(@schemas_path, jar)
74
+ raise ArgumentError,
75
+ "#{jar} not found: #{path} – run bin/setup-schemas" \
76
+ unless File.exist?(path)
77
+ path
78
+ end.join(":")
79
+ end
80
+
81
+ def run_saxon(xml_string, xslt_path)
82
+ input = Tempfile.new(["input", ".xml"])
83
+ input.write(xml_string)
84
+ input.close
85
+
86
+ stdout, stderr, status = Open3.capture3(
87
+ "java", "-cp", saxon_classpath,
88
+ "net.sf.saxon.Transform",
89
+ "-s:#{input.path}",
90
+ "-xsl:#{xslt_path}"
91
+ )
92
+
93
+ unless status.success?
94
+ raise TransformError, "Saxon transform failed: #{stderr}"
95
+ end
96
+
97
+ stdout
98
+ ensure
99
+ input&.unlink
100
+ end
101
+
102
+ def parse_failed_asserts(svrl_doc)
103
+ svrl_doc.xpath("//svrl:failed-assert", "svrl" => SVRL_NS).map do |node|
104
+ Result.new(
105
+ id: node["id"],
106
+ location: node["location"],
107
+ text: node.at_xpath("svrl:text", "svrl" => SVRL_NS)&.text&.strip,
108
+ flag: node["flag"]
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "validation/schema_validator"
2
+ require_relative "validation/schematron_validator"
Binary file
@@ -0,0 +1,527 @@
1
+ %!PS
2
+
3
+ % Copyright (C) 2001-2024 Artifex Software, Inc.
4
+ % All Rights Reserved.
5
+ %
6
+ % This software is provided AS-IS with no warranty, either express or
7
+ % implied.
8
+ %
9
+ % This software is distributed under license and may not be copied,
10
+ % modified or distributed except as expressly authorized under the terms
11
+ % of the license contained in the file LICENSE in this distribution.
12
+ %
13
+ % Refer to licensing information at http://www.artifex.com or contact
14
+ % Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
15
+ % CA 94129, USA, for further information.
16
+ %
17
+ % ZUGFeRD.ps
18
+ % This program will create an (unsigned) ZUGFeRD compliant PDF file.
19
+ % In order to do so the user must provide certain information, or edit
20
+ % this program.
21
+ %
22
+ % Required information is the path to the XML file containing the invoice
23
+ % data, and the path to an ICC profile appropriate for the chosen
24
+ % ColorConversionStrategy.
25
+ %
26
+ % -sZUGFeRDXMLFile defines a path to the XML invoice file.
27
+ %
28
+ % -sZUGFeRDProfile defines the path to the ICC profile.
29
+ %
30
+ % -sZUGFeRDVersion defines the version of the ZUGFeRD standard to be used.
31
+ % Missing or invalid values would be silently replaced by the default ("2p1").
32
+ %
33
+ % -sZUGFeRDConformanceLevel defines the level of conformance.
34
+ % Missing or invalid values would be silently replaced by the default ("BASIC").
35
+ %
36
+ % Note that the ZUGFeRD standard states:
37
+ %
38
+ % The content of the field fx:ConformanceLevel has to be picked from
39
+ % the content of the element "GuidelineSpecifiedDocumentContextParameter"
40
+ % (specification identifier BT-24) of the XML instance file.
41
+ %
42
+ % Optionally:
43
+ % -sZUGFeRDDateTime can be used to set a string representing the modification
44
+ % date of the XML invoice file. If this is ommitted a dummy value will be
45
+ % used. It is up to the user to create a correctly formatted PDF date/time
46
+ % string. See section 7.9.4 of the PDF 2.0 specification (ISO-32000-2:2017)
47
+ % for details of the format.
48
+ %
49
+ % The user must additionally set -dPDFA=3 and -sColorConversionStrategy
50
+ % on the Ghostscript command line, and set the permissions for Ghostscript
51
+ % to read both these files. It is simplest to put the files in a directory
52
+ % and then permit reading of the entire directory.
53
+ %
54
+ % Example command line :
55
+ %
56
+ % gs --permit-file-read=/usr/home/me/zugferd/ \
57
+ % -sDEVICE=pdfwrite \
58
+ % -dPDFA=3 \
59
+ % -sColorConversionStrategy=RGB \
60
+ % -sZUGFeRDXMLFile=/usr/home/me/zugferd/invoice.xml \
61
+ % -sZUGFeRDProfile=/usr/home/me/zugferd/rgb.icc \
62
+ % -sZUGFeRDVersion=2p1 \
63
+ % -sZUGFeRDConformanceLevel=BASIC \
64
+ % -o /usr/home/me/zugferd/zugferd.pdf \
65
+ % /usr/home/me/zugferd/zugferd.ps \
66
+ % /usr/home/me/zugferd/original.pdf
67
+ %
68
+ % Much of this program results from a Ghostscript bug report, the thread
69
+ % can be found at
70
+ % https://bugs.ghostscript.com/show_bug.cgi?id=696472
71
+ % Portions of the code below were supplied by Reinhard Nissl and
72
+ % I'm indebted to him for his efforts in helping me create a solution for
73
+ % this problem as well as for the code he supplied, particularly for the
74
+ % SimpleUTF16BE routine.
75
+ %
76
+ % The program was further refined and expanded by Adrian Devries in :
77
+ % https://bugs.ghostscript.com/show_bug.cgi?id=703862
78
+ %
79
+ % And refined again following feedback from Thorsten Engel in:
80
+ % https://bugs.ghostscript.com/show_bug.cgi?id=707694
81
+ %
82
+ % It should not be necessary to modify this program, the comments in the
83
+ % code are there purely for information, but there is one area which
84
+ % might reasonably be altered. The section with the --8<-- lines could be
85
+ % replaced with a simpler /N 3 or /N 4 if you always intend to produce
86
+ % the same kind of files; RGB or CMYK.
87
+ %
88
+ % Remaining tasks have been marked with "TODO".
89
+
90
+ % istring SimpleUTF16BE ostring
91
+ /SimpleUTF16BE
92
+ {
93
+ dup length
94
+ 1 add
95
+ 2 mul
96
+ string
97
+ % istring ostring
98
+ dup 0 16#FE put
99
+ dup 1 16#FF put
100
+ 2
101
+ 3 -1 roll
102
+ % ostring index istring
103
+ {
104
+ % ostring index ichar
105
+ 3 1 roll
106
+ % ichar ostring index
107
+ 2 copy 16#00 put
108
+ 1 add
109
+ 2 copy
110
+ 5 -1 roll
111
+ % ostring index ostring index ichar
112
+ put
113
+ 1 add
114
+ % ostring index
115
+ }
116
+ forall
117
+ % ostring index
118
+ pop
119
+ }
120
+ bind def
121
+
122
+ % Cf. https://en.wikibooks.org/wiki/PostScript_FAQ#How_to_concatenate_strings%3F
123
+ /concatstringarray { % [(a) (b) ... (z)] --> (ab...z)
124
+ 0 1 index {
125
+ length add
126
+ } forall
127
+ string
128
+ 0 3 2 roll {
129
+ 3 copy putinterval
130
+ length add
131
+ } forall
132
+ pop
133
+ } bind def
134
+
135
+ /ZUGFeRDVersion where {
136
+ pop % Discard the dictionary
137
+ ZUGFeRDVersion (rc) ne {
138
+ ZUGFeRDVersion (1p0) ne {
139
+ ZUGFeRDVersion (2p0) ne {
140
+ ZUGFeRDVersion (2p1) ne {
141
+ /ZUGFeRDVersion (2p1) def
142
+ } if
143
+ } if
144
+ } if
145
+ } if
146
+ }{
147
+ /ZUGFeRDVersion (2p1) def
148
+ } ifelse
149
+
150
+ /ZUGFeRDConformanceLevel where {
151
+ pop % Discard the dictionary
152
+ ZUGFeRDVersion (rc) eq
153
+ ZUGFeRDVersion (1p0) eq or {
154
+ ZUGFeRDConformanceLevel (BASIC) ne {
155
+ ZUGFeRDConformanceLevel (COMFORT) ne {
156
+ ZUGFeRDConformanceLevel (EXTENDED) ne {
157
+ /ZUGFeRDConformanceLevel (BASIC) def
158
+ } if
159
+ } if
160
+ } if
161
+ } if
162
+ ZUGFeRDVersion (2p0) eq
163
+ ZUGFeRDVersion (2p1) eq or {
164
+ ZUGFeRDConformanceLevel (MINIMUM) ne {
165
+ ZUGFeRDConformanceLevel (BASIC WL) ne {
166
+ ZUGFeRDConformanceLevel (BASIC) ne {
167
+ ZUGFeRDConformanceLevel (EN 16931) ne {
168
+ ZUGFeRDConformanceLevel (EXTENDED) ne {
169
+ ZUGFeRDConformanceLevel (XRECHNUNG) ne {
170
+ /ZUGFeRDConformanceLevel (BASIC) def
171
+ } if
172
+ } if
173
+ } if
174
+ } if
175
+ } if
176
+ } if
177
+ } if
178
+ }{
179
+ /ZUGFeRDConformanceLevel (BASIC) def
180
+ } ifelse
181
+
182
+ % ZUGFeRDSchema
183
+ /ZUGFeRDSchema () def
184
+ ZUGFeRDVersion (rc) eq
185
+ ZUGFeRDVersion (1p0) eq or
186
+ ZUGFeRDVersion (2p0) eq or {
187
+ /ZUGFeRDSchema (ZUGFeRD PDFA Extension Schema) def
188
+ } if
189
+ ZUGFeRDVersion (2p1) eq {
190
+ /ZUGFeRDSchema (Factur-X PDFA Extension Schema) def
191
+ } if
192
+
193
+ % ZUGFeRDNamespaceURI
194
+ /ZUGFeRDNamespaceURI () def
195
+ ZUGFeRDVersion (rc) eq {
196
+ /ZUGFeRDNamespaceURI (urn:ferd:pdfa:invoice:rc#) def
197
+ } if
198
+ ZUGFeRDVersion (1p0) eq {
199
+ /ZUGFeRDNamespaceURI (urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#) def
200
+ } if
201
+ ZUGFeRDVersion (2p0) eq {
202
+ /ZUGFeRDNamespaceURI (urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#) def
203
+ } if
204
+ ZUGFeRDVersion (2p1) eq {
205
+ /ZUGFeRDNamespaceURI (urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#) def
206
+ } if
207
+
208
+ % ZUGFeRDPrefix
209
+ /ZUGFeRDPrefix () def
210
+ ZUGFeRDVersion (rc) eq
211
+ ZUGFeRDVersion (1p0) eq or
212
+ ZUGFeRDVersion (2p0) eq or {
213
+ /ZUGFeRDPrefix (zf) def
214
+ } if
215
+ ZUGFeRDVersion (2p1) eq {
216
+ /ZUGFeRDPrefix (fx) def
217
+ } if
218
+
219
+ % ZUGFeRDVersionDescription
220
+ /ZUGFeRDVersionDescription () def
221
+ ZUGFeRDVersion (rc) eq
222
+ ZUGFeRDVersion (1p0) eq or
223
+ ZUGFeRDVersion (2p0) eq or {
224
+ /ZUGFeRDVersionDescription (The actual version of the ZUGFeRD XML schema) def
225
+ } if
226
+ ZUGFeRDVersion (2p1) eq {
227
+ /ZUGFeRDVersionDescription (The actual version of the Factur-X XML schema) def
228
+ } if
229
+
230
+ % ZUGFeRDConformanceLevelDescription
231
+ /ZUGFeRDConformanceLevelDescription () def
232
+ ZUGFeRDVersion (rc) eq
233
+ ZUGFeRDVersion (1p0) eq or
234
+ ZUGFeRDVersion (2p0) eq or {
235
+ /ZUGFeRDConformanceLevelDescription (The conformance level of the embedded ZUGFeRD data) def
236
+ } if
237
+ ZUGFeRDVersion (2p1) eq {
238
+ /ZUGFeRDConformanceLevelDescription (The conformance level of the embedded Factur-X data) def
239
+ } if
240
+
241
+ % ZUGFeRDDocumentFileName
242
+ /ZUGFeRDDocumentFileName () def
243
+ ZUGFeRDVersion (rc) eq {
244
+ /ZUGFeRDDocumentFileName (ZUGFeRD-invoice.xml) def
245
+ } if
246
+ ZUGFeRDVersion (1p0) eq {
247
+ /ZUGFeRDDocumentFileName (ZUGFeRD-invoice.xml) def
248
+ } if
249
+ ZUGFeRDVersion (2p0) eq {
250
+ ZUGFeRDConformanceLevel (XRECHNUNG) ne {
251
+ /ZUGFeRDDocumentFileName (zugferd-invoice.xml) def
252
+ }{
253
+ /ZUGFeRDDocumentFileName (xrechnung.xml) def
254
+ } ifelse
255
+ } if
256
+ ZUGFeRDVersion (2p1) eq {
257
+ ZUGFeRDConformanceLevel (XRECHNUNG) ne {
258
+ /ZUGFeRDDocumentFileName (factur-x.xml) def
259
+ }{
260
+ /ZUGFeRDDocumentFileName (xrechnung.xml) def
261
+ } ifelse
262
+ } if
263
+
264
+ % ZUGFeRDVersionData
265
+ /ZUGFeRDVersionData () def
266
+ ZUGFeRDVersion (rc) eq {
267
+ /ZUGFeRDVersionData (RC) def
268
+ } if
269
+ ZUGFeRDVersion (1p0) eq {
270
+ /ZUGFeRDVersionData (1.0) def
271
+ } if
272
+ ZUGFeRDVersion (2p0) eq {
273
+ ZUGFeRDConformanceLevel (XRECHNUNG) ne {
274
+ /ZUGFeRDVersionData (2p0) def
275
+ }{
276
+ /ZUGFeRDVersionData (1p2) def
277
+ } ifelse
278
+ } if
279
+ ZUGFeRDVersion (2p1) eq {
280
+ ZUGFeRDConformanceLevel (XRECHNUNG) ne {
281
+ /ZUGFeRDVersionData (1.0) def
282
+ }{
283
+ /ZUGFeRDVersionData (1p2) def
284
+ } ifelse
285
+ } if
286
+
287
+ /ZUGFeRDMetadata [
288
+ (
289
+ <rdf:Description)
290
+ ( xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/")
291
+ ( xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#")
292
+ ( xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#")
293
+ ( rdf:about="">
294
+ <pdfaExtension:schemas>
295
+ <rdf:Bag>
296
+ <rdf:li rdf:parseType="Resource">
297
+ <pdfaSchema:schema>)ZUGFeRDSchema(</pdfaSchema:schema>
298
+ <pdfaSchema:namespaceURI>)ZUGFeRDNamespaceURI(</pdfaSchema:namespaceURI>
299
+ <pdfaSchema:prefix>)ZUGFeRDPrefix(</pdfaSchema:prefix>
300
+ <pdfaSchema:property>
301
+ <rdf:Seq>
302
+ <rdf:li rdf:parseType="Resource">
303
+ <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
304
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
305
+ <pdfaProperty:category>external</pdfaProperty:category>
306
+ <pdfaProperty:description>Name of the embedded XML invoice file</pdfaProperty:description>
307
+ </rdf:li>
308
+ <rdf:li rdf:parseType="Resource">
309
+ <pdfaProperty:name>DocumentType</pdfaProperty:name>
310
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
311
+ <pdfaProperty:category>external</pdfaProperty:category>
312
+ <pdfaProperty:description>INVOICE</pdfaProperty:description>
313
+ </rdf:li>
314
+ <rdf:li rdf:parseType="Resource">
315
+ <pdfaProperty:name>Version</pdfaProperty:name>
316
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
317
+ <pdfaProperty:category>external</pdfaProperty:category>
318
+ <pdfaProperty:description>)ZUGFeRDVersionDescription(</pdfaProperty:description>
319
+ </rdf:li>
320
+ <rdf:li rdf:parseType="Resource">
321
+ <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
322
+ <pdfaProperty:valueType>Text</pdfaProperty:valueType>
323
+ <pdfaProperty:category>external</pdfaProperty:category>
324
+ <pdfaProperty:description>)ZUGFeRDConformanceLevelDescription(</pdfaProperty:description>
325
+ </rdf:li>
326
+ </rdf:Seq>
327
+ </pdfaSchema:property>
328
+ </rdf:li>
329
+ </rdf:Bag>
330
+ </pdfaExtension:schemas>
331
+ </rdf:Description>
332
+ <rdf:Description xmlns:)ZUGFeRDPrefix(=")ZUGFeRDNamespaceURI(" rdf:about="">
333
+ <)ZUGFeRDPrefix(:ConformanceLevel>)ZUGFeRDConformanceLevel(</)ZUGFeRDPrefix(:ConformanceLevel>
334
+ <)ZUGFeRDPrefix(:DocumentFileName>)ZUGFeRDDocumentFileName(</)ZUGFeRDPrefix(:DocumentFileName>
335
+ <)ZUGFeRDPrefix(:DocumentType>INVOICE</)ZUGFeRDPrefix(:DocumentType>
336
+ <)ZUGFeRDPrefix(:Version>)ZUGFeRDVersionData(</)ZUGFeRDPrefix(:Version>
337
+ </rdf:Description>
338
+ )
339
+ ] concatstringarray def
340
+
341
+ /Usage {
342
+ (example usage: \n) print
343
+ ( gs --permit-file-read=/usr/home/me/zugferd/ \\\n) print
344
+ ( -sDEVICE=pdfwrite \\\n) print
345
+ ( -dPDFA=3 \\\n) print
346
+ ( -sColorConversionStrategy=RGB \\\n) print
347
+ ( -sZUGFeRDXMLFile=/usr/home/me/zugferd/invoice.xml \\\n) print
348
+ ( -sZUGFeRDProfile=/usr/home/me/zugferd\rgb.icc \\\n) print
349
+ ( -sZUGFeRDVersion=2p1 \\\n) print
350
+ ( -sZUGFeRDConformanceLevel=BASIC \\\n) print
351
+ ( -o /usr/home/me/zugferd/zugferd.pdf \\\n) print
352
+ ( /usr/home/me/zugferd/zugferd.ps \\\n) print
353
+ ( /usr/home/me/zugferd/original.pdf \n) print
354
+ flush
355
+ } def
356
+
357
+ % First check that the user has defined the XML invoice file on the command line
358
+ %
359
+ /ZUGFeRDXMLFile where {
360
+ pop % Discard the dictionary
361
+ %
362
+ % Now check that the ICC Profile is defined
363
+ %
364
+ /ZUGFeRDProfile where {
365
+ pop % Discard the dictionary
366
+
367
+ % Step 1, add the required PDF/A boilerplate.
368
+ % This is mostly copied from lib/pdfa_def.ps
369
+
370
+ % Create a PDF stream object to hold the ICC profile.
371
+ [ /_objdef {icc_PDFA} /type /stream /OBJ pdfmark
372
+
373
+ % Add the required entries to the stream dictionary (/N only)
374
+ [ {icc_PDFA}
375
+ <<
376
+ %% This code attempts to set the /N (number of components) key for the ICC colour space.
377
+ %% To do this it checks the ColorConversionStrategy or the device ProcessColorModel if
378
+ %% ColorConversionStrategy is not set.
379
+ %% This is not 100% reliable. A better solution is for the user to edit this and replace
380
+ %% the code between the ---8<--- lines with a simple declaration like:
381
+ %% /N 3
382
+ %% where the value of N is the number of components from the profile defined in ZUGFeRDProfile.
383
+ %%
384
+ %% ----------8<--------------8<-------------8<--------------8<----------
385
+ systemdict /ColorConversionStrategy known {
386
+ systemdict /ColorConversionStrategy get cvn dup /Gray eq {
387
+ pop /N 1 false
388
+ }{
389
+ dup /RGB eq {
390
+ pop /N 3 false
391
+ }{
392
+ /CMYK eq {
393
+ /N 4 false
394
+ }{
395
+ (ColorConversionStrategy not a device space, falling back to ProcessColorModel, output may not be valid PDF/A.)=
396
+ true
397
+ } ifelse
398
+ } ifelse
399
+ } ifelse
400
+ } {
401
+ (ColorConversionStrategy not set, falling back to ProcessColorModel, output may not be valid PDF/A.)=
402
+ true
403
+ } ifelse
404
+
405
+ {
406
+ currentpagedevice /ProcessColorModel get
407
+ dup /DeviceGray eq {
408
+ pop /N 1
409
+ }{
410
+ dup /DeviceRGB eq {
411
+ pop /N 3
412
+ }{
413
+ dup /DeviceCMYK eq {
414
+ pop /N 4
415
+ } {
416
+ (ProcessColorModel not a device space.)=
417
+ /ProcessColorModel cvx /rangecheck signalerror
418
+ } ifelse
419
+ } ifelse
420
+ } ifelse
421
+ } if
422
+ %% ----------8<--------------8<-------------8<--------------8<----------
423
+ >> /PUT pdfmark
424
+
425
+ % Now read the ICC profile from the file into the stream
426
+ [ {icc_PDFA} ZUGFeRDProfile (r) file /PUT pdfmark
427
+
428
+ % Define the output intent dictionary :
429
+ [/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
430
+
431
+ % Add the required keys to the dictionary
432
+ [{OutputIntent_PDFA} <<
433
+ /Type /OutputIntent
434
+ /S /GTS_PDFA1 % Required for PDF/A.
435
+ /DestOutputProfile {icc_PDFA} % The actual profile.
436
+ /OutputConditionIdentifier (Custom) % TODO: A better solution is a
437
+ % a string from the ICC
438
+ % Registry, but Custom
439
+ % is always valid.
440
+ >> /PUT pdfmark
441
+
442
+ % And now add the OutputIntent to the Catalog dictionary
443
+ [ {Catalog} << /OutputIntents [ {OutputIntent_PDFA} ]>> /PUT pdfmark
444
+
445
+ % Step 2, define the XML file and read it into the PDF
446
+ % First we define the PDF stream to contain the XML invoice
447
+ [ /_objdef {InvoiceStream} /type /stream /OBJ pdfmark
448
+ % Fill in the dictionary elements we need. We believe the
449
+ % ModDate is not useful so it's just set to a valid value.
450
+ [ {InvoiceStream} <<
451
+ /Type /EmbeddedFile
452
+ /Subtype (text/xml) cvn
453
+ /Params <<
454
+ /ModDate systemdict /ZUGFeRDDateTime known
455
+ {ZUGFeRDDateTime}
456
+ {(D:20130121081433+01'00')}
457
+ ifelse
458
+ /Size ZUGFeRDXMLFile status
459
+ {pop pop exch pop}
460
+ {(Failed to get file status!\n)print /status load /ioerror signalerror}
461
+ ifelse
462
+ >>
463
+ >> /PUT pdfmark
464
+ % Now read the data from the file and store it in the stream
465
+ [ {InvoiceStream} ZUGFeRDXMLFile (r) file /PUT pdfmark
466
+ % and close the stream
467
+ [ {InvoiceStream} /CLOSE pdfmark
468
+
469
+ % Step 3 create the File Specification dictionary for the embedded file
470
+ % Create the dictionary
471
+ [ /_objdef {FSDict} /type /dict /OBJ pdfmark
472
+ % Fill in the required dictionary elements
473
+ [ {FSDict} <<
474
+ /Type /Filespec
475
+ /F ZUGFeRDDocumentFileName
476
+ /UF ZUGFeRDDocumentFileName SimpleUTF16BE
477
+ /Desc (ZUGFeRD electronic invoice)
478
+ /AFRelationship /Alternative
479
+ /EF <<
480
+ /F {InvoiceStream}
481
+ /UF {InvoiceStream}
482
+ >>
483
+ >>
484
+ /PUT pdfmark
485
+
486
+ % Step 4 Create the Associated Files dictionary to hold the FS dict
487
+ % Create the dictionary
488
+ [ /_objdef {AFArray} /type /array /OBJ pdfmark
489
+ % Put (append) the FS dictionary into the Associated Files array
490
+ [ {AFArray} {FSDict} /APPEND pdfmark
491
+
492
+ % Step 5 Add an entry in the Catalog dictionary containing the AF array
493
+ % Since Ghostscript 10.04.0 this is no longer required, providing the output file is
494
+ % PDF/A-3 or higher. The PDF/A-3 tech note 0010 (p28) clarifies that the /AF in the Catalog
495
+ % is mandatory and GS 10.04.0 will emit one itself.
496
+ %
497
+ % This line retained for historical reference, and as an example for cases where the output
498
+ % is not PDF/A-3
499
+ %
500
+ % [ {Catalog} << /AF {AFArray} >> /PUT pdfmark
501
+
502
+ % Step 6 use the EMBED pdfmark to add the XML file and FS dictionary to the PDF name tree
503
+ [ /Name ZUGFeRDDocumentFileName /FS {FSDict} /EMBED pdfmark
504
+
505
+ % Step 7 Add the extra ZUGFeRD XML data to the Metadata
506
+ [ /XML ZUGFeRDMetadata /Ext_Metadata pdfmark
507
+ }
508
+ {
509
+ % No ICC Profile definition on the command line;
510
+ % chide the user and give them an example
511
+ (\nERROR - ZUGFeRDProfile has not been supplied, you must supply an ICC profile) print
512
+ (\n Producing a potentially INVALID PDF/A file. \n) print
513
+ Usage
514
+ } ifelse
515
+ }
516
+ {
517
+ % No XML invoice definition on the command line;
518
+ % chide the user and give them an example
519
+ (\nERROR - ZUGFeRDXMLFile has not been supplied, you must supply a XML invoice file) print
520
+ (\n Producing a PDF/A file, NOT a ZUGFeRD file. \n) print
521
+ Usage
522
+ } ifelse
523
+
524
+ % That's all the ZUGFeRD and PDF/A-3 setup completed,
525
+ % all that remains now is to run the input file
526
+
527
+ %%EOF
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zugpferd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Zeitler
@@ -66,9 +66,10 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '13.0'
69
- description: Read, write and convert electronic invoices according to EN 16931. Supports
70
- UBL 2.1 and UN/CEFACT CII syntaxes with dedicated classes for Invoice, Credit Note,
71
- Corrected Invoice, Self-billed Invoice, Partial Invoice and Prepayment Invoice.
69
+ description: Read, write and convert XRechnung and ZUGFeRD electronic invoices (e-Rechnung)
70
+ according to EN 16931. Supports UBL 2.1 and UN/CEFACT CII syntaxes with dedicated
71
+ classes for Invoice, Credit Note, Corrected Invoice, Self-billed Invoice, Partial
72
+ Invoice and Prepayment Invoice.
72
73
  email:
73
74
  executables: []
74
75
  extensions: []
@@ -96,9 +97,18 @@ files:
96
97
  - lib/zugpferd/model/tax_breakdown.rb
97
98
  - lib/zugpferd/model/tax_subtotal.rb
98
99
  - lib/zugpferd/model/trade_party.rb
100
+ - lib/zugpferd/pdf.rb
101
+ - lib/zugpferd/pdf/embedder.rb
99
102
  - lib/zugpferd/ubl/mapping.rb
100
103
  - lib/zugpferd/ubl/reader.rb
101
104
  - lib/zugpferd/ubl/writer.rb
105
+ - lib/zugpferd/validation.rb
106
+ - lib/zugpferd/validation/mustang_validator.rb
107
+ - lib/zugpferd/validation/pdf_validator.rb
108
+ - lib/zugpferd/validation/schema_validator.rb
109
+ - lib/zugpferd/validation/schematron_validator.rb
110
+ - vendor/zugferd/default_rgb.icc
111
+ - vendor/zugferd/zugferd.ps
102
112
  homepage: https://alexzeitler.github.io/zugpferd/
103
113
  licenses:
104
114
  - MIT
@@ -126,5 +136,5 @@ requirements: []
126
136
  rubygems_version: 3.5.20
127
137
  signing_key:
128
138
  specification_version: 4
129
- summary: EN 16931 E-Invoice library for Ruby (UBL + CII)
139
+ summary: XRechnung & ZUGFeRD e-invoicing library for Ruby (UBL + CII)
130
140
  test_files: []