From 25871839a2885f5479bdd7efd2e78ca0aa1a7e09 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Fri, 1 Oct 2021 11:37:50 +0200 Subject: Add thumbnail cache inspection tools --- tools/.gitignore | 1 + tools/Makefile | 13 ++ tools/clean-up-thumbnail-cache.sh | 15 +++ tools/pnginfo.c | 251 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 tools/.gitignore create mode 100644 tools/Makefile create mode 100755 tools/clean-up-thumbnail-cache.sh create mode 100644 tools/pnginfo.c (limited to 'tools') diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..8463b6e --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1 @@ +/pnginfo diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..d024620 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,13 @@ +SHELL = /bin/sh +# libjq 1.6 lacks a pkg-config file, and there is no release in sight. +CFLAGS = -g -O2 -Wall -Wextra `pkg-config --cflags $(deps)` +LDFLAGS = -ljq `pkg-config --libs $(deps)` + +deps = libpng +targets = pnginfo + +all: $(targets) +clean: + rm -f $(targets) + +.PHONY: all clean diff --git a/tools/clean-up-thumbnail-cache.sh b/tools/clean-up-thumbnail-cache.sh new file mode 100755 index 0000000..490e317 --- /dev/null +++ b/tools/clean-up-thumbnail-cache.sh @@ -0,0 +1,15 @@ +#!/bin/sh -e +# Remove thumbnails with URIs pointing to at this moment non-existing files. +make pnginfo + +pnginfo=$(pwd)/pnginfo cache_home=${XDG_CACHE_HOME:-$HOME/.cache} +for size in normal large x-large xx-large; do + cd "$cache_home/thumbnails/$size" 2>/dev/null || continue + find . -name '*.png' -print0 | PNGINFO_SKIP_TRAILING=1 xargs -0 "$pnginfo" \ + | jq -r '.info.texts."Thumb::URI"' | grep '^file://' \ + | grep -v '^file:///run/media/[^/]*/NIKON/' \ + | perl -MURI -MURI::Escape -MDigest::MD5 -lne \ + 'print Digest::MD5->new()->add($_)->hexdigest . ".png" + if !stat(uri_unescape(URI->new($_)->path))' \ + | xargs rm +done diff --git a/tools/pnginfo.c b/tools/pnginfo.c new file mode 100644 index 0000000..afb3d50 --- /dev/null +++ b/tools/pnginfo.c @@ -0,0 +1,251 @@ +// +// pnginfo.c: acquire information about PNG files in JSON format +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include +#include + +#include +#include +#include +#include +#include +#include + +// --- Utilities --------------------------------------------------------------- + +#if defined __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) __attribute__((format (printf, x, y))) +#else // ! __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) +#endif // ! __GNUC__ + +static char *strfmt(const char *format, ...) ATTRIBUTE_PRINTF(1, 2); + +static char * +strvfmt(const char *format, va_list ap) +{ + va_list aq; + va_copy(aq, ap); + int size = vsnprintf(NULL, 0, format, aq); + va_end(aq); + if (size < 0) + return NULL; + + char buf[size + 1]; + size = vsnprintf(buf, sizeof buf, format, ap); + if (size < 0) + return NULL; + + return strdup(buf); +} + +static char * +strfmt(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + char *result = strvfmt(format, ap); + va_end(ap); + return result; +} + +// --- Analysis ---------------------------------------------------------------- + +static void +redirect_libpng_error(png_structp pngp, const char* message) +{ + char **storage = png_get_error_ptr(pngp); + *storage = strfmt("%s", message); +} + +static jv +retrieve_texts(png_structp pngp, png_infop infop) +{ + int texts_len = 0; + png_textp texts = NULL; + png_get_text(pngp, infop, &texts, &texts_len); + + jv o = jv_object(); + for (int i = 0; i < texts_len; i++) { + png_textp text = texts + i; + o = jv_object_set(o, jv_string(text->key), jv_string(text->text)); + } + return o; +} + +static jv +extract_chunks(png_structp pngp, png_infop infop) +{ + jv o = jv_object(); + + // With a fully separated infop from the end of the file, + // png_get_IHDR() causes a warning and an error. Avoid that. + uint32_t width = 0, height = 0; + int bit_depth = 0, color_type = 0, interlace_method = 0; + if (png_get_image_width(pngp, infop) + && png_get_IHDR(pngp, infop, &width, &height, &bit_depth, &color_type, + &interlace_method, NULL, NULL)) { + const char *color = "?"; + switch (color_type) { + case 0: + case 4: color = "gray"; break; + case 2: + case 6: color = "rgb"; break; + case 3: color = "palette"; break; + } + + o = jv_object_set(o, jv_string("depth"), jv_number(bit_depth)); + o = jv_object_set(o, jv_string("width"), jv_number(width)); + o = jv_object_set(o, jv_string("height"), jv_number(height)); + o = jv_object_set(o, jv_string("interlace"), jv_bool(interlace_method)); + o = jv_object_set(o, jv_string("color"), jv_string(color)); + o = jv_object_set(o, jv_string("alpha"), + jv_bool((color_type & PNG_COLOR_MASK_ALPHA) || + png_get_valid(pngp, infop, PNG_INFO_tRNS))); + } + + double gamma = 0; + if (png_get_gAMA(pngp, infop, &gamma)) + // XXX: Might want to round it or store as integer. + o = jv_object_set(o, jv_string("gamma"), jv_number(1 / gamma)); + + // Note that sRGB overrides both gAMA and cHRM. + int intent = -1; + if (png_get_sRGB(pngp, infop, &intent)) { + const char *name = "?"; + switch (intent) { + case PNG_sRGB_INTENT_PERCEPTUAL: name = "perceptual"; break; + case PNG_sRGB_INTENT_RELATIVE: name = "relative"; break; + case PNG_sRGB_INTENT_SATURATION: name = "saturation"; break; + case PNG_sRGB_INTENT_ABSOLUTE: name = "absolute"; break; + } + o = jv_object_set(o, jv_string("sRGB"), jv_string(name)); + } + + // Note that iCCP overrides both gAMA and cHRM. + char *name = NULL; + png_bytep profile = NULL; + uint32_t profile_len = 0; + if (png_get_iCCP(pngp, infop, &name, NULL, &profile, &profile_len)) + o = jv_object_set(o, jv_string("ICC"), jv_string(name)); + + jv set = jv_object(); + png_unknown_chunkp unknowns = NULL; + int unknowns_len = png_get_unknown_chunks(pngp, infop, &unknowns); + for (int i = 0; i < unknowns_len; i++) + set = jv_object_set(set, + jv_string((const char *) unknowns[i].name), jv_true()); + + jv a = jv_array(); + jv_object_keys_foreach(set, key) + a = jv_array_append(a, jv_copy(key)); + o = jv_object_set(o, jv_string("chunks"), a); + jv_free(set); + + o = jv_object_set(o, jv_string("texts"), retrieve_texts(pngp, infop)); + return o; +} + +static jv +do_file(const char *filename, volatile jv o) +{ + png_bytep volatile buffer = NULL; + png_bytepp volatile rows = NULL; + + char *volatile err = NULL; + FILE *fp = fopen(filename, "rb"); + if (!fp) { + err = strfmt("%s", strerror(errno)); + goto error; + } + + png_structp pngp = png_create_read_struct(PNG_LIBPNG_VER_STRING, + (png_voidp) &err, redirect_libpng_error, NULL); + if (!pngp) { + err = strfmt("%s", strerror(errno)); + goto error_png; + } + + // We want to read these separately, which libpng allows and spng doesn't. + png_infop infop = png_create_info_struct(pngp); + png_infop endp = png_create_info_struct(pngp); + if (!infop || !endp) { + err = strfmt("%s", strerror(errno)); + goto error_decode; + } + + png_init_io(pngp, fp); + if (setjmp(png_jmpbuf(pngp))) + goto error_decode; + + // Following the list of PNG_INFO_*, we just scan for their existence. + png_byte basic[] = + "cHRM\0bKGD\0hIST\0pHYs\0oFFs\0tIME\0pCAL\0sPLT\0sCAL\0eXIf"; + png_set_keep_unknown_chunks(pngp, PNG_HANDLE_CHUNK_ALWAYS, basic, + sizeof basic / 5); + png_set_keep_unknown_chunks(pngp, PNG_HANDLE_CHUNK_ALWAYS, NULL, 0); + + png_read_info(pngp, infop); + o = jv_object_set(o, jv_string("info"), extract_chunks(pngp, infop)); + + // Run over the data in the simplest possible manner. + size_t height = png_get_image_height(pngp, infop); + size_t row_bytes = png_get_rowbytes(pngp, infop); + + if (!(buffer = calloc(height, row_bytes)) + || !(rows = calloc(height, sizeof(png_bytep)))) + png_error(pngp, strerror(errno)); + for (size_t i = 0; i < height; i++) + rows[i] = buffer + i * row_bytes; + + if (!getenv("PNGINFO_SKIP_TRAILING")) { + png_read_image(pngp, rows); + + png_read_end(pngp, endp); + o = jv_object_set(o, jv_string("end"), extract_chunks(pngp, endp)); + } + +error_decode: + free(buffer); + free(rows); + png_destroy_read_struct(&pngp, &infop, &endp); +error_png: + fclose(fp); +error: + if (err) { + o = jv_object_set(o, jv_string("error"), jv_string(err)); + free(err); + } + return o; +} + +int +main(int argc, char *argv[]) +{ + // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes. + // Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo + for (int i = 1; i < argc; i++) { + const char *filename = argv[i]; + + jv o = jv_object(); + o = jv_object_set(o, jv_string("filename"), jv_string(filename)); + o = do_file(filename, o); + jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */); + fputc('\n', stdout); + } + return 0; +} -- cgit v1.2.3-70-g09d2