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
@@ -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