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