Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- [Developer docs: Testing](#developer-docs-testing)

`draw_tree` is a game tree drawing tool for publication-ready extensive form games in Game Theory.
It can generate TikZ code, LaTeX documents, PDFs, and PNGs from game specifications.
It can generate TikZ code, LaTeX documents, PDFs, PNGs, and SVGs from game specifications.

Games can be specified via `.ef` format files which include layout formatting.
These can be created via [Game Theory Explorer](https://gametheoryexplorer-a68c7.web.app/), or by hand, see [specs.pdf](specs.pdf) for details.
Expand All @@ -33,8 +33,9 @@ pip install -e .
## Requirements

- Python 3.10+ (tested on 3.13)
- LaTeX with TikZ (for PDF/PNG generation)
- LaTeX with TikZ (for PDF/PNG/SVG generation)
- (optional) ImageMagick or Ghostscript or Poppler (for PNG generation)
- (optional) pdf2svg (for SVG generation)

### Installing LaTeX

Expand All @@ -60,6 +61,15 @@ PNG generation will default to using any of ImageMagick or Ghostscript or Popple
- `sudo apt-get install poppler-utils`
- Windows: Install ImageMagick or Ghostscript from their websites

### SVG generation

SVG generation requires `pdf2svg` to be installed and available in PATH.
- macOS:
- `brew install pdf2svg`
- Ubuntu:
- `sudo apt-get install pdf2svg`
- Windows: Download binaries from GitHub or use WSL

## CLI

By default, `draw_tree` generates TikZ code and prints it to standard output.
Expand All @@ -71,6 +81,7 @@ draw_tree games/example.ef --tex # Creates example.tex
draw_tree games/example.ef --output=custom.tex # Creates custom.tex
draw_tree games/example.ef --pdf # Creates example.pdf
draw_tree games/example.ef --png # Creates example.png
draw_tree games/example.ef --svg # Creates example.svg
draw_tree games/example.ef --png --dpi=600 # Creates high-res example.png (72-2400, default: 300)
draw_tree games/example.ef --output=mygame.png scale=0.8 # Creates mygame.png with 0.8 scaling (0.01 to 100)
```
Expand All @@ -80,11 +91,12 @@ draw_tree games/example.ef --output=mygame.png scale=0.8 # Creates mygame.png
You can also use `draw_tree` as a Python library:

```python
from draw_tree import generate_tex, generate_pdf, generate_png
from draw_tree import generate_tex, generate_pdf, generate_png, generate_svg
generate_tex('games/example.ef') # Creates example.tex
generate_tex('games/example.ef', save_to='custom') # Creates custom.tex
generate_pdf('games/example.ef') # Creates example.pdf
generate_png('games/example.ef') # Creates example.png
generate_svg('games/example.ef') # Creates example.svg
generate_png('games/example.ef', dpi=600) # Creates high-res example.png (72-2400, default: 300)
generate_png('games/example.ef', save_to='mygame', scale_factor=0.8) # Creates mygame.png with 0.8 scaling (0.01 to 100)
```
Expand All @@ -111,12 +123,13 @@ In particular read [Tutorial 4) Creating publication-ready game images](https://
In short, you can do:
```python
import pygambit as gbt
from draw_tree import draw_tree, generate_tex, generate_pdf, generate_png
from draw_tree import draw_tree, generate_tex, generate_pdf, generate_png, generate_svg
g = gbt.read_efg('somegame.efg')
draw_tree(g)
generate_tex(g)
generate_pdf(g)
generate_png(g)
generate_svg(g)
```

> Note: Without setting the `save_to` parameter, the saved file will be based on the title field of the pygambit game object.
Expand Down
2 changes: 2 additions & 0 deletions src/draw_tree/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
generate_tex,
generate_pdf,
generate_png,
generate_svg,
ef_to_tex,
latex_wrapper,
efg_dl_ef
Expand All @@ -26,6 +27,7 @@
"generate_tex",
"generate_pdf",
"generate_png",
"generate_svg",
"ef_to_tex",
"latex_wrapper",
"efg_dl_ef",
Expand Down
27 changes: 21 additions & 6 deletions src/draw_tree/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys
from pathlib import Path
from .core import (
commandline, draw_tree, generate_pdf, generate_png, generate_tex
commandline, draw_tree, generate_pdf, generate_png, generate_svg, generate_tex
)


Expand All @@ -19,16 +19,18 @@ def main():
print(" draw_tree <file.ef> [options] # Generate TikZ code")
print(" draw_tree <file.ef> --pdf [options] # Generate PDF (requires pdflatex)")
print(" draw_tree <file.ef> --png [options] # Generate PNG (requires pdflatex + imagemagick/ghostscript)")
print(" draw_tree <file.ef> --svg [options] # Generate SVG (requires pdflatex + pdf2svg)")
print(" draw_tree <file.ef> --tex [options] # Generate LaTeX document")
print(" draw_tree <file.ef> --output=name.ext # Generate with custom filename (.pdf, .png, or .tex)")
print(" draw_tree <file.ef> --output=name.ext # Generate with custom filename (.pdf, .png, .svg, or .tex)")
print()
print("Options:")
print(" scale=X.X Set scale factor (0.01 to 100)")
print(" grid Show helper grid")
print(" --pdf Generate PDF output instead of TikZ")
print(" --png Generate PNG output instead of TikZ")
print(" --svg Generate SVG output instead of TikZ")
print(" --tex Generate LaTeX document instead of TikZ")
print(" --output=X Specify output filename (.pdf, .png, or .tex extension determines format)")
print(" --output=X Specify output filename (.pdf, .png, .svg, or .tex extension determines format)")
print(" --dpi=X Set PNG resolution in DPI (72-2400, default: 300)")
print()
print("Examples:")
Expand All @@ -37,11 +39,11 @@ def main():
print(" draw_tree games/example.ef --tex")
print(" draw_tree games/example.ef --output=mygame.tex scale=0.8")
print()
print("Note: PDF/PNG generation requires pdflatex. PNG also needs ImageMagick or Ghostscript.")
print("Note: PDF/PNG/SVG generation requires pdflatex. PNG also needs ImageMagick or Ghostscript. SVG needs pdf2svg.")
sys.exit(0)

# Process command-line arguments
output_mode, pdf_requested, png_requested, tex_requested, output_file, dpi = commandline(sys.argv)
output_mode, pdf_requested, png_requested, svg_requested, tex_requested, output_file, dpi = commandline(sys.argv)

# Import the core module to access global variables after commandline() has set them
from . import core
Expand Down Expand Up @@ -79,7 +81,20 @@ def main():
dpi=dpi if dpi is not None else 300
)
print(f"PNG generated successfully: {png_path}")


elif output_mode == "svg":
if output_file.endswith(".svg"):
print(f"Generating SVG: {output_file}")
else:
print(f"Generating SVG: {output_file}.svg")
svg_path = generate_svg(
game=current_ef_file,
save_to=output_file,
scale_factor=current_scale,
show_grid=current_grid,
)
print(f"SVG generated successfully: {svg_path}")

elif output_mode == "tex":
if output_file.endswith(".tex"):
print(f"Generating LaTeX: {output_file}")
Expand Down
123 changes: 118 additions & 5 deletions src/draw_tree/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ def isetgen(words: List[str], color_scheme: str = "default") -> None:

########### command-line arguments

def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str], Optional[int]]:
def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, bool, Optional[str], Optional[int]]:
"""
Process command-line arguments to set global configuration.

Expand All @@ -1324,10 +1324,11 @@ def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str],
argv: List of command-line arguments (including script name).

Returns:
Tuple of (output_mode, pdf_requested, png_requested, tex_requested, output_file, dpi) where:
- output_mode: 'tikz', 'pdf', 'png', or 'tex'
Tuple of (output_mode, pdf_requested, png_requested, svg_requested, tex_requested, output_file, dpi) where:
- output_mode: 'tikz', 'pdf', 'png', 'svg', or 'tex'
- pdf_requested: True if --pdf flag was provided
- png_requested: True if --png flag was provided
- svg_requested: True if --svg flag was provided
- tex_requested: True if --tex flag was provided
- output_file: Custom output filename if specified
- dpi: DPI setting for PNG output (None if not specified)
Expand All @@ -1338,6 +1339,7 @@ def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str],

pdf_requested = False
png_requested = False
svg_requested = False
tex_requested = False
output_file = None
dpi = None
Expand All @@ -1359,6 +1361,8 @@ def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str],
pdf_requested = True
elif arg == "--png":
png_requested = True
elif arg == "--svg":
svg_requested = True
elif arg == "--tex":
tex_requested = True
elif arg.startswith("--output="):
Expand All @@ -1367,6 +1371,8 @@ def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str],
pdf_requested = True
elif output_file.endswith('.png'):
png_requested = True
elif output_file.endswith('.svg'):
svg_requested = True
elif output_file.endswith('.tex'):
tex_requested = True
elif arg.startswith("--dpi="):
Expand All @@ -1387,14 +1393,16 @@ def commandline(argv: List[str]) -> tuple[str, bool, bool, bool, Optional[str],
# Determine output mode
if png_requested:
output_mode = "png"
elif svg_requested:
output_mode = "svg"
elif pdf_requested:
output_mode = "pdf"
elif tex_requested:
output_mode = "tex"
else:
output_mode = "tikz"

return (output_mode, pdf_requested, png_requested, tex_requested, output_file, dpi)
return (output_mode, pdf_requested, png_requested, svg_requested, tex_requested, output_file, dpi)

def ef_to_tex(
ef_file: str,
Expand Down Expand Up @@ -1992,7 +2000,7 @@ def generate_png(
# Generate PDF using existing function
generate_pdf(
game=game,
save_to=save_to,
save_to=str(temp_pdf.with_suffix('')),
scale_factor=scale_factor,
level_scaling=level_scaling,
sublevel_scaling=sublevel_scaling,
Expand Down Expand Up @@ -2092,6 +2100,111 @@ def generate_png(
raise RuntimeError(f"PNG generation failed: {e}")


def generate_svg(
game: str | "pygambit.gambit.Game",
save_to: Optional[str] = None,
scale_factor: float = 1.0,
level_scaling: int = 1,
sublevel_scaling: int = 1,
width_scaling: int = 1,
hide_action_labels: bool = False,
shared_terminal_depth: bool = False,
show_grid: bool = False,
color_scheme: str = "default",
edge_thickness: float = 1.0,
action_label_position: float = 0.5,
) -> str:
"""
Generate an SVG image directly from an extensive form (.ef) file.

Creates a PDF first, then converts it to SVG using pdf2svg.
Requires pdflatex and pdf2svg to be installed.

Args:
game: Path to the .ef or .efg file, or a pygambit.gambit.Game object.
save_to: Output path (without extension) or full filename.
scale_factor: Scale factor for the diagram.
level_scaling: Level spacing multiplier (pygambit only).
sublevel_scaling: Sublevel spacing multiplier (pygambit only).
width_scaling: Width spacing multiplier (pygambit only).
hide_action_labels: Hide action labels (pygambit only).
shared_terminal_depth: Enforce shared terminal depth (pygambit only).
show_grid: Whether to show grid lines.
color_scheme: Color scheme for player nodes.
edge_thickness: Thickness of edges.
action_label_position: Position of action labels along edges.

Returns:
Absolute path to the generated SVG file.
"""
if save_to is None:
if isinstance(game, str):
game_path = Path(game)
else:
game_path = Path(game.title + ".ef")
output_svg = game_path.with_suffix(".svg").name
else:
if not save_to.endswith(".svg"):
output_svg = save_to + ".svg"
else:
output_svg = save_to

if isinstance(game, str) and game.lower().endswith(".efg"):
try:
game = efg_dl_ef(game)
except Exception:
pass

with tempfile.TemporaryDirectory() as temp_dir:
temp_pdf = Path(temp_dir) / "temp_output.pdf"

try:
generate_pdf(
game=game,
save_to=str(temp_pdf.with_suffix('')),
scale_factor=scale_factor,
level_scaling=level_scaling,
sublevel_scaling=sublevel_scaling,
width_scaling=width_scaling,
hide_action_labels=hide_action_labels,
shared_terminal_depth=shared_terminal_depth,
show_grid=show_grid,
color_scheme=color_scheme,
edge_thickness=edge_thickness,
action_label_position=action_label_position,
)

# Convert PDF to SVG using pdf2svg
final_svg_path = Path(output_svg)

try:
subprocess.run([
'pdf2svg',
str(temp_pdf),
str(final_svg_path)
], capture_output=True, text=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
raise RuntimeError(
"SVG conversion failed. Please install pdf2svg.\n\n"
"Installation examples:\n"
" macOS: brew install pdf2svg\n"
" Ubuntu: sudo apt-get install pdf2svg\n"
" Windows: Download binaries from GitHub or use WSL"
)

if final_svg_path.exists():
return str(final_svg_path.absolute())
else:
raise RuntimeError("SVG was not generated successfully")

except FileNotFoundError:
raise
except RuntimeError:
raise
except Exception as e:
raise RuntimeError(f"SVG generation failed: {e}")


def efg_dl_ef(efg_file: str) -> str:
"""Convert a Gambit .efg file to the `.ef` format used by generate_tikz.

Expand Down
Loading