vectory 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +1 -1
- data/.github/workflows/release.yml +24 -0
- data/.gitignore +4 -0
- data/Gemfile +3 -1
- data/README.adoc +307 -1
- data/Rakefile +4 -0
- data/bin/vectory +9 -0
- data/emf.emf +1 -0
- data/lib/vectory/cli.rb +140 -0
- data/lib/vectory/datauri.rb +48 -0
- data/lib/vectory/emf.rb +31 -0
- data/lib/vectory/eps.rb +31 -0
- data/lib/vectory/file_magic.rb +53 -0
- data/lib/vectory/image.rb +27 -0
- data/lib/vectory/inkscape_converter.rb +99 -0
- data/lib/vectory/ps.rb +25 -0
- data/lib/vectory/svg.rb +110 -0
- data/lib/vectory/svg_mapping.rb +118 -0
- data/lib/vectory/system_call.rb +58 -0
- data/lib/vectory/utils.rb +165 -0
- data/lib/vectory/vector.rb +75 -0
- data/lib/vectory/version.rb +1 -1
- data/lib/vectory.rb +35 -1
- data/spec/examples/emf2eps/img.emf +0 -0
- data/spec/examples/emf2eps/img.emf.datauri +1 -0
- data/spec/examples/emf2eps/ref.eps +88 -0
- data/spec/examples/emf2ps/img.emf +0 -0
- data/spec/examples/emf2ps/ref.ps +125 -0
- data/spec/examples/emf2svg/img.emf +0 -0
- data/spec/examples/emf2svg/ref.svg +9 -0
- data/spec/examples/eps2emf/img.eps +199 -0
- data/spec/examples/eps2emf/img.eps.datauri +1 -0
- data/spec/examples/eps2emf/ref.emf +0 -0
- data/spec/examples/eps2ps/img.eps +199 -0
- data/spec/examples/eps2ps/ref.ps +549 -0
- data/spec/examples/eps2svg/img.eps +199 -0
- data/spec/examples/eps2svg/ref.svg +173 -0
- data/spec/examples/eps_but_svg_extension.svg +199 -0
- data/spec/examples/img.jpg +0 -0
- data/spec/examples/ps2emf/img.ps +549 -0
- data/spec/examples/ps2emf/img.ps.datauri +1 -0
- data/spec/examples/ps2emf/ref.emf +0 -0
- data/spec/examples/ps2eps/img.ps +549 -0
- data/spec/examples/ps2eps/ref.eps +844 -0
- data/spec/examples/ps2svg/img.ps +549 -0
- data/spec/examples/ps2svg/ref.svg +476 -0
- data/spec/examples/svg/action_schemaexpg1.svg +124 -0
- data/spec/examples/svg/action_schemaexpg2.svg +124 -0
- data/spec/examples/svg/doc-ref.xml +109 -0
- data/spec/examples/svg/doc.xml +51 -0
- data/spec/examples/svg/doc2-ref.xml +59 -0
- data/spec/examples/svg/doc2.xml +28 -0
- data/spec/examples/svg2emf/img.svg +1 -0
- data/spec/examples/svg2emf/img.svg.datauri +1 -0
- data/spec/examples/svg2emf/ref.emf +0 -0
- data/spec/examples/svg2eps/img.svg +1 -0
- data/spec/examples/svg2eps/ref.eps +88 -0
- data/spec/examples/svg2ps/img.svg +1 -0
- data/spec/examples/svg2ps/ref.ps +125 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers.rb +39 -0
- data/spec/support/text_matcher.rb +63 -0
- data/spec/support/vectory_helper.rb +31 -0
- data/spec/vectory/cli_spec.rb +214 -0
- data/spec/vectory/datauri_spec.rb +101 -0
- data/spec/vectory/emf_spec.rb +38 -0
- data/spec/vectory/eps_spec.rb +40 -0
- data/spec/vectory/file_magic_spec.rb +24 -0
- data/spec/vectory/inkscape_converter_spec.rb +43 -0
- data/spec/vectory/ps_spec.rb +33 -0
- data/spec/vectory/svg_mapping_spec.rb +42 -0
- data/spec/vectory/svg_spec.rb +41 -0
- data/spec/vectory/vector_spec.rb +60 -0
- data/tmp/.keep +0 -0
- data/vectory.gemspec +6 -3
- metadata +116 -21
@@ -0,0 +1,53 @@
|
|
1
|
+
module Vectory
|
2
|
+
class FileMagic
|
3
|
+
EPS_30_MAGIC = "\x25\x21\x50\x53\x2d\x41\x64\x6f\x62\x65\x2d\x33\x2e\x30" \
|
4
|
+
"\x20\x45\x50\x53\x46\x2d\x33\x2e\x30"
|
5
|
+
.force_encoding("BINARY") # "%!PS-Adobe-3.0 EPSF-3.0"
|
6
|
+
|
7
|
+
EPS_31_MAGIC = "\x25\x21\x50\x53\x2d\x41\x64\x6f\x62\x65\x2d\x33\x2e\x31" \
|
8
|
+
"\x20\x45\x50\x53\x46\x2d\x33\x2e\x30"
|
9
|
+
.force_encoding("BINARY") # "%!PS-Adobe-3.1 EPSF-3.0"
|
10
|
+
|
11
|
+
PS_MAGIC = "\x25\x21\x50\x53\x2d\x41\x64\x6f\x62\x65\x2d\x33\x2e\x30"
|
12
|
+
.force_encoding("BINARY") # "%!PS-Adobe-3.0"
|
13
|
+
|
14
|
+
EMF_MAGIC = "\x01\x00\x00\x00".force_encoding("BINARY")
|
15
|
+
|
16
|
+
def self.detect(path)
|
17
|
+
new(path).detect
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(path)
|
21
|
+
@path = path
|
22
|
+
end
|
23
|
+
|
24
|
+
def detect
|
25
|
+
beginning = File.read(@path, 100, mode: "rb")
|
26
|
+
|
27
|
+
eps_slice = beginning.byteslice(0, EPS_30_MAGIC.size)
|
28
|
+
Vectory.ui.debug("File magic is '#{to_bytes(beginning)}' of '#{@path}'.")
|
29
|
+
|
30
|
+
return :eps if [EPS_30_MAGIC, EPS_31_MAGIC].include?(eps_slice)
|
31
|
+
|
32
|
+
ps_slice = beginning.byteslice(0, PS_MAGIC.size)
|
33
|
+
return :ps if ps_slice == PS_MAGIC
|
34
|
+
|
35
|
+
emf_slice = beginning.byteslice(0, EMF_MAGIC.size)
|
36
|
+
return :emf if emf_slice == EMF_MAGIC
|
37
|
+
|
38
|
+
return :svg if contain_svg_tag?
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def to_bytes(str)
|
44
|
+
str.unpack("c*").map { |e| "\\x#{e.to_s(16).rjust(2, '0')}" }.join
|
45
|
+
end
|
46
|
+
|
47
|
+
def contain_svg_tag?
|
48
|
+
content = File.read(@path, 4096)
|
49
|
+
|
50
|
+
return :svg if content.include?("<svg")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
module Vectory
|
6
|
+
class Image
|
7
|
+
class << self
|
8
|
+
def from_path(path)
|
9
|
+
new(File.read(path, mode: "rb"))
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_content(content)
|
13
|
+
new(content)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :content
|
18
|
+
|
19
|
+
def initialize(content)
|
20
|
+
@content = content
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_writer :content
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
require_relative "system_call"
|
5
|
+
|
6
|
+
module Vectory
|
7
|
+
class InkscapeConverter
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def self.convert(uri, output_extension, option)
|
11
|
+
instance.convert(uri, output_extension, option)
|
12
|
+
end
|
13
|
+
|
14
|
+
def convert(uri, output_extension, option)
|
15
|
+
exe = inkscape_path_or_raise_error(uri)
|
16
|
+
uri = external_path uri
|
17
|
+
exe = external_path exe
|
18
|
+
cmd = %(#{exe} #{option} #{uri})
|
19
|
+
|
20
|
+
call = SystemCall.new(cmd).call
|
21
|
+
|
22
|
+
output_path = find_output(uri, output_extension)
|
23
|
+
raise_conversion_error(call) unless output_path
|
24
|
+
|
25
|
+
# and return Vectory::Utils::datauri(file)
|
26
|
+
|
27
|
+
output_path
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def inkscape_path_or_raise_error(path)
|
33
|
+
inkscape_path or raise(InkscapeNotFoundError,
|
34
|
+
"Inkscape missing in PATH, unable to " \
|
35
|
+
"convert image #{path}. Aborting.")
|
36
|
+
end
|
37
|
+
|
38
|
+
def inkscape_path
|
39
|
+
@inkscape_path ||= find_inkscape
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_inkscape
|
43
|
+
cmds.each do |cmd|
|
44
|
+
extensions.each do |ext|
|
45
|
+
paths.each do |path|
|
46
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
47
|
+
|
48
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def cmds
|
57
|
+
["inkscapecom", "inkscape"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def extensions
|
61
|
+
ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
62
|
+
end
|
63
|
+
|
64
|
+
def paths
|
65
|
+
ENV["PATH"].split(File::PATH_SEPARATOR)
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_output(source_path, output_extension)
|
69
|
+
basenames = [File.basename(source_path, ".*"),
|
70
|
+
File.basename(source_path)]
|
71
|
+
|
72
|
+
paths = basenames.map do |basename|
|
73
|
+
"#{File.join(File.dirname(source_path), basename)}.#{output_extension}"
|
74
|
+
end
|
75
|
+
|
76
|
+
paths.find { |p| File.exist?(p) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def raise_conversion_error(call)
|
80
|
+
raise Vectory::ConversionError,
|
81
|
+
"Could not convert with Inkscape. " \
|
82
|
+
"Inkscape cmd: '#{call.cmd}',\n" \
|
83
|
+
"status: '#{call.status}',\n" \
|
84
|
+
"stdout: '#{call.stdout.strip}',\n" \
|
85
|
+
"stderr: '#{call.stderr.strip}'."
|
86
|
+
end
|
87
|
+
|
88
|
+
def external_path(path)
|
89
|
+
win = !!((RUBY_PLATFORM =~ /(win|w)(32|64)$/) ||
|
90
|
+
(RUBY_PLATFORM =~ /mswin|mingw/))
|
91
|
+
if win
|
92
|
+
path.gsub!(%{/}, "\\")
|
93
|
+
path[/\s/] ? "\"#{path}\"" : path
|
94
|
+
else
|
95
|
+
path
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/vectory/ps.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vectory
|
4
|
+
class Ps < Vector
|
5
|
+
def self.default_extension
|
6
|
+
"ps"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.mimetype
|
10
|
+
"application/postscript"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_eps
|
14
|
+
convert_with_inkscape("--export-type=eps", Eps)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_emf
|
18
|
+
convert_with_inkscape("--export-type=emf", Emf)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_svg
|
22
|
+
convert_with_inkscape("--export-type=svg", Svg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/vectory/svg.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
module Vectory
|
6
|
+
class Svg < Vector
|
7
|
+
SVG_NS = "http://www.w3.org/2000/svg"
|
8
|
+
|
9
|
+
def self.default_extension
|
10
|
+
"svg"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.mimetype
|
14
|
+
"image/svg+xml"
|
15
|
+
end
|
16
|
+
|
17
|
+
def content
|
18
|
+
@document&.to_xml || @content
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_emf
|
22
|
+
convert_with_inkscape("--export-type=emf", Emf)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_eps
|
26
|
+
convert_with_inkscape("--export-type=eps", Eps)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_ps
|
30
|
+
convert_with_inkscape("--export-type=ps", Ps)
|
31
|
+
end
|
32
|
+
|
33
|
+
def namespace(suffix, links, xpath_to_remove)
|
34
|
+
remap_links(links)
|
35
|
+
suffix_ids(suffix)
|
36
|
+
remove_xpath(xpath_to_remove)
|
37
|
+
end
|
38
|
+
|
39
|
+
def remap_links(map)
|
40
|
+
document.xpath(".//m:a", "m" => SVG_NS).each do |a|
|
41
|
+
href_attrs = ["xlink:href", "href"]
|
42
|
+
href_attrs.each do |p|
|
43
|
+
a[p] and x = map[File.expand_path(a[p])] and a[p] = x
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def suffix_ids(suffix)
|
51
|
+
ids = collect_ids
|
52
|
+
return if ids.empty?
|
53
|
+
|
54
|
+
update_ids_attrs(ids, suffix)
|
55
|
+
update_ids_css(ids, suffix)
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_xpath(xpath)
|
61
|
+
document.xpath(xpath).remove
|
62
|
+
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def content=(content)
|
69
|
+
if @document
|
70
|
+
@document = Nokogiri::XML(content)
|
71
|
+
else
|
72
|
+
@content = content
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def document
|
77
|
+
@document ||= begin
|
78
|
+
doc = Nokogiri::XML(@content)
|
79
|
+
@content = nil
|
80
|
+
doc
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def collect_ids
|
85
|
+
document.xpath("./@id | .//@id").map(&:value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def update_ids_attrs(ids, suffix)
|
89
|
+
document.xpath(". | .//*[@*]").each do |a|
|
90
|
+
a.attribute_nodes.each do |x|
|
91
|
+
ids.include?(x.value) and x.value += sprintf("_%09d", suffix)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def update_ids_css(ids, suffix)
|
97
|
+
document.xpath("//m:style", "m" => SVG_NS).each do |s|
|
98
|
+
c = s.children.to_xml
|
99
|
+
ids.each do |i|
|
100
|
+
c = c.gsub(%r[##{i}\b],
|
101
|
+
sprintf("#%<id>s_%<suffix>09d", id: i, suffix: suffix))
|
102
|
+
.gsub(%r(\[id\s*=\s*['"]?#{i}['"]?\]),
|
103
|
+
sprintf("[id='%<id>s_%<suffix>09d']", id: i, suffix: suffix))
|
104
|
+
end
|
105
|
+
|
106
|
+
s.children = c
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require_relative "svg"
|
2
|
+
|
3
|
+
module Vectory
|
4
|
+
class SvgMapping
|
5
|
+
class Namespace
|
6
|
+
def initialize(xmldoc)
|
7
|
+
@namespace = xmldoc.root.namespace
|
8
|
+
end
|
9
|
+
|
10
|
+
def ns(path)
|
11
|
+
return path if @namespace.nil?
|
12
|
+
|
13
|
+
path.gsub(%r{/([a-zA-z])}, "/xmlns:\\1")
|
14
|
+
.gsub(%r{::([a-zA-z])}, "::xmlns:\\1")
|
15
|
+
.gsub(%r{\[([a-zA-z][a-z0-9A-Z@/]* ?=)}, "[xmlns:\\1")
|
16
|
+
.gsub(%r{\[([a-zA-z][a-z0-9A-Z@/]*\])}, "[xmlns:\\1")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
SVG_NS = "http://www.w3.org/2000/svg".freeze
|
21
|
+
PROCESSING_XPATH =
|
22
|
+
"processing-instruction()|.//processing-instruction()".freeze
|
23
|
+
|
24
|
+
def self.from_path(path)
|
25
|
+
new(File.read(path))
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(xml, local_directory = "")
|
29
|
+
@xml = xml
|
30
|
+
@local_directory = local_directory
|
31
|
+
end
|
32
|
+
|
33
|
+
def call
|
34
|
+
xmldoc = Nokogiri::XML(@xml)
|
35
|
+
@namespace = Namespace.new(xmldoc)
|
36
|
+
|
37
|
+
xmldoc.xpath(@namespace.ns("//svgmap")).each_with_index do |svgmap, index|
|
38
|
+
process_svgmap(svgmap, index)
|
39
|
+
end
|
40
|
+
|
41
|
+
xmldoc.to_xml
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def process_svgmap(svgmap, suffix)
|
47
|
+
image = extract_image_tag(svgmap)
|
48
|
+
return unless image
|
49
|
+
|
50
|
+
content = generate_content(image, svgmap, suffix)
|
51
|
+
return unless content
|
52
|
+
|
53
|
+
image.replace(content)
|
54
|
+
|
55
|
+
simplify_svgmap(svgmap)
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_image_tag(svgmap)
|
59
|
+
image = svgmap.at(@namespace.ns(".//image"))
|
60
|
+
return image if image && image["src"] && !image["src"].empty?
|
61
|
+
|
62
|
+
svgmap.at(".//m:svg", "m" => SVG_NS)
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_content(image, svgmap, suffix)
|
66
|
+
vector = build_vector(image)
|
67
|
+
return unless vector
|
68
|
+
|
69
|
+
links_map = from_targets_to_links_map(svgmap)
|
70
|
+
vector.namespace(suffix, links_map, PROCESSING_XPATH)
|
71
|
+
|
72
|
+
vector.content
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_vector(image)
|
76
|
+
return Vectory::Svg.from_content(image.to_xml) if image.name == "svg"
|
77
|
+
|
78
|
+
return unless image.name == "image"
|
79
|
+
|
80
|
+
src = image["src"]
|
81
|
+
return Vectory::Datauri.new(src).to_vector if /^data:/.match?(src)
|
82
|
+
|
83
|
+
path = @local_directory.empty? ? src : File.join(@local_directory, src)
|
84
|
+
return unless File.exist?(path)
|
85
|
+
|
86
|
+
Vectory::Svg.from_path(path)
|
87
|
+
end
|
88
|
+
|
89
|
+
def from_targets_to_links_map(svgmap)
|
90
|
+
targets = svgmap.xpath(@namespace.ns("./target"))
|
91
|
+
targets.each_with_object({}) do |target_tag, m|
|
92
|
+
target = link_target(target_tag)
|
93
|
+
next unless target
|
94
|
+
|
95
|
+
href = File.expand_path(target_tag["href"])
|
96
|
+
m[href] = target
|
97
|
+
|
98
|
+
target_tag.remove
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def link_target(target_tag)
|
103
|
+
xref = target_tag.at(@namespace.ns("./xref"))
|
104
|
+
return "##{xref['target']}" if xref
|
105
|
+
|
106
|
+
link = target_tag.at(@namespace.ns("./link"))
|
107
|
+
return unless link
|
108
|
+
|
109
|
+
link["target"]
|
110
|
+
end
|
111
|
+
|
112
|
+
def simplify_svgmap(svgmap)
|
113
|
+
return if svgmap.at(@namespace.ns("./target/eref"))
|
114
|
+
|
115
|
+
svgmap.replace(svgmap.at(@namespace.ns("./figure")))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module Vectory
|
4
|
+
class SystemCall
|
5
|
+
TIMEOUT = 60
|
6
|
+
|
7
|
+
attr_reader :status, :stdout, :stderr, :cmd
|
8
|
+
|
9
|
+
def initialize(cmd, timeout = TIMEOUT)
|
10
|
+
@cmd = cmd
|
11
|
+
@timeout = timeout
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
log_cmd(@cmd)
|
16
|
+
|
17
|
+
execute(@cmd)
|
18
|
+
|
19
|
+
log_result
|
20
|
+
|
21
|
+
raise_error unless @status.success?
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def log_cmd(cmd)
|
29
|
+
Vectory.ui.debug("Cmd: '#{cmd}'")
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute(cmd)
|
33
|
+
result = Utils.capture3_with_timeout(cmd,
|
34
|
+
timeout: @timeout,
|
35
|
+
kill_after: @timeout)
|
36
|
+
Vectory.ui.error(result.inspect)
|
37
|
+
@stdout = result[:stdout]
|
38
|
+
@stderr = result[:stderr]
|
39
|
+
@status = result[:status]
|
40
|
+
rescue Errno::ENOENT => e
|
41
|
+
raise SystemCallError, e.inspect
|
42
|
+
end
|
43
|
+
|
44
|
+
def log_result
|
45
|
+
Vectory.ui.debug("Status: #{@status.inspect}")
|
46
|
+
Vectory.ui.debug("Stdout: '#{@stdout.strip}'")
|
47
|
+
Vectory.ui.debug("Stderr: '#{@stderr.strip}'")
|
48
|
+
end
|
49
|
+
|
50
|
+
def raise_error
|
51
|
+
raise SystemCallError,
|
52
|
+
"Failed to run #{@cmd},\n " \
|
53
|
+
"status: #{@status.exitstatus},\n " \
|
54
|
+
"stdout: '#{@stdout.strip}',\n " \
|
55
|
+
"stderr: '#{@stderr.strip}'"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "marcel"
|
4
|
+
require "timeout"
|
5
|
+
|
6
|
+
module Vectory
|
7
|
+
class Utils
|
8
|
+
# Extracted from https://github.com/metanorma/metanorma-utils/blob/v1.5.2/lib/utils/image.rb
|
9
|
+
class << self
|
10
|
+
# sources/plantuml/plantuml20200524-90467-1iqek5i.png
|
11
|
+
# already includes localdir
|
12
|
+
# Check whether just the local path or the other specified relative path
|
13
|
+
# works.
|
14
|
+
def datauri(uri, local_dir = ".")
|
15
|
+
return uri if datauri?(uri) || url?(uri)
|
16
|
+
|
17
|
+
options = absolute_path?(uri) ? [uri] : [uri, File.join(local_dir, uri)]
|
18
|
+
path = options.detect do |p|
|
19
|
+
File.exist?(p) ? p : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
unless path
|
23
|
+
warn "Image specified at `#{uri}` does not exist."
|
24
|
+
return uri # Return original provided location
|
25
|
+
end
|
26
|
+
|
27
|
+
encode_datauri(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def encode_datauri(path)
|
31
|
+
return nil unless File.exist?(path)
|
32
|
+
|
33
|
+
type = Marcel::MimeType.for(Pathname.new(path)) ||
|
34
|
+
'text/plain; charset="utf-8"'
|
35
|
+
|
36
|
+
bin = File.binread(path)
|
37
|
+
data = Base64.strict_encode64(bin)
|
38
|
+
"data:#{type};base64,#{data}"
|
39
|
+
rescue StandardError
|
40
|
+
warn "Data-URI encoding of `#{path}` failed."
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def datauri?(uri)
|
45
|
+
/^data:/.match?(uri)
|
46
|
+
end
|
47
|
+
|
48
|
+
def url?(url)
|
49
|
+
%r{^[A-Z]{2,}://}i.match?(url)
|
50
|
+
end
|
51
|
+
|
52
|
+
def absolute_path?(uri)
|
53
|
+
%r{^/}.match?(uri) || %r{^[A-Z]:/}.match?(uri)
|
54
|
+
end
|
55
|
+
|
56
|
+
# rubocop:disable all
|
57
|
+
#
|
58
|
+
# Originally from https://gist.github.com/pasela/9392115
|
59
|
+
#
|
60
|
+
# Capture the standard output and the standard error of a command.
|
61
|
+
# Almost same as Open3.capture3 method except for timeout handling and return value.
|
62
|
+
# See Open3.capture3.
|
63
|
+
#
|
64
|
+
# result = capture3_with_timeout([env,] cmd... [, opts])
|
65
|
+
#
|
66
|
+
# The arguments env, cmd and opts are passed to Process.spawn except
|
67
|
+
# opts[:stdin_data], opts[:binmode], opts[:timeout], opts[:signal]
|
68
|
+
# and opts[:kill_after]. See Process.spawn.
|
69
|
+
#
|
70
|
+
# If opts[:stdin_data] is specified, it is sent to the command's standard input.
|
71
|
+
#
|
72
|
+
# If opts[:binmode] is true, internal pipes are set to binary mode.
|
73
|
+
#
|
74
|
+
# If opts[:timeout] is specified, SIGTERM is sent to the command after specified seconds.
|
75
|
+
#
|
76
|
+
# If opts[:signal] is specified, it is used instead of SIGTERM on timeout.
|
77
|
+
#
|
78
|
+
# If opts[:kill_after] is specified, also send a SIGKILL after specified seconds.
|
79
|
+
# it is only sent if the command is still running after the initial signal was sent.
|
80
|
+
#
|
81
|
+
# The return value is a Hash as shown below.
|
82
|
+
#
|
83
|
+
# {
|
84
|
+
# :pid => PID of the command,
|
85
|
+
# :status => Process::Status of the command,
|
86
|
+
# :stdout => the standard output of the command,
|
87
|
+
# :stderr => the standard error of the command,
|
88
|
+
# :timeout => whether the command was timed out,
|
89
|
+
# }
|
90
|
+
def capture3_with_timeout(*cmd)
|
91
|
+
spawn_opts = Hash === cmd.last ? cmd.pop.dup : {}
|
92
|
+
opts = {
|
93
|
+
:stdin_data => spawn_opts.delete(:stdin_data) || "",
|
94
|
+
:binmode => spawn_opts.delete(:binmode) || false,
|
95
|
+
:timeout => spawn_opts.delete(:timeout),
|
96
|
+
:signal => spawn_opts.delete(:signal) || :TERM,
|
97
|
+
:kill_after => spawn_opts.delete(:kill_after),
|
98
|
+
}
|
99
|
+
|
100
|
+
in_r, in_w = IO.pipe
|
101
|
+
out_r, out_w = IO.pipe
|
102
|
+
err_r, err_w = IO.pipe
|
103
|
+
in_w.sync = true
|
104
|
+
|
105
|
+
if opts[:binmode]
|
106
|
+
in_w.binmode
|
107
|
+
out_r.binmode
|
108
|
+
err_r.binmode
|
109
|
+
end
|
110
|
+
|
111
|
+
spawn_opts[:in] = in_r
|
112
|
+
spawn_opts[:out] = out_w
|
113
|
+
spawn_opts[:err] = err_w
|
114
|
+
|
115
|
+
result = {
|
116
|
+
:pid => nil,
|
117
|
+
:status => nil,
|
118
|
+
:stdout => nil,
|
119
|
+
:stderr => nil,
|
120
|
+
:timeout => false,
|
121
|
+
}
|
122
|
+
|
123
|
+
out_reader = nil
|
124
|
+
err_reader = nil
|
125
|
+
wait_thr = nil
|
126
|
+
|
127
|
+
begin
|
128
|
+
Timeout.timeout(opts[:timeout]) do
|
129
|
+
result[:pid] = spawn(*cmd, spawn_opts)
|
130
|
+
wait_thr = Process.detach(result[:pid])
|
131
|
+
in_r.close
|
132
|
+
out_w.close
|
133
|
+
err_w.close
|
134
|
+
|
135
|
+
out_reader = Thread.new { out_r.read }
|
136
|
+
err_reader = Thread.new { err_r.read }
|
137
|
+
|
138
|
+
in_w.write opts[:stdin_data]
|
139
|
+
in_w.close
|
140
|
+
|
141
|
+
result[:status] = wait_thr.value
|
142
|
+
end
|
143
|
+
rescue Timeout::Error
|
144
|
+
result[:timeout] = true
|
145
|
+
pid = spawn_opts[:pgroup] ? -result[:pid] : result[:pid]
|
146
|
+
Process.kill(opts[:signal], pid)
|
147
|
+
if opts[:kill_after]
|
148
|
+
unless wait_thr.join(opts[:kill_after])
|
149
|
+
Process.kill(:KILL, pid)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
ensure
|
153
|
+
result[:status] = wait_thr.value if wait_thr
|
154
|
+
result[:stdout] = out_reader.value if out_reader
|
155
|
+
result[:stderr] = err_reader.value if err_reader
|
156
|
+
out_r.close unless out_r.closed?
|
157
|
+
err_r.close unless err_r.closed?
|
158
|
+
end
|
159
|
+
|
160
|
+
result
|
161
|
+
end
|
162
|
+
# rubocop:enable all
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|