vectory 0.8.1 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29078da81dfd3def401e196f9c13654a5b2ccc4eefed851c261796cca0e8e9d2
4
- data.tar.gz: 602ba638f3ff426687d778c87d4f93bde391cd7aaaa565902374bede727e0bd5
3
+ metadata.gz: fc294acc45e36e018340d14a047047d5addc5c712fabc42fec2642b82bc77aa8
4
+ data.tar.gz: '09b35052e8b019a2553eaa34ae14c6fce0344c5fc8d0e61b42fc4bb574bbe569'
5
5
  SHA512:
6
- metadata.gz: 32c32817c07c2578914427b923f82f97cbfd71249d11d10c07dcab6ad051d647679bd7cffdcaf6b104a6bd928dc8fe71bba32e686ebb00d1fa2c2b3758b93cd0
7
- data.tar.gz: 3636e536af14c71d4efd99136c0408528412a20b7e50e12efd33147d8c7e5bea1bb70128c35af71b9b439febd2ab3636c29a58984e04b49ecb146c292891180e
6
+ metadata.gz: ffb21fcdc31b15763547be90085ec49288588c7fabc9d9a2f803f5a0fcf5f958c3bf020e7f5aa96abeb2d31de2c1077e6cb9b9fa57b108e28355328e2fa64eb3
7
+ data.tar.gz: 989f2bb3ade3b0416571b67d7cf8262def78291e9730ee73050052ecc3fa84639be2f93d21af392fc7f7b2a0eb3be3aedd19afe7628598aa60f01afcd4d4f5ae
@@ -15,5 +15,7 @@ permissions:
15
15
  jobs:
16
16
  rake:
17
17
  uses: metanorma/ci/.github/workflows/inkscape-rake.yml@main
18
+ with:
19
+ private-fonts: 'false'
18
20
  secrets:
19
21
  pat_token: ${{ secrets.CLARICLE_CI_PAT_TOKEN }}
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "canon", "~> 0.1.7"
9
+ gem "connection_pool", "~> 2.0"
9
10
  gem "openssl", "~> 3.0"
10
11
  gem "rake"
11
12
  gem "rspec"
data/README.adoc CHANGED
@@ -169,15 +169,49 @@ Vectory::Eps.from_path("img.eps").to_uri.content
169
169
  ==== SVG mapping (for the metanorma project)
170
170
 
171
171
  Vectory can integrate SVG files into XML or HTML, respecting internal id and
172
- link references:
172
+ link references. It supports ID disambiguation for multi-document and multi-svgmap
173
+ scenarios.
174
+
175
+ ===== Basic usage
173
176
 
174
177
  [source,ruby]
175
178
  ----
176
179
  xml_string = Vectory::SvgMapping.from_path("doc.xml").to_xml
177
180
  ----
178
181
 
179
- In order to do that an initial XML should support the `svgmap` tag with links
180
- mapping. For example, it can convert XML like this:
182
+ ===== ID suffixing for uniqueness
183
+
184
+ When processing multiple documents or multiple svgmaps within a document,
185
+ SVG IDs must be unique to avoid conflicts. Vectory applies two types of suffixes:
186
+
187
+ *ID suffix* (Cross-document uniqueness)::
188
+ An optional suffix derived from document or container identity (e.g., `_ISO_17301-1_2016`).
189
+ This provides uniqueness across documents or collections.
190
+
191
+ *Index suffix* (Multi-svgmap uniqueness)::
192
+ Automatically applied based on the svgmap's position in the document (0, 1, 2, ...).
193
+ Formatted as a 9-digit zero-padded number (e.g., `_000000000`, `_000000001`).
194
+ This provides uniqueness when a document contains multiple svgmaps.
195
+
196
+ .Final ID composition
197
+ [source]
198
+ ----
199
+ Original ID: fig1
200
+ With ID suffix only: fig1_ISO_17301-1_2016
201
+ With both suffixes: fig1_ISO_17301-1_2016_000000000
202
+ ----
203
+
204
+ ===== Usage with ID suffix
205
+
206
+ [source,ruby]
207
+ ----
208
+ mapping = Vectory::SvgMapping.new(doc, "", id_suffix: "_ISO_17301-1_2016")
209
+ xml_string = mapping.call.to_xml
210
+ ----
211
+
212
+ ===== Input format
213
+
214
+ The input XML must support the `svgmap` tag with link mapping.
181
215
 
182
216
  [source,xml]
183
217
  ----
@@ -187,8 +221,8 @@ mapping. For example, it can convert XML like this:
187
221
  <xref target="ref1">Computer</xref>
188
222
  </target>
189
223
  <target href="http://www.example.com">
190
- <link target="http://www.example.com">Phone</link><
191
- /target>
224
+ <link target="http://www.example.com">Phone</link>
225
+ </target>
192
226
  </svgmap>
193
227
  ----
194
228
 
@@ -196,16 +230,12 @@ mapping. For example, it can convert XML like this:
196
230
  [source,xml]
197
231
  ----
198
232
  <?xml version="1.0" encoding="utf-8"?>
199
- <!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
200
- <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
201
- viewBox="0 0 595.28 841.89" style="enable-background:new 0 0 595.28 841.89;" xml:space="preserve">
233
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
202
234
  <style type="text/css">
203
- #Layer_1 { fill:none }
204
- svg[id = 'Layer_1'] { fill:none }
235
+ #Layer_1 { fill:none }
236
+ svg[id = 'Layer_1'] { fill:none }
205
237
  .st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
206
238
  </style>
207
- <image style="overflow:visible;" width="368" height="315" xlink:href="data:image/gif;base64,R0lG..ommited to save space" transform="matrix(1 0 0 1 114 263.8898)">
208
- </image>
209
239
  <a xlink:href="mn://action_schema" xlink:dummy="Layer_1">
210
240
  <rect x="123.28" y="273.93" class="st0" width="88.05" height="41.84"/>
211
241
  </a>
@@ -218,15 +248,14 @@ mapping. For example, it can convert XML like this:
218
248
  </svg>
219
249
  ----
220
250
 
221
- into XML containing inline SVG tags. Notice changes in the `id` attributes and
222
- the `a` tags:
251
+ ===== Output format
223
252
 
224
253
  [source,xml]
225
254
  ----
226
255
  <figure>
227
- <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='Layer_1_000000001' x='0px' y='0px' viewBox='0 0 595.28 841.89' style='enable-background:new 0 0 595.28 841.89;' xml:space='preserve'>
228
- <style> ..ommited to save space </style>
229
- <image> ..ommited </image>
256
+ <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1'
257
+ id='Layer_1_000000001' x='0px' y='0px' viewBox='0 0 595.28 841.89'>
258
+ <style> #Layer_1_000000001 { fill:none }</style>
230
259
  <a xlink:href='#ref1' xlink:dummy='Layer_1_000000001'>
231
260
  <rect x='123.28' y='273.93' class='st0' width='88.05' height='41.84'/>
232
261
  </a>
@@ -240,53 +269,39 @@ the `a` tags:
240
269
  </figure>
241
270
  ----
242
271
 
243
- It also supports SVG in a form of an inline tag:
272
+ ===== Inline SVG support
273
+
274
+ SVG can also be provided inline within the svgmap:
244
275
 
245
276
  [source,xml]
246
277
  ----
247
278
  <svgmap id="_60dadf08-48d4-4164-845c-b4e293e00abd">
248
279
  <figure>
249
- <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='Layer_1' x='0px' y='0px' viewBox='0 0 595.28 841.89' style='enable-background:new 0 0 595.28 841.89;' xml:space='preserve'>
250
- <a href="mn://action_schema" >
251
- <rect x="123.28" y="273.93" class="st0" width="88.05" height="41.84"/>
252
- </a>
253
- <a href="mn://basic_attribute_schema" >
254
- <rect x="324.69" y="450.52" class="st0" width="132.62" height="40.75"/>
255
- </a>
256
- <a xlink:href="mn://support_resource_schema" >
257
- <rect x="324.69" y="528.36" class="st0" width="148.16" height="40.75"/>
280
+ <svg xmlns='http://www.w3.org/2000/svg' id='Layer_1'>
281
+ <a href="mn://action_schema">
282
+ <rect x="123.28" y="273.93" class="st0"/>
258
283
  </a>
259
284
  </svg>
260
285
  </figure>
261
286
  <target href="mn://action_schema">
262
287
  <xref target="ref1">Computer</xref>
263
288
  </target>
264
- <target href="http://www.example.com">
265
- <link target="http://www.example.com">Phone</link>
266
- </target>
267
289
  </svgmap>
268
290
  ----
269
291
 
270
- and datauri:
292
+ ===== Data URI support
293
+
294
+ Images can be provided as data URIs:
271
295
 
272
296
  [source,xml]
273
297
  ----
274
298
  <svgmap id="_60dadf08-48d4-4164-845c-b4e293e00abd">
275
299
  <figure>
276
- <image src='data:image/svg+xml;base64,PD94..ommited to save space' id='__ISO_17301-1_2016' mimetype='image/svg+xml' height='auto' width='auto' alt='Workmap1'/>
300
+ <image src='data:image/svg+xml;base64,PD94...'/>
277
301
  </figure>
278
302
  <target href="href1.htm">
279
303
  <xref target="ref1">Computer</xref>
280
304
  </target>
281
- <target href="mn://basic_attribute_schema">
282
- <link target="http://www.example.com">Phone</link>
283
- </target>
284
- <target href="mn://support_resource_schema">
285
- <eref type="express" bibitemid="express_action_schema" citeas="">
286
- <localityStack><locality type="anchor"><referenceFrom>action_schema.basic</referenceFrom></locality></localityStack>
287
- Coffee
288
- </eref>
289
- </target>
290
305
  </svgmap>
291
306
  ----
292
307
 
@@ -355,7 +370,7 @@ Vector#width
355
370
 
356
371
  == Development
357
372
 
358
- === Regenerating Test Fixtures
373
+ === Regenerating test fixtures
359
374
 
360
375
  Some test fixtures are generated by external tools (Ghostscript, Inkscape, cairo).
361
376
  When these tools are updated, the reference files may need to be regenerated.
@@ -339,7 +339,7 @@ rescue Vectory::ConversionError => e
339
339
  end
340
340
  ----
341
341
 
342
- === Dimension Query
342
+ === Dimension query
343
343
 
344
344
  [source,ruby]
345
345
  ----
@@ -348,7 +348,89 @@ width, height = eps.dimensions
348
348
  puts "Dimensions: #{width}x#{height}"
349
349
  ----
350
350
 
351
- ## See Also
351
+ == SVG mapping
352
+
353
+ Integrates SVG content into XML documents with ID disambiguation for multi-document scenarios.
354
+
355
+ === Vectory::SvgMapping
356
+
357
+ Processes XML documents containing `<svgmap>` elements, extracting and integrating SVG content.
358
+
359
+ [source,ruby]
360
+ ----
361
+ # Basic usage
362
+ xml_string = Vectory::SvgMapping.from_path("doc.xml").to_xml
363
+
364
+ # With ID suffix for cross-document uniqueness
365
+ mapping = Vectory::SvgMapping.new(doc, "", id_suffix: "_ISO_17301-1_2016")
366
+ xml_string = mapping.call.to_xml
367
+ ----
368
+
369
+ **Factory methods**:
370
+
371
+ * `Vectory::SvgMapping.from_path(path)` - Load from file path
372
+ * `Vectory::SvgMapping.from_xml(xml)` - Load from XML string
373
+ * `Vectory::SvgMapping.new(doc, local_directory, id_suffix: nil)` - Direct instantiation
374
+
375
+ **Parameters**:
376
+
377
+ * `doc` - Nokogiri XML Document containing svgmap elements
378
+ * `local_directory` - Directory for resolving relative file paths
379
+ * `id_suffix` - Optional suffix for cross-document ID uniqueness (e.g., "_ISO_17301-1_2016")
380
+
381
+ **Methods**:
382
+
383
+ * `#call` - Process all svgmap elements, returns processed document
384
+ * `#to_xml` - Process and return XML string
385
+
386
+ === Vectory::SvgDocument
387
+
388
+ Represents an SVG document and handles ID suffixing for uniqueness.
389
+
390
+ [source,ruby]
391
+ ----
392
+ # Create from SVG content
393
+ svg_doc = Vectory::SvgDocument.new(svg_content)
394
+
395
+ # Apply both ID suffix and index suffix
396
+ svg_doc.namespace(index, links, xpath_to_remove, id_suffix: "_ISO_17301-1_2016")
397
+ result = svg_doc.content
398
+ ----
399
+
400
+ **Constructor**:
401
+
402
+ * `Vectory::SvgDocument.new(content)` - Create from SVG XML string
403
+
404
+ **Methods**:
405
+
406
+ * `namespace(index_suffix, links, xpath_to_remove, id_suffix: nil)` - Apply transformations
407
+ ** `index_suffix` - Position-based suffix (0, 1, 2...) formatted as 9-digit zero-padded
408
+ ** `links` - Hash mapping hrefs to targets
409
+ ** `xpath_to_remove` - XPath for elements to remove (processing instructions)
410
+ ** `id_suffix` - Optional identity-based suffix for cross-document uniqueness
411
+
412
+ * `content` - Returns processed SVG as XML string
413
+ * `remap_links(map)` - Remap internal links
414
+ * `apply_id_suffix(id_suffix)` - Apply ID suffix
415
+ * `apply_index_suffix(index)` - Apply index suffix
416
+
417
+ === ID Suffixing
418
+
419
+ SVG IDs are suffixed in two stages:
420
+
421
+ [source]
422
+ ----
423
+ # Stage 1: ID suffix (cross-document uniqueness)
424
+ fig1 → fig1_ISO_17301-1_2016
425
+
426
+ # Stage 2: Index suffix (multi-svgmap uniqueness)
427
+ fig1_ISO_17301-1_2016 → fig1_ISO_17301-1_2016_000000000
428
+ ----
429
+
430
+ * **ID suffix** (`id_suffix` parameter): Derived from document/container identity. Optional.
431
+ * **Index suffix** (`index_suffix` parameter): Zero-padded position (0, 1, 2... → _000000000, _000000001). Automatically applied.
432
+
433
+ == See Also
352
434
 
353
435
  * link:../guides/error-handling/[Error Handling Guide] - Common errors and solutions
354
436
  * link:../understanding/architecture/[Architecture] - Design patterns
data/lib/vectory/cli.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "thor"
2
- require_relative "../vectory"
3
- require_relative "file_magic"
2
+ require "vectory"
4
3
 
5
4
  module Vectory
6
5
  class CLI < Thor
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../ghostscript_wrapper"
4
- require_relative "strategy"
5
-
6
3
  module Vectory
7
4
  module Conversion
8
5
  # Ghostscript-based conversion strategy
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../inkscape_wrapper"
4
- require_relative "strategy"
5
-
6
3
  module Vectory
7
4
  module Conversion
8
5
  # Inkscape-based conversion strategy
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion/strategy"
4
- require_relative "conversion/inkscape_strategy"
5
- require_relative "conversion/ghostscript_strategy"
6
-
7
3
  module Vectory
8
4
  # Conversion module provides strategy-based conversion interface
9
5
  #
@@ -16,6 +12,10 @@ module Vectory
16
12
  # @example Get available strategies for a conversion
17
13
  # Vectory::Conversion.strategies_for(:svg, :eps)
18
14
  module Conversion
15
+ # Autoload strategy classes
16
+ autoload :Strategy, "vectory/conversion/strategy"
17
+ autoload :InkscapeStrategy, "vectory/conversion/inkscape_strategy"
18
+ autoload :GhostscriptStrategy, "vectory/conversion/ghostscript_strategy"
19
19
  class << self
20
20
  # Convert content from one format to another
21
21
  #
data/lib/vectory/eps.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ghostscript_wrapper"
4
- require_relative "pdf"
5
-
6
3
  module Vectory
7
4
  class Eps < Vector
8
5
  def self.default_extension
@@ -2,18 +2,18 @@
2
2
 
3
3
  require "tempfile"
4
4
  require "fileutils"
5
- require_relative "errors"
6
- require_relative "system_call"
7
- require_relative "platform"
5
+ require "ukiryu"
8
6
 
9
7
  module Vectory
10
8
  # GhostscriptWrapper converts PS and EPS files to PDF using Ghostscript
9
+ #
10
+ # Uses Ukiryu for platform-adaptive command execution.
11
11
  class GhostscriptWrapper
12
12
  SUPPORTED_INPUT_FORMATS = %w[ps eps].freeze
13
13
 
14
14
  class << self
15
15
  def available?
16
- ghostscript_path
16
+ ghostscript_tool
17
17
  true
18
18
  rescue GhostscriptNotFoundError
19
19
  false
@@ -22,9 +22,8 @@ module Vectory
22
22
  def version
23
23
  return nil unless available?
24
24
 
25
- cmd = [ghostscript_path, "--version"]
26
- call = SystemCall.new(cmd).call
27
- call.stdout.strip
25
+ tool = ghostscript_tool
26
+ tool.version
28
27
  rescue StandardError
29
28
  nil
30
29
  end
@@ -49,15 +48,22 @@ module Vectory
49
48
  # Close output file so GhostScript can write to it
50
49
  output_file.close
51
50
 
52
- cmd = build_command(input_file.path, output_file.path,
53
- eps_crop: eps_crop)
51
+ # Get the tool and execute
52
+ tool = ghostscript_tool
53
+ params = build_convert_params(input_file.path, output_file.path,
54
+ eps_crop: eps_crop)
54
55
 
55
- call = nil
56
- begin
57
- call = SystemCall.new(cmd).call
58
- rescue SystemCallError => e
56
+ result = tool.execute(:convert,
57
+ execution_timeout: Configuration.instance.timeout,
58
+ **params)
59
+
60
+ unless result.success?
59
61
  raise ConversionError,
60
- "GhostScript conversion failed: #{e.message}"
62
+ "GhostScript conversion failed. " \
63
+ "Command: #{result.command}, " \
64
+ "Exit status: #{result.status}, " \
65
+ "stdout: '#{result.stdout.strip}', " \
66
+ "stderr: '#{result.stderr.strip}'"
61
67
  end
62
68
 
63
69
  unless File.exist?(output_file.path)
@@ -71,9 +77,9 @@ module Vectory
71
77
  if output_content.size < 100
72
78
  raise ConversionError,
73
79
  "GhostScript created invalid PDF (#{output_content.size} bytes). " \
74
- "Command: #{cmd.join(' ')}, " \
75
- "stdout: '#{call&.stdout&.strip}', " \
76
- "stderr: '#{call&.stderr&.strip}'"
80
+ "Command: #{result.command}, " \
81
+ "stdout: '#{result.stdout.strip}', " \
82
+ "stderr: '#{result.stderr.strip}'"
77
83
  end
78
84
 
79
85
  output_content
@@ -86,74 +92,91 @@ module Vectory
86
92
  end
87
93
  end
88
94
 
89
- private
95
+ # Convert PDF content to PostScript
96
+ #
97
+ # This is useful as a fallback when Inkscape's PDF import fails.
98
+ # Ghostscript can reliably convert PDF to EPS, and Inkscape can then
99
+ # import the EPS file.
100
+ #
101
+ # @param pdf_content [String] the PDF content to convert
102
+ # @return [String] the EPS content
103
+ # @raise [Vectory::ConversionError] if conversion fails
104
+ # @raise [Vectory::GhostscriptNotFoundError] if Ghostscript is not available
105
+ def pdf_to_eps(pdf_content)
106
+ raise GhostscriptNotFoundError unless available?
107
+
108
+ input_file = Tempfile.new(["pdf_input", ".pdf"])
109
+ output_file = Tempfile.new(["eps_output", ".eps"])
110
+
111
+ begin
112
+ input_file.binmode
113
+ input_file.write(pdf_content)
114
+ input_file.flush
115
+ input_file.close
116
+ output_file.close
90
117
 
91
- def ghostscript_path
92
- # First try common installation paths specific to each platform
93
- if Platform.windows?
94
- # Check common Windows installation directories first
95
- common_windows_paths = [
96
- "C:/Program Files/gs/gs*/bin/gswin64c.exe",
97
- "C:/Program Files (x86)/gs/gs*/bin/gswin32c.exe",
98
- ]
99
-
100
- common_windows_paths.each do |pattern|
101
- Dir.glob(pattern).sort.reverse.each do |path|
102
- return path if File.executable?(path)
103
- end
118
+ tool = ghostscript_tool
119
+ params = {
120
+ inputs: [input_file.path],
121
+ device: :eps2write,
122
+ output: output_file.path,
123
+ batch: true,
124
+ no_pause: true,
125
+ quiet: true,
126
+ }
127
+
128
+ result = tool.execute(:convert,
129
+ execution_timeout: Configuration.instance.timeout,
130
+ **params)
131
+
132
+ unless result.success?
133
+ raise ConversionError,
134
+ "GhostScript PDF to EPS conversion failed. " \
135
+ "Command: #{result.command}, " \
136
+ "Exit status: #{result.status}, " \
137
+ "stdout: '#{result.stdout.strip}', " \
138
+ "stderr: '#{result.stderr.strip}'"
104
139
  end
105
140
 
106
- # Then try PATH for Windows executables
107
- ["gswin64c.exe", "gswin32c.exe", "gs"].each do |cmd|
108
- path = find_in_path(cmd)
109
- return path if path
141
+ unless File.exist?(output_file.path)
142
+ raise ConversionError,
143
+ "GhostScript did not create output file: #{output_file.path}"
110
144
  end
111
- else
112
- # On Unix-like systems, check PATH
113
- path = find_in_path("gs")
114
- return path if path
115
- end
116
145
 
117
- raise GhostscriptNotFoundError
146
+ File.binread(output_file.path)
147
+ ensure
148
+ input_file.close unless input_file.closed?
149
+ input_file.unlink
150
+ output_file.close unless output_file.closed?
151
+ output_file.unlink
152
+ end
118
153
  end
119
154
 
120
- def find_in_path(cmd)
121
- # If command already has an extension, try it as-is first
122
- if File.extname(cmd) != ""
123
- Platform.executable_search_paths.each do |path|
124
- exe = File.join(path, cmd)
125
- return exe if File.executable?(exe) && !File.directory?(exe)
126
- end
127
- end
155
+ private
128
156
 
129
- # Try with PATHEXT extensions
130
- exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
131
- Platform.executable_search_paths.each do |path|
132
- exts.each do |ext|
133
- exe = File.join(path, "#{cmd}#{ext}")
134
- return exe if File.executable?(exe) && !File.directory?(exe)
135
- end
136
- end
137
- nil
157
+ # Get the Ghostscript tool from Ukiryu
158
+ def ghostscript_tool
159
+ Ukiryu::Tool.get("ghostscript")
160
+ rescue Ukiryu::Errors::ToolNotFoundError => e
161
+ # Tool not found - raise the original GhostscriptNotFoundError
162
+ raise GhostscriptNotFoundError, "Ghostscript not available: #{e.message}"
138
163
  end
139
164
 
140
- def build_command(input_path, output_path, options = {})
141
- cmd_parts = []
142
- cmd_parts << ghostscript_path
143
- cmd_parts << "-sDEVICE=pdfwrite"
144
- cmd_parts << "-dNOPAUSE"
145
- cmd_parts << "-dBATCH"
146
- cmd_parts << "-dSAFER"
147
- # Use separate arguments for output file to ensure proper path handling
148
- cmd_parts << "-sOutputFile=#{output_path}"
149
- cmd_parts << "-dEPSCrop" if options[:eps_crop]
150
- cmd_parts << "-dAutoRotatePages=/None"
151
- cmd_parts << "-dQUIET"
152
- # Use -f to explicitly specify input file
153
- cmd_parts << "-f"
154
- cmd_parts << input_path
155
-
156
- cmd_parts
165
+ # Build convert parameters for Ukiryu
166
+ def build_convert_params(input_path, output_path, options = {})
167
+ params = {
168
+ inputs: [input_path],
169
+ device: :pdfwrite,
170
+ output: output_path,
171
+ batch: true,
172
+ no_pause: true,
173
+ quiet: true,
174
+ }
175
+
176
+ # Add EPS crop option
177
+ params[:eps_crop] = true if options[:eps_crop]
178
+
179
+ params
157
180
  end
158
181
  end
159
182
  end