Handling errors¶
Every call that can fail says so through its return value, and a program decides from that value alone what to do next: skip the input, fix the call, or try again. This page lists every code with that decision, shows which step of a load returns which code, explains the detail message a failed load leaves behind, and says what a failure leaves in the outputs and in the context.
Reading a code¶
Every function that can fail returns a
ph_error_t:
PH_SUCCESS, which is 0, or a negative code. A test for
!= PH_SUCCESS catches every failure; there is no code that means "succeeded with a
warning". Every call whose output is useless without its code, such as
ph_create(), a load, a hash, a batch or a text
conversion, is declared PH_NODISCARD, so the compiler warns when its result is dropped.
The setters are not.
The values are part of the ABI and stay fixed for the whole 2.x series: a binding or a log may store the number. A new code is added at the end with the next free value, and -2 and -4 are never assigned.
ph_get_error_string() turns a code into a fixed
English sentence, the same for every call that returns it, such as "File could not be
opened or read" for PH_ERR_IO. It never returns NULL: a number that is not a code,
including -2 and -4, reads "Unknown error". The string is static and needs no freeing.
Comparisons are the exception to the rule. A distance or similarity returned as a number
signals a refused comparison with -1 rather than a code, and only
ph_radial_similarity() and
ph_histogram_intersection() return a
ph_error_t (Comparing hashes).
The codes¶
The codes fall into four groups by what the caller does about them. A mistake in the call is fixed in the code and is never retried. A verdict on the input is final for that input in that build and configuration: the same bytes fail the same way every time, so the input is skipped and reported. Out of memory is the one transient code. The last two codes report how a batch or a comparison ended, not a fault.
| Code | Value | When | What the caller does |
|---|---|---|---|
PH_ERR_INVALID_ARGUMENT |
-3 | any function: a NULL pointer, a value outside the documented range, an empty buffer, a digest of the wrong kind | fixes the call; retrying the same call fails the same way |
PH_ERR_EMPTY_IMAGE |
-5 | a hash on a context with no image: nothing loaded yet, or the last file or buffer load failed | checks the load's result before hashing |
PH_ERR_REQUIRES_COLOR |
-11 | ColorHash or ColorMoments on a one-channel image, as ph_context_set_load_grayscale() loads it |
loads in color for the color hashes; a context for gray hashes and one for color |
PH_ERR_IO |
-10 | ph_load_from_file(): the path is missing, unreadable, not a regular file, or empty |
skips the path and reports it with the detail message |
PH_ERR_UNSUPPORTED_FORMAT |
-7 | a load: the bytes are in no format the library recognizes, such as TIFF or a text file | skips the input |
PH_ERR_CORRUPT_DATA |
-8 | a load: a recognized format whose data is damaged or truncated, or an animated WebP | skips the input; it fails again on retry |
PH_ERR_DECODER_UNAVAILABLE |
-9 | a load: WebP, in a build without libwebp | uses a build with libwebp; ph_can_use_webp() tells in advance |
PH_ERR_IMAGE_TOO_LARGE |
-6 | a load: the image is over the context's max_pixels, over a fixed limit, or the encoded data is over 2 GiB |
skips the input, or raises max_pixels for input it trusts |
PH_ERR_ALLOCATION_FAILED |
-1 | anything that allocates: ph_create(), loads, hashes, batches |
retries later, or with less in flight: fewer batch threads, a lower max_pixels |
PH_ERR_CANCELLED |
-12 | ph_hash_files_ex(), ph_hash_buffers_ex(): the should_continue callback stopped the batch |
treats the items marked with it as not processed |
PH_ERR_NO_STRUCTURE |
-13 | ph_radial_similarity(): a digest is the all-zero Radial digest of an image with no angular structure |
treats the pair as not comparable by Radial, and compares it by another hash |
The decision is the same whichever function returned the code. The
table under ph_error_t in the API reference lists, for
every code, each function that can return it.
Where a load fails¶
A load runs its steps in order, and each step can fail with its own codes, on the dashed branches. A failure at any step ends the load with no image in the context, so the codes of a later step are never seen for an input an earlier step refused.
flowchart TD
A[Check the arguments] --> F[Open the file]
F --> R[Recognize the format]
R --> H[Read the header]
H --> D[Decode and orient]
D --> L[Image loaded]
A -.-> EA([INVALID_ARGUMENT])
F -.-> EF([IO])
R -.-> ER([UNSUPPORTED_FORMAT<br/>DECODER_UNAVAILABLE<br/>IMAGE_TOO_LARGE])
H -.-> EH([IMAGE_TOO_LARGE<br/>CORRUPT_DATA])
D -.-> ED([CORRUPT_DATA<br/>ALLOCATION_FAILED])
- The file is opened once, and everything about the path is decided before a decoder
sees a byte: a missing path, a dangling link, a directory and an empty file are all
PH_ERR_IO, identically on POSIX and on Windows.ph_load_from_memory()skips this step, and an empty buffer isPH_ERR_INVALID_ARGUMENTat the first. - The format is read from the first bytes, never from the file name. Data no decoder
claims is
PH_ERR_UNSUPPORTED_FORMAT; encoded data over 2 GiB is refused here, before any format is looked at. - The header gives the dimensions, and the size limits are checked against them before the pixels are allocated: a decompression bomb costs a header read, not its pixels.
- The decode: once a decoder has claimed the data, every failure from there on is a
verdict on that data,
PH_ERR_CORRUPT_DATA, and no other decoder tries it. Running out of memory is the exception,PH_ERR_ALLOCATION_FAILED, here and while the image is turned by its orientation tag.
ph_load_from_pixels() has none of these steps.
It fails only on its arguments, the size limit and memory, and a failure keeps the image
loaded before it. Loading images says which formats each
build decodes.
A hash fails before it computes anything, in this order: PH_ERR_INVALID_ARGUMENT for an
invalid argument, PH_ERR_EMPTY_IMAGE when no image is loaded, and PH_ERR_REQUIRES_COLOR
for a color hash of a gray image. After that the only failure left is memory.
The detail of a failed load¶
A code says what kind of failure it was; it cannot say which file, or what the operating
system or the decoder said about it.
ph_get_last_error_message() adds that for
the last load on a context:
| Code | What the detail adds |
|---|---|
PH_ERR_IO |
the path and the reason, such as Cannot open 'a.jpg': No such file or directory or Cannot read 'photos': not a regular file; the path is whole when the system can open a path that long, and otherwise keeps its two ends around ..., so the reason is always there |
PH_ERR_CORRUPT_DATA |
the decoder's own complaint, such as libjpeg-turbo's Premature end of JPEG file |
PH_ERR_UNSUPPORTED_FORMAT |
the reason stb_image, the decoder of last resort, gave, such as unknown image type |
PH_ERR_IMAGE_TOO_LARGE |
which limit the image is over |
The message belongs to loads. Every load clears it on the way in, so after a successful
load it is an empty string; a failed load leaves its detail, or an empty string when the
failure has nothing to add to its code, as with every failed ph_load_from_pixels().
Hashes, setters and comparisons neither write nor clear it: after a failed hash, the
message still describes the load before it.
The context owns the string. Its text is replaced by the next load on that context and
is gone after ph_free(), so a program that keeps it copies
it first. It is in English and meant for a log or a person, not for a program to parse;
the code is what a program branches on. Like the rest of a context, it is read on the
thread that uses the context, not alongside a load on another.
What a failure leaves behind¶
An output is read only after a PH_SUCCESS. A call that fails leaves its outputs as they
were, and the API reference states it for every hash and for the functions that read a
hash back from text. Two calls write several results and answer for them differently:
ph_compute_multi()stops at the first algorithm that fails, and the slots of those before it already hold their hashes. Nothing in the array says how far it got, so on a failure the whole array is unusable.- A batch item that fails has every hash slot zeroed and its code in
status.
A setter that refuses a value leaves the configuration as it was: no value is clamped
into range or half applied (Configuring a context). A failed file or
buffer load drops the image the context held, so a hash after it fails with
PH_ERR_EMPTY_IMAGE instead of hashing the image before; a failed pixel load keeps it.
In code¶
The example loads and hashes each path it is given and prints, for each, the code's
sentence, what to do about it and the detail of the load. The decisions are one
switch over the codes:
/* What a caller does with each outcome of a load or a hash. */
static const char *what_to_do(ph_error_t err) {
switch (err) {
case PH_SUCCESS:
return "hashed";
case PH_ERR_IO:
return "path unusable (missing, unreadable, not a file, empty): skip it";
case PH_ERR_UNSUPPORTED_FORMAT:
return "not an image format the library reads: skip it";
case PH_ERR_CORRUPT_DATA:
return "a damaged image: skip it, retrying will not help";
case PH_ERR_DECODER_UNAVAILABLE:
return "a format this build has no decoder for: use a build with it";
case PH_ERR_IMAGE_TOO_LARGE:
return "over the max_pixels limit: raise it only for trusted input";
case PH_ERR_ALLOCATION_FAILED:
return "out of memory: retry later or with less in flight";
case PH_ERR_REQUIRES_COLOR:
return "a color hash of a grayscale load: load in color";
case PH_ERR_EMPTY_IMAGE:
return "no image loaded -- none yet, or the last load failed: load before hashing";
case PH_ERR_INVALID_ARGUMENT:
return "an invalid argument: a bug in the calling code";
default:
return "unexpected";
}
}
Each path is loaded into the same context, the hash runs only after a successful load, and the detail is printed only for a failure, since after a success it is empty:
for (int i = 1; i < argc; i++) {
ph_error_t err = ph_load_from_file(ctx, argv[i]);
uint64_t hash = 0;
if (err == PH_SUCCESS) {
err = ph_compute_phash(ctx, &hash);
}
printf("%s\n %s: %s\n", argv[i], ph_get_error_string(err), what_to_do(err));
/* The last load's own words: the decoder's complaint, the limit it hit. */
const char *detail = ph_get_last_error_message(ctx);
if (err != PH_SUCCESS && detail[0] != '\0') {
printf(" detail: %s\n", detail);
}
if (err == PH_SUCCESS) {
int width = 0, height = 0, channels = 0;
ph_context_get_dimensions(ctx, &width, &height, &channels);
printf(" %dx%d, %d channels, pHash %016llx\n", width, height, channels,
(unsigned long long)hash);
}
}
When the last path fails, the context holds no image, and a hash says so rather than hashing an earlier one:
/* A hash on a context whose last load failed is refused, not computed from a
* previous image. */
if (!ph_is_loaded(ctx)) {
uint64_t hash = 0;
ph_error_t err = ph_compute_phash(ctx, &hash);
printf("hash with no image loaded\n %s: %s\n", ph_get_error_string(err), what_to_do(err));
}
The context's max_pixels is set low in this example, so that an ordinary photograph shows
PH_ERR_IMAGE_TOO_LARGE as well.
Errors in a batch¶
A batch reports twice: the call returns a code for the batch as a whole, and every item
carries its own in status. One unreadable file fails only its own item.
| The call returns | Which means | The items |
|---|---|---|
PH_SUCCESS |
every item was processed | each status holds the code its load or hash returned |
PH_ERR_INVALID_ARGUMENT |
the call itself is malformed: the flags, the thread count, the options | nothing was written |
PH_ERR_ALLOCATION_FAILED |
not one worker could start | no item was worked on |
PH_ERR_CANCELLED |
should_continue stopped the batch |
started items have their real status; the rest have PH_ERR_CANCELLED |
An item's status takes the same codes a load and
ph_compute_multi() return, and an entry with a NULL
path, a NULL buffer or a zero length is a PH_ERR_INVALID_ARGUMENT of that item alone.
The hashes of an item are valid only when its status is PH_SUCCESS.
if (err != PH_SUCCESS && err != PH_ERR_CANCELLED) {
/* The batch as a whole failed (bad arguments, no memory for the pool). */
fprintf(stderr, "batch failed: %s\n", ph_get_error_string(err));
ph_free(config);
free(items);
return 1;
}
/* A file that could not be read or decoded fails on its own; the others are hashed.
* After a cancellation, the files never started carry PH_ERR_CANCELLED. */
int failed = 0, skipped = 0;
for (size_t i = 0; i < n; i++) {
if (items[i].status == PH_SUCCESS) {
char dhex[17], phex[17];
if (ph_hash_to_hex(items[i].hashes[0], dhex, sizeof(dhex)) == PH_SUCCESS &&
ph_hash_to_hex(items[i].hashes[1], phex, sizeof(phex)) == PH_SUCCESS) {
printf("%s dHash %s pHash %s\n", items[i].path, dhex, phex);
}
} else if (items[i].status == PH_ERR_CANCELLED) {
skipped++;
} else {
printf("%s %s\n", items[i].path, ph_get_error_string(items[i].status));
failed++;
}
}
printf("%zu hashed, %d failed, %d not started\n", n - (size_t)(failed + skipped), failed,
skipped);
An item has a code and no detail message: each worker's context is freed when the batch
ends. A program that wants the detail for a failed item loads that one file again with
ph_load_from_file() and reads
ph_get_last_error_message().
Hashing many files covers the rest of a batch.