7 n__icc_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

src/n_icc.c

Finding the ICC colour profile embedded in an image file. More...

Functions

Name
int _tiff_file(FILE * file, unsigned char ** icc, size_t * icc_len)
helper: read the TIFF profile of a file where its directory says, so a large TIFF is not read whole to find a profile at its end
size_t _le32(const unsigned char * p)
helper: a little endian 32 bit value
int _give(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: hand a found profile back, a copy of len bytes at data, checked
int _from_webp(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: the profile of a WebP, from its ICCP chunk
int _from_tiff(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: the profile of a TIFF held whole in memory
int _from_png(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: the profile of a PNG, from its iCCP chunk, inflated
int _from_jpeg(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: the profile of a JPEG, from its APP2 chunks put back in order
int _from_buffer(const unsigned char * data, size_t len, unsigned char ** icc, size_t * icc_len)
helper: n_icc_from_buffer(), with N_ICC_MORE when the bytes ran out before the answer
size_t _be32(const unsigned char * p)
helper: a big endian 32 bit value
size_t _be16(const unsigned char * p)
helper: a big endian 16 bit value

Attributes

Name
const char[12] n_icc_jpeg_id
JPEG APP2 identifier of a profile chunk, terminator included.

Defines

Name
N_ICC_TIFF_TAG
TIFF tag of an embedded ICC profile.
N_ICC_MORE
What a parser answers when it ran off the end of the bytes it was given before it could say: the file reader then reads more.
N_ICC_MIN_BYTES
Smallest ICC profile: its 128 byte header and a tag count.
N_ICC_FIRST_READ
Bytes n_icc_from_file() reads first; a profile, when there is one, is almost always in them.

Detailed Description

Finding the ICC colour profile embedded in an image file.

Author: Castagnier Mickael

Version: 1.0

Date: 28/09/2026

Functions Documentation

function _tiff_file

static int _tiff_file(
    FILE * file,
    unsigned char ** icc,
    size_t * icc_len
)

helper: read the TIFF profile of a file where its directory says, so a large TIFF is not read whole to find a profile at its end

function _le32

static size_t _le32(
    const unsigned char * p
)

helper: a little endian 32 bit value

function _give

static int _give(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: hand a found profile back, a copy of len bytes at data, checked

Return: 1, or -1 when it is not a profile or there is no memory

function _from_webp

static int _from_webp(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: the profile of a WebP, from its ICCP chunk

function _from_tiff

static int _from_tiff(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: the profile of a TIFF held whole in memory

function _from_png

static int _from_png(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: the profile of a PNG, from its iCCP chunk, inflated

function _from_jpeg

static int _from_jpeg(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: the profile of a JPEG, from its APP2 chunks put back in order

function _from_buffer

static int _from_buffer(
    const unsigned char * data,
    size_t len,
    unsigned char ** icc,
    size_t * icc_len
)

helper: n_icc_from_buffer(), with N_ICC_MORE when the bytes ran out before the answer

function _be32

static size_t _be32(
    const unsigned char * p
)

helper: a big endian 32 bit value

function _be16

static size_t _be16(
    const unsigned char * p
)

helper: a big endian 16 bit value

Attributes Documentation

variable n_icc_jpeg_id

static const char[12] n_icc_jpeg_id = {'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', 0};

JPEG APP2 identifier of a profile chunk, terminator included.

Macros Documentation

define N_ICC_TIFF_TAG

#define N_ICC_TIFF_TAG 34675

TIFF tag of an embedded ICC profile.

define N_ICC_MORE

#define N_ICC_MORE 2

What a parser answers when it ran off the end of the bytes it was given before it could say: the file reader then reads more.

define N_ICC_MIN_BYTES

#define N_ICC_MIN_BYTES 132u

Smallest ICC profile: its 128 byte header and a tag count.

define N_ICC_FIRST_READ

#define N_ICC_FIRST_READ (256u * 1024u)

Bytes n_icc_from_file() reads first; a profile, when there is one, is almost always in them.

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include "nilorea/n_icc.h"
#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_tiff.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>

#define N_ICC_MIN_BYTES 132u

#define N_ICC_MORE 2

#define N_ICC_FIRST_READ (256u * 1024u)

#define N_ICC_TIFF_TAG 34675

static const char n_icc_jpeg_id[12] = {'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', 0};

static size_t _be16(const unsigned char* p) {
    return ((size_t)p[0] << 8) | (size_t)p[1];
}

static size_t _be32(const unsigned char* p) {
    return ((size_t)p[0] << 24) | ((size_t)p[1] << 16) | ((size_t)p[2] << 8) | (size_t)p[3];
}

static size_t _le32(const unsigned char* p) {
    return ((size_t)p[3] << 24) | ((size_t)p[2] << 16) | ((size_t)p[1] << 8) | (size_t)p[0];
}

int n_icc_is_profile(const unsigned char* data, size_t len) {
    size_t declared = 0;

    if (!data || len < N_ICC_MIN_BYTES)
        return 0;
    declared = _be32(data);
    if (declared < N_ICC_MIN_BYTES || declared > len)
        return 0;
    return memcmp(data + 36, "acsp", 4) == 0;
}

static int _give(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    unsigned char* copy = NULL;

    if (len > N_ICC_MAX_BYTES || !n_icc_is_profile(data, len)) {
        n_log(LOG_ERR, "icc: an embedded profile of %zu bytes is not a valid profile", len);
        return -1;
    }
    copy = malloc(len);
    if (!copy)
        return -1;
    memcpy(copy, data, len);
    *icc = copy;
    *icc_len = len;
    return 1;
}

static int _from_jpeg(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    /* Where each numbered chunk is: numbers run from 1 to the count. */
    const unsigned char* chunk[256];
    size_t chunk_len[256];
    size_t at = 2, total = 0;
    int count = 0, seen = 0, n = 0;
    unsigned char* out = NULL;

    int complete = 0;

    memset(chunk, 0, sizeof(chunk));
    memset(chunk_len, 0, sizeof(chunk_len));
    while (at + 4 <= len) {
        unsigned marker = 0;
        size_t seglen = 0;

        if (data[at] != 0xFF) {
            /* Not a marker where one must be: nothing more can be read. */
            complete = 1;
            break;
        }
        /* Any number of fill bytes may come before a marker. */
        while (at < len && data[at] == 0xFF)
            at++;
        if (at >= len)
            break;
        marker = data[at++];
        /* Start of scan or end of image: the profile comes before both. */
        if (marker == 0xDA || marker == 0xD9) {
            complete = 1;
            break;
        }
        /* Markers with no length. */
        if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7))
            continue;
        if (at + 2 > len)
            break;
        seglen = _be16(data + at);
        if (seglen < 2) {
            complete = 1;
            break;
        }
        if (at + seglen > len)
            break;
        if (marker == 0xE2 && seglen >= 2 + 12 + 2 && memcmp(data + at + 2, n_icc_jpeg_id, 12) == 0) {
            int seq = data[at + 14], of = data[at + 15];

            if (seq < 1 || of < 1 || seq > of || (count && of != count) || chunk[seq]) {
                n_log(LOG_ERR, "icc: JPEG profile chunks do not agree");
                return -1;
            }
            count = of;
            chunk[seq] = data + at + 16;
            chunk_len[seq] = seglen - 16;
            total += chunk_len[seq];
            seen++;
        }
        at += seglen;
    }
    if (!complete && seen != count)
        return N_ICC_MORE;
    if (!seen)
        return complete ? 0 : N_ICC_MORE;
    if (seen != count || total > N_ICC_MAX_BYTES) {
        n_log(LOG_ERR, "icc: JPEG profile has %d of %d chunks", seen, count);
        return -1;
    }
    out = malloc(total ? total : 1);
    if (!out)
        return -1;
    total = 0;
    for (n = 1; n <= count; n++) {
        memcpy(out + total, chunk[n], chunk_len[n]);
        total += chunk_len[n];
    }
    if (!n_icc_is_profile(out, total)) {
        n_log(LOG_ERR, "icc: the JPEG's profile chunks do not make a profile");
        free(out);
        return -1;
    }
    *icc = out;
    *icc_len = total;
    return 1;
}

static int _from_png(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    size_t at = 8;

    while (at + 12 <= len) {
        size_t clen = _be32(data + at);
        const unsigned char* type = data + at + 4;
        const unsigned char* body = data + at + 8;

        if (clen > len - at - 12)
            return N_ICC_MORE;
        if (memcmp(type, "IDAT", 4) == 0 || memcmp(type, "IEND", 4) == 0)
            return 0;
        if (memcmp(type, "iCCP", 4) == 0) {
            size_t name = 0;
            z_stream zip;
            unsigned char* out = NULL;
            size_t room = 64u * 1024u;
            int zerr = Z_OK;

            /* A name of 1 to 79 characters, its terminator, and the
             * compression method, which must be 0, deflate. */
            while (name < clen && name < 80 && body[name])
                name++;
            if (name == 0 || name >= 80 || name + 2 > clen || body[name + 1] != 0) {
                n_log(LOG_ERR, "icc: a PNG iCCP chunk with a broken header");
                return -1;
            }
            memset(&zip, 0, sizeof(zip));
            if (inflateInit(&zip) != Z_OK)
                return -1;
            out = malloc(room);
            if (!out) {
                inflateEnd(&zip);
                return -1;
            }
            zip.next_in = (unsigned char*)(body + name + 2);
            zip.avail_in = (unsigned)(clen - name - 2);
            while (zerr == Z_OK) {
                if (zip.total_out == room) {
                    unsigned char* more = NULL;

                    if (room >= N_ICC_MAX_BYTES)
                        break;
                    more = realloc(out, room * 2u);
                    if (!more)
                        break;
                    out = more;
                    room *= 2u;
                }
                zip.next_out = out + zip.total_out;
                zip.avail_out = (unsigned)(room - zip.total_out);
                zerr = inflate(&zip, Z_NO_FLUSH);
            }
            inflateEnd(&zip);
            if (zerr != Z_STREAM_END || !n_icc_is_profile(out, zip.total_out)) {
                n_log(LOG_ERR, "icc: a PNG iCCP chunk that does not inflate to a profile");
                free(out);
                return -1;
            }
            *icc = out;
            *icc_len = zip.total_out;
            return 1;
        }
        at += 12 + clen;
    }
    return N_ICC_MORE;
}

static int _from_webp(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    size_t at = 12;

    while (at + 8 <= len) {
        size_t clen = _le32(data + at + 4);

        if (clen > len - at - 8)
            return N_ICC_MORE;
        /* The image data: an ICCP chunk comes before it. */
        if (memcmp(data + at, "VP8 ", 4) == 0 || memcmp(data + at, "VP8L", 4) == 0 ||
            memcmp(data + at, "ANIM", 4) == 0)
            return 0;
        if (memcmp(data + at, "ICCP", 4) == 0)
            return _give(data + at + 8, clen, icc, icc_len);
        /* Chunks are padded to an even length. */
        at += 8 + clen + (clen & 1u);
    }
    return N_ICC_MORE;
}

static int _from_tiff(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    N_TIFF* tiff = n_tiff_parse(data, len);
    const N_TIFF_ENTRY* entry = NULL;
    int result = 0;

    if (!tiff)
        return 0;
    entry = n_tiff_find(tiff, N_ICC_TIFF_TAG);
    /* UNDEFINED bytes, or BYTE as some writers put it. */
    if (entry && (entry->type == 7 || entry->type == 1))
        result = _give(data + entry->offset, entry->count, icc, icc_len);
    n_tiff_free(&tiff);
    return result;
}

static int _from_buffer(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    static const unsigned char png_sig[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};

    *icc = NULL;
    *icc_len = 0;
    if (len >= 4 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
        return _from_jpeg(data, len, icc, icc_len);
    if (len >= 8 && memcmp(data, png_sig, 8) == 0)
        return _from_png(data, len, icc, icc_len);
    if (len >= 12 && memcmp(data, "RIFF", 4) == 0 && memcmp(data + 8, "WEBP", 4) == 0)
        return _from_webp(data, len, icc, icc_len);
    if (len >= 8 && ((data[0] == 'I' && data[1] == 'I') || (data[0] == 'M' && data[1] == 'M')))
        return _from_tiff(data, len, icc, icc_len);
    return 0;
}

int n_icc_from_buffer(const unsigned char* data, size_t len, unsigned char** icc, size_t* icc_len) {
    int result = 0;

    __n_assert(data, return -1);
    __n_assert(icc, return -1);
    __n_assert(icc_len, return -1);
    result = _from_buffer(data, len, icc, icc_len);
    /* The whole file was given: running off its end is having none. */
    return result == N_ICC_MORE ? 0 : result;
}

static int _tiff_file(FILE* file, unsigned char** icc, size_t* icc_len) {
    unsigned char header[8], entry[12];
    int big = 0;
    size_t ifd = 0, count = 0, i = 0;

    if (fseek(file, 0, SEEK_SET) != 0 || fread(header, 1, 8, file) != 8)
        return 0;
    big = header[0] == 'M';
    ifd = big ? _be32(header + 4) : _le32(header + 4);
    if (fseek(file, (long)ifd, SEEK_SET) != 0 || fread(entry, 1, 2, file) != 2)
        return 0;
    count = big ? _be16(entry) : (size_t)entry[0] | ((size_t)entry[1] << 8);
    if (count > 4096)
        return 0;
    for (i = 0; i < count; i++) {
        size_t tag = 0, type = 0, n = 0, offset = 0;
        unsigned char* out = NULL;

        if (fread(entry, 1, 12, file) != 12)
            return 0;
        tag = big ? _be16(entry) : (size_t)entry[0] | ((size_t)entry[1] << 8);
        if (tag != N_ICC_TIFF_TAG)
            continue;
        type = big ? _be16(entry + 2) : (size_t)entry[2] | ((size_t)entry[3] << 8);
        n = big ? _be32(entry + 4) : _le32(entry + 4);
        offset = big ? _be32(entry + 8) : _le32(entry + 8);
        if ((type != 7 && type != 1) || n < N_ICC_MIN_BYTES || n > N_ICC_MAX_BYTES)
            return -1;
        out = malloc(n);
        if (!out)
            return -1;
        if (fseek(file, (long)offset, SEEK_SET) != 0 || fread(out, 1, n, file) != n ||
            !n_icc_is_profile(out, n)) {
            n_log(LOG_ERR, "icc: the TIFF's profile tag does not hold a profile");
            free(out);
            return -1;
        }
        *icc = out;
        *icc_len = n;
        return 1;
    }
    return 0;
}

int n_icc_from_file(const char* path, unsigned char** icc, size_t* icc_len) {
    FILE* file = NULL;
    unsigned char* head = NULL;
    size_t got = 0, room = N_ICC_FIRST_READ;
    int result = 0;

    __n_assert(path, return -1);
    __n_assert(icc, return -1);
    __n_assert(icc_len, return -1);
    *icc = NULL;
    *icc_len = 0;
    file = fopen(path, "rb");
    if (!file)
        return -1;
    /* A little first, and more only while the parser runs off the end of
     * what it has: most files answer in the first read, and the thumbnail
     * workers ask for thousands of them. */
    for (;;) {
        unsigned char* more = realloc(head, room);

        if (!more) {
            result = -1;
            break;
        }
        head = more;
        got += fread(head + got, 1, room - got, file);
        int at_end = got < room;
        if (got >= 8 && ((head[0] == 'I' && head[1] == 'I') || (head[0] == 'M' && head[1] == 'M'))) {
            result = _tiff_file(file, icc, icc_len);
            break;
        }
        result = _from_buffer(head, got, icc, icc_len);
        if (result != N_ICC_MORE)
            break;
        if (at_end || room >= N_ICC_HEAD_BYTES) {
            result = 0;
            break;
        }
        room = room * 4u > N_ICC_HEAD_BYTES ? N_ICC_HEAD_BYTES : room * 4u;
    }
    free(head);
    fclose(file);
    return result;
}