Getting Started

Sevgi creates SVG with Ruby. A drawing can be an executable .sevgi script or a value built inside another Ruby application.

Script and library modes

Use script mode when the drawing is the program, such as a generated asset or build job. The runner supplies Sevgi's entry points, and the script usually writes or prints its result:

#!/usr/bin/env -S ruby -S sevgi

canvas = Canvas width: 24, height: 24, unit: :px

SVG :minimal, canvas do
  circle cx: 12, cy: 12, r: 10, fill: "tomato"
end.Save "badge.svg"

Use library mode when an application owns the drawing and decides where its rendered string goes:

require "sevgi"

canvas = SVG.Canvas width: 24, height: 24, unit: :px

drawing = SVG :minimal, canvas do
  circle cx: 12, cy: 12, r: 10, fill: "tomato"
end

File.write "badge.svg", drawing.Render

The SVG block is the same in both modes. Operations around it are bare words in a script and capitalized methods on the SVG facade in library code:

Role.sevgi scriptRuby library
Build a documentSVG(...)SVG(...)
Create a canvasCanvas(...)SVG.Canvas(...)
Refer to the canvas typeSVG::CanvasSVG::Canvas

The script passes its document to Save. The application keeps the document and passes its Render result to ordinary Ruby code. Script Mode and Library Mode cover each form in detail.

See a complete drawing

The tabs below come from the same files that the test suite runs. The Ruby tab contains the script; the SVG tab contains its output.

#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true

LED_COUNT = 10
LED_GAP = 2
LED_WIDTH = 10
TOTAL_WIDTH = (LED_COUNT * (LED_WIDTH + LED_GAP)) + 8

dim = proc do |led, x:, **|
  led[:"fill-opacity"] = 0.3 if x >= 7
end

SVG :minimal, width: TOTAL_WIDTH, height: 30, viewBox: "0 0 #{TOTAL_WIDTH} 30" do
  rect x: 2, y: 2, width: TOTAL_WIDTH - 4, height: 26,
       rx: 4, fill: "none", stroke: "#666"

  TileX "led", n: LED_COUNT, d: LED_WIDTH + LED_GAP, o: 5, proc: dim do
    rect y: 5, width: LED_WIDTH, height: 20, rx: 3, fill: "green"
  end
end.Save
#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true

LED_COUNT = 10
LED_GAP = 2
LED_WIDTH = 10
TOTAL_WIDTH = (LED_COUNT * (LED_WIDTH + LED_GAP)) + 8

dim = proc do |led, x:, **|
  led[:"fill-opacity"] = 0.3 if x >= 7
end

SVG :minimal, width: TOTAL_WIDTH, height: 30, viewBox: "0 0 #{TOTAL_WIDTH} 30" do
  rect x: 2, y: 2, width: TOTAL_WIDTH - 4, height: 26,
       rx: 4, fill: "none", stroke: "#d8d8d8"

  TileX "led", n: LED_COUNT, d: LED_WIDTH + LED_GAP, o: 5, proc: dim do
    rect y: 5, width: LED_WIDTH, height: 20, rx: 3, fill: "#7bd88f"
  end
end.Save
<svg width="128" height="30" viewBox="0 0 128 30">
  <rect x="2" y="2" width="124" height="26" rx="4" fill="none" stroke="#666"/>
  <defs>
    <g id="led">
      <rect y="5" width="10" height="20" rx="3" fill="green"/>
    </g>
  </defs>
  <use id="led-1" href="#led" class="tile-col-1 tile-col-first" x="5"/>
  <use id="led-2" href="#led" class="tile-col-2" x="17"/>
  <use id="led-3" href="#led" class="tile-col-3" x="29"/>
  <use id="led-4" href="#led" class="tile-col-4" x="41"/>
  <use id="led-5" href="#led" class="tile-col-5" x="53"/>
  <use id="led-6" href="#led" class="tile-col-6" x="65"/>
  <use id="led-7" href="#led" class="tile-col-7" x="77"/>
  <use id="led-8" href="#led" class="tile-col-8" x="89" fill-opacity="0.3"/>
  <use id="led-9" href="#led" class="tile-col-9" x="101" fill-opacity="0.3"/>
  <use id="led-10" href="#led" class="tile-col-10 tile-col-last" x="113" fill-opacity="0.3"/>
</svg>
<svg width="128" height="30" viewBox="0 0 128 30">
  <rect x="2" y="2" width="124" height="26" rx="4" fill="none" stroke="#d8d8d8"/>
  <defs>
    <g id="led">
      <rect y="5" width="10" height="20" rx="3" fill="#7bd88f"/>
    </g>
  </defs>
  <use id="led-1" href="#led" class="tile-col-1 tile-col-first" x="5"/>
  <use id="led-2" href="#led" class="tile-col-2" x="17"/>
  <use id="led-3" href="#led" class="tile-col-3" x="29"/>
  <use id="led-4" href="#led" class="tile-col-4" x="41"/>
  <use id="led-5" href="#led" class="tile-col-5" x="53"/>
  <use id="led-6" href="#led" class="tile-col-6" x="65"/>
  <use id="led-7" href="#led" class="tile-col-7" x="77"/>
  <use id="led-8" href="#led" class="tile-col-8" x="89" fill-opacity="0.3"/>
  <use id="led-9" href="#led" class="tile-col-9" x="101" fill-opacity="0.3"/>
  <use id="led-10" href="#led" class="tile-col-10 tile-col-last" x="113" fill-opacity="0.3"/>
</svg>

Run an example

From a checkout, run the showcase scripts with Bundler:

bundle exec showcase/srv/meter.sevgi

The script writes showcase/srv/meter.svg because it ends with Save. To write SVG to standard output instead, use Out in the script.

Install Sevgi

For the complete command-line toolkit, install Sevgi through Homebrew on macOS or Linux:

brew install roktas/tap/sevgi

This installs the sevgi executable, Ruby, the native PDF and PNG export stack, and the headless pdfcpu and Poppler tools. Inkscape remains an optional external backend.

When Sevgi is a dependency of a Ruby application, add the umbrella gem to its bundle instead:

gem "sevgi"

The umbrella gem is the right choice for most applications and drawing scripts. It installs the script runner, the SVG facade, the Appendix development extras, and all runtime components.

Choose a gem

Libraries that need a smaller dependency surface can install focused component gems:

ScenarioInstallEntry point
Build and render SVG onlysevgi-graphicsrequire "sevgi/graphics"
Build and validate SVG without the full toolkitsevgi-graphics sevgi-standardrequire "sevgi/graphics"
Use geometry values and transformations without the DSLsevgi-geometryrequire "sevgi/geometry"
Convert SVG or XML back into Sevgi sourcesevgi-derenderrequire "sevgi/derender"
Use grids, rulers, tiles, or export integrationssevgi-sundriesrequire "sevgi/sundries"
Package the agent skill or lint .sevgi sourcesevgi-appendixrequire "sevgi/appendix" or the RuboCop plugin

For example, a service that only builds SVG can install sevgi-graphics and use Sevgi::Graphics.SVG(...). The full SVG facade and the sevgi executable belong to the umbrella gem. Add sevgi-standard when the service should validate element and attribute names. Bundler installs shared support gems such as sevgi-function as transitive dependencies of the components that use them. The umbrella gem adds the sevgi --skill query for locating the matching Appendix skill.

SVG-only library use needs no native graphics packages. Applications that install gems directly and use PDF or PNG export must provide the optional cairo, rsvg2, and hexapdf gems and their system libraries themselves. See the sevgi-sundries package guide for those prerequisites.

The full installation also packages Sevgi's agent skill. Run sevgi --skill to locate it, then follow the Appendix setup guide.

Choose a document profile

SVG accepts a document profile. :minimal produces compact output, :default writes a standalone SVG document, :html is suitable for embedding, and :inkscape adds editor metadata and helpers. See the document-profile matrix for the exact capabilities.

#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true
# Example taken from: https://svg-tutorial.com/svg/use

SVG :minimal, width: 200, height: 200, viewBox: "-100 -100 200 200" do
  path id: "branch", stroke: "purple", "stroke-width": 5, d: <<~PATH
    M 0 0 L 0 -90
    M 0 -20 L 20 -34
    M 0 -20 L -20 -34
    M 0 -40 L 20 -54
    M 0 -40 L -20 -54
    M 0 -60 L 20 -74
    M 0 -60 L -20 -74
  PATH

  60.step(300, 60) { use(href: "#branch").Rotate it }
end.Save
#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true
# Example taken from: https://svg-tutorial.com/svg/use

SVG :minimal, width: 200, height: 200, viewBox: "-100 -100 200 200" do
  path id: "branch", stroke: "#e5c39c", "stroke-width": 5, d: <<~PATH
    M 0 0 L 0 -90
    M 0 -20 L 20 -34
    M 0 -20 L -20 -34
    M 0 -40 L 20 -54
    M 0 -40 L -20 -54
    M 0 -60 L 20 -74
    M 0 -60 L -20 -74
  PATH

  60.step(300, 60) { use(href: "#branch").Rotate it }
end.Save
<svg width="200" height="200" viewBox="-100 -100 200 200">
  <path
    id="branch"
    stroke="purple"
    stroke-width="5"
    d="M 0 0 L 0 -90
M 0 -20 L 20 -34
M 0 -20 L -20 -34
M 0 -40 L 20 -54
M 0 -40 L -20 -54
M 0 -60 L 20 -74
M 0 -60 L -20 -74
"
  />
  <use href="#branch" transform="rotate(60)"/>
  <use href="#branch" transform="rotate(120)"/>
  <use href="#branch" transform="rotate(180)"/>
  <use href="#branch" transform="rotate(240)"/>
  <use href="#branch" transform="rotate(300)"/>
</svg>
<svg width="200" height="200" viewBox="-100 -100 200 200">
  <path
    id="branch"
    stroke="#e5c39c"
    stroke-width="5"
    d="M 0 0 L 0 -90
M 0 -20 L 20 -34
M 0 -20 L -20 -34
M 0 -40 L 20 -54
M 0 -40 L -20 -54
M 0 -60 L 20 -74
M 0 -60 L -20 -74
"
  />
  <use href="#branch" transform="rotate(60)"/>
  <use href="#branch" transform="rotate(120)"/>
  <use href="#branch" transform="rotate(180)"/>
  <use href="#branch" transform="rotate(240)"/>
  <use href="#branch" transform="rotate(300)"/>
</svg>