Files
klammertext/sks/image/image_cache.py
Andy Kopra 2ba7ceee7a Initial commit: Klammertext source distribution
Curated source subset assembled by klammertext-dev's doc/make_dist.sh: the Klammermachine (mac), the Standard Klammer Set (sks), the commands (com), editor plugins and install guides (doc), a test subset (tst), and lib/bin placeholders. Builds with 'make -C com'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:32:38 +02:00

233 lines
8.9 KiB
Python

"""
Does every target has a preferred format?
HTML - JPEG (or SVG if available?)
LaTeX - PDF or EPS?
Plaintext - A URL?
EPUB - JPEG?
"""
import sys, os, glob, shutil, subprocess, re, codecs
import filecmp
import pdf_image
import kutil
import OpenImageIO as oiio
def find_ext(filenames, ext):
for f in filenames:
if f.endswith(ext):
return f
return None
def cache_update_required(source, cached):
return cached is None \
or not(os.path.exists(cached)) \
or (os.path.getmtime(cached) < os.path.getmtime(source))
def image_dimensions(filename):
resize = True
if filename.endswith(".pdf"):
width, height = pdf_image.dimensions(filename)
resize = False
else:
inp = oiio.ImageInput.open(filename)
if not inp:
raise Exception(f'Cannot read image "{filename}": {oiio.geterror()}')
spec = inp.spec()
width, height = spec.width, spec.height
inp.close()
return width, height, resize
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):
#self.cache_dir = kutil.cache_directory(
self.dir = kutil.cache_directory(cache_dir, "_image_cache")
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"],
"pdf" : ["png", "jpg", "jpeg", "JPEG", "pdf", "eps"],
"tex" : ["png", "jpg", "jpeg", "JPEG", "pdf", "eps"],
"latex" : ["png", "jpg", "jpeg", "JPEG", "pdf", "eps"] }
self.error_image_filename = f"{self.dir}/{Image_cache.error_marker_filename}"
if not os.path.exists(self.dir):
os.makedirs(self.dir)
self.search = self.search + [f"{os.path.dirname(cache_dir)}/imgsrc"]
def make_error_marker(self):
if not os.path.exists(self.error_image_filename):
err_buf = oiio.ImageBuf(oiio.ImageSpec(300, 100, 3, oiio.UINT8))
oiio.ImageBufAlgo.fill(err_buf, (1.0, 0.0, 0.0))
err_buf.write(self.error_image_filename)
def construct_cache_filename(self, target, filename):
basename, ext = os.path.splitext(filename)
required = self.required_format[target]
if ext in self.required_format[target]:
cache_ext = ext
else:
cache_ext = required[0]
return f"{self.dir}/{filename}.{cache_ext}"
def find_file(self, target, basename):
result = None
candidates = []
matches = []
for path in self.search:
candidates += glob.glob(f"{path}/{basename}.*")
candidates = sorted(candidates, key=lambda p: 0 if p.endswith(".png") else 1)
if False:
print("Candidates:")
for c in candidates:
print(f" {c}")
if candidates:
matches = []
for ext in self.required_format[target]:
candidate = find_ext(candidates, ext)
if candidate:
matches.append(candidate)
if not matches:
result = candidates[0]
else:
kutil.msg(kutil.red_text(f'No image with basename "{basename}" found in search path:'))
kutil.msg(kutil.red_text(" | " + " | ".join(self.search) + " | "))
self.make_error_marker()
matches = [self.error_image_filename]
result = matches[0] if matches else candidates[0]
"""
if len(matches) > 1:
target = f"for .{target} format"
print(f'{Image_cache.verbose_label} Warning: Multiple matches for "{basename}" {target}:')
sp = " "*len(Image_cache.verbose_label)
for f in matches:
print(f"{sp} {f}")
"""
return result
def find_cached_file(self, target, basename):
exts = self.required_format.get(target)
if exts is None:
raise Exception(
f'No preferred image format defined for target "{target}"')
result = None
for ext in exts:
cached_filename = f"{self.dir}/{basename}.{ext}"
if os.path.exists(cached_filename):
return cached_filename
return None
def describe_caching(self, source_filename, cached_filename, copied,
width, height, new_width, new_height):
src = os.path.relpath(source_filename)
if src[:2] == "..":
src = source_filename
dest = os.path.splitext(cached_filename)[1]
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}")
kutil.msg("To cache: {src} [{width}x{height}] {desc}")
def update_cache(self, target, basename, source_filename):
cached_filename = self.construct_cache_filename(target, basename)
width, height, resize = image_dimensions(source_filename)
new_width = width
new_height = height
source_format = os.path.splitext(source_filename)[-1][1:]
#print("Source:", source_format, basename)
if source_format in self.required_format[target]:
cached_filename = f"{os.path.dirname(cached_filename)}/{os.path.basename(source_filename)}"
copied = False
if 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"):
pdf_image.convert(source_filename, cached_filename)
buf = oiio.ImageBuf(cached_filename)
else:
buf = oiio.ImageBuf(source_filename)
roi = oiio.ROI(0, new_width, 0, new_height, 0, 1, 0, buf.nchannels)
resized = oiio.ImageBufAlgo.resize(buf, roi=roi)
resized.write(cached_filename)
elif source_format in self.required_format[target]:
#print("COPY", source_filename, cached_filename)
if not os.path.exists(cached_filename) or \
not filecmp.cmp(source_filename, cached_filename, shallow=False):
shutil.copy2(source_filename, cached_filename)
copied = True
else:
try:
if source_filename.endswith(".pdf"):
pdf_image.convert(source_filename, cached_filename)
else:
buf = oiio.ImageBuf(source_filename)
if not buf.has_error:
buf.write(cached_filename)
else:
raise Exception(buf.geterror())
except Exception as err:
print(f'Cannot convert "{source_filename}" to '
f'"{os.path.splitext(cached_filename)[-1][1:]}":')
print(" ", err)
sys.exit(1)
if self.verbose:
self.describe_caching(source_filename, cached_filename, copied,
width, height, new_width, new_height)
return cached_filename, new_width, new_height
def get(self, target, basename):
cached_filename = self.find_cached_file(target, basename)
source_filename = self.find_file(target, basename)
if cache_update_required(source_filename, cached_filename):
cached_filename, width, height = self.update_cache(target, basename, source_filename)
else:
width, height, _ = image_dimensions(cached_filename)
if self.verbose:
base = os.path.basename(cached_filename)
#print(f"{Image_cache.verbose_label} From cache: {base} [{width}x{height}]")
kutil.msg("From cache: {base} [{width}x{height}]")
# print("image_cache:", cached_filename, Image_cache.error_marker_filename)
return cached_filename, width, height, cached_filename == Image_cache.error_marker_filename
def clear(self):
shutil.rmtree(self.dir)
os.makedirs(self.dir)
if __name__ == '__main__':
def getter(format, basename):
C.get(format, basename)
C.get(format, basename)
def label(format=""):
print("-"*80)
print(format)
home = os.environ.get("HOME")
C = Image_cache(f"{home}",
f"{home}/projects/klammertext/K/doc/handbook/imgsrc".split())
a = "heringsdorf_stairwell"
b = "usedom_clouds"
C.clear()
label("HTML")
getter("html", a)
getter("html", b)
label("LATEX")
getter("latex", a)
getter("latex", b)
label()