preflight 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +8 -1
- data/README.rdoc +21 -4
- data/lib/preflight.rb +1 -0
- data/lib/preflight/issue.rb +35 -0
- data/lib/preflight/profile.rb +38 -16
- data/lib/preflight/profiles/pdfa1a.rb +0 -1
- data/lib/preflight/profiles/pdfx1a.rb +4 -3
- data/lib/preflight/rules.rb +17 -2
- data/lib/preflight/rules/box_nesting.rb +35 -23
- data/lib/preflight/rules/compression_algorithms.rb +14 -4
- data/lib/preflight/rules/consistent_boxes.rb +63 -0
- data/lib/preflight/rules/cropbox_matches_mediabox.rb +38 -0
- data/lib/preflight/rules/document_id.rb +14 -2
- data/lib/preflight/rules/info_has_keys.rb +15 -3
- data/lib/preflight/rules/info_specifies_trapping.rb +16 -3
- data/lib/preflight/rules/match_info_entries.rb +17 -3
- data/lib/preflight/rules/max_ink_density.rb +69 -0
- data/lib/preflight/rules/max_version.rb +14 -2
- data/lib/preflight/rules/mediabox_at_origin.rb +42 -0
- data/lib/preflight/rules/min_bleed.rb +171 -0
- data/lib/preflight/rules/min_ppi.rb +54 -116
- data/lib/preflight/rules/no_cmyk.rb +113 -0
- data/lib/preflight/rules/no_filespecs.rb +15 -5
- data/lib/preflight/rules/no_font_subsets.rb +15 -6
- data/lib/preflight/rules/no_gray.rb +105 -0
- data/lib/preflight/rules/no_page_rotation.rb +36 -0
- data/lib/preflight/rules/no_private_data.rb +37 -0
- data/lib/preflight/rules/no_registration_black.rb +102 -0
- data/lib/preflight/rules/no_rgb.rb +112 -0
- data/lib/preflight/rules/no_separation.rb +85 -0
- data/lib/preflight/rules/no_transparency.rb +90 -0
- data/lib/preflight/rules/only_embedded_fonts.rb +28 -14
- data/lib/preflight/rules/output_intent_for_pdfx.rb +14 -2
- data/lib/preflight/rules/page_box_height.rb +88 -0
- data/lib/preflight/rules/page_box_size.rb +106 -0
- data/lib/preflight/rules/page_box_width.rb +88 -0
- data/lib/preflight/rules/page_count.rb +87 -0
- data/lib/preflight/rules/pdfx_output_intent_has_keys.rb +12 -2
- data/lib/preflight/rules/print_boxes.rb +21 -19
- data/lib/preflight/rules/root_has_keys.rb +15 -3
- metadata +97 -113
- data/lib/preflight/rules/no_encryption.rb +0 -16
- 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
|
-
#
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
array
|
30
|
+
}
|
19
31
|
end
|
20
32
|
|
21
33
|
private
|
22
34
|
|
23
|
-
def embedded?(
|
35
|
+
def embedded?(objects, font)
|
36
|
+
return true if font[:Subtype] == :Type3
|
37
|
+
|
24
38
|
if font.has_key?(:FontDescriptor)
|
25
|
-
descriptor =
|
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 =
|
42
|
+
descendants = objects.deref(font[:DescendantFonts])
|
29
43
|
descendants.all? { |f|
|
30
|
-
f =
|
31
|
-
embedded?(
|
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
|
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
|