preflight 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +8 -1
  2. data/README.rdoc +21 -4
  3. data/lib/preflight.rb +1 -0
  4. data/lib/preflight/issue.rb +35 -0
  5. data/lib/preflight/profile.rb +38 -16
  6. data/lib/preflight/profiles/pdfa1a.rb +0 -1
  7. data/lib/preflight/profiles/pdfx1a.rb +4 -3
  8. data/lib/preflight/rules.rb +17 -2
  9. data/lib/preflight/rules/box_nesting.rb +35 -23
  10. data/lib/preflight/rules/compression_algorithms.rb +14 -4
  11. data/lib/preflight/rules/consistent_boxes.rb +63 -0
  12. data/lib/preflight/rules/cropbox_matches_mediabox.rb +38 -0
  13. data/lib/preflight/rules/document_id.rb +14 -2
  14. data/lib/preflight/rules/info_has_keys.rb +15 -3
  15. data/lib/preflight/rules/info_specifies_trapping.rb +16 -3
  16. data/lib/preflight/rules/match_info_entries.rb +17 -3
  17. data/lib/preflight/rules/max_ink_density.rb +69 -0
  18. data/lib/preflight/rules/max_version.rb +14 -2
  19. data/lib/preflight/rules/mediabox_at_origin.rb +42 -0
  20. data/lib/preflight/rules/min_bleed.rb +171 -0
  21. data/lib/preflight/rules/min_ppi.rb +54 -116
  22. data/lib/preflight/rules/no_cmyk.rb +113 -0
  23. data/lib/preflight/rules/no_filespecs.rb +15 -5
  24. data/lib/preflight/rules/no_font_subsets.rb +15 -6
  25. data/lib/preflight/rules/no_gray.rb +105 -0
  26. data/lib/preflight/rules/no_page_rotation.rb +36 -0
  27. data/lib/preflight/rules/no_private_data.rb +37 -0
  28. data/lib/preflight/rules/no_registration_black.rb +102 -0
  29. data/lib/preflight/rules/no_rgb.rb +112 -0
  30. data/lib/preflight/rules/no_separation.rb +85 -0
  31. data/lib/preflight/rules/no_transparency.rb +90 -0
  32. data/lib/preflight/rules/only_embedded_fonts.rb +28 -14
  33. data/lib/preflight/rules/output_intent_for_pdfx.rb +14 -2
  34. data/lib/preflight/rules/page_box_height.rb +88 -0
  35. data/lib/preflight/rules/page_box_size.rb +106 -0
  36. data/lib/preflight/rules/page_box_width.rb +88 -0
  37. data/lib/preflight/rules/page_count.rb +87 -0
  38. data/lib/preflight/rules/pdfx_output_intent_has_keys.rb +12 -2
  39. data/lib/preflight/rules/print_boxes.rb +21 -19
  40. data/lib/preflight/rules/root_has_keys.rb +15 -3
  41. metadata +97 -113
  42. data/lib/preflight/rules/no_encryption.rb +0 -16
  43. data/lib/preflight/rules/no_proprietary_fonts.rb +0 -50
data/CHANGELOG CHANGED
@@ -1,4 +1,11 @@
1
- v0.1.0 (6th July 2011)
1
+ v0.2.0 (25th March 2012)
2
+ - BREAKING API CHANGES FROM 0.1.1 release
3
+ - Detected issues are reported with an Issue object instead of a simple string
4
+ - new rules
5
+ - bug fixes and improvements to existing rules
6
+ - improved documentation
7
+
8
+ v0.1.1 (6th July 2011)
2
9
  - bump required version of PDF::Reader to take advantage of a more
3
10
  efficient API
4
11
 
@@ -13,6 +13,10 @@ difficult to script and you know, expensive.
13
13
  This may not check as comprehensively as the official preflight tools from Adobe
14
14
  and friends, but hopefully it'll get you most of the way with less stress.
15
15
 
16
+ Unlike some preflight tools, this library doesn't attempt to fix any issues. It
17
+ only reports them. That may change one day, but for now you will need to fix
18
+ any issues that are detected by using some other tool.
19
+
16
20
  == Installation
17
21
 
18
22
  gem install preflight
@@ -77,9 +81,8 @@ Use an existing rule set as the base for a larger set of rules.
77
81
 
78
82
  === Adding Rules to a Profile Instance
79
83
 
80
- Use an existing rule set as the base for a larger set of rules by adding
81
- rules to a profile instance. Useful when the required set of rules depends
82
- dynamic conditions.
84
+ Dynamically add rules to a profile instance. Useful when the required set of
85
+ rules depends on dynamic conditions like user input or database entries.
83
86
 
84
87
  require "preflight"
85
88
 
@@ -88,6 +91,21 @@ dynamic conditions.
88
91
 
89
92
  puts preflight.check("somefile.pdf").inspect
90
93
 
94
+ === Available Rules
95
+
96
+ All rules shipped with this gem are in the Preflight::Rules namespace. Explore
97
+ the classes and related documentation in there to see what's available.
98
+
99
+ If you need a rule that we don't ship it is perfectly valid to build your own.
100
+ A profile will accept any class that behaves appropriately. If you think your
101
+ new rule may be of general interest please send me a pull request so I can
102
+ include it in the next release.
103
+
104
+ I'm yet to document the API that a rule class must follow. There are two
105
+ types of rule, each designed to check a different aspect of the target file
106
+ - raw PDF objects and individual pages. At this stage your best option when
107
+ writing your own rule is to copy an existing rule as a starting point.
108
+
91
109
  == Status
92
110
 
93
111
  This library is in an early stage of development. Use at your own risk.
@@ -98,7 +116,6 @@ This is pure ruby should run on most ruby VMs. I develop on MRI 1.9.2.
98
116
 
99
117
  == Further Reading
100
118
 
101
- * http://chneukirchen.github.com/rps/
102
119
  * http://en.wikipedia.org/wiki/PDF/A
103
120
  * http://en.wikipedia.org/wiki/PDF/X
104
121
  * http://www.planetpdf.com/planetpdf/pdfs/pdfx3_draft4.pdf
@@ -1,6 +1,7 @@
1
1
  require 'pdf/reader'
2
2
 
3
3
  require 'preflight/measurements'
4
+ require 'preflight/issue'
4
5
  require 'preflight/profile'
5
6
 
6
7
  require 'preflight/rules'
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ module Preflight
4
+ class Issue
5
+
6
+ attr_reader :description, :rule, :attributes
7
+
8
+ def initialize(description, rule, attributes = {})
9
+ @description = description
10
+ if rule.is_a?(Class)
11
+ @rule = rule.to_s.to_sym
12
+ else
13
+ @rule = rule.class.to_s.to_sym
14
+ end
15
+ @attributes = attributes || {}
16
+
17
+ attach_attributes
18
+ end
19
+
20
+ def to_s
21
+ @description
22
+ end
23
+
24
+ private
25
+
26
+ def attach_attributes
27
+ singleton = class << self; self end
28
+
29
+ @attributes.each do |key, value|
30
+ singleton.send(:define_method, key, lambda { value })
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -36,6 +36,8 @@ module Preflight
36
36
 
37
37
  module InstanceMethods
38
38
  def check(input)
39
+ valid_rules?
40
+
39
41
  if File.file?(input)
40
42
  check_filename(input)
41
43
  elsif input.is_a?(IO)
@@ -58,7 +60,12 @@ module Preflight
58
60
  end
59
61
 
60
62
  def check_io(io)
61
- check_receivers(io) + check_hash(io)
63
+ PDF::Reader.open(io) do |reader|
64
+ raise PDF::Reader::EncryptedPDFError if reader.objects.encrypted?
65
+ check_pages(reader) + check_hash(reader)
66
+ end
67
+ rescue PDF::Reader::EncryptedPDFError
68
+ ["Can't preflight an encrypted PDF"]
62
69
  end
63
70
 
64
71
  def instance_rules
@@ -69,38 +76,53 @@ module Preflight
69
76
  self.class.rules + instance_rules
70
77
  end
71
78
 
72
- def check_receivers(io)
73
- rules_array = receiver_rules
79
+ def check_hash(reader)
80
+ hash_rules.map { |chk|
81
+ chk.check_hash(reader.objects)
82
+ }.flatten.compact
83
+ rescue PDF::Reader::UnsupportedFeatureError
84
+ []
85
+ end
86
+
87
+ def check_pages(reader)
88
+ rules_array = page_rules
89
+ issues = []
90
+
74
91
  begin
75
- PDF::Reader.new.parse(io, rules_array)
92
+ reader.pages.each do |page|
93
+ page.walk(*rules_array)
94
+ issues += rules_array.map(&:issues).flatten.compact
95
+ end
76
96
  rescue PDF::Reader::UnsupportedFeatureError
77
97
  nil
78
98
  end
79
- rules_array.map(&:messages).flatten.compact
99
+ issues
80
100
  end
81
101
 
82
- def check_hash(io)
83
- ohash = PDF::Reader::ObjectHash.new(io)
84
-
85
- hash_rules.map { |chk|
86
- chk.messages(ohash)
87
- }.flatten.compact
102
+ # ensure all rules follow the prescribed API
103
+ #
104
+ def valid_rules?
105
+ invalid_rules = all_rules.reject { |arr|
106
+ arr.first.instance_methods.map(&:to_sym).include?(:check_hash) ||
107
+ arr.first.instance_methods.map(&:to_sym).include?(:issues)
108
+ }
109
+ if invalid_rules.size > 0
110
+ raise "The following rules are invalid: #{invalid_rules.join(", ")}. Preflight rules MUST respond to either check_hash() or issues()."
111
+ end
88
112
  end
89
113
 
90
114
  def hash_rules
91
115
  all_rules.select { |arr|
92
- meth = arr.first.instance_method(:messages)
93
- meth && meth.arity == 1
116
+ arr.first.instance_methods.map(&:to_sym).include?(:check_hash)
94
117
  }.map { |arr|
95
118
  klass = arr[0]
96
119
  klass.new(*arr[1,10])
97
120
  }
98
121
  end
99
122
 
100
- def receiver_rules
123
+ def page_rules
101
124
  all_rules.select { |arr|
102
- meth = arr.first.instance_method(:messages)
103
- meth && meth.arity == 0
125
+ arr.first.instance_methods.map(&:to_sym).include?(:issues)
104
126
  }.map { |arr|
105
127
  klass = arr[0]
106
128
  klass.new(*arr[1,10])
@@ -9,7 +9,6 @@ module Preflight
9
9
 
10
10
  # hard failures of the pdfx/1a spec
11
11
  rule Preflight::Rules::CompressionAlgorithms, :CCITTFaxDecode, :DCTDecode, :FlateDecode, :RunLengthDecode
12
- rule Preflight::Rules::NoEncryption
13
12
  rule Preflight::Rules::OnlyEmbeddedFonts
14
13
 
15
14
  end
@@ -12,16 +12,17 @@ module Preflight
12
12
  rule Preflight::Rules::RootHasKeys, :OutputIntents
13
13
  rule Preflight::Rules::InfoHasKeys, :Title, :CreationDate, :ModDate
14
14
  rule Preflight::Rules::InfoSpecifiesTrapping
15
- rule Preflight::Rules::CompressionAlgorithms, :CCITTFaxDecode, :DCTDecode, :FlateDecode, :RunLengthDecode
15
+ rule Preflight::Rules::CompressionAlgorithms, :ASCII85Decode, :CCITTFaxDecode, :DCTDecode, :FlateDecode, :RunLengthDecode
16
16
  rule Preflight::Rules::DocumentId
17
- rule Preflight::Rules::NoEncryption
18
17
  rule Preflight::Rules::NoFilespecs
18
+ rule Preflight::Rules::NoTransparency
19
19
  rule Preflight::Rules::OnlyEmbeddedFonts
20
20
  rule Preflight::Rules::BoxNesting
21
- rule Preflight::Rules::MaxVersion, 1.4
21
+ rule Preflight::Rules::MaxVersion, 1.3
22
22
  rule Preflight::Rules::PrintBoxes
23
23
  rule Preflight::Rules::OutputIntentForPdfx
24
24
  rule Preflight::Rules::PdfxOutputIntentHasKeys, :OutputConditionIdentifier, :Info
25
+ rule Preflight::Rules::NoRgb
25
26
 
26
27
  end
27
28
  end
@@ -1,17 +1,32 @@
1
1
  require 'preflight/rules/box_nesting'
2
2
  require 'preflight/rules/compression_algorithms'
3
+ require 'preflight/rules/consistent_boxes'
4
+ require 'preflight/rules/cropbox_matches_mediabox'
3
5
  require 'preflight/rules/document_id'
4
6
  require 'preflight/rules/info_has_keys'
5
7
  require 'preflight/rules/info_specifies_trapping'
6
8
  require 'preflight/rules/match_info_entries'
9
+ require 'preflight/rules/max_ink_density'
7
10
  require 'preflight/rules/max_version'
11
+ require 'preflight/rules/mediabox_at_origin'
12
+ require 'preflight/rules/min_bleed'
8
13
  require 'preflight/rules/min_ppi'
9
- require 'preflight/rules/no_encryption'
14
+ require 'preflight/rules/no_cmyk'
10
15
  require 'preflight/rules/no_filespecs'
11
16
  require 'preflight/rules/no_font_subsets'
12
- require 'preflight/rules/no_proprietary_fonts'
17
+ require 'preflight/rules/no_gray'
18
+ require 'preflight/rules/no_page_rotation'
19
+ require 'preflight/rules/no_registration_black'
20
+ require 'preflight/rules/no_rgb'
21
+ require 'preflight/rules/no_private_data'
22
+ require 'preflight/rules/no_separation'
23
+ require 'preflight/rules/no_transparency'
13
24
  require 'preflight/rules/only_embedded_fonts'
14
25
  require 'preflight/rules/output_intent_for_pdfx'
26
+ require 'preflight/rules/page_count'
27
+ require 'preflight/rules/page_box_height'
28
+ require 'preflight/rules/page_box_width'
29
+ require 'preflight/rules/page_box_size'
15
30
  require 'preflight/rules/pdfx_output_intent_has_keys'
16
31
  require 'preflight/rules/print_boxes'
17
32
  require 'preflight/rules/root_has_keys'
@@ -6,38 +6,50 @@ module Preflight
6
6
  # For each page MediaBox must be the biggest box, followed by the
7
7
  # BleedBox or ArtBox, followed by the TrimBox.
8
8
  #
9
+ # Boxes may be omitted, but if they're provided they must be correctly nested.
10
+ #
11
+ # Arguments: none
12
+ #
13
+ # Usage:
14
+ #
15
+ # class MyPreflight
16
+ # include Preflight::Profile
17
+ #
18
+ # rule Preflight::Rules::BoxNesting
19
+ # end
20
+ #
9
21
  class BoxNesting
10
- attr_reader :messages
11
-
12
- def initialize
13
- @messages = []
14
- @page_num = 0
15
- @parent = {}
16
- end
17
-
18
- def begin_page_container(hash = {})
19
- @parent.merge!(hash)
20
- end
21
22
 
22
- def begin_page(hash = {})
23
- @page_num += 1
24
- hash = @parent.merge(hash)
23
+ attr_reader :issues
25
24
 
26
- media = hash[:MediaBox]
27
- bleed = hash[:BleedBox]
28
- trim = hash[:TrimBox]
29
- art = hash[:ArtBox]
25
+ def page=(page)
26
+ media = page.attributes[:MediaBox]
27
+ bleed = page.attributes[:BleedBox]
28
+ trim = page.attributes[:TrimBox]
29
+ art = page.attributes[:ArtBox]
30
30
 
31
31
  if media && bleed && (bleed[2] > media[2] || bleed[3] > media[3])
32
- @messages << "BleedBox must be smaller than MediaBox (page #{@page_num})"
32
+ @issues = [Issue.new("BleedBox must be smaller than MediaBox", self, :page => page.number,
33
+ :large_box => "BleedBox",
34
+ :small_box => "MediaBox")]
33
35
  elsif trim && bleed && (trim[2] > bleed[2] || trim[3] > bleed[3])
34
- @messages << "TrimBox must be smaller than BleedBox (page #{@page_num})"
36
+ @issues = [Issue.new("TrimBox must be smaller than BleedBox", self, :page => page.number,
37
+ :large_box => "TrimBox",
38
+ :small_box => "BleedBox")]
35
39
  elsif art && bleed && (art[2] > bleed[2] || art[3] > bleed[3])
36
- @messages << "ArtBox must be smaller than BleedBox (page #{@page_num})"
40
+ @issues = [Issue.new("ArtBox must be smaller than BleedBox", self, :page => page.number,
41
+ :large_box => "ArtBox",
42
+ :small_box => "BleedBox")]
37
43
  elsif trim && media && (trim[2] > media[2] || trim[3] > media[3])
38
- @messages << "TrimBox must be smaller than MediaBox (page #{@page_num})"
44
+ @issues = [Issue.new("TrimBox must be smaller than MediaBox", self, :page => page.number,
45
+ :large_box => "TrimBox",
46
+ :small_box => "MediaBox")]
39
47
  elsif art && media && (art[2] > media[2] || art[3] > media[3])
40
- @messages << "ArtBox must be smaller than MediaBox (page #{@page_num})"
48
+ @issues = [Issue.new("ArtBox must be smaller than MediaBox", self, :page => page.number,
49
+ :large_box => "ArtBox",
50
+ :small_box => "MediaBox")]
51
+ else
52
+ @issues = []
41
53
  end
42
54
  end
43
55
  end
@@ -3,19 +3,29 @@
3
3
  module Preflight
4
4
  module Rules
5
5
 
6
- # check a file only uses permitted compression algorithms
6
+ # check a file doesn't use unwanted compression algorithms
7
+ #
8
+ # Arguments: a list of permitted compression algorithms.
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::CompressionAlgorithms, :CCITTFaxDecode, :DCTDecode, :FlateDecode
16
+ # end
7
17
  #
8
18
  class CompressionAlgorithms
9
19
 
10
20
  def initialize(*algorithms)
11
- @algorithms = algorithms.flatten
21
+ @algorithms = algorithms.flatten
12
22
  end
13
23
 
14
- def messages(ohash)
24
+ def check_hash(ohash)
15
25
  algorithms = banned_algorithms(ohash)
16
26
 
17
27
  if algorithms.size > 0
18
- ["File uses excluded compression algorithm (#{algorithms.join(", ")})"]
28
+ [Issue.new("File uses excluded compression algorithm", self, :algorithms => algorithms)]
19
29
  else
20
30
  []
21
31
  end
@@ -0,0 +1,63 @@
1
+ # coding: utf-8
2
+
3
+ module Preflight
4
+ module Rules
5
+
6
+ # Every page should have identical page boxes
7
+ #
8
+ # Arguments: none
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::BoxNesting
16
+ # end
17
+ #
18
+ class ConsistentBoxes
19
+
20
+ # each page box MUST be within .03 PDF points of the same box
21
+ # on all other pages
22
+ TOLERANCE = (BigDecimal.new("-0.03")..BigDecimal.new("0.03"))
23
+
24
+ attr_reader :issues
25
+
26
+ def initialize
27
+ @boxes = {}
28
+ end
29
+
30
+ def page=(page)
31
+ @issues = []
32
+ dict = page.attributes
33
+
34
+ @boxes[:MediaBox] ||= dict[:MediaBox]
35
+ @boxes[:CropBox] ||= dict[:CropBox]
36
+ @boxes[:BleedBox] ||= dict[:BleedBox]
37
+ @boxes[:TrimBox] ||= dict[:TrimBox]
38
+ @boxes[:ArtBox] ||= dict[:ArtBox]
39
+
40
+ %w(MediaBox CropBox BleedBox TrimBox ArtBox).map(&:to_sym).each do |box_type|
41
+ unless subtract_all(@boxes[box_type], dict[box_type]).all? { |diff| TOLERANCE.include?(diff) }
42
+ @issues << Issue.new("#{box_type} must be consistent across every page", self, :page => page.number,
43
+ :inconsistent_box => box_type)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def subtract_all(one, two)
51
+ one ||= [0, 0, 0, 0]
52
+ two ||= [0, 0, 0, 0]
53
+
54
+ [
55
+ one[0] - two[0],
56
+ one[1] - two[1],
57
+ one[2] - two[2],
58
+ one[3] - two[3]
59
+ ]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+
3
+ module Preflight
4
+ module Rules
5
+
6
+ # Every page should have a CropBox that matches the MediaBox
7
+ #
8
+ # Arguments: none
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::CropboxMatchesMediabox
16
+ # end
17
+ #
18
+ class CropboxMatchesMediabox
19
+
20
+ attr_reader :issues
21
+
22
+ def page=(page)
23
+ @issues = []
24
+ dict = page.attributes
25
+
26
+ if dict[:CropBox] && round_off(dict[:CropBox]) != round_off(dict[:MediaBox])
27
+ @issues << Issue.new("CropBox must match MediaBox", self, :page => page.number)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def round_off(*arr)
34
+ arr.flatten.compact.map { |n| BigDecimal.new(n.to_s).round(2) }
35
+ end
36
+ end
37
+ end
38
+ end