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:
1 parent
760fda5470
commit
9b0d0d7897
34 files changed
+895
-394
No files matched your search
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user