preflight 0.0.1
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 +2 -0
- data/README.rdoc +87 -0
- data/bin/is_pdfx_1a +22 -0
- data/lib/preflight.rb +11 -0
- data/lib/preflight/measurements.rb +47 -0
- data/lib/preflight/profile.rb +101 -0
- data/lib/preflight/profiles.rb +2 -0
- data/lib/preflight/profiles/pdfa1a.rb +17 -0
- data/lib/preflight/profiles/pdfx1a.rb +25 -0
- data/lib/preflight/rules.rb +14 -0
- data/lib/preflight/rules/box_nesting.rb +43 -0
- data/lib/preflight/rules/compression_algorithms.rb +42 -0
- data/lib/preflight/rules/document_id.rb +20 -0
- data/lib/preflight/rules/info_has_keys.rb +26 -0
- data/lib/preflight/rules/info_specifies_trapping.rb +24 -0
- data/lib/preflight/rules/match_info_entries.rb +29 -0
- data/lib/preflight/rules/max_version.rb +26 -0
- data/lib/preflight/rules/min_ppi.rb +68 -0
- data/lib/preflight/rules/no_encryption.rb +20 -0
- data/lib/preflight/rules/no_font_subsets.rb +34 -0
- data/lib/preflight/rules/no_proprietary_fonts.rb +54 -0
- data/lib/preflight/rules/only_embedded_fonts.rb +44 -0
- data/lib/preflight/rules/print_boxes.rb +34 -0
- data/lib/preflight/rules/root_has_keys.rb +26 -0
- metadata +145 -0
data/CHANGELOG
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
= Preflight
|
2
|
+
|
3
|
+
Check your PDF files meet the standard you require.
|
4
|
+
|
5
|
+
The full PDF spec is a beast, and there are any number of ways of producing a
|
6
|
+
valid PDF. Receivers of PDF files may need to ensure the files they're sent
|
7
|
+
meet a particular set of requirements, be it PDF/X, PDF/A or just "images must
|
8
|
+
be 300 dpi or greater". These checks are called preflight.
|
9
|
+
|
10
|
+
There's expensive software around that can preflight for you, but it's often
|
11
|
+
difficult to script and you know, expensive.
|
12
|
+
|
13
|
+
This may not check as comprehensively as the official preflight tools from Adobe
|
14
|
+
and friends, but hopefully it'll get you most of the way with less stress.
|
15
|
+
|
16
|
+
== Installation
|
17
|
+
|
18
|
+
gem install preflight
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
== Standard Profile ( PDF/X-1a )
|
23
|
+
|
24
|
+
require "preflight"
|
25
|
+
|
26
|
+
preflight = Preflight::Profiles::PDFX1A.new
|
27
|
+
|
28
|
+
puts preflight.check("somefile.pdf").inspect
|
29
|
+
|
30
|
+
File.open("somefile.pdf", "rb") do |file|
|
31
|
+
puts preflight.check(file).inspect
|
32
|
+
end
|
33
|
+
|
34
|
+
== Custom Profile
|
35
|
+
|
36
|
+
require "preflight"
|
37
|
+
|
38
|
+
class MyPreflight
|
39
|
+
include Preflight::Profile
|
40
|
+
|
41
|
+
profile_name "simple-pdf"
|
42
|
+
|
43
|
+
rule Preflight::Rules::MaxVersion, 1.4
|
44
|
+
rule Preflight::Rules::NoEncryption
|
45
|
+
rule Preflight::Rules::DocumentId
|
46
|
+
end
|
47
|
+
|
48
|
+
preflight = MyPreflight
|
49
|
+
|
50
|
+
puts preflight.check("somefile.pdf").inspect
|
51
|
+
|
52
|
+
== Extend A Profile
|
53
|
+
|
54
|
+
require "preflight"
|
55
|
+
|
56
|
+
class MyPreflight
|
57
|
+
include Preflight::Profile
|
58
|
+
|
59
|
+
profile_name "simple-pdf"
|
60
|
+
|
61
|
+
import Preflight::Profiles::PDFX1A
|
62
|
+
|
63
|
+
rule Preflight::Rules::MaxVersion, 1.4
|
64
|
+
rule Preflight::Rules::NoEncryption
|
65
|
+
rule Preflight::Rules::DocumentId
|
66
|
+
end
|
67
|
+
|
68
|
+
preflight = MyPreflight
|
69
|
+
|
70
|
+
puts preflight.check("somefile.pdf").inspect
|
71
|
+
|
72
|
+
== Status
|
73
|
+
|
74
|
+
This library is in an early stage of development. Use at your own risk.
|
75
|
+
|
76
|
+
== Compatability
|
77
|
+
|
78
|
+
This is pure ruby should run on most ruby VMs. I develop on MRI 1.9.2.
|
79
|
+
|
80
|
+
== Further Reading
|
81
|
+
|
82
|
+
* http://chneukirchen.github.com/rps/
|
83
|
+
* http://en.wikipedia.org/wiki/PDF/A
|
84
|
+
* http://en.wikipedia.org/wiki/PDF/X
|
85
|
+
* http://www.planetpdf.com/planetpdf/pdfs/pdfx3_draft4.pdf
|
86
|
+
* http://www.gwg.org/doc_pdfxfaq.phtml
|
87
|
+
* http://www.pdfxreport.com/lib/exe/fetch.php?id=en%3Adownloads&cache=cache&media=en:technote_pdfx_checks.pdf
|
data/bin/is_pdfx_1a
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'preflight'
|
7
|
+
rescue LoadError
|
8
|
+
require 'bundler'
|
9
|
+
Bundler.setup
|
10
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib/"
|
11
|
+
require 'preflight'
|
12
|
+
end
|
13
|
+
|
14
|
+
filename = ARGV.shift
|
15
|
+
preflight = Preflight::Profiles::PDFX1A.new
|
16
|
+
messages = preflight.check(filename)
|
17
|
+
|
18
|
+
messages.each do |msg|
|
19
|
+
$stderr.puts msg
|
20
|
+
end
|
21
|
+
|
22
|
+
exit messages.size
|
data/lib/preflight.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'bigdecimal'
|
5
|
+
|
6
|
+
module Preflight
|
7
|
+
module Measurements
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# convert inches to standard PDF points
|
12
|
+
#
|
13
|
+
def in2pt(inch)
|
14
|
+
return inch * BigDecimal.new("72")
|
15
|
+
end
|
16
|
+
|
17
|
+
# convert mm to standard PDF points
|
18
|
+
#
|
19
|
+
def mm2pt(mm)
|
20
|
+
return mm * (BigDecimal.new("72") / BigDecimal.new("24.4"))
|
21
|
+
end
|
22
|
+
|
23
|
+
# convert mm to standard PDF points
|
24
|
+
#
|
25
|
+
def cm2pt(cm)
|
26
|
+
return cm * mm2pt(10)
|
27
|
+
end
|
28
|
+
|
29
|
+
# convert standard PDF points to inches
|
30
|
+
#
|
31
|
+
def pt2in(pt)
|
32
|
+
return pt / in2pt(1)
|
33
|
+
end
|
34
|
+
|
35
|
+
# convert standard PDF points to mm
|
36
|
+
#
|
37
|
+
def pt2mm(pt)
|
38
|
+
return pt / mm2pt(1)
|
39
|
+
end
|
40
|
+
|
41
|
+
# convert standard PDF points to cm
|
42
|
+
#
|
43
|
+
def pt2cm(pt)
|
44
|
+
return pt / cm2pt(1)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
|
5
|
+
# base functionality for all profiles.
|
6
|
+
#
|
7
|
+
module Profile
|
8
|
+
|
9
|
+
def self.included(base) # :nodoc:
|
10
|
+
base.class_eval do
|
11
|
+
extend Preflight::Profile::ClassMethods
|
12
|
+
include InstanceMethods
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def profile_name(str)
|
18
|
+
@profile_name = str
|
19
|
+
end
|
20
|
+
|
21
|
+
def import(profile)
|
22
|
+
profile.rules.each do |array|
|
23
|
+
rules << array.flatten
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def rule(*args)
|
28
|
+
rules << args.flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
def rules
|
32
|
+
@rules ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
def check(input)
|
39
|
+
if File.file?(input)
|
40
|
+
check_filename(input)
|
41
|
+
elsif input.is_a?(IO)
|
42
|
+
check_io(input)
|
43
|
+
else
|
44
|
+
raise ArgumentError, "input must be a string with a filename or an IO object"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def check_filename(filename)
|
51
|
+
File.open(filename, "rb") do |file|
|
52
|
+
return check_io(file)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_io(io)
|
57
|
+
check_receivers(io) + check_hash(io)
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO: this is nasty, we parse the full file once for each receiver.
|
61
|
+
# PDF::Reader needs to be updated to support multiple receivers
|
62
|
+
#
|
63
|
+
def check_receivers(io)
|
64
|
+
receiver_rules.map { |rec|
|
65
|
+
begin
|
66
|
+
PDF::Reader.new.parse(io, rec)
|
67
|
+
rec.messages
|
68
|
+
rescue PDF::Reader::UnsupportedFeatureError
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
}.flatten.compact
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_hash(io)
|
75
|
+
ohash = PDF::Reader::ObjectHash.new(io)
|
76
|
+
|
77
|
+
hash_rules.map { |chk|
|
78
|
+
chk.messages(ohash)
|
79
|
+
}.flatten.compact
|
80
|
+
end
|
81
|
+
|
82
|
+
def hash_rules
|
83
|
+
self.class.rules.select { |arr|
|
84
|
+
arr.first.rule_type == :hash
|
85
|
+
}.map { |arr|
|
86
|
+
klass = arr[0]
|
87
|
+
klass.new(*arr[1,10])
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def receiver_rules
|
92
|
+
self.class.rules.select { |arr|
|
93
|
+
arr.first.rule_type == :receiver
|
94
|
+
}.map { |arr|
|
95
|
+
klass = arr[0]
|
96
|
+
klass.new(*arr[1,10])
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Profiles
|
5
|
+
class PDFA1A
|
6
|
+
include Preflight::Profile
|
7
|
+
|
8
|
+
profile_name "pdfa-1a"
|
9
|
+
|
10
|
+
# hard failures of the pdfx/1a spec
|
11
|
+
rule Preflight::Rules::CompressionAlgorithms, :CCITTFaxDecode, :DCTDecode, :FlateDecode, :RunLengthDecode
|
12
|
+
rule Preflight::Rules::NoEncryption
|
13
|
+
rule Preflight::Rules::OnlyEmbeddedFonts
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Profiles
|
5
|
+
class PDFX1A
|
6
|
+
include Preflight::Profile
|
7
|
+
|
8
|
+
profile_name "pdfx-1a"
|
9
|
+
|
10
|
+
rule Preflight::Rules::MatchInfoEntries, {:GTS_PDFXVersion => /\APDF\/X/,
|
11
|
+
:GTS_PDFXConformance => /\APDF\/X-1a/}
|
12
|
+
rule Preflight::Rules::RootHasKeys, :OutputIntents
|
13
|
+
rule Preflight::Rules::InfoHasKeys, :Title, :CreationDate, :ModDate
|
14
|
+
rule Preflight::Rules::InfoSpecifiesTrapping
|
15
|
+
rule Preflight::Rules::CompressionAlgorithms, :CCITTFaxDecode, :DCTDecode, :FlateDecode, :RunLengthDecode
|
16
|
+
rule Preflight::Rules::DocumentId
|
17
|
+
rule Preflight::Rules::NoEncryption
|
18
|
+
rule Preflight::Rules::OnlyEmbeddedFonts
|
19
|
+
rule Preflight::Rules::BoxNesting
|
20
|
+
rule Preflight::Rules::MaxVersion, 1.4
|
21
|
+
rule Preflight::Rules::PrintBoxes
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'preflight/rules/box_nesting'
|
2
|
+
require 'preflight/rules/compression_algorithms'
|
3
|
+
require 'preflight/rules/document_id'
|
4
|
+
require 'preflight/rules/info_has_keys'
|
5
|
+
require 'preflight/rules/info_specifies_trapping'
|
6
|
+
require 'preflight/rules/match_info_entries'
|
7
|
+
require 'preflight/rules/max_version'
|
8
|
+
require 'preflight/rules/min_ppi'
|
9
|
+
require 'preflight/rules/no_encryption'
|
10
|
+
require 'preflight/rules/no_font_subsets'
|
11
|
+
require 'preflight/rules/no_proprietary_fonts'
|
12
|
+
require 'preflight/rules/only_embedded_fonts'
|
13
|
+
require 'preflight/rules/print_boxes'
|
14
|
+
require 'preflight/rules/root_has_keys'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# For each page MediaBox must be the biggest box, followed by the
|
7
|
+
# BleedBox or ArtBox, followed by the TrimBox.
|
8
|
+
#
|
9
|
+
class BoxNesting
|
10
|
+
attr_reader :messages
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@messages = []
|
14
|
+
@page_num = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.rule_type
|
18
|
+
:receiver
|
19
|
+
end
|
20
|
+
|
21
|
+
def begin_page(hash = {})
|
22
|
+
@page_num += 1
|
23
|
+
|
24
|
+
media = hash[:MediaBox]
|
25
|
+
bleed = hash[:BleedBox]
|
26
|
+
trim = hash[:TrimBox]
|
27
|
+
art = hash[:ArtBox]
|
28
|
+
|
29
|
+
if media && bleed && (bleed[2] > media[2] || bleed[3] > media[3])
|
30
|
+
@messages << "BleedBox must be smaller than MediaBox (page #{@page_num})"
|
31
|
+
elsif trim && bleed && (trim[2] > bleed[2] || trim[3] > bleed[3])
|
32
|
+
@messages << "TrimBox must be smaller than BleedBox (page #{@page_num})"
|
33
|
+
elsif art && bleed && (art[2] > bleed[2] || art[3] > bleed[3])
|
34
|
+
@messages << "ArtBox must be smaller than BleedBox (page #{@page_num})"
|
35
|
+
elsif trim && media && (trim[2] > media[2] || trim[3] > media[3])
|
36
|
+
@messages << "TrimBox must be smaller than MediaBox (page #{@page_num})"
|
37
|
+
elsif art && media && (art[2] > media[2] || art[3] > media[3])
|
38
|
+
@messages << "ArtBox must be smaller than MediaBox (page #{@page_num})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# check a file only uses permitted compression algorithms
|
7
|
+
#
|
8
|
+
class CompressionAlgorithms
|
9
|
+
|
10
|
+
def initialize(*algorithms)
|
11
|
+
@algorithms = algorithms.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.rule_type
|
15
|
+
:hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def messages(ohash)
|
19
|
+
algorithms = banned_algorithms(ohash)
|
20
|
+
|
21
|
+
if algorithms.size > 0
|
22
|
+
["File uses excluded compression algorithm (#{algorithms.join(", ")})"]
|
23
|
+
else
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def banned_algorithms(ohash)
|
31
|
+
array = []
|
32
|
+
ohash.each do |key, obj|
|
33
|
+
if obj.is_a?(PDF::Reader::Stream)
|
34
|
+
filters = [obj.hash[:Filter]].flatten.compact
|
35
|
+
array += (filters - @algorithms)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
array.uniq
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# ensure the info dict has the specified keys
|
7
|
+
class InfoHasKeys
|
8
|
+
|
9
|
+
def initialize(*keys)
|
10
|
+
@keys = keys.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rule_type
|
14
|
+
:hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages(ohash)
|
18
|
+
info = ohash.object(ohash.trailer[:Info])
|
19
|
+
missing = @keys - info.keys
|
20
|
+
missing.map { |key|
|
21
|
+
"Info dict missing required key #{key}"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
class InfoSpecifiesTrapping
|
6
|
+
|
7
|
+
def self.rule_type
|
8
|
+
:hash
|
9
|
+
end
|
10
|
+
|
11
|
+
def messages(ohash)
|
12
|
+
info = ohash.object(ohash.trailer[:Info])
|
13
|
+
|
14
|
+
if !info.has_key?(:Trapped)
|
15
|
+
[ "Info dict does not specify Trapped" ]
|
16
|
+
elsif info[:Trapped] != :True && info[:Trapped] != :False
|
17
|
+
[ "Trapped value of Info dict must be True or False" ]
|
18
|
+
else
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
class MatchInfoEntries
|
6
|
+
|
7
|
+
def initialize(matches = {})
|
8
|
+
@matches = matches
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.rule_type
|
12
|
+
:hash
|
13
|
+
end
|
14
|
+
|
15
|
+
def messages(ohash)
|
16
|
+
array = []
|
17
|
+
info = ohash.object(ohash.trailer[:Info])
|
18
|
+
@matches.each do |key, regexp|
|
19
|
+
if !info.has_key?(key)
|
20
|
+
array << "Info dict missing required key #{key}"
|
21
|
+
elsif !info[key].to_s.match(regexp)
|
22
|
+
array << "value of Info entry #{key} doesn't match (#{info[key]} != #{regexp})"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
array
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
# ensure the PDF version of the file under review is not more recent
|
6
|
+
# than desired
|
7
|
+
class MaxVersion
|
8
|
+
|
9
|
+
def initialize(max_version)
|
10
|
+
@max_version = max_version.to_f
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rule_type
|
14
|
+
:hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages(ohash)
|
18
|
+
if ohash.pdf_version > @max_version
|
19
|
+
["PDF version should be #{@max_version} or lower (value: #{ohash.pdf_version})"]
|
20
|
+
else
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# For high quality prints, you generally want raster images to be
|
7
|
+
# AT LEAST 300 points-per-inch (ppi). 600 is better, 1200 better again.
|
8
|
+
#
|
9
|
+
class MinPpi
|
10
|
+
include Preflight::Measurements
|
11
|
+
|
12
|
+
attr_reader :messages
|
13
|
+
|
14
|
+
def initialize(min_ppi)
|
15
|
+
@min_ppi = min_ppi.to_i
|
16
|
+
@messages = []
|
17
|
+
@last_matrix = []
|
18
|
+
@page_num = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.rule_type
|
22
|
+
:receiver
|
23
|
+
end
|
24
|
+
|
25
|
+
# store sample width and height for each image on the current page
|
26
|
+
#
|
27
|
+
def resource_xobject(label, stream)
|
28
|
+
return unless stream.hash[:Subtype] == :Image
|
29
|
+
|
30
|
+
@images[label] = [
|
31
|
+
stream.hash[:Width],
|
32
|
+
stream.hash[:Height]
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
# track the most recent matrix transform.
|
37
|
+
#
|
38
|
+
# TODO: This needs to be smarter at tracking the graphics state stack
|
39
|
+
#
|
40
|
+
def concatenate_matrix(*args)
|
41
|
+
@last_matrix = args
|
42
|
+
end
|
43
|
+
|
44
|
+
# As each image is drawn on the canvas, determine the amount of device
|
45
|
+
# space it's being crammed into and therefore the PPI.
|
46
|
+
#
|
47
|
+
def invoke_xobject(label)
|
48
|
+
sample_w, sample_h = *@images[label]
|
49
|
+
device_w = pt2in(@last_matrix[0])
|
50
|
+
device_h = pt2in(@last_matrix[3])
|
51
|
+
|
52
|
+
horizontal_ppi = (sample_w / device_w).round(3)
|
53
|
+
vertical_ppi = (sample_h / device_h).round(3)
|
54
|
+
|
55
|
+
if horizontal_ppi < @min_ppi || vertical_ppi < @min_ppi
|
56
|
+
@messages << "Image with low PPI/DPI on page #{@page_num} (h:#{horizontal_ppi} v:#{vertical_ppi})"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# start fresh on every page
|
61
|
+
#
|
62
|
+
def begin_page(hash = {})
|
63
|
+
@images = {}
|
64
|
+
@page_num += 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# check a file has no font subsets. Subsets are handy and valid
|
7
|
+
# in standards like PDFX/1a, but they make it hard to edit a
|
8
|
+
# file
|
9
|
+
#
|
10
|
+
class NoFontSubsets
|
11
|
+
|
12
|
+
def self.rule_type
|
13
|
+
:hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def messages(ohash)
|
17
|
+
array = []
|
18
|
+
ohash.each do |key, obj|
|
19
|
+
next unless obj.is_a?(::Hash) && obj[:Type] == :Font
|
20
|
+
if subset?(obj)
|
21
|
+
array << "Font #{obj[:BaseFont]} is a partial subset"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
array
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def subset?(font)
|
30
|
+
font[:BaseFont] && font[:BaseFont].match(/.+\+.+/)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'ttfunk'
|
4
|
+
|
5
|
+
module Preflight
|
6
|
+
module Rules
|
7
|
+
|
8
|
+
# check a file has no proprietary fonts. They look nice, but we can't
|
9
|
+
# print the damn things.
|
10
|
+
#
|
11
|
+
class NoProprietaryFonts
|
12
|
+
|
13
|
+
def self.rule_type
|
14
|
+
:hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages(ohash)
|
18
|
+
array = []
|
19
|
+
ohash.each do |key, obj|
|
20
|
+
next unless obj.is_a?(::Hash) && obj[:Type] == :Font
|
21
|
+
if proprietary?(ohash, obj[:FontDescriptor])
|
22
|
+
array << "Font #{obj[:BaseFont]} is proprietary"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
array
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def proprietary?(ohash, descriptor)
|
31
|
+
descriptor = ohash.object(descriptor)
|
32
|
+
|
33
|
+
return false if descriptor.nil?
|
34
|
+
raise ArgumentError, 'expected a FontDescriptor hash' unless descriptor[:Type] == :FontDescriptor
|
35
|
+
|
36
|
+
if descriptor.has_key?(:FontFile)
|
37
|
+
# TODO embedded type 1 font
|
38
|
+
false
|
39
|
+
elsif descriptor.has_key?(:FontFile2) && ttf_proprietary?(ohash, descriptor[:FontFile2])
|
40
|
+
true
|
41
|
+
else
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def ttf_proprietary?(ohash, font_file)
|
47
|
+
font_file = ohash.object(font_file)
|
48
|
+
ttf = TTFunk::File.new(font_file.unfiltered_data)
|
49
|
+
#puts ttf.name.strings
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# check a file only uses embedded fonts
|
7
|
+
#
|
8
|
+
class OnlyEmbeddedFonts
|
9
|
+
|
10
|
+
def self.rule_type
|
11
|
+
:hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def messages(ohash)
|
15
|
+
array = []
|
16
|
+
ohash.each do |key, obj|
|
17
|
+
next unless obj.is_a?(::Hash) && obj[:Type] == :Font
|
18
|
+
if !embedded?(ohash, obj)
|
19
|
+
array << "Font #{obj[:BaseFont]} is not embedded"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
array
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def embedded?(ohash, font)
|
28
|
+
if font.has_key?(:FontDescriptor)
|
29
|
+
descriptor = ohash.object(font[:FontDescriptor])
|
30
|
+
descriptor.has_key?(:FontFile) || descriptor.has_key?(:FontFile2) || descriptor.has_key?(:FontFile3)
|
31
|
+
elsif font[:Subtype] == :Type0
|
32
|
+
descendants = ohash.object(font[:DescendantFonts])
|
33
|
+
descendants.all? { |f|
|
34
|
+
f = ohash.object(f)
|
35
|
+
embedded?(ohash, f)
|
36
|
+
}
|
37
|
+
else
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# For PDFX/1a, every page must have MediaBox, plus either ArtBox or
|
7
|
+
# TrimBox
|
8
|
+
#
|
9
|
+
class PrintBoxes
|
10
|
+
attr_reader :messages
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@messages = []
|
14
|
+
@page_num = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.rule_type
|
18
|
+
:receiver
|
19
|
+
end
|
20
|
+
|
21
|
+
def begin_page(hash = {})
|
22
|
+
@page_num += 1
|
23
|
+
|
24
|
+
if hash[:MediaBox].nil?
|
25
|
+
@messages << "every page must have a MediaBox (page #{@page_num})"
|
26
|
+
elsif hash[:ArtBox].nil? && hash[:TrimBox].nil?
|
27
|
+
@messages << "every page must have either an ArtBox or a TrimBox (page #{@page_num})"
|
28
|
+
elsif hash[:ArtBox] && hash[:TrimBox]
|
29
|
+
@messages << "no page can have both ArtBox and TrimBox - TrimBox is preferred (page #{@page_num})"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Preflight
|
4
|
+
module Rules
|
5
|
+
|
6
|
+
# ensure the root dict has the specified keys
|
7
|
+
class RootHasKeys
|
8
|
+
|
9
|
+
def initialize(*keys)
|
10
|
+
@keys = keys.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rule_type
|
14
|
+
:hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages(ohash)
|
18
|
+
root = ohash.object(ohash.trailer[:Root])
|
19
|
+
missing = @keys - root.keys
|
20
|
+
missing.map { |key|
|
21
|
+
"Root dict missing required key #{key}"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: preflight
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- James Healy
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-03-28 00:00:00 +11:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: pdf-reader
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 9
|
31
|
+
version: "0.9"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rake
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
version: "0"
|
45
|
+
type: :development
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: roodi
|
49
|
+
prerelease: false
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id003
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rspec
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 2
|
70
|
+
- 0
|
71
|
+
version: "2.0"
|
72
|
+
type: :development
|
73
|
+
version_requirements: *id004
|
74
|
+
description: Provides a programatic way to check a PDF file conforms to standards like PDF-X/1a
|
75
|
+
email:
|
76
|
+
- james@yob.id.au
|
77
|
+
executables:
|
78
|
+
- is_pdfx_1a
|
79
|
+
extensions: []
|
80
|
+
|
81
|
+
extra_rdoc_files: []
|
82
|
+
|
83
|
+
files:
|
84
|
+
- lib/preflight/profiles/pdfx1a.rb
|
85
|
+
- lib/preflight/profiles/pdfa1a.rb
|
86
|
+
- lib/preflight/profile.rb
|
87
|
+
- lib/preflight/rules/root_has_keys.rb
|
88
|
+
- lib/preflight/rules/min_ppi.rb
|
89
|
+
- lib/preflight/rules/print_boxes.rb
|
90
|
+
- lib/preflight/rules/info_specifies_trapping.rb
|
91
|
+
- lib/preflight/rules/only_embedded_fonts.rb
|
92
|
+
- lib/preflight/rules/compression_algorithms.rb
|
93
|
+
- lib/preflight/rules/no_encryption.rb
|
94
|
+
- lib/preflight/rules/no_font_subsets.rb
|
95
|
+
- lib/preflight/rules/match_info_entries.rb
|
96
|
+
- lib/preflight/rules/max_version.rb
|
97
|
+
- lib/preflight/rules/no_proprietary_fonts.rb
|
98
|
+
- lib/preflight/rules/info_has_keys.rb
|
99
|
+
- lib/preflight/rules/document_id.rb
|
100
|
+
- lib/preflight/rules/box_nesting.rb
|
101
|
+
- lib/preflight/measurements.rb
|
102
|
+
- lib/preflight/rules.rb
|
103
|
+
- lib/preflight/profiles.rb
|
104
|
+
- lib/preflight.rb
|
105
|
+
- bin/is_pdfx_1a
|
106
|
+
- README.rdoc
|
107
|
+
- CHANGELOG
|
108
|
+
has_rdoc: true
|
109
|
+
homepage: http://github.com/yob/pdf-preflight
|
110
|
+
licenses: []
|
111
|
+
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options:
|
114
|
+
- --title
|
115
|
+
- PDF::Preflight
|
116
|
+
- --line-numbers
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
segments:
|
133
|
+
- 1
|
134
|
+
- 3
|
135
|
+
- 2
|
136
|
+
version: 1.3.2
|
137
|
+
requirements: []
|
138
|
+
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 1.3.7
|
141
|
+
signing_key:
|
142
|
+
specification_version: 3
|
143
|
+
summary: Check PDF files conform to various standards
|
144
|
+
test_files: []
|
145
|
+
|