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
@@ -2,11 +2,23 @@
|
|
2
2
|
|
3
3
|
module Preflight
|
4
4
|
module Rules
|
5
|
+
# check the file has a document ID
|
6
|
+
#
|
7
|
+
# Arguments: none
|
8
|
+
#
|
9
|
+
# Usage:
|
10
|
+
#
|
11
|
+
# class MyPreflight
|
12
|
+
# include Preflight::Profile
|
13
|
+
#
|
14
|
+
# rule Preflight::Rules::DocumentId
|
15
|
+
# end
|
16
|
+
#
|
5
17
|
class DocumentId
|
6
18
|
|
7
|
-
def
|
19
|
+
def check_hash(ohash)
|
8
20
|
if ohash.trailer[:ID].nil?
|
9
|
-
["Document ID missing"]
|
21
|
+
[Issue.new("Document ID missing", self)]
|
10
22
|
else
|
11
23
|
[]
|
12
24
|
end
|
@@ -3,18 +3,30 @@
|
|
3
3
|
module Preflight
|
4
4
|
module Rules
|
5
5
|
|
6
|
-
#
|
6
|
+
# Every PDF has an optional 'Info' dictionary. Check that the target file
|
7
|
+
# has certain keys
|
8
|
+
#
|
9
|
+
# Arguments: the required keys
|
10
|
+
#
|
11
|
+
# Usage:
|
12
|
+
#
|
13
|
+
# class MyPreflight
|
14
|
+
# include Preflight::Profile
|
15
|
+
#
|
16
|
+
# rule Preflight::Rules::InfoHasKeys, :Title, :CreationDate, :ModDate
|
17
|
+
# end
|
18
|
+
#
|
7
19
|
class InfoHasKeys
|
8
20
|
|
9
21
|
def initialize(*keys)
|
10
22
|
@keys = keys.flatten
|
11
23
|
end
|
12
24
|
|
13
|
-
def
|
25
|
+
def check_hash(ohash)
|
14
26
|
info = ohash.object(ohash.trailer[:Info])
|
15
27
|
missing = @keys - info.keys
|
16
28
|
missing.map { |key|
|
17
|
-
"Info dict missing required key
|
29
|
+
Issue.new("Info dict missing required key", self, :key => key)
|
18
30
|
}
|
19
31
|
end
|
20
32
|
end
|
@@ -2,15 +2,28 @@
|
|
2
2
|
|
3
3
|
module Preflight
|
4
4
|
module Rules
|
5
|
+
# Every PDF has an optional 'Info' dictionary. Check that the dictionary
|
6
|
+
# has a 'Trapped' entry that is set to True or False
|
7
|
+
#
|
8
|
+
# Arguments: none
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# class MyPreflight
|
13
|
+
# include Preflight::Profile
|
14
|
+
#
|
15
|
+
# rule Preflight::Rules::InfoSpecifiesTrapping
|
16
|
+
# end
|
17
|
+
#
|
5
18
|
class InfoSpecifiesTrapping
|
6
19
|
|
7
|
-
def
|
20
|
+
def check_hash(ohash)
|
8
21
|
info = ohash.object(ohash.trailer[:Info])
|
9
22
|
|
10
23
|
if !info.has_key?(:Trapped)
|
11
|
-
[ "Info dict does not specify Trapped" ]
|
24
|
+
[ Issue.new("Info dict does not specify Trapped", self) ]
|
12
25
|
elsif info[:Trapped] != :True && info[:Trapped] != :False
|
13
|
-
[ "Trapped value of Info dict must be True or False" ]
|
26
|
+
[ Issue.new("Trapped value of Info dict must be True or False", self) ]
|
14
27
|
else
|
15
28
|
[]
|
16
29
|
end
|
@@ -2,20 +2,34 @@
|
|
2
2
|
|
3
3
|
module Preflight
|
4
4
|
module Rules
|
5
|
+
# Every PDF has an optional 'Info' dictionary. Check that the target file
|
6
|
+
# has certain keys and that the keys match a given regexp
|
7
|
+
#
|
8
|
+
# Arguments: the required keys
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# class MyPreflight
|
13
|
+
# include Preflight::Profile
|
14
|
+
#
|
15
|
+
# rule Preflight::Rules::MatchInfoEntries, {:GTS_PDFXVersion => /\APDF\/X/}
|
16
|
+
# end
|
17
|
+
#
|
5
18
|
class MatchInfoEntries
|
6
19
|
|
7
20
|
def initialize(matches = {})
|
8
21
|
@matches = matches
|
9
22
|
end
|
10
23
|
|
11
|
-
def
|
24
|
+
def check_hash(ohash)
|
12
25
|
array = []
|
13
26
|
info = ohash.object(ohash.trailer[:Info])
|
14
27
|
@matches.each do |key, regexp|
|
15
28
|
if !info.has_key?(key)
|
16
|
-
array << "Info dict missing required key
|
29
|
+
array << Issue.new("Info dict missing required key", self, :key => key)
|
17
30
|
elsif !info[key].to_s.match(regexp)
|
18
|
-
array << "value of Info entry #{key} doesn't match
|
31
|
+
array << Issue.new("value of Info entry #{key} doesn't match #{regexp}", self, :key => key,
|
32
|
+
:regexp => regexp)
|
19
33
|
end
|
20
34
|
end
|
21
35
|
array
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'matrix'
|
5
|
+
|
6
|
+
module Preflight
|
7
|
+
module Rules
|
8
|
+
|
9
|
+
# Most CMYK printers will have a stated upper tolerance for ink density. If
|
10
|
+
# the total percentage of the 4 components (C, M, Y and K) is over that
|
11
|
+
# tolerance then the result can be unpredictable and often ugly.
|
12
|
+
#
|
13
|
+
# Use this rule to detect CMYK ink densities over a certain threshold.
|
14
|
+
#
|
15
|
+
# Arguments: the highest density that is Ok
|
16
|
+
#
|
17
|
+
# Usage:
|
18
|
+
#
|
19
|
+
# class MyPreflight
|
20
|
+
# include Preflight::Profile
|
21
|
+
#
|
22
|
+
# rule Preflight::Rules::MaxInkDensity, 300
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# TODO:
|
26
|
+
#
|
27
|
+
# * check CMYK colours used as alternates in a separation color
|
28
|
+
# * check CMYK raster images
|
29
|
+
#
|
30
|
+
class MaxInkDensity
|
31
|
+
|
32
|
+
attr_reader :issues
|
33
|
+
|
34
|
+
def initialize(max_ink)
|
35
|
+
@max_ink = max_ink.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
# we're about to start a new page, reset state
|
39
|
+
#
|
40
|
+
def page=(page)
|
41
|
+
@issues = []
|
42
|
+
@page = page
|
43
|
+
@objects = page.objects
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_cmyk_color_for_nonstroking(c, m, y, k)
|
47
|
+
check_ink(c, m, y, k)
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_cmyk_color_for_stroking(c, m, y, k)
|
51
|
+
check_ink(c, m, y, k)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def check_ink(c, m, y, k)
|
57
|
+
ink = (c + m + y + k) * 100.0
|
58
|
+
if ink > @max_ink && @issues.empty?
|
59
|
+
@issues << Issue.new("Ink density too high", self, :page => @page.number,
|
60
|
+
:cyan => c,
|
61
|
+
:magenta => m,
|
62
|
+
:yellow => y,
|
63
|
+
:k => k)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -4,15 +4,27 @@ module Preflight
|
|
4
4
|
module Rules
|
5
5
|
# ensure the PDF version of the file under review is not more recent
|
6
6
|
# than desired
|
7
|
+
#
|
8
|
+
# Arguments: the maximum version
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# class MyPreflight
|
13
|
+
# include Preflight::Profile
|
14
|
+
#
|
15
|
+
# rule Preflight::Rules::MaxVersion, 1.4
|
16
|
+
# end
|
17
|
+
#
|
7
18
|
class MaxVersion
|
8
19
|
|
9
20
|
def initialize(max_version)
|
10
21
|
@max_version = max_version.to_f
|
11
22
|
end
|
12
23
|
|
13
|
-
def
|
24
|
+
def check_hash(ohash)
|
14
25
|
if ohash.pdf_version > @max_version
|
15
|
-
["PDF version should be #{@max_version} or lower
|
26
|
+
[Issue.new("PDF version should be #{@max_version} or lower", self, :max_version => @max_version,
|
27
|
+
:current_version => ohash.pdf_version)]
|
16
28
|
else
|
17
29
|
[]
|
18
30
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Preflight
|
6
|
+
module Rules
|
7
|
+
|
8
|
+
# Checks the MediaBox for every page is at 0,0. This isn't required by
|
9
|
+
# any standards but is good practice to ensure correct rendering with
|
10
|
+
# some applications.
|
11
|
+
#
|
12
|
+
# Arguments: none
|
13
|
+
#
|
14
|
+
# Usage:
|
15
|
+
#
|
16
|
+
# class MyPreflight
|
17
|
+
# include Preflight::Profile
|
18
|
+
#
|
19
|
+
# rule Preflight::Rules::MediaboxAtOrigin
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
class MediaboxAtOrigin
|
23
|
+
|
24
|
+
attr_reader :issues
|
25
|
+
|
26
|
+
def page=(page)
|
27
|
+
@issues = []
|
28
|
+
dict = page.attributes
|
29
|
+
|
30
|
+
if round_off(dict[:MediaBox][0,2]) != [0,0]
|
31
|
+
@issues << Issue.new("MediaBox must begin at 0,0", self, :page => page.number)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def round_off(*arr)
|
38
|
+
arr.flatten.compact.map { |n| BigDecimal.new(n.to_s).round(2) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'matrix'
|
5
|
+
|
6
|
+
module Preflight
|
7
|
+
module Rules
|
8
|
+
|
9
|
+
# To print colour to the edge of the page you must print past the intended
|
10
|
+
# page edge and then trim the printed sheet. The printed area that will be
|
11
|
+
# trimmed is called bleed. Generally you will probably want 3-4mm of bleed.
|
12
|
+
#
|
13
|
+
# Arguments: the distance from the TrimBox within which objects MUST include bleed
|
14
|
+
# the distance past the TrimBox that objects MUST bleed
|
15
|
+
# the units (:pt, :mm, :in)
|
16
|
+
#
|
17
|
+
# Usage:
|
18
|
+
#
|
19
|
+
# class MyPreflight
|
20
|
+
# include Preflight::Profile
|
21
|
+
#
|
22
|
+
# rule Preflight::Rules::MinBleed, 1, 4, :mm
|
23
|
+
# rule Preflight::Rules::MinBleed, 12, 50, :pt
|
24
|
+
# rule Preflight::Rules::MinBleed, 0.1, 0.25, :in
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class MinBleed
|
28
|
+
include Preflight::Measurements
|
29
|
+
extend Forwardable
|
30
|
+
|
31
|
+
attr_reader :issues
|
32
|
+
|
33
|
+
# Graphics State Operators
|
34
|
+
def_delegators :@state, :save_graphics_state, :restore_graphics_state
|
35
|
+
|
36
|
+
# Matrix Operators
|
37
|
+
def_delegators :@state, :concatenate_matrix
|
38
|
+
|
39
|
+
def initialize(range, bleed, units)
|
40
|
+
@range, @bleed, @units = range, bleed, units
|
41
|
+
end
|
42
|
+
|
43
|
+
# we're about to start a new page, reset state
|
44
|
+
#
|
45
|
+
def page=(page)
|
46
|
+
@issues = []
|
47
|
+
@page = page
|
48
|
+
@state = PDF::Reader::PageState.new(page)
|
49
|
+
@objects = page.objects
|
50
|
+
|
51
|
+
attrs = @page.attributes
|
52
|
+
box = attrs[:TrimBox] || attrs[:ArtBox] || attrs[:MediaBox]
|
53
|
+
@warning_min_x = box[0] + to_points(@range, @units)
|
54
|
+
@warning_min_y = box[1] + to_points(@range, @units)
|
55
|
+
@warning_max_x = box[2] - to_points(@range, @units)
|
56
|
+
@warning_max_y = box[3] - to_points(@range, @units)
|
57
|
+
@error_min_x = box[0] - to_points(@bleed, @units)
|
58
|
+
@error_min_y = box[1] - to_points(@bleed, @units)
|
59
|
+
@error_max_x = box[2] + to_points(@bleed, @units)
|
60
|
+
@error_max_y = box[3] + to_points(@bleed, @units)
|
61
|
+
end
|
62
|
+
|
63
|
+
# As each image is drawn on the canvas, determine the amount of device
|
64
|
+
# space it's being crammed into and therefore the PPI.
|
65
|
+
#
|
66
|
+
def invoke_xobject(label)
|
67
|
+
@state.invoke_xobject(label) do |xobj|
|
68
|
+
case xobj
|
69
|
+
when PDF::Reader::FormXObject then
|
70
|
+
xobj.walk(self)
|
71
|
+
when PDF::Reader::Stream
|
72
|
+
invoke_image_xobject(xobj) if xobj.hash[:Subtype] == :Image
|
73
|
+
else
|
74
|
+
raise xobj.inspect
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def append_rectangle(x1, y1, x2, y2)
|
80
|
+
@path ||= []
|
81
|
+
@path << @state.ctm_transform(x1, y1)
|
82
|
+
@path << @state.ctm_transform(x1, y2)
|
83
|
+
@path << @state.ctm_transform(x2, y1)
|
84
|
+
@path << @state.ctm_transform(x2, y2)
|
85
|
+
end
|
86
|
+
|
87
|
+
def fill_path_with_nonzero
|
88
|
+
@path ||= []
|
89
|
+
points = select_points_in_danger_zone(@path)
|
90
|
+
|
91
|
+
if points.size > 0
|
92
|
+
@issues << Issue.new("Filled object with insufficient bleed", self, :page => @page.number,
|
93
|
+
:object_type => :filled_object,
|
94
|
+
:bleed => @bleed,
|
95
|
+
:units => @units)
|
96
|
+
end
|
97
|
+
|
98
|
+
@path = []
|
99
|
+
end
|
100
|
+
alias :fill_path_with_even_odd :fill_path_with_nonzero
|
101
|
+
|
102
|
+
def close_and_stroke_path
|
103
|
+
@path = []
|
104
|
+
end
|
105
|
+
|
106
|
+
def stroke_path
|
107
|
+
@path = []
|
108
|
+
end
|
109
|
+
|
110
|
+
def end_path
|
111
|
+
@path = []
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def invoke_image_xobject(xobject)
|
117
|
+
return unless @page.attributes[:TrimBox] || @page.attributes[:ArtBox]
|
118
|
+
|
119
|
+
points = select_points_in_danger_zone(image_points)
|
120
|
+
|
121
|
+
if points.size > 0
|
122
|
+
@issues << Issue.new("Image with insufficient bleed", self, :page => @page.number,
|
123
|
+
:object_type => :image,
|
124
|
+
:bleed => @bleed,
|
125
|
+
:units => @units)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def deref(obj)
|
130
|
+
@objects ? @objects.deref(obj) : obj
|
131
|
+
end
|
132
|
+
|
133
|
+
# convert value units to PDF points. units should be :mm, :in or :pt
|
134
|
+
#
|
135
|
+
def to_points(value, units)
|
136
|
+
case units
|
137
|
+
when :mm then mm2pt(value)
|
138
|
+
when :in then in2pt(value)
|
139
|
+
else
|
140
|
+
value
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# return the co-ordinates for the 4 corners of an image according
|
145
|
+
# to the current CTM
|
146
|
+
#
|
147
|
+
def image_points
|
148
|
+
[
|
149
|
+
@state.ctm_transform(0, 0),
|
150
|
+
@state.ctm_transform(0, 1),
|
151
|
+
@state.ctm_transform(1, 0),
|
152
|
+
@state.ctm_transform(1, 1)
|
153
|
+
]
|
154
|
+
end
|
155
|
+
|
156
|
+
# given an array of points, returns the subset of points (if any) that
|
157
|
+
# fall within the danger zone indicating they're close to the TrimBox
|
158
|
+
# without enough bleed
|
159
|
+
#
|
160
|
+
def select_points_in_danger_zone(points)
|
161
|
+
points.select { |p|
|
162
|
+
(p.first < @warning_min_x && p.first > @error_min_x) ||
|
163
|
+
(p.last < @warning_min_y && p.last > @error_min_y) ||
|
164
|
+
(p.first > @warning_max_x && p.first < @error_max_x) ||
|
165
|
+
(p.last > @warning_max_x && p.last < @error_max_y)
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require 'yaml'
|
4
3
|
require 'matrix'
|
4
|
+
require 'forwardable'
|
5
5
|
|
6
6
|
module Preflight
|
7
7
|
module Rules
|
@@ -9,160 +9,98 @@ module Preflight
|
|
9
9
|
# For high quality prints, you generally want raster images to be
|
10
10
|
# AT LEAST 300 points-per-inch (ppi). 600 is better, 1200 better again.
|
11
11
|
#
|
12
|
+
# Arguments: the lowest PPI that is ok
|
13
|
+
#
|
14
|
+
# Usage:
|
15
|
+
#
|
16
|
+
# class MyPreflight
|
17
|
+
# include Preflight::Profile
|
18
|
+
#
|
19
|
+
# rule Preflight::Rules::MinPpi, 300
|
20
|
+
# end
|
21
|
+
#
|
12
22
|
class MinPpi
|
13
23
|
include Preflight::Measurements
|
24
|
+
extend Forwardable
|
25
|
+
|
26
|
+
attr_reader :issues
|
14
27
|
|
15
|
-
|
16
|
-
|
17
|
-
}
|
28
|
+
# Graphics State Operators
|
29
|
+
def_delegators :@state, :save_graphics_state, :restore_graphics_state
|
18
30
|
|
19
|
-
|
31
|
+
# Matrix Operators
|
32
|
+
def_delegators :@state, :concatenate_matrix
|
20
33
|
|
21
34
|
def initialize(min_ppi)
|
22
35
|
@min_ppi = min_ppi.to_i
|
23
|
-
@messages = []
|
24
|
-
@page_num = 0
|
25
|
-
end
|
26
|
-
|
27
|
-
def save_graphics_state
|
28
|
-
@stack.push clone_state
|
29
|
-
end
|
30
|
-
|
31
|
-
def restore_graphics_state
|
32
|
-
@stack.pop
|
33
|
-
end
|
34
|
-
|
35
|
-
def state
|
36
|
-
@stack.last
|
37
36
|
end
|
38
37
|
|
39
|
-
#
|
38
|
+
# we're about to start a new page, reset state
|
40
39
|
#
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
stream.hash[:Width],
|
46
|
-
stream.hash[:Height]
|
47
|
-
]
|
48
|
-
end
|
49
|
-
|
50
|
-
# update the current transformation matrix.
|
51
|
-
#
|
52
|
-
# If the CTM is currently undefined, just store the new values.
|
53
|
-
#
|
54
|
-
# If there's an existing CTM, then multiple the existing matrix
|
55
|
-
# with the new matrix to form the updated matrix.
|
56
|
-
#
|
57
|
-
def concatenate_matrix(*args)
|
58
|
-
transform = Matrix[
|
59
|
-
[args[0], args[1], 0],
|
60
|
-
[args[2], args[3], 0],
|
61
|
-
[args[4], args[5], 1]
|
62
|
-
]
|
63
|
-
if state[:ctm]
|
64
|
-
state[:ctm] = transform * state[:ctm]
|
65
|
-
else
|
66
|
-
state[:ctm] = transform
|
67
|
-
end
|
40
|
+
def page=(page)
|
41
|
+
@page = page
|
42
|
+
@state = PDF::Reader::PageState.new(page)
|
43
|
+
@issues = []
|
68
44
|
end
|
69
45
|
|
70
46
|
# As each image is drawn on the canvas, determine the amount of device
|
71
47
|
# space it's being crammed into and therefore the PPI.
|
72
48
|
#
|
73
49
|
def invoke_xobject(label)
|
74
|
-
|
50
|
+
@state.invoke_xobject(label) do |xobj|
|
51
|
+
case xobj
|
52
|
+
when PDF::Reader::FormXObject then
|
53
|
+
xobj.walk(self)
|
54
|
+
when PDF::Reader::Stream
|
55
|
+
invoke_image_xobject(xobj) if xobj.hash[:Subtype] == :Image
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
75
61
|
|
76
|
-
|
62
|
+
def invoke_image_xobject(xobject)
|
63
|
+
sample_w = deref(xobject.hash[:Width]) || 0
|
64
|
+
sample_h = deref(xobject.hash[:Height]) || 0
|
77
65
|
device_w = pt2in(image_width)
|
78
66
|
device_h = pt2in(image_height)
|
79
67
|
|
80
|
-
horizontal_ppi = (sample_w / device_w).round(3)
|
81
|
-
vertical_ppi = (sample_h / device_h).round(3)
|
68
|
+
horizontal_ppi = BigDecimal.new((sample_w / device_w).to_s).round(3)
|
69
|
+
vertical_ppi = BigDecimal.new((sample_h / device_h).to_s).round(3)
|
82
70
|
|
83
71
|
if horizontal_ppi < @min_ppi || vertical_ppi < @min_ppi
|
84
|
-
@
|
72
|
+
@issues << Issue.new("Image with low PPI/DPI", self, :page => @page.number,
|
73
|
+
:horizontal_ppi => horizontal_ppi,
|
74
|
+
:vertical_ppi => vertical_ppi,
|
75
|
+
:top_left => @state.ctm_transform(0, 1),
|
76
|
+
:bottom_left => @state.ctm_transform(0, 0),
|
77
|
+
:bottom_right => @state.ctm_transform(1, 0),
|
78
|
+
:top_right => @state.ctm_transform(1, 1))
|
85
79
|
end
|
86
80
|
end
|
87
81
|
|
88
|
-
|
89
|
-
|
90
|
-
def begin_page(hash = {})
|
91
|
-
@images = {}
|
92
|
-
@page_num += 1
|
93
|
-
@stack = [DEFAULT_GRAPHICS_STATE]
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
# return the current transformation matrix
|
99
|
-
#
|
100
|
-
def ctm
|
101
|
-
state[:ctm]
|
102
|
-
end
|
103
|
-
|
104
|
-
# transform x and y co-ordinates from the current user space to the
|
105
|
-
# underlying device space.
|
106
|
-
#
|
107
|
-
def transform(point, z = 1)
|
108
|
-
Point.new(
|
109
|
-
(ctm[0,0] * point.x) + (ctm[1,0] * point.y) + (ctm[2,0] * z),
|
110
|
-
(ctm[0,1] * point.x) + (ctm[1,1] * point.y) + (ctm[2,1] * z)
|
111
|
-
)
|
82
|
+
def deref(obj)
|
83
|
+
@objects ? @objects.deref(obj) : obj
|
112
84
|
end
|
113
85
|
|
114
86
|
# return a height of an image in the current device space. Auto
|
115
87
|
# handles the translation from image space to device space.
|
116
88
|
#
|
117
89
|
def image_height
|
118
|
-
bottom_left =
|
119
|
-
top_left =
|
90
|
+
bottom_left = @state.ctm_transform(0, 0)
|
91
|
+
top_left = @state.ctm_transform(0, 1)
|
120
92
|
|
121
|
-
bottom_left.
|
93
|
+
Math.hypot(bottom_left.first-top_left.first, bottom_left.last-top_left.last)
|
122
94
|
end
|
123
95
|
|
124
96
|
# return a width of an image in the current device space. Auto
|
125
97
|
# handles the translation from image space to device space.
|
126
98
|
#
|
127
99
|
def image_width
|
128
|
-
bottom_left =
|
129
|
-
bottom_right =
|
130
|
-
|
131
|
-
bottom_left.distance(bottom_right)
|
132
|
-
end
|
100
|
+
bottom_left = @state.ctm_transform(0, 0)
|
101
|
+
bottom_right = @state.ctm_transform(1, 0)
|
133
102
|
|
134
|
-
|
135
|
-
# current state onto the stack. That way any modifications to the state
|
136
|
-
# will be undone once restore_graphics_state is called.
|
137
|
-
#
|
138
|
-
# This returns a deep clone of the current state, ensuring changes are
|
139
|
-
# keep separate from earlier states.
|
140
|
-
#
|
141
|
-
# YAML is used to round-trip the state through a string to easily perform
|
142
|
-
# the deep clone. Kinda hacky, but effective.
|
143
|
-
#
|
144
|
-
def clone_state
|
145
|
-
if @stack.empty?
|
146
|
-
{}
|
147
|
-
else
|
148
|
-
yaml_state = YAML.dump(@stack.last)
|
149
|
-
YAML.load(yaml_state)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
# private class for representing points on a cartesian plain. Used
|
154
|
-
# to simplify maths in the MinPpi class.
|
155
|
-
#
|
156
|
-
class Point
|
157
|
-
attr_reader :x, :y
|
158
|
-
|
159
|
-
def initialize(x,y)
|
160
|
-
@x, @y = x,y
|
161
|
-
end
|
162
|
-
|
163
|
-
def distance(point)
|
164
|
-
Math.hypot(point.x - x, point.y - y)
|
165
|
-
end
|
103
|
+
Math.hypot(bottom_left.first-bottom_right.first, bottom_left.last-bottom_right.last)
|
166
104
|
end
|
167
105
|
|
168
106
|
end
|