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