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>
This commit is contained in:
232
sks/image/image_cache.py
Normal file
232
sks/image/image_cache.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user