claricle 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +10 -1
- data/LICENSE +25 -0
- data/README.adoc +439 -157
- data/TODO.plan/00-overview.md +502 -0
- data/TODO.plan/01-core.md +296 -0
- data/TODO.plan/02-inspect.md +163 -0
- data/TODO.plan/03-conform.md +208 -0
- data/TODO.plan/04-convert.md +216 -0
- data/exe/claricle +1 -1
- data/lib/claricle/batch.rb +240 -0
- data/lib/claricle/cli.rb +514 -40
- data/lib/claricle/conformance_options.rb +49 -0
- data/lib/claricle/conversion_engine.rb +21 -0
- data/lib/claricle/convert.rb +21 -0
- data/lib/claricle/detector.rb +696 -0
- data/lib/claricle/emf_header.rb +33 -0
- data/lib/claricle/errors.rb +73 -0
- data/lib/claricle/fault.rb +69 -0
- data/lib/claricle/handlers/base.rb +169 -0
- data/lib/claricle/handlers/extension_points.rb +124 -0
- data/lib/claricle/handlers/metafile.rb +1011 -0
- data/lib/claricle/handlers/pdf.rb +961 -0
- data/lib/claricle/handlers/pdf_arlington/definitions.rb +54 -0
- data/lib/claricle/handlers/pdf_arlington/kind.rb +62 -0
- data/lib/claricle/handlers/pdf_arlington/predicate_gate.rb +39 -0
- data/lib/claricle/handlers/pdf_arlington/rules.rb +86 -0
- data/lib/claricle/handlers/pdf_arlington/walker.rb +131 -0
- data/lib/claricle/handlers/pdf_arlington.rb +38 -0
- data/lib/claricle/handlers/pdf_profiles.rb +66 -0
- data/lib/claricle/handlers/png.rb +1107 -0
- data/lib/claricle/handlers/postscript.rb +1357 -0
- data/lib/claricle/handlers/svg.rb +787 -0
- data/lib/claricle/image.rb +272 -0
- data/lib/claricle/lossiness/levels.rb +13 -0
- data/lib/claricle/lossiness.rb +864 -0
- data/lib/claricle/models/base.rb +460 -0
- data/lib/claricle/models/batch_item.rb +127 -0
- data/lib/claricle/models/conversion.rb +167 -0
- data/lib/claricle/models/format_capability.rb +40 -0
- data/lib/claricle/models/free_form_hash.rb +77 -0
- data/lib/claricle/models/inspection.rb +188 -0
- data/lib/claricle/models/issue.rb +23 -0
- data/lib/claricle/models/location.rb +51 -0
- data/lib/claricle/models/report.rb +54 -0
- data/lib/claricle/models/uncoerced_integer.rb +38 -0
- data/lib/claricle/postscript_fidelity/verdict.rb +37 -0
- data/lib/claricle/postscript_fidelity/view_box.rb +82 -0
- data/lib/claricle/postscript_fidelity.rb +160 -0
- data/lib/claricle/registry.rb +125 -0
- data/lib/claricle/version.rb +1 -1
- data/lib/claricle/writer.rb +468 -0
- data/lib/claricle.rb +244 -7
- metadata +201 -19
- data/claricle.gemspec +0 -42
- data/sig/claricle.rbs +0 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kind"
|
|
4
|
+
|
|
5
|
+
module Claricle
|
|
6
|
+
module Handlers
|
|
7
|
+
module PdfArlington
|
|
8
|
+
# Chooses the Arlington table a value is checked against.
|
|
9
|
+
module Definitions
|
|
10
|
+
LINK_GROUP = /\A\[([\w,-]+)\]\z/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The one candidate table for this value, or nil. With several
|
|
15
|
+
# candidates (`[PageTreeNode,PageObject]`) the value's own /Type
|
|
16
|
+
# must single one out; otherwise no claim is made.
|
|
17
|
+
def pick(names, dict)
|
|
18
|
+
candidates = names.filter_map { |name| load(name) }
|
|
19
|
+
return candidates.first if candidates.one?
|
|
20
|
+
|
|
21
|
+
matching = candidates.select { |candidate| types_of(candidate).include?(dict&.fetch(:Type, nil).to_s) }
|
|
22
|
+
matching.first if matching.one?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def types_of(definition)
|
|
26
|
+
definition.field_for("Type")&.possible_value_list || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def load(name)
|
|
30
|
+
::Pdfrb::Arlington::Loader.object_definition(name)
|
|
31
|
+
rescue ::Pdfrb::Error
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# One link group per type in the field's type list (`array;stream`
|
|
36
|
+
# pairs with `[A];[B]`): the group of the type the value really is.
|
|
37
|
+
def link_names(field, kind)
|
|
38
|
+
match = LINK_GROUP.match(link_group(field, kind).to_s)
|
|
39
|
+
match && match[1].split(",")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def link_group(field, kind)
|
|
43
|
+
types = field.types_raw.split(";").map(&:strip)
|
|
44
|
+
groups = field.link.to_s.split(";")
|
|
45
|
+
return groups.first if types.one?
|
|
46
|
+
return unless groups.size == types.size
|
|
47
|
+
|
|
48
|
+
index = types.index { |type| Kind::ACCEPTS[type]&.include?(kind) }
|
|
49
|
+
groups[index] if index
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claricle
|
|
4
|
+
module Handlers
|
|
5
|
+
module PdfArlington
|
|
6
|
+
# Maps a Ruby value read from pdfrb onto the Arlington type names it
|
|
7
|
+
# satisfies. A kind of nil means "not one pdfrb type this table knows",
|
|
8
|
+
# and the caller then makes no claim about the value.
|
|
9
|
+
module Kind
|
|
10
|
+
ACCEPTS = {
|
|
11
|
+
"integer" => %i[integer], "bitmask" => %i[integer], "number" => %i[integer number],
|
|
12
|
+
"boolean" => %i[boolean], "name" => %i[name], "null" => %i[null], "stream" => %i[stream],
|
|
13
|
+
"string" => %i[string], "string-ascii" => %i[string], "string-byte" => %i[string],
|
|
14
|
+
"string-text" => %i[string], "date" => %i[string],
|
|
15
|
+
"dictionary" => %i[dictionary], "name-tree" => %i[dictionary],
|
|
16
|
+
"number-tree" => %i[dictionary],
|
|
17
|
+
"array" => %i[array], "rectangle" => %i[array], "matrix" => %i[array]
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
CLASS_KINDS = {
|
|
21
|
+
::Hash => :dictionary, ::Array => :array, ::Symbol => :name, ::Integer => :integer,
|
|
22
|
+
::Float => :number, ::String => :string, ::NilClass => :null,
|
|
23
|
+
::TrueClass => :boolean, ::FalseClass => :boolean
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
CONTAINERS = %i[dictionary stream array].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# A stream keeps its wrapper (the dictionary is only part of it); any
|
|
31
|
+
# other pdfrb object is reduced to the plain value it holds.
|
|
32
|
+
def plain(object)
|
|
33
|
+
return object if stream?(object)
|
|
34
|
+
|
|
35
|
+
object.respond_to?(:value) ? object.value : object
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stream?(object)
|
|
39
|
+
::Pdfrb::Model::Cos::Stream === object # rubocop:disable Style/CaseEquality
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def of(value)
|
|
43
|
+
return :stream if stream?(value)
|
|
44
|
+
|
|
45
|
+
CLASS_KINDS.find { |klass, _| value.is_a?(klass) }&.last
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The Arlington type names of a field, or nil when any of them is a
|
|
49
|
+
# predicate (`fn:SinceVersion(1.3,dictionary)`) this walker does not
|
|
50
|
+
# evaluate -- a partly understood type list is not checked.
|
|
51
|
+
def types(field)
|
|
52
|
+
names = field.types_raw.split(";").map(&:strip)
|
|
53
|
+
names if names.any? && names.all? { |name| ACCEPTS.key?(name) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def allows?(names, kind)
|
|
57
|
+
names.any? { |name| ACCEPTS.fetch(name).include?(kind) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claricle
|
|
4
|
+
module Handlers
|
|
5
|
+
module PdfArlington
|
|
6
|
+
# Runs a field's `fn:IsRequired(...)` predicate through pdfrb's
|
|
7
|
+
# evaluator. pdfrb 0.7.49 does not parse every predicate in its own
|
|
8
|
+
# tables (measured: 78 of 192 Required predicates raise SyntaxError)
|
|
9
|
+
# and lacks some functions, so anything it cannot parse or run is
|
|
10
|
+
# "not known to be required" -- never an issue.
|
|
11
|
+
class PredicateGate
|
|
12
|
+
def initialize(version)
|
|
13
|
+
@version = version
|
|
14
|
+
@parsed = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def required?(source, dict)
|
|
18
|
+
ast = @parsed.fetch(source) { @parsed[source] = parse(source) }
|
|
19
|
+
ast ? evaluate(ast, dict) == true : false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse(source)
|
|
25
|
+
::Pdfrb::Arlington::Predicate::Parser.parse(source)
|
|
26
|
+
rescue ::Pdfrb::Error
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def evaluate(ast, dict)
|
|
31
|
+
context = ::Pdfrb::Arlington::Predicate::Context.new(current: dict, version: @version)
|
|
32
|
+
::Pdfrb::Arlington::Predicate::Evaluator.call(ast, context)
|
|
33
|
+
rescue ::Pdfrb::Error, NoMethodError, TypeError, ArgumentError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/issue"
|
|
4
|
+
require_relative "../../models/location"
|
|
5
|
+
require_relative "kind"
|
|
6
|
+
require_relative "predicate_gate"
|
|
7
|
+
|
|
8
|
+
module Claricle
|
|
9
|
+
module Handlers
|
|
10
|
+
module PdfArlington
|
|
11
|
+
# What Arlington says about ONE dictionary or ONE value, as Issues.
|
|
12
|
+
# Knows nothing about traversal.
|
|
13
|
+
class Rules
|
|
14
|
+
REQUIRED_CODE = "PDF_ARLINGTON_REQUIRED_KEY"
|
|
15
|
+
TYPE_CODE = "PDF_ARLINGTON_TYPE"
|
|
16
|
+
VALUE_CODE = "PDF_ARLINGTON_VALUE"
|
|
17
|
+
|
|
18
|
+
# `[Pages]` or `[1.0,1.1]`: plain tokens only, so a predicate
|
|
19
|
+
# (`[fn:Eval(...)]`) or a wildcard is never read as a literal.
|
|
20
|
+
PLAIN_VALUES = /\A\[([^\[\]()@*;]+)\]\z/
|
|
21
|
+
|
|
22
|
+
def initialize(version)
|
|
23
|
+
@version = version
|
|
24
|
+
@gate = PredicateGate.new(version)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `inherited` answers "does an ancestor supply this key", for the
|
|
28
|
+
# fields Arlington marks inheritable (a Page's /Resources, /MediaBox).
|
|
29
|
+
def dictionary_issues(definition, dict, path, inherited)
|
|
30
|
+
definition.fields.filter_map do |field|
|
|
31
|
+
next unless missing_required?(field, dict, inherited)
|
|
32
|
+
|
|
33
|
+
issue(REQUIRED_CODE, "#{definition.name} is missing required key /#{field.key}", path)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def value_issues(field, kind, value, path)
|
|
38
|
+
return [] if kind.nil?
|
|
39
|
+
|
|
40
|
+
[type_issue(field, kind, path), name_issue(field, kind, value, path)].compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def missing_required?(field, dict, inherited)
|
|
46
|
+
return false if field.key == "*" || !dict[field.key.to_sym].nil? || !applies?(field)
|
|
47
|
+
return false if field.inheritable? && inherited.call(field.key)
|
|
48
|
+
|
|
49
|
+
required_now?(field, dict)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def required_now?(field, dict)
|
|
53
|
+
field.required? || (field.required_predicate? && @gate.required?(field.required_literal, dict))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A key added after the document's version, or by an extension, is
|
|
57
|
+
# not required of it.
|
|
58
|
+
def applies?(field)
|
|
59
|
+
field.since_version.extensions.empty? && (field.since_version <=> @version) <= 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def type_issue(field, kind, path)
|
|
63
|
+
names = Kind.types(field)
|
|
64
|
+
return if names.nil? || kind == :null || Kind.allows?(names, kind)
|
|
65
|
+
|
|
66
|
+
issue(TYPE_CODE, "#{path} is a #{kind}; Arlington allows #{names.join(" or ")}", path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def name_issue(field, kind, value, path)
|
|
70
|
+
return unless kind == :name && field.types_raw == "name"
|
|
71
|
+
|
|
72
|
+
match = PLAIN_VALUES.match(field.possible_values.to_s)
|
|
73
|
+
allowed = match && match[1].split(",").map(&:strip)
|
|
74
|
+
return if allowed.nil? || allowed.include?(value.to_s)
|
|
75
|
+
|
|
76
|
+
issue(VALUE_CODE, "#{path} is /#{value}; Arlington allows #{allowed.join(", ")}", path)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def issue(code, message, path)
|
|
80
|
+
Models::Issue.new(severity: "error", code: code, message: message,
|
|
81
|
+
location: Models::Location.new(node_path: path))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/issue"
|
|
4
|
+
require_relative "definitions"
|
|
5
|
+
require_relative "kind"
|
|
6
|
+
require_relative "rules"
|
|
7
|
+
|
|
8
|
+
module Claricle
|
|
9
|
+
module Handlers
|
|
10
|
+
module PdfArlington
|
|
11
|
+
# Walks a parsed document breadth-first from the Catalog, following
|
|
12
|
+
# every link Arlington's tables name, and collects what `Rules` finds.
|
|
13
|
+
#
|
|
14
|
+
# Bounded three ways: an object reached through a reference is
|
|
15
|
+
# visited once (cycles end), the walk is a worklist rather than
|
|
16
|
+
# recursion (depth costs no stack), and it stops after MAX_NODES
|
|
17
|
+
# values with a warning that says it stopped.
|
|
18
|
+
class Walker
|
|
19
|
+
MAX_NODES = 50_000
|
|
20
|
+
LIMIT_CODE = "PDF_ARLINGTON_LIMIT"
|
|
21
|
+
MAX_ANCESTORS = 64
|
|
22
|
+
|
|
23
|
+
Item = Data.define(:value, :field, :names, :path)
|
|
24
|
+
|
|
25
|
+
def initialize(document, version)
|
|
26
|
+
@document = document
|
|
27
|
+
@rules = Rules.new(version)
|
|
28
|
+
@issues = []
|
|
29
|
+
@seen = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def issues
|
|
33
|
+
queue = [Item.new(@document.catalog, nil, %w[Catalog], "Catalog")]
|
|
34
|
+
count = 0
|
|
35
|
+
until queue.empty?
|
|
36
|
+
return limit_reached if (count += 1) > MAX_NODES || count + queue.size > MAX_NODES * 2
|
|
37
|
+
|
|
38
|
+
visit(queue.shift, queue)
|
|
39
|
+
end
|
|
40
|
+
@issues
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def visit(item, queue)
|
|
46
|
+
value = Kind.plain(resolve(item.value))
|
|
47
|
+
kind = Kind.of(value)
|
|
48
|
+
@issues.concat(@rules.value_issues(item.field, kind, value, item.path)) if item.field
|
|
49
|
+
return unless Kind::CONTAINERS.include?(kind) && first_visit?(item.value)
|
|
50
|
+
|
|
51
|
+
definition = definition_for(item, value, kind)
|
|
52
|
+
descend(definition, value, kind, item.path, queue) if definition
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def definition_for(item, value, kind)
|
|
56
|
+
names = item.names || Definitions.link_names(item.field, kind)
|
|
57
|
+
names && Definitions.pick(names, kind == :array ? nil : dictionary_of(value))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def descend(definition, value, kind, path, queue)
|
|
61
|
+
dict = kind == :array ? nil : dictionary_of(value)
|
|
62
|
+
check(definition, dict, path) if dict
|
|
63
|
+
entries(dict || value).each do |key, child, child_path|
|
|
64
|
+
field = definition.field_for(key.to_s) || definition.field_for("*")
|
|
65
|
+
queue << Item.new(child, field, nil, "#{path}#{child_path}") if field
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check(definition, dict, path)
|
|
70
|
+
@issues.concat(@rules.dictionary_issues(definition, dict, path, ->(key) { inherited?(dict, key) }))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entries(container)
|
|
74
|
+
if container.is_a?(::Hash)
|
|
75
|
+
container.map { |key, child| [key, child, "/#{key}"] }
|
|
76
|
+
else
|
|
77
|
+
container.each_with_index.map { |child, index| [index, child, "[#{index}]"] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dictionary_of(value)
|
|
82
|
+
Kind.stream?(value) ? value.value : value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def first_visit?(raw)
|
|
86
|
+
return true unless raw.is_a?(::Pdfrb::Model::Reference)
|
|
87
|
+
|
|
88
|
+
@seen.key?(raw.oid) ? false : (@seen[raw.oid] = true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Walks /Parent upward, never revisiting an object and never more
|
|
92
|
+
# than MAX_ANCESTORS steps, so a looping page tree ends.
|
|
93
|
+
def inherited?(dict, key)
|
|
94
|
+
seen = {}
|
|
95
|
+
current = dict
|
|
96
|
+
MAX_ANCESTORS.times do
|
|
97
|
+
current = parent_of(current, seen)
|
|
98
|
+
return false unless current
|
|
99
|
+
return true unless current[key.to_sym].nil?
|
|
100
|
+
end
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parent_of(dict, seen)
|
|
105
|
+
parent = dict[:Parent]
|
|
106
|
+
return unless parent.is_a?(::Pdfrb::Model::Reference) && !seen.key?(parent.oid)
|
|
107
|
+
|
|
108
|
+
seen[parent.oid] = true
|
|
109
|
+
resolved = Kind.plain(resolve(parent))
|
|
110
|
+
resolved if resolved.is_a?(::Hash)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# nil when pdfrb cannot produce the object. Everything pdfrb
|
|
114
|
+
# reports by raising on a malformed file is "no object here"; the
|
|
115
|
+
# structural check owns reporting the file as broken.
|
|
116
|
+
def resolve(value)
|
|
117
|
+
@document.object(value)
|
|
118
|
+
rescue ::Pdfrb::Error, NoMethodError, TypeError, ArgumentError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def limit_reached
|
|
123
|
+
@issues << Models::Issue.new(
|
|
124
|
+
severity: "warning", code: LIMIT_CODE,
|
|
125
|
+
message: "Arlington check stopped after #{MAX_NODES} values; the rest was not checked"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pdf_arlington/walker"
|
|
4
|
+
|
|
5
|
+
module Claricle
|
|
6
|
+
module Handlers
|
|
7
|
+
# Arlington-model conformance for a parsed pdfrb document: required
|
|
8
|
+
# keys, value types and enumerated names, checked from the Catalog
|
|
9
|
+
# down. pdfrb ships Arlington's tables, predicate evaluator and
|
|
10
|
+
# loader but no document runner (measured on 0.7.49), so the walk is
|
|
11
|
+
# ours.
|
|
12
|
+
module PdfArlington
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# `document` is a `Pdfrb::Document`. Returns an Array of
|
|
16
|
+
# `Models::Issue`; empty when the document has no Catalog (the
|
|
17
|
+
# structural check reports that).
|
|
18
|
+
def issues(document)
|
|
19
|
+
return [] unless catalog?(document)
|
|
20
|
+
|
|
21
|
+
Walker.new(document, version_of(document)).issues
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A Catalog that cannot be read is the structural check's finding.
|
|
25
|
+
def catalog?(document)
|
|
26
|
+
Kind.plain(document.catalog).is_a?(::Hash)
|
|
27
|
+
rescue ::Pdfrb::Error, NoMethodError, SystemStackError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def version_of(document)
|
|
32
|
+
::Pdfrb::Arlington::PdfVersion.new(document.version.to_s)
|
|
33
|
+
rescue ::Pdfrb::Error, ArgumentError
|
|
34
|
+
::Pdfrb::Arlington::PdfVersion.new("1.0")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/issue"
|
|
4
|
+
require_relative "../models/location"
|
|
5
|
+
|
|
6
|
+
module Claricle
|
|
7
|
+
module Handlers
|
|
8
|
+
# Dispatches pdfrb's named conformance families and maps their typed
|
|
9
|
+
# violations into Claricle's common issue model.
|
|
10
|
+
class PdfProfiles
|
|
11
|
+
PROFILE_LEVELS = {
|
|
12
|
+
pdf_a: %i[a1b a1a a2b a2a a3b a3a a4].freeze,
|
|
13
|
+
pdf_ua: nil,
|
|
14
|
+
pdf_x: %i[x1a x3 x4 x6].freeze,
|
|
15
|
+
pdf_vt: %i[vt1 vt2].freeze,
|
|
16
|
+
pades: %i[b-b b-t b-lt b-lta].freeze,
|
|
17
|
+
ltv: nil,
|
|
18
|
+
pdf_2_af: nil,
|
|
19
|
+
tagged_pdf: nil
|
|
20
|
+
}.freeze
|
|
21
|
+
VALIDATORS = {
|
|
22
|
+
pdf_a: :PdfA,
|
|
23
|
+
pdf_ua: :PdfUA,
|
|
24
|
+
pdf_x: :PdfX,
|
|
25
|
+
pdf_vt: :PdfVT,
|
|
26
|
+
pades: :Pades,
|
|
27
|
+
ltv: :Ltv,
|
|
28
|
+
pdf_2_af: :Pdf2AF,
|
|
29
|
+
tagged_pdf: :TaggedPdf
|
|
30
|
+
}.freeze
|
|
31
|
+
PADES_LEVELS = {
|
|
32
|
+
"b-b": :"B-B", "b-t": :"B-T",
|
|
33
|
+
"b-lt": :"B-LT", "b-lta": :"B-LTA"
|
|
34
|
+
}.freeze
|
|
35
|
+
private_constant :PROFILE_LEVELS, :VALIDATORS, :PADES_LEVELS
|
|
36
|
+
|
|
37
|
+
def self.profiles = PROFILE_LEVELS.keys
|
|
38
|
+
|
|
39
|
+
def self.levels_for(profile) = PROFILE_LEVELS.fetch(profile)
|
|
40
|
+
|
|
41
|
+
def self.issues(document, profile, level)
|
|
42
|
+
validator = ::Pdfrb::Conformance.const_get(VALIDATORS.fetch(profile))
|
|
43
|
+
arguments = level ? { level: delegate_level(profile, level) } : {}
|
|
44
|
+
validator.validate(document, **arguments).violations.map { |violation| issue(violation) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.delegate_level(profile, level)
|
|
48
|
+
profile == :pades ? PADES_LEVELS.fetch(level) : level
|
|
49
|
+
end
|
|
50
|
+
private_class_method :delegate_level
|
|
51
|
+
|
|
52
|
+
def self.issue(violation)
|
|
53
|
+
location = violation.object && Models::Location.new(node_path: violation.object.to_s)
|
|
54
|
+
Models::Issue.new(
|
|
55
|
+
severity: violation.severity.to_s,
|
|
56
|
+
code: violation.rule_id.to_s,
|
|
57
|
+
message: violation.message,
|
|
58
|
+
location: location
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :issue
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private_constant :PdfProfiles
|
|
65
|
+
end
|
|
66
|
+
end
|