preflight 0.1.1 → 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.
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
@@ -0,0 +1,112 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module Preflight
6
+ module Rules
7
+
8
+ # Some print workflows forbid the use of RGB colour.
9
+ #
10
+ # Arguments: none
11
+ #
12
+ # Usage:
13
+ #
14
+ # class MyPreflight
15
+ # include Preflight::Profile
16
+ #
17
+ # rule Preflight::Rules::NoRgb
18
+ # end
19
+ #
20
+ class NoRgb
21
+ extend Forwardable
22
+
23
+ # Graphics State Operators
24
+ def_delegators :@state, :save_graphics_state, :restore_graphics_state
25
+
26
+ # Matrix Operators
27
+ def_delegators :@state, :concatenate_matrix
28
+
29
+ attr_reader :issues
30
+
31
+ # we're about to start a new page, reset state
32
+ #
33
+ def page=(page)
34
+ @page = page
35
+ @state = PDF::Reader::PageState.new(page)
36
+ @issues = []
37
+ @resource_labels_seen = []
38
+ end
39
+
40
+ # descend into nested form xobjects
41
+ #
42
+ def invoke_xobject(label)
43
+ @state.invoke_xobject(label) do |xobj|
44
+ case xobj
45
+ when PDF::Reader::FormXObject then
46
+ xobj.walk(self)
47
+ when PDF::Reader::Stream then
48
+ check_xobject(xobj)
49
+ end
50
+ end
51
+ end
52
+
53
+ def set_rgb_color_for_stroking(r, g, b)
54
+ rgb_detected(r, g, b)
55
+ end
56
+
57
+ def set_rgb_color_for_nonstroking(r, g, b)
58
+ rgb_detected(r, g, b)
59
+ end
60
+
61
+ def set_stroke_color_space(label)
62
+ check_color_space(label)
63
+ end
64
+
65
+ def set_nonstroke_color_space(label)
66
+ check_color_space(label)
67
+ end
68
+
69
+ private
70
+
71
+ def color_space_is_rgb?(cs)
72
+ case cs
73
+ when Symbol then cs == :DeviceRGB
74
+ when Array then
75
+ cs[2] == :DeviceRGB
76
+ else
77
+ false
78
+ end
79
+ end
80
+
81
+ def check_color_space(label)
82
+ return if @resource_labels_seen.include?(label)
83
+
84
+ if color_space_is_rgb?(@state.find_color_space(label))
85
+ @issues << Issue.new("RGB color detected", self, :page => @page.number)
86
+ end
87
+
88
+ @resource_labels_seen << label
89
+ end
90
+
91
+ def check_xobject(xobject)
92
+ cs = xobject.hash[:ColorSpace]
93
+ if cs == :DeviceRGB
94
+ @issues << Issue.new("RGB image detected", self, :page => @page.number)
95
+ end
96
+ end
97
+
98
+ def rgb_detected(r, g, b)
99
+ @issues << Issue.new("RGB color detected", self, :page => @page.number,
100
+ :red => r,
101
+ :green => g,
102
+ :blue => b)
103
+ end
104
+
105
+ def deref(obj)
106
+ @page.objects.deref(obj)
107
+ end
108
+
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,85 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module Preflight
6
+ module Rules
7
+
8
+ # Some print workflows forbid the use of Separation colours (like
9
+ # Pantones).
10
+ #
11
+ # Arguments: none
12
+ #
13
+ # Usage:
14
+ #
15
+ # class MyPreflight
16
+ # include Preflight::Profile
17
+ #
18
+ # rule Preflight::Rules::NoSeparation
19
+ # end
20
+ #
21
+ class NoSeparation
22
+ extend Forwardable
23
+
24
+ # Graphics State Operators
25
+ def_delegators :@state, :save_graphics_state, :restore_graphics_state
26
+
27
+ # Matrix Operators
28
+ def_delegators :@state, :concatenate_matrix
29
+
30
+ attr_reader :issues
31
+
32
+ # we're about to start a new page, reset state
33
+ #
34
+ def page=(page)
35
+ @page = page
36
+ @state = PDF::Reader::PageState.new(page)
37
+ @issues = []
38
+ @resource_labels_seen = []
39
+ end
40
+
41
+ # descend into nested form xobjects
42
+ #
43
+ def invoke_xobject(label)
44
+ @state.invoke_xobject(label) do |xobj|
45
+ case xobj
46
+ when PDF::Reader::FormXObject then
47
+ xobj.walk(self)
48
+ end
49
+ end
50
+ end
51
+
52
+ def set_stroke_color_space(label)
53
+ check_color_space(label)
54
+ end
55
+
56
+ def set_nonstroke_color_space(label)
57
+ check_color_space(label)
58
+ end
59
+
60
+ private
61
+
62
+ def separation_name(cs)
63
+ if cs.is_a?(Array) && cs[0] == :Separation
64
+ cs[1]
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ def check_color_space(label)
71
+ return if @resource_labels_seen.include?(label)
72
+
73
+ spot_name = separation_name(@state.find_color_space(label))
74
+ if spot_name
75
+ @issues << Issue.new("Separation color detected #{spot_name}", self, :page => @page.number,
76
+ :name => spot_name)
77
+ end
78
+
79
+ @resource_labels_seen << label
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,90 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module Preflight
6
+ module Rules
7
+
8
+ # For some print workflows transparency is forbidden.
9
+ #
10
+ # Arguments: none
11
+ #
12
+ # Usage:
13
+ #
14
+ # class MyPreflight
15
+ # include Preflight::Profile
16
+ #
17
+ # rule Preflight::Rules::NoTransparency
18
+ # end
19
+ #
20
+ class NoTransparency
21
+ include Preflight::Measurements
22
+ extend Forwardable
23
+
24
+ # Graphics State Operators
25
+ def_delegators :@state, :save_graphics_state, :restore_graphics_state
26
+
27
+ # Matrix Operators
28
+ def_delegators :@state, :concatenate_matrix
29
+
30
+ attr_reader :issues
31
+
32
+ # we're about to start a new page, reset state
33
+ #
34
+ def page=(page)
35
+ @page = page
36
+ @state = PDF::Reader::PageState.new(page)
37
+ @issues = []
38
+ end
39
+
40
+ def invoke_xobject(label)
41
+ @state.invoke_xobject(label) do |xobj|
42
+ case xobj
43
+ when PDF::Reader::FormXObject then
44
+ detect_transparent_form(xobj)
45
+ xobj.walk(self)
46
+ else
47
+ # TODO. can other xobjects have transparency?
48
+ end
49
+ end
50
+ end
51
+
52
+ # As each xobject is drawn on the canvas, record if it's Group XObject
53
+ # with transparency
54
+ #
55
+ def detect_transparent_form(form)
56
+ xobject = form.xobject
57
+ group = deref(xobject.hash[:Group])
58
+ stype = deref(group[:S]) if group
59
+
60
+ if stype == :Transparency
61
+ bbox = xobject.hash[:BBox] || @page.attributes[:MediaBox]
62
+ bbox = translate_to_device_space(bbox)
63
+ @issues << Issue.new("Transparent xobject found", self, :page => @page.number,
64
+ :top_left => bbox[:tl],
65
+ :bottom_left => bbox[:bl],
66
+ :bottom_right => bbox[:br],
67
+ :top_right => bbox[:tr])
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def translate_to_device_space(bbox)
74
+ bl_x, bl_y, tr_x, tr_y = *bbox
75
+ {
76
+ :tl => @state.ctm_transform(bl_x, tr_y),
77
+ :bl => @state.ctm_transform(bl_x, bl_y),
78
+ :br => @state.ctm_transform(tr_x, bl_y),
79
+ :tr => @state.ctm_transform(tr_x, tr_y)
80
+ }
81
+ end
82
+
83
+ def deref(obj)
84
+ @objects ? @objects.deref(obj) : obj
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
@@ -3,32 +3,46 @@
3
3
  module Preflight
4
4
  module Rules
5
5
 
6
- # check a file only uses embedded fonts
6
+ # Check the target PDF only uses embedded fonts
7
+ #
8
+ # Arguments: none
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::OnlyEmbeddedFonts
16
+ # end
7
17
  #
8
18
  class OnlyEmbeddedFonts
9
19
 
10
- def messages(ohash)
11
- array = []
12
- ohash.each do |key, obj|
13
- next unless obj.is_a?(::Hash) && obj[:Type] == :Font
14
- if !embedded?(ohash, obj)
15
- array << "Font #{obj[:BaseFont]} is not embedded"
20
+ attr_reader :issues
21
+
22
+ def page=(page)
23
+ @issues = []
24
+
25
+ page.fonts.each { |key, obj|
26
+ obj = page.objects.deref(obj)
27
+ if !embedded?(page.objects, obj)
28
+ @issues << Issue.new("Font not embedded", self, :base_font => obj[:BaseFont])
16
29
  end
17
- end
18
- array
30
+ }
19
31
  end
20
32
 
21
33
  private
22
34
 
23
- def embedded?(ohash, font)
35
+ def embedded?(objects, font)
36
+ return true if font[:Subtype] == :Type3
37
+
24
38
  if font.has_key?(:FontDescriptor)
25
- descriptor = ohash.object(font[:FontDescriptor])
39
+ descriptor = objects.deref(font[:FontDescriptor])
26
40
  descriptor.has_key?(:FontFile) || descriptor.has_key?(:FontFile2) || descriptor.has_key?(:FontFile3)
27
41
  elsif font[:Subtype] == :Type0
28
- descendants = ohash.object(font[:DescendantFonts])
42
+ descendants = objects.deref(font[:DescendantFonts])
29
43
  descendants.all? { |f|
30
- f = ohash.object(f)
31
- embedded?(ohash, f)
44
+ f = objects.deref(f)
45
+ embedded?(objects, f)
32
46
  }
33
47
  else
34
48
  false
@@ -3,15 +3,27 @@
3
3
  module Preflight
4
4
  module Rules
5
5
 
6
+ # Check the target PDF contains an output intent suitable for PDFX
7
+ #
8
+ # Arguments: none
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::OutputIntentForPdfx
16
+ # end
17
+ #
6
18
  class OutputIntentForPdfx
7
19
 
8
- def messages(ohash)
20
+ def check_hash(ohash)
9
21
  intents = output_intents(ohash).select { |dict|
10
22
  ohash.object(dict)[:S] == :GTS_PDFX
11
23
  }
12
24
 
13
25
  if intents.size != 1
14
- ["There must be exactly 1 OutputIntent with a subtype of GTS_PDFX"]
26
+ [Issue.new("There must be exactly 1 OutputIntent with a subtype of GTS_PDFX", self)]
15
27
  else
16
28
  []
17
29
  end
@@ -0,0 +1,88 @@
1
+ # coding: utf-8
2
+
3
+ module Preflight
4
+ module Rules
5
+
6
+ # Ensure the requested page box (MediaBox, TrimBox, etc) on every page has
7
+ # the requested height. Dimensions can be in points, mm or inches. Skips
8
+ # the page if the requested box isn't defined, it's up to other rules to
9
+ # check for the existence of the box.
10
+ #
11
+ # Arguments: the target page box, the target height and the the units
12
+ #
13
+ # Usage:
14
+ #
15
+ # class MyPreflight
16
+ # include Preflight::Profile
17
+ #
18
+ # rule Preflight::Rules::PageBoxHeight, :MediaBox, 100, :mm
19
+ # rule Preflight::Rules::PageBoxHeight, :TrimBox, 600, :pts
20
+ # rule Preflight::Rules::PageBoxHeight, :CropBox, 5, :in
21
+ # rule Preflight::Rules::PageBoxHeight, :MediaBox, 100..101, :mm
22
+ # rule Preflight::Rules::PageBoxHeight, :TrimBox, 600..700, :pts
23
+ # rule Preflight::Rules::PageBoxHeight, :CropBox, 5..6, :in
24
+ # end
25
+ #
26
+ class PageBoxHeight
27
+ include Preflight::Measurements
28
+
29
+ attr_reader :issues
30
+
31
+ def initialize(box, height, units)
32
+ @box, @units = box, units
33
+ @orig_height = height
34
+ @height = case units
35
+ when :mm then mm_to_points(height)
36
+ when :in then inches_to_points(height)
37
+ else
38
+ points_to_points(height)
39
+ end
40
+ end
41
+
42
+ def page=(page)
43
+ @issues = []
44
+ dict = page.attributes
45
+
46
+ if dict[@box]
47
+ box_height = dict[@box][3] - dict[@box][1]
48
+
49
+ if !@height.include?(box_height)
50
+ @issues << Issue.new("#{@box} height must be #{@orig_height}#{@units}", self, :page => page.number,
51
+ :box => @box,
52
+ :height => @orig_height,
53
+ :units => @units)
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def inches_to_points(height)
61
+ case height
62
+ when Numeric then Range.new(in2pt(height)-1, in2pt(height)+1)
63
+ when Range then Range.new(in2pt(height.min), in2pt(height.max))
64
+ else
65
+ raise ArgumentError, "height must be a Numeric or Range object"
66
+ end
67
+ end
68
+
69
+ def mm_to_points(height)
70
+ case height
71
+ when Numeric then Range.new(mm2pt(height)-1, mm2pt(height)+1)
72
+ when Range then Range.new(mm2pt(height.min), mm2pt(height.max))
73
+ else
74
+ raise ArgumentError, "height must be a Numeric or Range object"
75
+ end
76
+ end
77
+
78
+ def points_to_points(height)
79
+ case height
80
+ when Numeric then Range.new(height, height)
81
+ when Range then height
82
+ else
83
+ raise ArgumentError, "height must be a Numeric or Range object"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end