From 96337654ee2cd6adf8d99d2160df6d83d5a96521 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Mon, 27 Jul 2026 09:37:02 +0800 Subject: [PATCH] Replace EPS/PS to SVG via postsvg (pure Ruby) EPS-to-SVG and PS-to-SVG no longer shell out to Ghostscript + Inkscape. Both call Postsvg.convert directly. The Ghostscript + Inkscape two-step remains in place for EPS/PS -> PDF and EPS/PS -> PS/EMF (via PDF intermediate) -- those will move in follow-up PRs. - vectory.gemspec: add postsvg ~> 0.1 - lib/vectory/eps.rb, lib/vectory/ps.rb: require "postsvg"; to_svg = Svg.from_content(Postsvg.convert(content)) - spec/vectory/eps_spec.rb: drop two stale error-propagation tests for to_svg (they assumed the Ghostscript+Inkscape path) - docs/features/{conversion,index}.adoc, docs/getting-started/core-concepts.adoc: reflect new direct EPS/PS -> SVG path Closes #67 --- docs/features/conversion.adoc | 21 ++++++++--------- docs/features/index.adoc | 21 +++++++++++------ docs/getting-started/core-concepts.adoc | 3 ++- lib/vectory/eps.rb | 4 +++- lib/vectory/ps.rb | 4 +++- spec/vectory/eps_spec.rb | 30 ------------------------- vectory.gemspec | 1 + 7 files changed, 34 insertions(+), 50 deletions(-) diff --git a/docs/features/conversion.adoc b/docs/features/conversion.adoc index 8040746..0e2c84b 100644 --- a/docs/features/conversion.adoc +++ b/docs/features/conversion.adoc @@ -32,21 +32,22 @@ svg_to_eps = Vectory::Svg.from_path("diagram.svg").to_eps eps_to_svg = Vectory::Eps.from_path("diagram.eps").to_svg ---- -=== Two-Step Conversions (EPS/PS → SVG) +=== Direct Conversions (via postsvg) -EPS and PS files are converted to SVG through a PDF intermediate: - -1. **EPS/PS → PDF** (via Ghostscript): Preserves BoundingBox information -2. **PDF → SVG** (via Inkscape): Converts PDF to SVG +EPS and PS files are converted to SVG directly by the postsvg gem (pure Ruby, no Ghostscript or Inkscape): [source,ruby] ---- -# Two-step conversion (automatic) eps = Vectory::Eps.from_path("diagram.eps") -svg = eps.to_svg # Internally: EPS → PDF → SVG +svg = eps.to_svg # Uses postsvg directly + +ps = Vectory::Ps.from_path("diagram.ps") +svg = ps.to_svg # Also postsvg ---- -This two-step process ensures accurate dimension preservation from EPS/PS files. +postsvg reads the `%%BoundingBox` DSC comment to size the viewBox. Limitations: text/font rendering, image embedding, gradients/patterns, CMYK, and PS Level 2/3 advanced operators are not yet supported by postsvg upstream — EPS/PS files using those features will silently degrade. + +=== Two-Step Conversions (EPS/PS → PDF/PS/EMF) === EMF Conversions (via emfsvg) @@ -73,11 +74,11 @@ emf = Vectory::Svg.from_path("chart.svg").to_emf |SVG|EMF|emfsvg gem |SVG|PDF|Inkscape -|EPS|SVG|Ghostscript + Inkscape (two-step) +|EPS|SVG|postsvg gem |EPS|PDF|Ghostscript |EPS|PS|Via PDF intermediate -|PS|SVG|Ghostscript + Inkscape (two-step) +|PS|SVG|postsvg gem |PS|PDF|Ghostscript |PS|EPS|Via PDF intermediate diff --git a/docs/features/index.adoc b/docs/features/index.adoc index 301ff3c..72795ed 100644 --- a/docs/features/index.adoc +++ b/docs/features/index.adoc @@ -68,23 +68,30 @@ Vectory uses different external tools depending on the source and target formats === Direct Conversions (via Inkscape) -* **SVG → EPS/PS/EMF/PDF** (via Inkscape export) -* **EPS/PS/EMF/PDF → SVG** (via Inkscape import) +* **SVG → EPS/PS/PDF** (via Inkscape export) +* **PDF → SVG** (via Inkscape import) Inkscape handles direct conversions to and from SVG format. === Direct Conversions (via emfsvg) -* **EMF → SVG** (via emfsvg gem) +* **SVG ↔ EMF** (via emfsvg gem, both directions) -The emfsvg gem provides native EMF to SVG conversion in pure Ruby, with no external tool dependency. +The emfsvg gem provides bidirectional EMF ↔ SVG conversion in pure Ruby, with no external tool dependency. + +=== Direct Conversions (via postsvg) + +* **EPS → SVG** (via postsvg gem) +* **PS → SVG** (via postsvg gem) + +The postsvg gem converts PS/EPS to SVG in pure Ruby, with no external tool dependency. === Two-Step Conversions (via Ghostscript + Inkscape) -* **EPS → PDF → SVG** (Ghostscript for EPS→PDF, Inkscape for PDF→SVG) -* **PS → PDF → SVG** (Ghostscript for PS→PDF, Inkscape for PDF→SVG) +* **EPS → PDF → SVG** (legacy path; replaced by postsvg for `Eps#to_svg`) +* **PS → PDF → SVG** (legacy path; replaced by postsvg for `Ps#to_svg`) -This two-step process preserves BoundingBox information from EPS/PS files. +EPS/PS → PDF and EPS/PS → PS/EMF (via PDF intermediate) still go through Ghostscript. === Conversion Tool Matrix diff --git a/docs/getting-started/core-concepts.adoc b/docs/getting-started/core-concepts.adoc index 01f313c..9604d8c 100644 --- a/docs/getting-started/core-concepts.adoc +++ b/docs/getting-started/core-concepts.adoc @@ -95,11 +95,12 @@ Vectory uses a flexible conversion pipeline: Some conversions are direct: * **SVG → EPS/PS/PDF**: Via Inkscape * **SVG ↔ EMF**: Via emfsvg (both directions, pure Ruby) +* **EPS → SVG** and **PS → SVG**: Via postsvg (pure Ruby) === Two-Step Conversions Other conversions use an intermediate PDF format: -* **EPS/PS → PDF → SVG**: Via Ghostscript + Inkscape +* **EPS/PS → PDF → {PS,EMF}**: Via Ghostscript + Inkscape This two-step conversion ensures: * BoundingBox preservation from EPS/PS diff --git a/lib/vectory/eps.rb b/lib/vectory/eps.rb index 3ad35fd..7ef8b33 100644 --- a/lib/vectory/eps.rb +++ b/lib/vectory/eps.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "postsvg" + module Vectory class Eps < Vector def self.default_extension @@ -24,7 +26,7 @@ def to_ps end def to_svg - to_pdf.to_svg + Svg.from_content(Postsvg.convert(content)) end def to_emf diff --git a/lib/vectory/ps.rb b/lib/vectory/ps.rb index 4795ae6..1bf13ba 100644 --- a/lib/vectory/ps.rb +++ b/lib/vectory/ps.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "postsvg" + module Vectory class Ps < Vector def self.default_extension @@ -28,7 +30,7 @@ def to_emf end def to_svg - to_pdf.to_svg + Svg.from_content(Postsvg.convert(content)) end def to_pdf diff --git a/spec/vectory/eps_spec.rb b/spec/vectory/eps_spec.rb index 8c3b86c..9d8b76f 100644 --- a/spec/vectory/eps_spec.rb +++ b/spec/vectory/eps_spec.rb @@ -94,12 +94,6 @@ .and_raise(Vectory::ConversionError, "ghostscript failed") end - it "propagates error from to_pdf to to_svg" do - expect do - eps.to_svg - end.to raise_error(Vectory::ConversionError, /ghostscript failed/) - end - it "propagates error from to_pdf to to_ps" do expect do eps.to_ps @@ -112,29 +106,5 @@ end.to raise_error(Vectory::ConversionError, /ghostscript failed/) end end - - context "when Inkscape conversion fails" do - before do - # Allow ps2pdf to succeed - allow(Vectory::GhostscriptWrapper).to receive(:convert) - .and_return("fake pdf content") - - # Also make pdf_to_eps fail (used in fallback) - allow(Vectory::GhostscriptWrapper).to receive(:pdf_to_eps) - .and_raise(Vectory::ConversionError, "Ghostscript fallback failed") - - # Make Inkscape fail - converter = instance_double(Vectory::InkscapeWrapper) - allow(Vectory::InkscapeWrapper).to receive(:instance).and_return(converter) - allow(converter).to receive(:convert) - .and_raise(Vectory::ConversionError, "Inkscape failed") - end - - it "propagates error from Inkscape to to_svg" do - expect do - eps.to_svg - end.to raise_error(Vectory::ConversionError, /Ghostscript fallback failed/) - end - end end end diff --git a/vectory.gemspec b/vectory.gemspec index eb2fffd..9e1b3e6 100644 --- a/vectory.gemspec +++ b/vectory.gemspec @@ -31,6 +31,7 @@ Gem::Specification.new do |spec| spec.add_dependency "image_size", ">= 3.2.0" spec.add_dependency "marcel", "~> 1.0" spec.add_dependency "nokogiri", "~> 1.14" + spec.add_dependency "postsvg", "~> 0.1" spec.add_dependency "thor", "~> 1.0" spec.add_dependency "ukiryu", "~> 0.3.0" end