pd4ml 1.0.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.
- checksums.yaml +7 -0
- data/README.md +91 -0
- data/docs/api.md +546 -0
- data/docs/guide.md +782 -0
- data/exe/pd4ml +132 -0
- data/lib/pd4ml/bridge.rb +362 -0
- data/lib/pd4ml/errors.rb +42 -0
- data/lib/pd4ml/installer.rb +183 -0
- data/lib/pd4ml/java.rb +209 -0
- data/lib/pd4ml/pd4ml-bridge.jar +0 -0
- data/lib/pd4ml/pd4ml.rb +184 -0
- data/lib/pd4ml/version.rb +5 -0
- data/lib/pd4ml.rb +169 -0
- metadata +58 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
require 'time'
|
|
8
|
+
|
|
9
|
+
module Pd4ml
|
|
10
|
+
# Downloads a PD4ML build from the PD4ML Maven repository and makes it the gem's default:
|
|
11
|
+
#
|
|
12
|
+
# pd4ml install the most recent 4.* release
|
|
13
|
+
# pd4ml install --snapshots the most recent 4.* build, snapshots included
|
|
14
|
+
# pd4ml install 4.0.23fx2 an exact version
|
|
15
|
+
#
|
|
16
|
+
# or Pd4ml.install from code. The jar is stored in the user's cache directory (PD4ML_HOME to
|
|
17
|
+
# choose another) with the Bouncy Castle jars PD4ML needs for encryption and signing, every
|
|
18
|
+
# file verified against its published SHA-1 checksum. With no build installed or configured,
|
|
19
|
+
# the first use of PD4ML downloads one (PD4ML_AUTO_INSTALL=0 turns that off).
|
|
20
|
+
module Installer
|
|
21
|
+
REPOSITORY = 'https://pd4ml.com/maven2'
|
|
22
|
+
CENTRAL = 'https://repo1.maven.org/maven2'
|
|
23
|
+
GROUP_PATH = 'com/pd4ml/pd4ml'
|
|
24
|
+
# The Bouncy Castle version PD4ML is built against (https://pd4ml.com/docs/#2.-obtaining-pd4ml).
|
|
25
|
+
BOUNCYCASTLE_VERSION = '1.84'
|
|
26
|
+
BOUNCYCASTLE = %w[bcprov-jdk18on bcpkix-jdk18on bcutil-jdk18on].freeze
|
|
27
|
+
MANIFEST = 'installed.json'
|
|
28
|
+
VERSION_PATTERN = /\A(\d+)\.(\d+)\.(\d+)(?:fx(\d+)(?:a(\d+))?)?(-jakarta)?(-SNAPSHOT)?\z/
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------------- versions
|
|
33
|
+
|
|
34
|
+
# An ordering key for a PD4ML version (4.1.1, 4.0.23fx2, 4.0.23fx1a1, 4.1.2-SNAPSHOT), or nil
|
|
35
|
+
# for an unknown form. A snapshot sorts below its release, an fxNaM pre-release below fxN.
|
|
36
|
+
def version_key(version)
|
|
37
|
+
m = VERSION_PATTERN.match(version)
|
|
38
|
+
return nil unless m
|
|
39
|
+
|
|
40
|
+
[m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, m[5] ? 0 : 1, m[5].to_i, m[7] ? 0 : 1]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Versions sorted oldest first (unknown forms dropped).
|
|
44
|
+
def sort_versions(versions) = versions.select { |v| version_key(v) }.sort_by { |v| version_key(v) }
|
|
45
|
+
|
|
46
|
+
# The most recent `major`.* version: releases only unless `snapshots`, -jakarta only with `jakarta`.
|
|
47
|
+
def select_version(versions, major: 4, snapshots: false, jakarta: false)
|
|
48
|
+
candidates = versions.select do |v|
|
|
49
|
+
k = version_key(v)
|
|
50
|
+
k && k[0] == major && (snapshots || !v.end_with?('-SNAPSHOT')) && v.include?('-jakarta') == jakarta
|
|
51
|
+
end
|
|
52
|
+
raise InstallError, "no #{major}.* version in the repository" if candidates.empty?
|
|
53
|
+
|
|
54
|
+
sort_versions(candidates).last
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The versions the repository lists.
|
|
58
|
+
def available_versions(repository = REPOSITORY)
|
|
59
|
+
fetch("#{repository}/#{GROUP_PATH}/maven-metadata.xml").scan(%r{<version>([^<]+)</version>}).flatten.map(&:strip)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------------------ install
|
|
63
|
+
|
|
64
|
+
# Downloads a PD4ML build (unless already there) and makes it the default one; returns the
|
|
65
|
+
# jar's path. version: 'latest' (the most recent 4.* release; with snapshots: true, the most
|
|
66
|
+
# recent 4.* build) or an exact one such as '4.0.23fx2'.
|
|
67
|
+
def install(version = 'latest', snapshots: false, jakarta: false, repository: REPOSITORY, home: nil,
|
|
68
|
+
force: false, quiet: false, bouncycastle: true)
|
|
69
|
+
home ||= default_home
|
|
70
|
+
version = select_version(available_versions(repository), snapshots: snapshots, jakarta: jakarta) if version == 'latest'
|
|
71
|
+
name = jar_name(version, repository)
|
|
72
|
+
jar = File.join(home, name)
|
|
73
|
+
download("#{repository}/#{GROUP_PATH}/#{version}/#{name}", jar, force, quiet)
|
|
74
|
+
libs = []
|
|
75
|
+
if bouncycastle
|
|
76
|
+
BOUNCYCASTLE.each do |artifact|
|
|
77
|
+
lib = "#{artifact}-#{BOUNCYCASTLE_VERSION}.jar"
|
|
78
|
+
download("#{CENTRAL}/org/bouncycastle/#{artifact}/#{BOUNCYCASTLE_VERSION}/#{lib}", File.join(home, 'lib', lib),
|
|
79
|
+
force, quiet)
|
|
80
|
+
libs << "lib/#{lib}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
manifest = read_manifest(home).merge('version' => version, 'jar' => name, 'libs' => libs,
|
|
84
|
+
'repository' => repository, 'installed' => Time.now.utc.iso8601)
|
|
85
|
+
tmp = File.join(home, "#{MANIFEST}.#{Process.pid}.part") # several processes may install at once
|
|
86
|
+
File.write(tmp, JSON.pretty_generate(manifest))
|
|
87
|
+
File.rename(tmp, File.join(home, MANIFEST))
|
|
88
|
+
warn "PD4ML #{version}: #{jar}" unless quiet
|
|
89
|
+
jar
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Where downloaded builds go: PD4ML_HOME, else the user's cache directory.
|
|
93
|
+
def default_home
|
|
94
|
+
return ENV['PD4ML_HOME'] if ENV['PD4ML_HOME'].to_s != ''
|
|
95
|
+
|
|
96
|
+
if Gem.win_platform?
|
|
97
|
+
File.join(ENV['LOCALAPPDATA'] || File.join(Dir.home, 'AppData', 'Local'), 'pd4ml-ruby')
|
|
98
|
+
else
|
|
99
|
+
File.join(ENV['XDG_CACHE_HOME'].to_s.empty? ? File.join(Dir.home, '.cache') : ENV['XDG_CACHE_HOME'], 'pd4ml-ruby')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The jar install made the default, or nil.
|
|
104
|
+
def installed_jar(home = default_home)
|
|
105
|
+
jar = read_manifest(home)['jar']
|
|
106
|
+
path = jar && File.join(home, jar)
|
|
107
|
+
path && File.exist?(path) ? path : nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The installed build's classpath: the PD4ML jar and its companion jars; empty if none.
|
|
111
|
+
def installed_classpath(home = default_home)
|
|
112
|
+
jar = installed_jar(home)
|
|
113
|
+
return [] unless jar
|
|
114
|
+
|
|
115
|
+
[jar, *Array(read_manifest(home)['libs']).map { |l| File.join(home, l) }.select { |p| File.exist?(p) }]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The version install made the default, or nil.
|
|
119
|
+
def installed_version(home = default_home)
|
|
120
|
+
installed_jar(home) ? read_manifest(home)['version'] : nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Whether the first use of PD4ML may download a build (PD4ML_AUTO_INSTALL, default on).
|
|
124
|
+
def auto_install?
|
|
125
|
+
!%w[0 false no off].include?(ENV.fetch('PD4ML_AUTO_INSTALL', '1').strip.downcase)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------- internals
|
|
129
|
+
|
|
130
|
+
def jar_name(version, repository)
|
|
131
|
+
return "pd4ml-#{version}.jar" unless version.end_with?('-SNAPSHOT')
|
|
132
|
+
|
|
133
|
+
xml = fetch("#{repository}/#{GROUP_PATH}/#{version}/maven-metadata.xml")
|
|
134
|
+
xml.scan(%r{<snapshotVersion>(.*?)</snapshotVersion>}m).flatten.each do |sv|
|
|
135
|
+
tag = ->(t) { sv[%r{<#{t}>([^<]*)</#{t}>}, 1] }
|
|
136
|
+
return "pd4ml-#{tag.call('value')}.jar" if tag.call('extension') == 'jar' && tag.call('classifier').to_s.empty? && tag.call('value')
|
|
137
|
+
end
|
|
138
|
+
ts = xml[%r{<timestamp>([^<]+)</timestamp>}, 1]
|
|
139
|
+
build = xml[%r{<buildNumber>([^<]+)</buildNumber>}, 1]
|
|
140
|
+
return "pd4ml-#{version.delete_suffix('-SNAPSHOT')}-#{ts}-#{build}.jar" if ts && build
|
|
141
|
+
|
|
142
|
+
raise InstallError, "no jar listed for #{version}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def download(url, path, force, quiet)
|
|
146
|
+
return if !force && File.exist?(path)
|
|
147
|
+
|
|
148
|
+
warn "downloading #{url}" unless quiet
|
|
149
|
+
data = fetch(url)
|
|
150
|
+
expected = fetch("#{url}.sha1").strip.split.first.to_s.downcase
|
|
151
|
+
actual = Digest::SHA1.hexdigest(data)
|
|
152
|
+
raise InstallError, "checksum mismatch for #{url}: #{actual} != #{expected}" if !expected.empty? && actual != expected
|
|
153
|
+
|
|
154
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
155
|
+
tmp = "#{path}.#{Process.pid}.part"
|
|
156
|
+
File.binwrite(tmp, data)
|
|
157
|
+
File.rename(tmp, path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def fetch(url, redirects = 5)
|
|
161
|
+
uri = URI(url)
|
|
162
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 300) do |http|
|
|
163
|
+
http.get(uri.request_uri, 'User-Agent' => 'pd4ml-ruby')
|
|
164
|
+
end
|
|
165
|
+
case response
|
|
166
|
+
when Net::HTTPSuccess then response.body.b
|
|
167
|
+
when Net::HTTPRedirection
|
|
168
|
+
raise InstallError, "too many redirects: #{url}" if redirects.zero?
|
|
169
|
+
|
|
170
|
+
fetch(URI.join(url, response['location']).to_s, redirects - 1)
|
|
171
|
+
else raise InstallError, "cannot download #{url} (HTTP #{response.code})"
|
|
172
|
+
end
|
|
173
|
+
rescue SocketError, SystemCallError, Timeout::Error, OpenSSL::SSL::SSLError => e
|
|
174
|
+
raise InstallError, "cannot download #{url}: #{e.message}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read_manifest(home)
|
|
178
|
+
JSON.parse(File.read(File.join(home, MANIFEST)))
|
|
179
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
180
|
+
{}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/pd4ml/java.rb
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pd4ml
|
|
4
|
+
# Converts a Ruby-style method name to the Java one: set_page_size -> setPageSize,
|
|
5
|
+
# demo_mode? -> isDemoMode. Names without underscores (Java names) pass through.
|
|
6
|
+
module Naming
|
|
7
|
+
@methods = {}
|
|
8
|
+
@lock = Mutex.new
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# The Java method `name` (Ruby-style or Java) means on the class: use_ttf -> useTTF,
|
|
13
|
+
# demo_mode? -> isDemoMode, read_html -> readHTML. Ruby-style names are matched against the
|
|
14
|
+
# class's real method names ignoring case and underscores, so acronyms resolve too.
|
|
15
|
+
def resolve(bridge, java_class, name, static)
|
|
16
|
+
n = name.to_s
|
|
17
|
+
return n unless n.include?('_') || n.end_with?('?')
|
|
18
|
+
|
|
19
|
+
key = (n.end_with?('?') ? "is#{n[0..-2]}" : n).delete('_').downcase
|
|
20
|
+
table = @lock.synchronize do
|
|
21
|
+
@methods[[bridge.object_id, java_class, static]] ||= begin
|
|
22
|
+
info = bridge.describe(java_class)
|
|
23
|
+
names = static ? info['methods'] : Array(info['instanceMethods']) + Array(info['methods'])
|
|
24
|
+
names.to_h { |m| [m.downcase, m] }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
table[key] || java_name(n)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def java_name(name)
|
|
31
|
+
n = name.to_s
|
|
32
|
+
if n.end_with?('?')
|
|
33
|
+
base = java_name(n[0..-2])
|
|
34
|
+
return "is#{base[0].upcase}#{base[1..]}"
|
|
35
|
+
end
|
|
36
|
+
return n unless n.include?('_') && n == n.downcase
|
|
37
|
+
|
|
38
|
+
parts = n.split('_').reject(&:empty?)
|
|
39
|
+
parts.first + parts.drop(1).map(&:capitalize).join
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A Java object living in the bridge. Call its Java methods by their Java names or in Ruby
|
|
44
|
+
# style (`size.rotate`, `pd.set_page_size(...)`, `pd.demo_mode?`). Public fields: `obj[:width]`
|
|
45
|
+
# / `obj[:width] = v`. Released in Java when garbage collected.
|
|
46
|
+
class JavaObject < BasicObject
|
|
47
|
+
def initialize(bridge, handle, java_class, enum_name)
|
|
48
|
+
@bridge = bridge
|
|
49
|
+
@handle = handle
|
|
50
|
+
@java_class = java_class
|
|
51
|
+
@enum_name = enum_name
|
|
52
|
+
::ObjectSpace.define_finalizer(self, JavaObject.releaser(bridge, handle))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.releaser(bridge, handle)
|
|
56
|
+
proc { bridge.release(handle) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# internal: the bridge handle
|
|
60
|
+
def __handle = @handle
|
|
61
|
+
|
|
62
|
+
# The Java class name ('com.pd4ml.PageSize').
|
|
63
|
+
def java_class = @java_class
|
|
64
|
+
|
|
65
|
+
# For an enum constant, its name ('v42', 'MM'); else nil.
|
|
66
|
+
def enum_name = @enum_name
|
|
67
|
+
|
|
68
|
+
# Reads a public field.
|
|
69
|
+
def [](field) = @bridge.get_field(self, field.to_s)
|
|
70
|
+
|
|
71
|
+
# Writes a public field.
|
|
72
|
+
def []=(field, value)
|
|
73
|
+
@bridge.set_field(self, field.to_s, value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Calls a Java method by its exact Java name.
|
|
77
|
+
def call_java(method, *args) = @bridge.call(self, method.to_s, args)
|
|
78
|
+
|
|
79
|
+
# Whether the object is an instance of the Java class or interface.
|
|
80
|
+
def instance_of_java?(name) = @bridge.instance_of?(self, name)
|
|
81
|
+
|
|
82
|
+
# Java toString().
|
|
83
|
+
def to_s = @bridge.call(self, 'toString', []).to_s
|
|
84
|
+
alias to_str to_s
|
|
85
|
+
|
|
86
|
+
def inspect = "#<Java #{@java_class} #{@enum_name || to_s}>"
|
|
87
|
+
|
|
88
|
+
# Java equals() (and the same object).
|
|
89
|
+
def ==(other)
|
|
90
|
+
equal?(other) || (JavaObject === other && @bridge.call(self, 'equals', [other]) == true)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def hash = @bridge.call(self, 'hashCode', [])
|
|
94
|
+
|
|
95
|
+
def eql?(other) = equal?(other)
|
|
96
|
+
|
|
97
|
+
def is_a?(klass) = klass == JavaObject || klass == ::BasicObject
|
|
98
|
+
alias kind_of? is_a?
|
|
99
|
+
|
|
100
|
+
def class = JavaObject
|
|
101
|
+
|
|
102
|
+
def nil? = false
|
|
103
|
+
|
|
104
|
+
def respond_to?(name, _include_all = false)
|
|
105
|
+
%i[java_class enum_name to_s inspect [] []= call_java instance_of_java?].include?(name.to_sym) ||
|
|
106
|
+
!name.to_s.end_with?('=')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def method_missing(name, *args, &block)
|
|
110
|
+
::Kernel.raise ::NoMethodError, "set Java fields with obj[:#{name.to_s.chomp('=')}] = value" if name.to_s.end_with?('=')
|
|
111
|
+
::Kernel.raise ::ArgumentError, 'Java methods take no block' if block
|
|
112
|
+
|
|
113
|
+
@bridge.call(self, Naming.resolve(@bridge, @java_class, name, false), args)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def respond_to_missing?(_name, _include_private = false) = true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A Java class as a Ruby class: constants and static fields (`PageSize::A4`, `PageSize.A4`,
|
|
120
|
+
# `LayoutEngine.v42`), static methods (`PdfSpec.get_instance('pdf/a-2b')`), constructors
|
|
121
|
+
# (`PageMargins.new(10, 10, 10, 10, Units::MM)`).
|
|
122
|
+
class JavaClass
|
|
123
|
+
class << self
|
|
124
|
+
attr_reader :java_name
|
|
125
|
+
|
|
126
|
+
def new(*args) = Bridge.get.new_object(@java_name, args)
|
|
127
|
+
|
|
128
|
+
def to_s = @java_name
|
|
129
|
+
alias inspect to_s
|
|
130
|
+
|
|
131
|
+
# Reads a static field.
|
|
132
|
+
def [](field) = Bridge.get.get_field(@java_name, field.to_s)
|
|
133
|
+
|
|
134
|
+
# Writes a static field.
|
|
135
|
+
def []=(field, value)
|
|
136
|
+
Bridge.get.set_field(@java_name, field.to_s, value)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Calls a static method by its exact Java name.
|
|
140
|
+
def call_java(method, *args) = Bridge.get.call_static(@java_name, method.to_s, args)
|
|
141
|
+
|
|
142
|
+
def const_missing(name)
|
|
143
|
+
value_of(name.to_s) { super }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def method_missing(name, *args, &block)
|
|
147
|
+
n = name.to_s
|
|
148
|
+
return super if block || n.end_with?('=', '!')
|
|
149
|
+
|
|
150
|
+
d = description
|
|
151
|
+
if d[:fields].include?(n) && args.empty?
|
|
152
|
+
value_of(n)
|
|
153
|
+
else
|
|
154
|
+
java = Naming.resolve(Bridge.get, @java_name, n, true)
|
|
155
|
+
return super unless d[:methods].include?(java)
|
|
156
|
+
|
|
157
|
+
Bridge.get.call_static(@java_name, java, args)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def respond_to_missing?(name, include_private = false)
|
|
162
|
+
d = description
|
|
163
|
+
d[:fields].include?(name.to_s) || d[:methods].include?(Naming.resolve(Bridge.get, @java_name, name, true)) || super
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# A static field: constants are read once per bridge.
|
|
169
|
+
def value_of(field)
|
|
170
|
+
d = description
|
|
171
|
+
return yield unless d[:fields].include?(field)
|
|
172
|
+
return Bridge.get.get_field(@java_name, field) unless d[:constants].include?(field)
|
|
173
|
+
|
|
174
|
+
d[:values].fetch(field) { d[:values][field] = Bridge.get.get_field(@java_name, field) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def description
|
|
178
|
+
bridge = Bridge.get
|
|
179
|
+
if @description.nil? || !@description[:bridge].equal?(bridge)
|
|
180
|
+
info = bridge.describe(@java_name)
|
|
181
|
+
@description = { bridge: bridge, fields: info['fields'], constants: info['constants'],
|
|
182
|
+
methods: info['methods'], values: {} }
|
|
183
|
+
end
|
|
184
|
+
@description
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Java classes by name, Java interfaces implemented by Ruby blocks.
|
|
190
|
+
module Java
|
|
191
|
+
@classes = {}
|
|
192
|
+
@lock = Mutex.new
|
|
193
|
+
|
|
194
|
+
# A Java class (`Pd4ml::Java.type('java.util.Locale').get_default`); nested classes by '.' or '$'.
|
|
195
|
+
def self.type(name)
|
|
196
|
+
@lock.synchronize do
|
|
197
|
+
@classes[name] ||= Class.new(JavaClass) { @java_name = name }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A Java object implementing the interface, its methods calling the block with
|
|
202
|
+
# (method, args) on this thread while PD4ML runs.
|
|
203
|
+
def self.proxy(iface, &block)
|
|
204
|
+
raise ArgumentError, 'a block is required' unless block
|
|
205
|
+
|
|
206
|
+
Bridge.get.proxy(iface, &block)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
Binary file
|
data/lib/pd4ml/pd4ml.rb
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pd4ml
|
|
4
|
+
# An HTML to PDF/DOCX/RTF/image converter: com.pd4ml.PD4ML with Ruby I/O and the v42 layout
|
|
5
|
+
# engine by default. Every Java method is available by its Java name or in Ruby style
|
|
6
|
+
# (`set_page_size`, `add_style`, `use_ttf`); these take Ruby I/O: read_html (a path, URL or
|
|
7
|
+
# binary String/IO), read_html_string, write_pdf/write_docx/write_rtf (the document returned as
|
|
8
|
+
# a binary String, or written to a path or IO), render_as_images and monitor_progress_with (a block).
|
|
9
|
+
#
|
|
10
|
+
# pd = Pd4ml::PD4ML.new
|
|
11
|
+
# pd.set_page_size(Pd4ml::PageSize::A4)
|
|
12
|
+
# pd.read_html_string('<h1>Hello</h1>')
|
|
13
|
+
# pdf = pd.write_pdf
|
|
14
|
+
class PD4ML
|
|
15
|
+
DEFAULT_ENGINE = 'v42'
|
|
16
|
+
|
|
17
|
+
@warned_no_engines = false
|
|
18
|
+
class << self
|
|
19
|
+
attr_accessor :warned_no_engines
|
|
20
|
+
|
|
21
|
+
# Static Java members: PD4ML.get_version / PD4ML.getVersion.
|
|
22
|
+
def method_missing(name, *args, &block)
|
|
23
|
+
java_class.respond_to?(name) ? java_class.public_send(name, *args, &block) : super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(name, include_private = false)
|
|
27
|
+
java_class.respond_to?(name) || super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def java_class = Java.type('com.pd4ml.PD4ML')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The Java object (com.pd4ml.PD4ML).
|
|
34
|
+
attr_reader :java
|
|
35
|
+
# The layout engine chosen ('v42', 'v40'), or nil for the build's own.
|
|
36
|
+
attr_reader :engine
|
|
37
|
+
|
|
38
|
+
# license: a license serial number or license file URL (default: pd4ml.lic on the classpath
|
|
39
|
+
# or in the working directory, else evaluation mode); html_width: the width in pixels the
|
|
40
|
+
# HTML is laid out in; engine: 'v42', 'v40' (or :v42, a LayoutEngine), default v42 where the
|
|
41
|
+
# build has layout engines, else the build's own; false: the Java default.
|
|
42
|
+
def initialize(license: nil, html_width: nil, engine: nil)
|
|
43
|
+
args = [license, html_width].compact
|
|
44
|
+
@java = Bridge.get.new_object('com.pd4ml.PD4ML', args)
|
|
45
|
+
@engine = nil
|
|
46
|
+
if engine.nil?
|
|
47
|
+
if Pd4ml.layout_engines?
|
|
48
|
+
engine = DEFAULT_ENGINE
|
|
49
|
+
else
|
|
50
|
+
engine = false
|
|
51
|
+
unless self.class.warned_no_engines
|
|
52
|
+
self.class.warned_no_engines = true
|
|
53
|
+
warn "PD4ML #{Pd4ml.version} has no #{DEFAULT_ENGINE} layout engine (from 4.2): its own engine is used"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
use_html_renderer(engine) unless engine == false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Java methods not defined here.
|
|
61
|
+
def method_missing(name, *args, &block)
|
|
62
|
+
return super if block
|
|
63
|
+
|
|
64
|
+
@java.__send__(name, *args)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def respond_to_missing?(_name, _include_private = false) = true
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------------- layout engine
|
|
70
|
+
|
|
71
|
+
# Chooses the layout engine ('v42', 'v40', a LayoutEngine), optionally with the HTML width
|
|
72
|
+
# and engine parameters (a Hash).
|
|
73
|
+
def use_html_renderer(engine = DEFAULT_ENGINE, html_width: nil, params: nil)
|
|
74
|
+
unless Pd4ml.layout_engines?
|
|
75
|
+
raise "PD4ML #{Pd4ml.version} has no layout engines to choose from (Constants.LayoutEngine is new in 4.2)"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
e = engine.is_a?(JavaObject) ? engine : Java.type('com.pd4ml.Constants$LayoutEngine').value_of(engine.to_s)
|
|
79
|
+
if params
|
|
80
|
+
@java.useHtmlRenderer(e, html_width || -1, params.to_h { |k, v| [k.to_s, v.to_s] })
|
|
81
|
+
elsif html_width
|
|
82
|
+
@java.useHtmlRenderer(e, html_width)
|
|
83
|
+
else
|
|
84
|
+
@java.useHtmlRenderer(e)
|
|
85
|
+
end
|
|
86
|
+
@engine = e.enum_name
|
|
87
|
+
end
|
|
88
|
+
alias useHtmlRenderer use_html_renderer
|
|
89
|
+
|
|
90
|
+
# --------------------------------------------------------------------------------- input
|
|
91
|
+
|
|
92
|
+
# Reads the document to convert: a URL ('https://...', 'file:...') or a local path (a String,
|
|
93
|
+
# Pathname or URI), its content (a binary String - File.binread - or an IO), or a Java
|
|
94
|
+
# URL/InputStream. base_url: where relative links resolve, for content (a URL or a local
|
|
95
|
+
# directory/file); encoding: the document's encoding when it does not declare one.
|
|
96
|
+
def read_html(source, base_url = nil, encoding = nil)
|
|
97
|
+
if location?(source)
|
|
98
|
+
url = Pd4ml.to_url(source)
|
|
99
|
+
encoding ? @java.readHTML(url, encoding) : @java.readHTML(url)
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
content = source.is_a?(JavaObject) ? source : Pd4ml.binary(source)
|
|
103
|
+
base = base_url && Pd4ml.to_url(base_url)
|
|
104
|
+
if base_url.nil? && encoding.nil?
|
|
105
|
+
@java.readHTML(content)
|
|
106
|
+
elsif encoding.nil?
|
|
107
|
+
@java.readHTML(content, base)
|
|
108
|
+
else
|
|
109
|
+
@java.readHTML(content, base, encoding)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
alias readHTML read_html
|
|
113
|
+
|
|
114
|
+
# Reads the document from a String of HTML.
|
|
115
|
+
def read_html_string(html, base_url = nil)
|
|
116
|
+
read_html(html.to_s.encode(Encoding::UTF_8).b, base_url, 'UTF-8')
|
|
117
|
+
end
|
|
118
|
+
alias readHtmlString read_html_string
|
|
119
|
+
|
|
120
|
+
# -------------------------------------------------------------------------------- output
|
|
121
|
+
|
|
122
|
+
# Writes the PDF: to `target` (a path or an IO), or returned as a binary String. spec: a
|
|
123
|
+
# PdfSpec (PdfSpec::PDFA_2B), one of its keywords ('pdf/a-2b', see PdfSpec.list_keywords),
|
|
124
|
+
# or 'PDF'/'PDFA'/'PDFUA'.
|
|
125
|
+
def write_pdf(target = nil, spec: nil)
|
|
126
|
+
s = Pd4ml.pdf_spec(spec)
|
|
127
|
+
outs = []
|
|
128
|
+
Bridge.get.call(@java, 'writePDF', s.nil? ? [Output.new(0)] : [Output.new(0), s], outs)
|
|
129
|
+
Pd4ml.deliver(outs[0] || ''.b, target)
|
|
130
|
+
end
|
|
131
|
+
alias writePDF write_pdf
|
|
132
|
+
|
|
133
|
+
# Writes a DOCX document (see write_pdf for `target`).
|
|
134
|
+
def write_docx(target = nil)
|
|
135
|
+
outs = []
|
|
136
|
+
Bridge.get.call(@java, 'writeDOCX', [Output.new(0)], outs)
|
|
137
|
+
Pd4ml.deliver(outs[0] || ''.b, target)
|
|
138
|
+
end
|
|
139
|
+
alias writeDOCX write_docx
|
|
140
|
+
|
|
141
|
+
# Writes an RTF document; rtf_wmf: images as WMF (see write_pdf for `target`).
|
|
142
|
+
def write_rtf(target = nil, rtf_wmf: false)
|
|
143
|
+
outs = []
|
|
144
|
+
Bridge.get.call(@java, 'writeRTF', [Output.new(0), rtf_wmf], outs)
|
|
145
|
+
Pd4ml.deliver(outs[0] || ''.b, target)
|
|
146
|
+
end
|
|
147
|
+
alias writeRTF write_rtf
|
|
148
|
+
|
|
149
|
+
# render_as_images(format): the pages as encoded images (Constants::PNG24...), binary Strings;
|
|
150
|
+
# render_as_images(dir, name, format): image files written to a directory;
|
|
151
|
+
# render_as_images: Java BufferedImage objects.
|
|
152
|
+
def render_as_images(*args)
|
|
153
|
+
if args.size == 3 && (args[0].is_a?(String) || args[0].is_a?(Pathname))
|
|
154
|
+
FileUtils.mkdir_p(args[0].to_s)
|
|
155
|
+
return @java.renderAsImages(Java.type('java.io.File').new(File.expand_path(args[0].to_s)), args[1], args[2])
|
|
156
|
+
end
|
|
157
|
+
@java.renderAsImages(*args)
|
|
158
|
+
end
|
|
159
|
+
alias renderAsImages render_as_images
|
|
160
|
+
|
|
161
|
+
# The pages as 24-bit PNG images (binary Strings).
|
|
162
|
+
def render_png = @java.renderAsImages(Java.type('com.pd4ml.Constants')::PNG24)
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------------------ progress
|
|
165
|
+
|
|
166
|
+
# Reports conversion progress to a block |message_id, progress, note, msec| - it runs on this
|
|
167
|
+
# thread while PD4ML works - or to a Java ProgressListener.
|
|
168
|
+
def monitor_progress_with(listener = nil, &block)
|
|
169
|
+
listener ||= Java.proxy('com.pd4ml.ProgressListener') do |_method, a|
|
|
170
|
+
block.call(a[0].to_i, a[1].to_i, a[2], a[3].to_i)
|
|
171
|
+
end
|
|
172
|
+
@java.monitorProgressWith(listener)
|
|
173
|
+
end
|
|
174
|
+
alias monitorProgressWith monitor_progress_with
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def location?(source)
|
|
179
|
+
source.is_a?(Pathname) || source.is_a?(URI::Generic) ||
|
|
180
|
+
(source.is_a?(String) && source.encoding != Encoding::BINARY && !source.lstrip.start_with?('<')) ||
|
|
181
|
+
(source.is_a?(JavaObject) && source.java_class == 'java.net.URL')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|