Module: Pdfrb::Conformance::PdfX
- Defined in:
- lib/pdfrb/conformance/pdf_x.rb
Overview
PDF/X conformance profiles (ISO 15930). PDF/X is the graphic arts exchange standard for print production. Key constraints:
PDF/X-1a (ISO 15930-1): CMYK + spot only, no RGB, no live ICC
PDF/X-3 (ISO 15930-3): allows calibrated RGB and Lab
PDF/X-4 (ISO 15930-7): allows transparency and layers
PDF/X-6 (ISO 15930-11): PDF 2.0 based
Shared requirements across all X profiles:
* Output intent required (/OutputIntents with /GTS_PDFX)
* BleedBox and TrimBox required
* All fonts embedded
* No encryption
* No actions (/AA), no JavaScript
* /Trapped key present (True/False/Unknown)
Constant Summary collapse
- SHARED =
rubocop:disable Metrics/BlockLength
RuleSet.new("PDF/X-shared").tap do |rs| # rubocop:disable Metrics/BlockLength rs.register(Rule.new( id: "px-1", description: "Output intent required", severity: :error, spec_clause: "ISO 15930 §6.1", check: ->(doc) { next nil if has_pdfx_output_intent?(doc) Violation.new(rule_id: "px-1", message: "PDF/X requires a /GTS_PDFX output intent", object: "Catalog", severity: :error, spec_clause: "ISO 15930 §6.1") } )) rs.register(Rule.new( id: "px-2", description: "TrimBox or ArtBox required on every page", severity: :error, spec_clause: "ISO 15930 §6.2.3", check: ->(doc) { violations = [] doc.pages.each do |page| next if page.value[:TrimBox] || page.value[:ArtBox] violations << Violation.new( rule_id: "px-2", message: "Page missing /TrimBox (required for PDF/X)", object: page.oid, severity: :error, spec_clause: "ISO 15930 §6.2.3" ) end violations.empty? ? nil : violations } )) rs.register(Rule.new( id: "px-3", description: "All fonts must be embedded", severity: :error, spec_clause: "ISO 15930 §6.3", check: ->(doc) { violations = [] doc.pages.each do |page| resources = page.value[:Resources] next unless resources fonts = if resources.is_a?(Pdfrb::Model::Cos::Dictionary) resources.value[:Font] else resources[:Font] end next unless fonts fonts_hash = fonts.is_a?(Pdfrb::Model::Cos::Dictionary) ? fonts.value : fonts fonts_hash&.each_value do |ref| font = ref.is_a?(Pdfrb::Model::Reference) ? doc.object(ref) : ref next unless font fd = font[:FontDescriptor] next if fd violations << Violation.new( rule_id: "px-3", message: "Font /#{font[:BaseFont]} not embedded", object: font[:BaseFont], severity: :error, spec_clause: "ISO 15930 §6.3" ) end end violations.empty? ? nil : violations } )) rs.register(Rule.new( id: "px-4", description: "Encryption prohibited", severity: :error, spec_clause: "ISO 15930 §6.1.3", check: ->(doc) { next nil unless doc.trailer && doc.trailer[:Encrypt] Violation.new(rule_id: "px-4", message: "PDF/X prohibits encryption", object: "trailer", severity: :error, spec_clause: "ISO 15930 §6.1.3") } )) rs.register(Rule.new( id: "px-5", description: "/Trapped key required", severity: :error, spec_clause: "ISO 15930 §6.1.6", check: ->(doc) { info = PdfX.info_dict(doc) next nil if info && info[:Trapped] Violation.new(rule_id: "px-5", message: "Document Info /Trapped key required (True, False, or Unknown)", object: "Info", severity: :error, spec_clause: "ISO 15930 §6.1.6") } )) rs.register(Rule.new( id: "px-6", description: "No JavaScript or form actions", severity: :error, spec_clause: "ISO 15930 §6.5", check: ->(doc) { violations = [] doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) s = obj[:S] next unless [:JavaScript, :JS, :Launch, :URI].include?(s) violations << Violation.new( rule_id: "px-6", message: "Action /#{s} prohibited in PDF/X", object: obj.oid, severity: :error, spec_clause: "ISO 15930 §6.5" ) end violations.empty? ? nil : violations } )) end
- X1A =
X-1a: strictest — CMYK + spot only
RuleSet.new("PDF/X-1a").tap do |rs| SHARED.rules.each { |r| rs.register(r) } rs.register(Rule.new( id: "x1a-1", description: "PDF/X-1a: no RGB or Lab color spaces", severity: :error, spec_clause: "ISO 15930-1 §6.3.8", check: ->(doc) { violations = [] scan_color_spaces(doc) do |cs, oid| case cs.to_s when "DeviceRGB", "CalRGB", "CalGray", "Lab" violations << Violation.new( rule_id: "x1a-1", message: "PDF/X-1a prohibits #{cs} color space", object: oid, severity: :error, spec_clause: "ISO 15930-1 §6.3.8" ) end end violations.empty? ? nil : violations } )) end
- X3 =
X-3: allows calibrated RGB and Lab
RuleSet.new("PDF/X-3").tap do |rs| SHARED.rules.each { |r| rs.register(r) } end
- X4 =
X-4: allows transparency and layers
RuleSet.new("PDF/X-4").tap do |rs| SHARED.rules.each { |r| rs.register(r) } end
Class Method Summary collapse
- .has_pdfx_output_intent?(doc) ⇒ Boolean
- .info_dict(doc) ⇒ Object
- .profiles ⇒ Object
- .rule_set_for(level) ⇒ Object
- .scan_color_spaces(doc, &block) ⇒ Object
- .validate(document, level: :x4) ⇒ Object
Class Method Details
.has_pdfx_output_intent?(doc) ⇒ Boolean
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 170 def has_pdfx_output_intent?(doc) intents = doc.catalog.value[:OutputIntents] return false unless intents intents = [intents] unless intents.is_a?(::Array) intents.any? do |ref| obj = ref.is_a?(Pdfrb::Model::Reference) ? doc.object(ref) : ref obj && obj.value[:S] == :GTS_PDFX end end |
.info_dict(doc) ⇒ Object
181 182 183 184 185 186 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 181 def info_dict(doc) info_ref = doc.trailer && doc.trailer[:Info] return nil unless info_ref doc.object(info_ref) end |
.profiles ⇒ Object
23 24 25 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 23 def profiles { x1a: X1A, x3: X3, x4: X4 } end |
.rule_set_for(level) ⇒ Object
31 32 33 34 35 36 37 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 31 def rule_set_for(level) case level.to_s when /\Ax1/i then X1A when /\Ax3/i then X3 else X4 end end |
.scan_color_spaces(doc, &block) ⇒ Object
224 225 226 227 228 229 230 231 232 233 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 224 def scan_color_spaces(doc, &block) doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) cs = obj.value[:ColorSpace] next unless cs yield_color_space(cs, obj.oid, &block) end end |
.validate(document, level: :x4) ⇒ Object
27 28 29 |
# File 'lib/pdfrb/conformance/pdf_x.rb', line 27 def validate(document, level: :x4) rule_set_for(level).validate(document) end |