The ansi target, txt table styles, kdesc reference from ref/, typeface and table-width fixes (from dev fcec5e2e77ac)

Since the previous release: Machine::read_string() (the fs::path/std::string
overload pair split); kdesc -c and -u rendered from the ref/ reflection
klammerset; a fit column's tex width measured in TeX over every entry;
@typeface resolving to the family its css declares (a store directory name
now works in html) and hieroglyphs in @unicode_examples set in their
typeface; the ansi target -- plain text with the type styles a terminal can
show, identical to txt once the escape sequences are stripped -- with the
txt @table carrying :header_style, :style and :hline; @block anchoring,
@table spacing and the Image_quality state variable; and the rewritten
doc/klammertext_for_llm.md.
This commit is contained in:
ack committed 2026-09-12 18:11:48 +02:00
1 parent 760fda5470
commit 9b0d0d7897
34 files changed
+895 -394

No files matched your search

+69 -2
View File
@@ -40,14 +40,49 @@ def image_dimensions(filename):
return width, height, resize
# Raster formats a photograph arrives in. Vector sources (pdf, eps) are
# never re-encoded, nor is a gif or webp (they may be animated, and only
# html takes them).
photograph_formats = {"jpg", "jpeg", "png", "tif", "tiff"}
def is_photograph(filename):
"""True if the file is a raster with no alpha channel: a photograph
(or a grayscale one) that JPEG stores well. A png with transparency,
or a palette image, is not one -- JPEG would flatten or posterize it."""
ext = os.path.splitext(filename)[1][1:].lower()
if ext not in photograph_formats:
return False
inp = oiio.ImageInput.open(filename)
if not inp:
return False
spec = inp.spec()
inp.close()
return spec.alpha_channel < 0 and spec.nchannels in (1, 3)
class Image_cache:
max_width = 1600 # Should be a state variable
cache_basedir = "_klammertext_imgsrc" # Should be a state variable
error_marker_filename = "_error_marker.png"
verbose_label = " [klammertext]" # Should parameterize verbosity
def __init__(self, cache_dir, search_path, verbose=False):
def __init__(self, cache_dir, search_path, verbose=False, quality=100):
#self.cache_dir = kutil.cache_directory(
self.dir = kutil.cache_directory(cache_dir, "_image_cache")
# JPEG quality (1-100) at which a photograph is written to the cache.
# 100 means the source is copied or converted as it is (the
# behaviour before 2026-09-09); below it, every photograph -- a
# JPEG source included -- is re-encoded at that quality, which is
# where a document's size mostly lives: a source JPEG is often
# near-lossless (quality 95+), and a png photograph is stored
# losslessly, at three to six times the size of a quality-85 JPEG
# with no visible difference at print resolution. The Image_quality
# state variable (sks/image/image.k) sets it.
try:
self.quality = int(quality)
except (TypeError, ValueError):
raise Exception(f'Image_quality must be an integer from 1 to 100, not "{quality}"')
if not 1 <= self.quality <= 100:
raise Exception(f"Image_quality must be from 1 to 100, not {self.quality}")
self.search = search_path.split(" ")
self.verbose = verbose or os.environ.get("KLAMMERTEXT_IMAGE_CACHE_DISPLAY")
self.required_format = { "html" : ["jpg", "jpeg", "JPEG", "png", "gif", "webp"],
@@ -75,6 +110,18 @@ class Image_cache:
return f"{self.dir}/{filename}.{cache_ext}"
def photograph_cache_filename(self, basename):
# The quality is part of the name, so changing it re-encodes and
# two documents at different qualities do not fight over one file.
return f"{self.dir}/{basename}.q{self.quality}.jpg"
def reencode(self, target, source_filename):
"""Whether this source is written to the cache as a quality-N JPEG."""
return self.quality < 100 \
and target in self.required_format \
and source_filename != self.error_image_filename \
and is_photograph(source_filename)
def find_file(self, target, basename):
result = None
candidates = []
@@ -131,6 +178,8 @@ class Image_cache:
if src[:2] == "..":
src = source_filename
dest = os.path.splitext(cached_filename)[1]
if cached_filename.endswith(f".q{self.quality}.jpg"):
dest = f"{dest} (quality {self.quality})"
dim = "" if width == new_width else f" [{new_width}x{new_height}]"
desc = "(copied)" if copied else f"-> {dest}{dim}"
#print(f"{Image_cache.verbose_label} To cache: {src} [{width}x{height}] {desc}")
@@ -148,7 +197,20 @@ class Image_cache:
cached_filename = f"{os.path.dirname(cached_filename)}/{os.path.basename(source_filename)}"
copied = False
if resize and width > Image_cache.max_width:
if self.reencode(target, source_filename):
cached_filename = self.photograph_cache_filename(basename)
buf = oiio.ImageBuf(source_filename)
if buf.has_error:
raise Exception(f'Cannot read image "{source_filename}": {buf.geterror()}')
if width > Image_cache.max_width:
new_width = Image_cache.max_width
new_height = int(round(new_width * float(height) / float(width)))
roi = oiio.ROI(0, new_width, 0, new_height, 0, 1, 0, buf.nchannels)
buf = oiio.ImageBufAlgo.resize(buf, roi=roi)
buf.specmod().attribute("CompressionQuality", self.quality)
if not buf.write(cached_filename):
raise Exception(f'Cannot write "{cached_filename}": {buf.geterror()}')
elif resize and width > Image_cache.max_width:
new_width = Image_cache.max_width
new_height = int(round(new_width * float(height) / float(width)))
if source_filename.endswith(".pdf"):
@@ -190,6 +252,11 @@ class Image_cache:
def get(self, target, basename):
cached_filename = self.find_cached_file(target, basename)
source_filename = self.find_file(target, basename)
if self.reencode(target, source_filename):
# A copy cached at another quality (or untouched, by an earlier
# run at 100) does not serve: the wanted file is the quality-N
# one, and cache_update_required() makes it if it is missing.
cached_filename = self.photograph_cache_filename(basename)
if cache_update_required(source_filename, cached_filename):
cached_filename, width, height = self.update_cache(target, basename, source_filename)
else: