mirror of
https://github.com/wassname/greater_tables_project.git
synced 2026-06-27 16:15:38 +08:00
166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""
|
|
Create and display SVG files from TikZ pictures embedded in LaTeX.
|
|
|
|
Good for testing. Outputs are cached by hash. PDF→SVG uses pdf2svg.
|
|
|
|
GPT re-write of my old great2.blog code.
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
from subprocess import run, Popen, PIPE
|
|
from IPython.display import SVG, display
|
|
|
|
from . hasher import txt_short_hash
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Etcher:
|
|
"""Create PDF and SVG files from Tikz blocks."""
|
|
# Full TeX preamble to generate a .fmt if needed
|
|
_tex_template_full = r"""\documentclass[11pt, border=5mm]{standalone}
|
|
\usepackage{newtxtext,newtxmath} % gpt recommended like STIX
|
|
%\usepackage{mathptmx} % gpt like times roman
|
|
\usepackage{amsfonts}
|
|
\usepackage{amsmath}
|
|
\usepackage{mathrsfs}
|
|
\usepackage{url}
|
|
\usepackage{tikz}
|
|
\usepackage{color}
|
|
\usetikzlibrary{arrows,calc,positioning,shadows.blur,decorations.pathreplacing}
|
|
\usetikzlibrary{automata,fit,snakes,intersections}
|
|
\usetikzlibrary{decorations.markings,decorations.text,decorations.pathmorphing,decorations.shapes}
|
|
\usetikzlibrary{decorations.fractals,decorations.footprints}
|
|
\usetikzlibrary{graphs,matrix,shapes.geometric}
|
|
\usetikzlibrary{mindmap,shadows,backgrounds,cd}
|
|
\dump
|
|
"""
|
|
|
|
# Minimal template to embed user tikz
|
|
_tex_template = r"""
|
|
\newcommand{{\I}}{{\vphantom{{lp}}}} % fka grtspacer
|
|
\def\dfrac{{\displaystyle\frac}}
|
|
\def\dint{{\displaystyle\int}}
|
|
|
|
\begin{{document}}
|
|
|
|
{tikz_begin}{tikz_code}{tikz_end}
|
|
|
|
\end{{document}}
|
|
"""
|
|
|
|
|
|
def __init__(self, txt, font_size=11, file_name='', base_path='.', tex_engine='pdflatex'):
|
|
"""Create object from txt, a TeX blob containing a tikzpicture."""
|
|
self.txt = txt
|
|
self.font_size = font_size
|
|
self.tex_engine = tex_engine
|
|
self.base_path = Path(base_path).resolve()
|
|
self.out_path = self.base_path / 'tikz'
|
|
self.out_path.mkdir(exist_ok=True)
|
|
file_name = file_name or txt_short_hash(txt)
|
|
self.file_path = self.out_path / file_name
|
|
self.format_file = self.out_path / f'tikz_format-{self.font_size}.fmt'
|
|
|
|
def split_tikz(self):
|
|
"""Split text to extract the TikZ picture."""
|
|
return re.split(r'(\\begin{tikz(?:cd|picture)}|\\end{tikz(?:cd|picture)})', self.txt)
|
|
|
|
def unlink_format_file(self):
|
|
"""Unlink the format file to force a rebuild."""
|
|
if self.format_file.exists():
|
|
self.format_file.unlink()
|
|
|
|
def ensure_format_file(self):
|
|
"""Create format file for faster compilation if missing."""
|
|
if self.format_file.exists():
|
|
return
|
|
print('Etcher: building TeX format fmt file...', end ='')
|
|
tmp = self.out_path / 'tikz_format.tex'
|
|
tmp.write_text(self._tex_template_full, encoding='utf-8')
|
|
cmd = [
|
|
'pdflatex',
|
|
f'-ini',
|
|
f'-jobname={self.format_file.stem}',
|
|
'&pdflatex',
|
|
tmp.name,
|
|
]
|
|
logger.info(f'Running {" ".join(cmd)} to build format file...')
|
|
(self.file_path.parent / 'make_format.bat').write_text(" ".join(cmd), encoding='utf-8')
|
|
self.run_command(cmd, raise_on_error=True, cwd=self.out_path)
|
|
# tidy up ... to some extent
|
|
for ext in ('.aux', '.log'):
|
|
path = tmp.with_suffix(ext)
|
|
if path.exists():
|
|
path.unlink()
|
|
logger.info('...success...format file built %s', self.format_file.resolve())
|
|
|
|
def process_tikz(self):
|
|
"""Compile TikZ to PDF and convert to SVG."""
|
|
tikz_begin, tikz_code, tikz_end = self.split_tikz()[1:4]
|
|
tex_code = self._tex_template.format(
|
|
tikz_begin=tikz_begin,
|
|
tikz_code=tikz_code,
|
|
tikz_end=tikz_end
|
|
)
|
|
|
|
tex_path = self.file_path.with_suffix('.tex')
|
|
tex_path.write_text(tex_code, encoding='utf-8')
|
|
pdf_path = tex_path.with_suffix('.pdf')
|
|
svg_path = tex_path.with_suffix('.svg')
|
|
|
|
self.ensure_format_file()
|
|
|
|
tex_cmd = [
|
|
'pdflatex',
|
|
"-interaction=nonstopmode",
|
|
f'--fmt={self.format_file.stem}',
|
|
f'--output-directory={str(tex_path.parent)}',
|
|
str(tex_path)
|
|
]
|
|
(tex_path.parent / 'make_tikz.bat').write_text(" ".join(tex_cmd), encoding='utf-8')
|
|
logger.info("Running: %s", " ".join(tex_cmd))
|
|
if self.run_command(tex_cmd):
|
|
raise ValueError('TeX failed to compile, not pdf or svg output.')
|
|
# no tidying up
|
|
else:
|
|
# no error: continue
|
|
svg_cmd = [
|
|
# 'C:\\temp\\pdf2svg-windows\\dist-64bits\\pdf2svg',
|
|
'pdf2svg',
|
|
str(pdf_path),
|
|
str(svg_path)
|
|
]
|
|
logger.info("Running: %s", " ".join(svg_cmd))
|
|
self.run_command(svg_cmd, raise_on_error=True)
|
|
|
|
for ext in ('.aux', '.log', '.pdf'):
|
|
path = tex_path.with_suffix(ext)
|
|
if path.exists():
|
|
path.unlink()
|
|
|
|
def display(self):
|
|
"""Display the SVG in Jupyter."""
|
|
display(SVG(self.file_path.with_suffix('.svg')))
|
|
|
|
def run_command(self, command, raise_on_error=True, cwd=None):
|
|
"""Run command with subprocess and show output."""
|
|
with Popen(command, cwd=cwd, stdout=PIPE, stderr=PIPE, universal_newlines=True) as p:
|
|
stdout, stderr = p.communicate()
|
|
if stdout:
|
|
logger.info('Run command output ends\n %s', stdout.strip()[-250:])
|
|
if stdout:
|
|
if stdout.find('no output PDF file produced') > 0:
|
|
logger.error("ERROR no pdf output\n"*5)
|
|
return -1
|
|
if stderr:
|
|
if raise_on_error:
|
|
raise RuntimeError(stderr.strip())
|
|
else:
|
|
logger.error(stderr.strip())
|
|
return -2
|
|
return 0
|