""" 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()