pre build changes for gtfont

This commit is contained in:
Stephen Mildenhall
2025-06-26 08:52:54 +01:00
parent 2611170c16
commit c3d2dfc057
11 changed files with 149 additions and 35 deletions
+3
View File
@@ -166,3 +166,6 @@ cython_debug/
#.idea/
docs/books.bib
docs/library.bib
# rust
gtfont/target
+2 -2
View File
@@ -1,5 +1,5 @@
Versions
==========
Versions and Change Log
==========================
5.0.0
-------
+1 -4
View File
@@ -1,10 +1,7 @@
__version__ = '5.0.0'
__project__ = 'greater_tables'
__author__ = 'Stephen J Mildenhall'
from . _version import __version__
from . core import GT
from . fabrications import Fabricator
from . etcher import Etcher
# from . gtbreaks import Breakability
+21
View File
@@ -0,0 +1,21 @@
# file generated by setuptools-scm
# don't change, don't track in version control
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple
from typing import Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
VERSION_TUPLE = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
__version__ = version = '5.0.0'
__version_tuple__ = version_tuple = (5, 0, 0)
+1 -1
View File
@@ -51,7 +51,7 @@ class Configurator(BaseModel):
default_ratio_str: str = Field(
"{x:.1%}", description="Format f-string for ratios. Example: '{x:.1%}'"
)
default_formatter: Optional[str] = Field(
default_formatter: Optional[Union[str, Callable[[Any, str], str]]] = Field(
None, description="Optional fallback formatter f-string"
)
+18 -11
View File
@@ -376,14 +376,14 @@ class GT(object):
# figure the default formatter (used in conjunction with raw columns)
if self.config.default_formatter is None:
self.default_formatter = self.default_formatter
self.default_formatter = self._default_formatter
else:
assert callable(
config.default_formatter), 'config.default_formatter must be callable'
self.config.default_formatter), 'config.default_formatter must be callable'
def wrapped_default_formatter(x):
try:
return config.default_formatter(x)
return self.config.default_formatter(x)
except ValueError:
return str(x)
self.default_formatter = wrapped_default_formatter
@@ -513,9 +513,9 @@ class GT(object):
if tabs is None:
self.tabs = None
elif isinstance(tabs, (int, float)):
self.tabs = (tabs,) * self.ncols
self.tabs = (tabs,) * (self.nindex + self.ncols)
elif isinstance(tabs, (np.ndarray, pd.Series, list, tuple)):
if len(tabs) == self.ncols:
if len(tabs) == self.nindex + self.ncols:
self.tabs = tabs # Already iterable and right length, self.tabs = as is
else:
logger.error(
@@ -579,7 +579,7 @@ class GT(object):
if self.config.tikz_escape_tex:
self.df_tex = Escaping.escape_df_tex(self.df)
else:
self.df_tex
self.df_tex = self.df
def __repr__(self):
"""Basic representation."""
@@ -710,8 +710,8 @@ class GT(object):
logger.debug(f'AttributeError {e}')
return str(x)
def default_formatter(self, x):
"""Default universal formatter for other types (GTP re-write of above cluster)."""
def _default_formatter(self, x):
"""Default universal formatter for other types."""
try:
f = float(x)
except (TypeError, ValueError):
@@ -920,7 +920,12 @@ class GT(object):
def tex_knowledge_df(self):
"""Uber source of information for tex formatting."""
if self._tex_knowledge_df is None:
self._tex_knowledge_df = self.estimate_column_widths_by_mode('tex')
if (all(self.df_tex.index == self.df_html.index)
and all(self.df_tex.columns == self.df_html.columns)
and all(self.df_tex == self.df_html)):
self._tex_knowledge_df = self.html_knowledge_df
else:
self._tex_knowledge_df = self.estimate_column_widths_by_mode('tex')
return self._tex_knowledge_df
def width_report(self):
@@ -959,7 +964,7 @@ class GT(object):
return bit
def estimate_column_widths_by_mode(self, mode):
"""
r"""
Return dataframe of width information: three modes for text, html, and tex.
Mode adjusts which df is used and how widths are estimated
@@ -2027,7 +2032,9 @@ class GT(object):
def make_svg(self):
"""Render tikz into svg text."""
tz = Etcher(self._repr_latex_(),
file_name=self.df_id, debug=self.config.debug)
self.config.table_font_pt_size,
file_name=self.df_id
)
p = tz.file_path.with_suffix('.svg')
if not p.exists():
try:
+23 -16
View File
@@ -21,7 +21,7 @@ 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[10pt, border=5mm]{standalone}
_tex_template_full = r"""\documentclass[11pt, border=5mm]{standalone}
\usepackage{newtxtext,newtxmath} % gpt recommended like STIX
%\usepackage{mathptmx} % gpt like times roman
\usepackage{amsfonts}
@@ -44,28 +44,36 @@ class Etcher:
\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, file_name='', base_path='.', tex_engine='pdflatex', debug=False):
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 / 'tikz_format.fmt'
self.debug = debug
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():
@@ -84,8 +92,10 @@ class Etcher:
(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
# tmp.unlink()
(self.out_path / f'{self.format_file.stem}.log').unlink()
for ext in ('.aux', '.log'):
path = tmp.with_suffix(ext)
if path.exists():
path.unlink()
logger.info('...success...format file built', self.format_file.resolve())
def process_tikz(self):
@@ -112,8 +122,7 @@ class Etcher:
str(tex_path)
]
(tex_path.parent / 'make_tikz.bat').write_text(" ".join(tex_cmd), encoding='utf-8')
if self.debug:
logger.info("Running:", " ".join(tex_cmd))
logger.info("Running:", " ".join(tex_cmd))
if self.run_command(tex_cmd):
raise ValueError('TeX failed to compile, not pdf or svg output.')
# no tidying up
@@ -125,15 +134,13 @@ class Etcher:
str(pdf_path),
str(svg_path)
]
if self.debug:
logger.info("Running:", " ".join(svg_cmd))
logger.info("Running:", " ".join(svg_cmd))
self.run_command(svg_cmd, raise_on_error=True)
if not self.debug:
for ext in ('.tex', '.aux', '.log', '.pdf'):
path = tex_path.with_suffix(ext)
if path.exists():
path.unlink()
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."""
@@ -143,7 +150,7 @@ class Etcher:
"""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 and self.debug:
if stdout:
logger.info('Run command output ends\n', stdout.strip()[-250:])
if stdout:
if stdout.find('no output PDF file produced') > 0:
+1 -1
View File
@@ -302,7 +302,7 @@ class TextLength:
"": 0.333,
"{": 0.48,
"}": 0.48,
"-": 0.333,
"-": 0.5, # 0.333,
}
char_width = {c: w for chars, w in width_table.items() for c in chars}
return char_width.get(c, 0.6)
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "gtfont"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.21", features = ["extension-module", "abi3-py38"] }
fontdue = "0.8"
[package.metadata.maturin]
name = "gtfont"
[env]
PYO3_USE_ABI3_FORWARD_COMPATIBILITY = "1"
+59
View File
@@ -0,0 +1,59 @@
use fontdue::Font;
use pyo3::prelude::*;
#[pyclass]
struct FontMeasurer {
font: Font,
}
#[pymethods]
impl FontMeasurer {
#[new]
fn new(font_bytes: &[u8]) -> PyResult<Self> {
let font = Font::from_bytes(font_bytes, fontdue::FontSettings::default())
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Font error: {e}")))?;
Ok(Self { font })
}
fn measure(&self, text: &str, px: f32) -> f32 {
text.chars()
.map(|c| self.font.metrics(c, px).advance_width)
.sum()
}
fn max_word_width(&self, text: &str, px: f32) -> f32 {
text.split(' ')
.map(|word| word.chars()
.map(|c| self.font.metrics(c, px).advance_width)
.sum::<f32>())
.fold(0.0, f32::max)
}
fn measure_and_max_word(&self, text: &str, px: f32) -> (f32, f32) {
let mut total: f32 = 0.0;
let mut max_word: f32 = 0.0;
for word in text.split(' ') {
let word_width: f32 = word.chars()
.map(|c| self.font.metrics(c, px).advance_width)
.sum();
total += word_width + self.font.metrics(' ', px).advance_width;
max_word = max_word.max(word_width);
}
if text.ends_with(' ') {
// trailing space is valid
} else if total > 0.0 {
total -= self.font.metrics(' ', px).advance_width;
}
(total, max_word)
}
}
#[pymodule]
fn gtfont(_py: &Bound<'_, PyModule>) -> PyResult<()> {
_py.add_class::<FontMeasurer>()?;
Ok(())
}
+3
View File
@@ -36,6 +36,7 @@ write_to = "greater_tables/_version.py"
version_scheme = "post-release"
local_scheme = "node-and-date" # "no-local-version"
[tool.setuptools.packages.find]
include = ["greater_tables", "greater_tables.data"]
exclude = ["img", "tests", "docs"]
@@ -48,6 +49,8 @@ version = { attr = "greater_tables.__version__" }
[project.urls]
"Source Code" = "https://github.com/mynl/greater_tables_project"
"Documentation" = "https://greater-tables-project.readthedocs.io/en/latest/"
"Changelog" = "https://greater-tables-project.readthedocs.io/en/latest/versions.html"
[project.optional-dependencies]
dev = [