#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>

#include "../../platform.h"

#if defined(_WIN32) && !defined(WIN32)
#define WIN32
#endif

#ifdef WIN32
#include <windows.h>
#include <io.h>
#include <process.h>
#include <sys/stat.h>
#define unlink _unlink
#define close _close
typedef intptr_t pid_t;
static int ows_mkstemp(char *templ)
{
    if (!templ || _mktemp_s(templ, strlen(templ) + 1) != 0)
        return -1;
    return _open(templ, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
}
#define mkstemp ows_mkstemp
#else
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

#include "../modHeader.h"
#if defined(__has_include)
  #if __has_include(<mysql/mysql.h>)
    #include <mysql/mysql.h>
  #elif __has_include(<mysql.h>)
    #include <mysql.h>
  #elif __has_include(<mariadb/mysql.h>)
    #include <mariadb/mysql.h>
  #else
    #include "../../mysql/mysql.h"
  #endif
#else
  #include "../../mysql/mysql.h"
#endif
#include "../../macro.h"

#ifndef MAXPACKETBUFSIZE
#define MAXPACKETBUFSIZE 200000
#endif

#ifdef WIN32
#define OWS_IMG_TMP_IN_TEMPLATE ".\\ows_img_ocr_XXXXXX"
#define OWS_IMG_TMP_OUT_TEMPLATE ".\\ows_img_ocr_out_XXXXXX"
#else
#define OWS_IMG_TMP_IN_TEMPLATE OWS_TMP_TEMPLATE("ows_img_ocr_XXXXXX")
#define OWS_IMG_TMP_OUT_TEMPLATE OWS_TMP_TEMPLATE("ows_img_ocr_out_XXXXXX")
#endif

#define OCR_MAX_TEXT_LEN 20000

static char gTesseractPath[512];
static char gTesseractLang[128];
static int gOcrEnabled = 0;
static int gOcrMinChars = 8;
static unsigned int gOcrMaxBytes = 3000000;
static unsigned int gOcrMaxPixels = 50000000;
static unsigned int gOcrTimeoutMs = 8000;
static unsigned int gOcrThreadsMax = 1;
static int gOcrDebugKeepTmp = 0;
static volatile int gOcrActive = 0;
static int gMediaSchemaReady = 0;

typedef struct IMAGE_META_TAG
{
    char mime[64];
    char title[256];
    char description[1024];
    char author[256];
    char software[256];
    unsigned int width;
    unsigned int height;
} IMAGE_META;

static int starts_with(const char *s, const char *prefix)
{
    if (!s || !prefix) return 0;
    return strncmp(s, prefix, strlen(prefix)) == 0;
}

static int ends_with_ci(const char *s, const char *suffix)
{
    size_t sl, xl, i;
    if (!s || !suffix) return 0;
    sl = strlen(s);
    xl = strlen(suffix);
    if (sl < xl) return 0;
    for (i = 0; i < xl; i++)
    {
        if (tolower((unsigned char)s[sl - xl + i]) != tolower((unsigned char)suffix[i]))
            return 0;
    }
    return 1;
}

static unsigned short rd16le(const unsigned char *p) { return (unsigned short)(p[0] | (p[1] << 8)); }
static unsigned short rd16be(const unsigned char *p) { return (unsigned short)((p[0] << 8) | p[1]); }
static unsigned int rd32le(const unsigned char *p) { return (unsigned int)(p[0] | (p[1] << 8) | (p[2] << 16) | ((unsigned int)p[3] << 24)); }
static unsigned int rd32be(const unsigned char *p) { return (unsigned int)(((unsigned int)p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]); }

static void trim_inplace(char *s)
{
    int start = 0;
    int end;
    int i;

    if (!s) return;
    while (s[start] && isspace((unsigned char)s[start])) start++;
    if (start > 0) memmove(s, s + start, strlen(s + start) + 1);
    end = (int)strlen(s) - 1;
    while (end >= 0 && isspace((unsigned char)s[end])) s[end--] = '\0';
    for (i = 0; s[i]; i++)
    {
        if ((unsigned char)s[i] < 32 && s[i] != '\t' && s[i] != '\n')
            s[i] = ' ';
    }
}

static int read_file_into_buffer(const char *path, char *out, size_t outSize)
{
    FILE *fp;
    size_t n;

    if (!path || !out || outSize < 2)
        return 0;

    out[0] = '\0';
    fp = fopen(path, "rb");
    if (!fp)
        return 0;

    n = fread(out, 1, outSize - 1, fp);
    out[n] = '\0';
    fclose(fp);
    return (int)n;
}

static void sanitize_token_copy(const char *src, char *dst, size_t dstSize)
{
    size_t i;
    size_t j = 0;

    if (!dst || dstSize == 0)
        return;

    dst[0] = '\0';
    if (!src)
        return;

    for (i = 0; src[i] != '\0' && j + 1 < dstSize; i++)
    {
        unsigned char c = (unsigned char)src[i];
        if (isalnum(c))
            dst[j++] = (char)c;
        else if (c == '_' || c == '-' || c == '.' || c == '/' || c == '\\' || isspace(c))
            dst[j++] = ' ';
    }
    dst[j] = '\0';
    trim_inplace(dst);
}

static void sanitize_token_copy_len(const unsigned char *src, size_t srcLen, char *dst, size_t dstSize)
{
    size_t i;
    size_t j = 0;

    if (!dst || dstSize == 0)
        return;

    dst[0] = '\0';
    if (!src || srcLen == 0)
        return;

    for (i = 0; i < srcLen && j + 1 < dstSize; i++)
    {
        unsigned char c = src[i];
        if (c == '\0')
            break;
        if (isalnum(c))
            dst[j++] = (char)c;
        else if (c == '_' || c == '-' || c == '.' || c == '/' || c == '\\' || isspace(c))
            dst[j++] = ' ';
    }
    dst[j] = '\0';
    trim_inplace(dst);
}

static void sanitize_ocr_copy(const char *src, char *dst, size_t dstSize)
{
    size_t i;
    size_t j = 0;
    int prevSpace = 1;

    if (!dst || dstSize == 0)
        return;

    dst[0] = '\0';
    if (!src)
        return;

    for (i = 0; src[i] != '\0' && j + 1 < dstSize; i++)
    {
        unsigned char c = (unsigned char)src[i];

        if (isalnum(c))
        {
            dst[j++] = (char)tolower(c);
            prevSpace = 0;
        }
        else if (strchr(".,;:!?@#%&()[]{}+-_/\\'\"", c))
        {
            dst[j++] = (char)c;
            prevSpace = 0;
        }
        else if (isspace(c))
        {
            if (!prevSpace)
            {
                dst[j++] = ' ';
                prevSpace = 1;
            }
        }
    }

    dst[j] = '\0';
    trim_inplace(dst);
    if (strlen(dst) > OCR_MAX_TEXT_LEN)
        dst[OCR_MAX_TEXT_LEN] = '\0';
}

static void append_field(char *dst, size_t dstSize, const char *label, const char *value)
{
    char clean[2048];
    size_t curLen;
    size_t need;

    if (!dst || !label || !value || value[0] == '\0')
        return;

    sanitize_token_copy(value, clean, sizeof(clean));
    if (clean[0] == '\0')
        return;

    curLen = strlen(dst);
    need = strlen(label) + strlen(clean) + 3;
    if (curLen + need >= dstSize)
        return;

    if (curLen > 0)
        strcat(dst, " ");
    strcat(dst, label);
    strcat(dst, " ");
    strcat(dst, clean);
}

static void append_number_field(char *dst, size_t dstSize, const char *label, unsigned int value)
{
    char num[64];
    if (value == 0) return;
    snprintf(num, sizeof(num), "%u", value);
    append_field(dst, dstSize, label, num);
}

static void append_free_text(char *dst, size_t dstSize, const char *label, const char *value)
{
    char clean[MAXPACKETBUFSIZE];
    size_t curLen;
    size_t need;

    if (!dst || !label || !value || value[0] == '\0')
        return;

    sanitize_ocr_copy(value, clean, sizeof(clean));
    if (clean[0] == '\0')
        return;

    curLen = strlen(dst);
    need = strlen(label) + strlen(clean) + 3;
    if (curLen + need >= dstSize)
        return;

    if (curLen > 0)
        strcat(dst, " ");
    strcat(dst, label);
    strcat(dst, " ");
    strcat(dst, clean);
}

static int str_contains_ci(const char *hay, const char *needle)
{
    size_t i, j, hn, nn;
    if (!hay || !needle) return 0;
    hn = strlen(hay);
    nn = strlen(needle);
    if (hn < nn || nn == 0) return 0;
    for (i = 0; i <= hn - nn; i++)
    {
        for (j = 0; j < nn; j++)
        {
            if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
                break;
        }
        if (j == nn) return 1;
    }
    return 0;
}

static int mem_contains(const unsigned char *buf, size_t bufLen, const char *needle)
{
    size_t i;
    size_t needleLen;

    if (!buf || !needle)
        return 0;

    needleLen = strlen(needle);
    if (needleLen == 0 || bufLen < needleLen)
        return 0;

    for (i = 0; i + needleLen <= bufLen; i++)
    {
        if (memcmp(buf + i, needle, needleLen) == 0)
            return 1;
    }

    return 0;
}

static char *mysql_escape_dyn(MYSQL *db, const char *src)
{
    size_t srcLen;
    char *dst;

    if (!db)
        return NULL;
    if (!src)
        src = "";
    srcLen = strlen(src);
    dst = (char*)malloc(srcLen * 2 + 1);
    if (!dst)
        return NULL;
    mysql_real_escape_string(db, dst, src, srcLen);
    return dst;
}

static int query_get_int(MYSQL *db, const char *sql, long *value)
{
    MYSQL_RES *res;
    MYSQL_ROW row;

    if (!db || !sql || !value)
        return 0;
    if (mysql_query(db, sql) != 0)
        return 0;
    res = mysql_store_result(db);
    if (!res)
        return 0;
    row = mysql_fetch_row(res);
    if (row && row[0])
    {
        *value = atol(row[0]);
        mysql_free_result(res);
        return 1;
    }
    mysql_free_result(res);
    return 0;
}

static int ensure_media_schema(MYSQL *db)
{
    const char *q1 =
        "CREATE TABLE IF NOT EXISTS attachments_media ("
        "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
        "hostname VARCHAR(100) NOT NULL,"
        "page VARCHAR(255) NOT NULL,"
        "url_id BIGINT UNSIGNED NULL,"
        "media_kind VARCHAR(16) NOT NULL,"
        "mime VARCHAR(128) NULL,"
        "title VARCHAR(512) NULL,"
        "descr VARCHAR(1024) NULL,"
        "descr_source VARCHAR(16) NULL,"
        "ocr_text MEDIUMTEXT NULL,"
        "author VARCHAR(255) NULL,"
        "software VARCHAR(255) NULL,"
        "artist VARCHAR(255) NULL,"
        "album VARCHAR(255) NULL,"
        "video_codec VARCHAR(64) NULL,"
        "audio_codec VARCHAR(64) NULL,"
        "duration_raw VARCHAR(64) NULL,"
        "bit_rate VARCHAR(64) NULL,"
        "width INT NULL,"
        "height INT NULL,"
        "fps VARCHAR(32) NULL,"
        "sample_rate VARCHAR(64) NULL,"
        "channels VARCHAR(32) NULL,"
        "size_bytes BIGINT UNSIGNED NULL,"
        "http_etag VARCHAR(255) NULL,"
        "http_last_modified VARCHAR(255) NULL,"
        "first_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
        "last_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
        "PRIMARY KEY (id),"
        "UNIQUE KEY uq_media_url (hostname, page),"
        "KEY idx_media_kind (media_kind),"
        "KEY idx_mime (mime),"
        "KEY idx_url_id (url_id)"
        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";

    if (!db)
        return 0;
    if (gMediaSchemaReady)
        return 1;
    if (mysql_query(db, q1) != 0)
        return 0;
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS descr_source VARCHAR(16) NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS ocr_text MEDIUMTEXT NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS author VARCHAR(255) NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS software VARCHAR(255) NULL");
    gMediaSchemaReady = 1;
    return 1;
}

static int save_image_meta(FUNCTION_ARGUMENT *arg, const IMAGE_META *meta, const char *ocrText, const char *descrSource)
{
    MYSQL *db;
    char sql[8192];
    char urlExpr[32];
    char *eHost, *ePage, *eMime, *eTitle, *eDescr, *eDescrSource, *eOcr, *eAuthor, *eSoftware, *eEtag, *eLm;
    long urlId = 0;

    if (!arg || !arg->hostInfo || !arg->mysqlDB2 || !meta)
        return 0;
    db = (MYSQL*)arg->mysqlDB2;
    if (!ensure_media_schema(db))
        return 0;

    eHost = mysql_escape_dyn(db, arg->hostInfo->Host);
    ePage = mysql_escape_dyn(db, arg->hostInfo->Page);
    eMime = mysql_escape_dyn(db, meta->mime);
    eTitle = mysql_escape_dyn(db, meta->title);
    eDescr = mysql_escape_dyn(db, meta->description);
    eDescrSource = mysql_escape_dyn(db, descrSource ? descrSource : "none");
    eOcr = mysql_escape_dyn(db, ocrText ? ocrText : "");
    eAuthor = mysql_escape_dyn(db, meta->author);
    eSoftware = mysql_escape_dyn(db, meta->software);
    eEtag = mysql_escape_dyn(db, arg->hostInfo->HttpETag);
    eLm = mysql_escape_dyn(db, arg->hostInfo->HttpLastModified);

    snprintf(sql, sizeof(sql), "SELECT id FROM pagelist WHERE hostname='%s' AND page='%s' LIMIT 1", eHost, ePage);
    query_get_int(db, sql, &urlId);
    if (urlId > 0) snprintf(urlExpr, sizeof(urlExpr), "%ld", urlId);
    else strcpy(urlExpr, "NULL");

    snprintf(sql, sizeof(sql),
             "INSERT INTO attachments_media(hostname,page,url_id,media_kind,mime,title,descr,descr_source,ocr_text,author,software,width,height,size_bytes,http_etag,http_last_modified) VALUES("
             "'%s','%s',%s,'image','%s','%s','%s','%s','%s','%s','%s',%u,%u,%u,'%s','%s'"
             ") ON DUPLICATE KEY UPDATE "
             "url_id=VALUES(url_id),media_kind='image',mime=VALUES(mime),title=VALUES(title),descr=VALUES(descr),descr_source=VALUES(descr_source),ocr_text=VALUES(ocr_text),author=VALUES(author),software=VALUES(software),"
             "width=VALUES(width),height=VALUES(height),size_bytes=VALUES(size_bytes),http_etag=VALUES(http_etag),http_last_modified=VALUES(http_last_modified),last_seen=CURRENT_TIMESTAMP",
             eHost, ePage, urlExpr, eMime, eTitle, eDescr, eDescrSource, eOcr, eAuthor, eSoftware, meta->width, meta->height, arg->htmlLength, eEtag, eLm);
    mysql_query(db, sql);

    if (eHost) free(eHost);
    if (ePage) free(ePage);
    if (eMime) free(eMime);
    if (eTitle) free(eTitle);
    if (eDescr) free(eDescr);
    if (eDescrSource) free(eDescrSource);
    if (eOcr) free(eOcr);
    if (eAuthor) free(eAuthor);
    if (eSoftware) free(eSoftware);
    if (eEtag) free(eEtag);
    if (eLm) free(eLm);
    return 1;
}

static int get_ascii_entry(const unsigned char *base, size_t len, int isLe, unsigned int type, unsigned int count, unsigned int valueOff, char *out, size_t outSize)
{
    const unsigned char *src;
    unsigned int i;

    if (!out || outSize < 2 || type != 2 || count == 0)
        return 0;

    out[0] = '\0';
    if (count <= 4)
        src = (const unsigned char *)&valueOff;
    else
    {
        if (valueOff >= len || valueOff + count > len)
            return 0;
        src = base + valueOff;
    }

    for (i = 0; i + 1 < outSize && i < count; i++)
    {
        unsigned char c = src[i];
        if (c == '\0')
            break;
        if (c < 32 && c != '\t' && c != '\n')
            c = ' ';
        out[i] = (char)c;
    }
    out[i] = '\0';
    trim_inplace(out);
    return out[0] != '\0';
}

static void parse_tiff_ifd(const unsigned char *tiff, size_t tiffLen, unsigned int ifdOff, int isLe, IMAGE_META *meta, int depth)
{
    unsigned int count;
    unsigned int i;
    unsigned int maxEntries;

    if (!meta || depth > 1 || ifdOff + 2 > tiffLen)
        return;

    count = isLe ? rd16le(tiff + ifdOff) : rd16be(tiff + ifdOff);
    maxEntries = (unsigned int)((tiffLen - ifdOff - 2) / 12);
    if (count > maxEntries)
        count = maxEntries;

    for (i = 0; i < count; i++)
    {
        unsigned int entryOff = ifdOff + 2 + i * 12;
        unsigned short tag;
        unsigned short type;
        unsigned int valueCount;
        unsigned int valueOff;
        char tmp[1024];

        if (entryOff + 12 > tiffLen)
            break;

        tag = isLe ? rd16le(tiff + entryOff) : rd16be(tiff + entryOff);
        type = isLe ? rd16le(tiff + entryOff + 2) : rd16be(tiff + entryOff + 2);
        valueCount = isLe ? rd32le(tiff + entryOff + 4) : rd32be(tiff + entryOff + 4);
        valueOff = isLe ? rd32le(tiff + entryOff + 8) : rd32be(tiff + entryOff + 8);

        if ((tag == 0x0100 || tag == 0xA002) && (type == 3 || type == 4) && meta->width == 0)
            meta->width = (type == 3) ? (isLe ? rd16le(tiff + entryOff + 8) : rd16be(tiff + entryOff + 8)) : valueOff;
        else if ((tag == 0x0101 || tag == 0xA003) && (type == 3 || type == 4) && meta->height == 0)
            meta->height = (type == 3) ? (isLe ? rd16le(tiff + entryOff + 8) : rd16be(tiff + entryOff + 8)) : valueOff;
        else if (tag == 0x010E && meta->description[0] == '\0' && get_ascii_entry(tiff, tiffLen, isLe, type, valueCount, valueOff, tmp, sizeof(tmp)))
            strncpy(meta->description, tmp, sizeof(meta->description) - 1);
        else if ((tag == 0x9C9B || tag == 0x0320) && meta->title[0] == '\0' && get_ascii_entry(tiff, tiffLen, isLe, type, valueCount, valueOff, tmp, sizeof(tmp)))
            strncpy(meta->title, tmp, sizeof(meta->title) - 1);
        else if (tag == 0x013B && meta->author[0] == '\0' && get_ascii_entry(tiff, tiffLen, isLe, type, valueCount, valueOff, tmp, sizeof(tmp)))
            strncpy(meta->author, tmp, sizeof(meta->author) - 1);
        else if (tag == 0x0131 && meta->software[0] == '\0' && get_ascii_entry(tiff, tiffLen, isLe, type, valueCount, valueOff, tmp, sizeof(tmp)))
            strncpy(meta->software, tmp, sizeof(meta->software) - 1);
        else if (tag == 0x8769 && valueOff < tiffLen)
            parse_tiff_ifd(tiff, tiffLen, valueOff, isLe, meta, depth + 1);
    }
}

static void parse_jpeg(const unsigned char *buf, size_t len, IMAGE_META *meta)
{
    size_t pos = 2;
    unsigned int segmentCount = 0;

    if (!buf || len < 4 || !meta)
        return;

    strncpy(meta->mime, "image/jpeg", sizeof(meta->mime) - 1);

    while (pos + 4 < len)
    {
        unsigned short segLen;
        unsigned char marker;

        segmentCount++;
        if (segmentCount > 2048)
            break;

        if (buf[pos] != 0xFF)
        {
            pos++;
            continue;
        }

        while (pos < len && buf[pos] == 0xFF) pos++;
        if (pos >= len) break;
        marker = buf[pos++];
        if (marker == 0xD9 || marker == 0xDA) break;
        if (pos + 2 > len) break;
        segLen = rd16be(buf + pos);
        if (segLen < 2 || pos + segLen > len) break;

        if ((marker >= 0xC0 && marker <= 0xC3) || (marker >= 0xC5 && marker <= 0xC7) ||
            (marker >= 0xC9 && marker <= 0xCB) || (marker >= 0xCD && marker <= 0xCF))
        {
            if (segLen >= 7)
            {
                meta->height = rd16be(buf + pos + 3);
                meta->width = rd16be(buf + pos + 5);
            }
        }
        else if (marker == 0xE1 && segLen > 10 && memcmp(buf + pos + 2, "Exif\0\0", 6) == 0)
        {
            const unsigned char *tiff = buf + pos + 8;
            size_t tiffLen = segLen - 8;
            unsigned int ifd0Off;
            int isLe;

            if (tiffLen < 8) goto next_segment;
            if (memcmp(tiff, "II", 2) == 0)
                isLe = 1;
            else if (memcmp(tiff, "MM", 2) == 0)
                isLe = 0;
            else
                goto next_segment;

            ifd0Off = isLe ? rd32le(tiff + 4) : rd32be(tiff + 4);
            if (ifd0Off < tiffLen)
                parse_tiff_ifd(tiff, tiffLen, ifd0Off, isLe, meta, 0);
        }

next_segment:
        pos += segLen;
    }
}

static void parse_png_text_chunk(const unsigned char *data, unsigned int dataLen, IMAGE_META *meta, int isIntl)
{
    const unsigned char *textStart = NULL;
    unsigned int textLen = 0;
    unsigned int i;
    char key[128];
    char value[1024];

    if (!data || !meta || dataLen == 0)
        return;

    for (i = 0; i < dataLen && i + 1 < sizeof(key); i++)
    {
        if (data[i] == '\0')
            break;
        key[i] = (char)data[i];
    }
    if (i == 0 || i >= dataLen)
        return;
    key[i] = '\0';

    if (!isIntl)
    {
        textStart = data + i + 1;
        textLen = dataLen - (i + 1);
    }
    else
    {
        unsigned int p = i + 1;
        if (p + 2 >= dataLen) return;
        if (data[p] != 0) return;
        p += 2;
        while (p < dataLen && data[p] != '\0') p++;
        if (p >= dataLen) return;
        p++;
        while (p < dataLen && data[p] != '\0') p++;
        if (p >= dataLen) return;
        p++;
        textStart = data + p;
        textLen = dataLen - p;
    }

    if (!textStart || textStart >= data + dataLen)
        return;

    sanitize_token_copy_len(textStart, textLen, value, sizeof(value));
    if (value[0] == '\0')
        return;

    if ((str_contains_ci(key, "title") || str_contains_ci(key, "name")) && meta->title[0] == '\0')
        strncpy(meta->title, value, sizeof(meta->title) - 1);
    else if ((str_contains_ci(key, "description") || str_contains_ci(key, "comment")) && meta->description[0] == '\0')
        strncpy(meta->description, value, sizeof(meta->description) - 1);
    else if ((str_contains_ci(key, "author") || str_contains_ci(key, "creator")) && meta->author[0] == '\0')
        strncpy(meta->author, value, sizeof(meta->author) - 1);
    else if (str_contains_ci(key, "software") && meta->software[0] == '\0')
        strncpy(meta->software, value, sizeof(meta->software) - 1);
}

static void parse_png(const unsigned char *buf, size_t len, IMAGE_META *meta)
{
    size_t pos = 8;

    if (!buf || len < 24 || !meta)
        return;

    strncpy(meta->mime, "image/png", sizeof(meta->mime) - 1);
    meta->width = rd32be(buf + 16);
    meta->height = rd32be(buf + 20);

    while (pos + 12 <= len)
    {
        unsigned int chunkLen = rd32be(buf + pos);
        const unsigned char *chunkType = buf + pos + 4;
        const unsigned char *chunkData = buf + pos + 8;
        size_t nextPos;

        if ((size_t)chunkLen > len || pos > len - 12 || (size_t)chunkLen > len - pos - 12)
            break;
        nextPos = pos + (size_t)chunkLen + 12;

        if (memcmp(chunkType, "tEXt", 4) == 0)
            parse_png_text_chunk(chunkData, chunkLen, meta, 0);
        else if (memcmp(chunkType, "iTXt", 4) == 0)
            parse_png_text_chunk(chunkData, chunkLen, meta, 1);
        else if (memcmp(chunkType, "IEND", 4) == 0)
            break;

        if (nextPos <= pos)
            break;
        pos = nextPos;
    }
}

static void parse_gif(const unsigned char *buf, size_t len, IMAGE_META *meta)
{
    if (!buf || len < 10 || !meta)
        return;
    strncpy(meta->mime, "image/gif", sizeof(meta->mime) - 1);
    meta->width = rd16le(buf + 6);
    meta->height = rd16le(buf + 8);
}

static void parse_webp(const unsigned char *buf, size_t len, IMAGE_META *meta)
{
    if (!buf || len < 30 || !meta)
        return;

    strncpy(meta->mime, "image/webp", sizeof(meta->mime) - 1);
    if (memcmp(buf + 12, "VP8X", 4) == 0 && len >= 30)
    {
        meta->width = 1 + buf[24] + (buf[25] << 8) + (buf[26] << 16);
        meta->height = 1 + buf[27] + (buf[28] << 8) + (buf[29] << 16);
    }
    else if (memcmp(buf + 12, "VP8 ", 4) == 0 && len >= 30)
    {
        meta->width = rd16le(buf + 26) & 0x3FFF;
        meta->height = rd16le(buf + 28) & 0x3FFF;
    }
    else if (memcmp(buf + 12, "VP8L", 4) == 0 && len >= 25)
    {
        unsigned int bits = rd32le(buf + 21);
        meta->width = (bits & 0x3FFF) + 1;
        meta->height = ((bits >> 14) & 0x3FFF) + 1;
    }
}

static void parse_tiff(const unsigned char *buf, size_t len, IMAGE_META *meta)
{
    int isLe;
    unsigned int ifd0Off;

    if (!buf || len < 8 || !meta)
        return;

    if (memcmp(buf, "II", 2) == 0)
        isLe = 1;
    else if (memcmp(buf, "MM", 2) == 0)
        isLe = 0;
    else
        return;

    strncpy(meta->mime, "image/tiff", sizeof(meta->mime) - 1);
    ifd0Off = isLe ? rd32le(buf + 4) : rd32be(buf + 4);
    if (ifd0Off < len)
        parse_tiff_ifd(buf, len, ifd0Off, isLe, meta, 0);
}

static int extract_svg_tag(const char *body, const char *tagName, char *out, size_t outSize)
{
    char openTag[32];
    char closeTag[32];
    char *start;
    char *end;
    size_t len;

    if (!body || !tagName || !out || outSize < 2)
        return 0;

    snprintf(openTag, sizeof(openTag), "<%s", tagName);
    start = strstr((char *)body, openTag);
    if (!start) return 0;
    start = strchr(start, '>');
    if (!start) return 0;
    start++;
    snprintf(closeTag, sizeof(closeTag), "</%s>", tagName);
    end = strstr(start, closeTag);
    if (!end) return 0;
    len = (size_t)(end - start);
    if (len >= outSize) len = outSize - 1;
    memcpy(out, start, len);
    out[len] = '\0';
    trim_inplace(out);
    return out[0] != '\0';
}

static void parse_svg(const char *body, size_t len, IMAGE_META *meta)
{
    char tmp[1024];
    (void)len;
    if (!body || !meta) return;
    strncpy(meta->mime, "image/svg+xml", sizeof(meta->mime) - 1);
    if (meta->title[0] == '\0' && extract_svg_tag(body, "title", tmp, sizeof(tmp)))
        strncpy(meta->title, tmp, sizeof(meta->title) - 1);
    if (meta->description[0] == '\0' && extract_svg_tag(body, "desc", tmp, sizeof(tmp)))
        strncpy(meta->description, tmp, sizeof(meta->description) - 1);
}

#define MAX_SVG_PARSE_BYTES 65536

static void parse_svg_limited(const char *body, size_t len, IMAGE_META *meta)
{
    char svgBuf[MAX_SVG_PARSE_BYTES + 1];
    size_t useLen;

    if (!body || !meta)
        return;

    useLen = len;
    if (useLen > MAX_SVG_PARSE_BYTES)
        useLen = MAX_SVG_PARSE_BYTES;

    memcpy(svgBuf, body, useLen);
    svgBuf[useLen] = '\0';
    parse_svg(svgBuf, useLen, meta);
}

static void basename_from_path(const struct sHost *host, char *out, size_t outSize)
{
    const char *start;
    const char *end;
    size_t len;
    char raw[512];

    if (!out || outSize < 2)
        return;

    out[0] = '\0';
    if (!host || !host->Page[0])
        return;

    start = strrchr(host->Page, '/');
    start = start ? start + 1 : host->Page;
    end = start;
    while (*end && *end != '?' && *end != '#')
        end++;
    len = (size_t)(end - start);
    if (len == 0 || len >= sizeof(raw))
        return;
    memcpy(raw, start, len);
    raw[len] = '\0';

    end = strrchr(raw, '.');
    if (end && end > raw)
        raw[end - raw] = '\0';

    sanitize_token_copy(raw, out, outSize);
}

static int detect_image(const struct functArg *arg, IMAGE_META *meta)
{
    const unsigned char *buf = (const unsigned char *)arg->html;
    size_t len = arg->htmlLength;
    const char *page = (arg && arg->hostInfo) ? arg->hostInfo->Page : NULL;

    if (!arg || !arg->html || arg->htmlLength < 4 || !meta)
        return 0;

    if (arg->hostInfo && str_contains_ci(arg->hostInfo->HttpContentType, "image/svg"))
    {
        parse_svg_limited(arg->html, len, meta);
        return 1;
    }
    if (len >= 8 && memcmp(buf, "\x89PNG\r\n\x1a\n", 8) == 0)
    {
        parse_png(buf, len, meta);
        return 1;
    }
    if (len >= 12 && memcmp(buf, "\xff\x4f\xff\x51", 4) == 0)
    {
        strncpy(meta->mime, "image/jp2", sizeof(meta->mime) - 1);
        return 1;
    }
    if (len >= 12 && memcmp(buf, "\x00\x00\x00\x0cjP  \r\n\x87\n", 12) == 0)
    {
        strncpy(meta->mime, "image/jp2", sizeof(meta->mime) - 1);
        return 1;
    }
    if (len >= 2 && buf[0] == 0xff && buf[1] == 0x0a)
    {
        strncpy(meta->mime, "image/jxl", sizeof(meta->mime) - 1);
        return 1;
    }
    if (len >= 3 && buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF)
    {
        parse_jpeg(buf, len, meta);
        return 1;
    }
    if (len >= 6 && (memcmp(buf, "GIF87a", 6) == 0 || memcmp(buf, "GIF89a", 6) == 0))
    {
        parse_gif(buf, len, meta);
        return 1;
    }
    if (len >= 12 && memcmp(buf, "RIFF", 4) == 0 && memcmp(buf + 8, "WEBP", 4) == 0)
    {
        parse_webp(buf, len, meta);
        return 1;
    }
    if (len >= 12 && memcmp(buf + 4, "ftyp", 4) == 0 &&
        (memcmp(buf + 8, "avif", 4) == 0 || memcmp(buf + 8, "avis", 4) == 0))
    {
        strncpy(meta->mime, "image/avif", sizeof(meta->mime) - 1);
        return 1;
    }
    if (len >= 12 && memcmp(buf + 4, "ftyp", 4) == 0 &&
        (memcmp(buf + 8, "heic", 4) == 0 || memcmp(buf + 8, "heix", 4) == 0 ||
         memcmp(buf + 8, "hevc", 4) == 0 || memcmp(buf + 8, "hevx", 4) == 0))
    {
        strncpy(meta->mime, "image/heic", sizeof(meta->mime) - 1);
        return 1;
    }
    if (len >= 4 && ((memcmp(buf, "II*\0", 4) == 0) || (memcmp(buf, "MM\0*", 4) == 0)))
    {
        parse_tiff(buf, len, meta);
        return 1;
    }
    if (len >= 26 && memcmp(buf, "BM", 2) == 0)
    {
        strncpy(meta->mime, "image/bmp", sizeof(meta->mime) - 1);
        meta->width = rd32le(buf + 18);
        meta->height = rd32le(buf + 22);
        return 1;
    }
    if (len >= 8 && rd16le(buf) == 0 && rd16le(buf + 2) == 1)
    {
        strncpy(meta->mime, "image/x-icon", sizeof(meta->mime) - 1);
        meta->width = buf[6] ? buf[6] : 256;
        meta->height = buf[7] ? buf[7] : 256;
        return 1;
    }
    if (len >= 4 && memcmp(buf, "<svg", 4) == 0)
    {
        parse_svg_limited(arg->html, len, meta);
        return 1;
    }
    if (len >= 5 && memcmp(buf, "<?xml", 5) == 0 && mem_contains(buf, len, "<svg"))
    {
        parse_svg_limited(arg->html, len, meta);
        return 1;
    }

    if (arg->hostInfo && str_contains_ci(arg->hostInfo->HttpContentType, "image/"))
    {
        strncpy(meta->mime, arg->hostInfo->HttpContentType, sizeof(meta->mime) - 1);
        return 1;
    }

    if (arg->hostInfo && str_contains_ci(arg->hostInfo->HttpContentType, "octet-stream") && page)
    {
        if (ends_with_ci(page, ".tif") || ends_with_ci(page, ".tiff"))
        {
            strncpy(meta->mime, "image/tiff", sizeof(meta->mime) - 1);
            return 1;
        }
    }

    return 0;
}

static const char *guess_ext_from_meta(const IMAGE_META *meta)
{
    if (!meta) return ".img";
    if (strcmp(meta->mime, "image/jpeg") == 0) return ".jpg";
    if (strcmp(meta->mime, "image/png") == 0) return ".png";
    if (strcmp(meta->mime, "image/gif") == 0) return ".gif";
    if (strcmp(meta->mime, "image/webp") == 0) return ".webp";
    if (strcmp(meta->mime, "image/tiff") == 0) return ".tif";
    if (strcmp(meta->mime, "image/svg+xml") == 0) return ".svg";
    if (strcmp(meta->mime, "image/bmp") == 0) return ".bmp";
    if (strcmp(meta->mime, "image/x-icon") == 0) return ".ico";
    if (strcmp(meta->mime, "image/avif") == 0) return ".avif";
    if (strcmp(meta->mime, "image/heic") == 0) return ".heic";
    if (strcmp(meta->mime, "image/jp2") == 0) return ".jp2";
    if (strcmp(meta->mime, "image/jxl") == 0) return ".jxl";
    return ".img";
}

static void cleanup_tmp_paths(const char *inputPath, const char *outputTxt, const char *outputBase)
{
    if (!gOcrDebugKeepTmp)
    {
        if (inputPath && inputPath[0]) unlink(inputPath);
        if (outputTxt && outputTxt[0]) unlink(outputTxt);
        if (outputBase && outputBase[0]) unlink(outputBase);
    }
}

static int ocr_try_acquire_slot(void)
{
    int current;

    if (gOcrThreadsMax == 0)
        return 0;

    for (;;)
    {
        current = gOcrActive;
        if ((unsigned int)current >= gOcrThreadsMax)
            return 0;
#ifdef WIN32
        if (InterlockedCompareExchange((volatile LONG*)&gOcrActive, current + 1, current) == current)
#else
        if (__sync_bool_compare_and_swap(&gOcrActive, current, current + 1))
#endif
            return 1;
    }
}

static void ocr_release_slot(void)
{
#ifdef WIN32
    InterlockedDecrement((volatile LONG*)&gOcrActive);
#else
    __sync_sub_and_fetch(&gOcrActive, 1);
#endif
}

static int spawn_tesseract(const char *inputPath, const char *outputBase)
{
#ifdef WIN32
    char cmd[1600];
    int rc;
    snprintf(cmd, sizeof(cmd), "\"%s\" \"%s\" \"%s\" -l \"%s\" quiet >NUL 2>NUL",
             gTesseractPath, inputPath, outputBase, gTesseractLang[0] ? gTesseractLang : "eng");
    rc = system(cmd);
    return (rc == 0) ? 0 : -1;
#else
    pid_t pid;

    pid = fork();
    if (pid < 0)
        return -1;

    if (pid == 0)
    {
        int nullfd = open("/dev/null", O_WRONLY);
        char *argv[8];

        if (nullfd >= 0)
        {
            dup2(nullfd, STDOUT_FILENO);
            dup2(nullfd, STDERR_FILENO);
            close(nullfd);
        }

        argv[0] = gTesseractPath;
        argv[1] = (char *)inputPath;
        argv[2] = (char *)outputBase;
        argv[3] = "-l";
        argv[4] = gTesseractLang[0] ? gTesseractLang : "eng";
        argv[5] = "quiet";
        argv[6] = NULL;
        execvp(gTesseractPath, argv);
        _exit(127);
    }

    return (int)pid;
#endif
}

static int wait_child_timeout(pid_t pid, unsigned int timeoutMs)
{
#ifdef WIN32
    (void)timeoutMs;
    return (pid == 0) ? 0 : -1;
#else
    unsigned int waited = 0;
    int status;

    for (;;)
    {
        pid_t rc = waitpid(pid, &status, WNOHANG);
        if (rc == pid)
            return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
        if (rc < 0)
            return -1;
        if (waited >= timeoutMs)
        {
            kill(pid, SIGKILL);
            waitpid(pid, &status, 0);
            return -2;
        }
        usleep(50000);
        waited += 50;
    }
#endif
}

static int run_ocr(const struct functArg *arg, const IMAGE_META *meta, char *ocrOut, size_t ocrOutSize)
{
    char inTemplate[] = OWS_IMG_TMP_IN_TEMPLATE;
    char outBaseTemplate[] = OWS_IMG_TMP_OUT_TEMPLATE;
    char inputPath[512];
    char outputTxt[512];
    char dbg[1024];
    int fd;
    FILE *fp;
    const char *ext;
    int readLen;
    int childPid = -1;
    int waitRc = 0;
    int slotAcquired = 0;

    if (!arg || !meta || !ocrOut || ocrOutSize < 2)
        return 0;

    ocrOut[0] = '\0';
    if (!gOcrEnabled || gTesseractPath[0] == '\0')
        return 0;

    if (strcmp(meta->mime, "image/svg+xml") == 0)
        return 0;
    if (arg->htmlLength == 0 || arg->htmlLength > gOcrMaxBytes)
    {
        snprintf(dbg, sizeof(dbg), "ocr skipped size page=%.255s bytes=%u max=%u", arg->hostInfo->Page, arg->htmlLength, gOcrMaxBytes);
        DEBUG_LOG(dbg);
        return 0;
    }
    if (meta->width > 0 && meta->height > 0)
    {
        unsigned long long pixels = (unsigned long long)meta->width * (unsigned long long)meta->height;
        if (pixels > (unsigned long long)gOcrMaxPixels)
        {
            snprintf(dbg, sizeof(dbg), "ocr skipped pixels page=%.255s pixels=%llu max=%u", arg->hostInfo->Page, pixels, gOcrMaxPixels);
            DEBUG_LOG(dbg);
            return 0;
        }
    }
    if (!ocr_try_acquire_slot())
    {
        snprintf(dbg, sizeof(dbg), "ocr skipped throttle page=%.255s active=%d max=%u", arg->hostInfo->Page, (int)gOcrActive, gOcrThreadsMax);
        DEBUG_LOG(dbg);
        return 0;
    }
    slotAcquired = 1;

    inputPath[0] = '\0';
    outputTxt[0] = '\0';

    fd = mkstemp(inTemplate);
    if (fd < 0)
        goto done;
    close(fd);

    ext = guess_ext_from_meta(meta);
    snprintf(inputPath, sizeof(inputPath), "%s%s", inTemplate, ext);
    if (rename(inTemplate, inputPath) != 0)
    {
        unlink(inTemplate);
        inputPath[0] = '\0';
        goto done;
    }

    fp = fopen(inputPath, "wb");
    if (!fp)
    {
        goto done;
    }
    fwrite(arg->html, 1, arg->htmlLength, fp);
    fclose(fp);

    fd = mkstemp(outBaseTemplate);
    if (fd < 0)
        goto done;
    close(fd);
    snprintf(outputTxt, sizeof(outputTxt), "%s.txt", outBaseTemplate);

    childPid = spawn_tesseract(inputPath, outBaseTemplate);
    if (childPid < 0)
        goto done;

    waitRc = wait_child_timeout((pid_t)childPid, gOcrTimeoutMs);
    if (waitRc == -2)
    {
        snprintf(dbg, sizeof(dbg), "ocr timeout page=%.255s timeout_ms=%u", arg->hostInfo->Page, gOcrTimeoutMs);
        DEBUG_LOG(dbg);
        goto done;
    }
    if (waitRc != 0)
    {
        snprintf(dbg, sizeof(dbg), "ocr failed page=%.255s exit=%d path=%.255s", arg->hostInfo->Page, waitRc, gTesseractPath);
        DEBUG_LOG(dbg);
        goto done;
    }

    readLen = read_file_into_buffer(outputTxt, ocrOut, ocrOutSize);
    if (readLen <= 0)
    {
        snprintf(dbg, sizeof(dbg), "ocr empty page=%.255s", arg->hostInfo->Page);
        DEBUG_LOG(dbg);
        ocrOut[0] = '\0';
        goto done;
    }

    {
        char cleaned[MAXPACKETBUFSIZE];
        sanitize_ocr_copy(ocrOut, cleaned, sizeof(cleaned));
        strncpy(ocrOut, cleaned, ocrOutSize - 1);
        ocrOut[ocrOutSize - 1] = '\0';
    }

    if ((int)strlen(ocrOut) < gOcrMinChars)
    {
        ocrOut[0] = '\0';
        goto done;
    }

    snprintf(dbg, sizeof(dbg), "ocr ok page=%.255s len=%u", arg->hostInfo->Page, (unsigned int)strlen(ocrOut));
    DEBUG_LOG(dbg);

done:
    cleanup_tmp_paths(inputPath, outputTxt, outBaseTemplate);
    if (slotAcquired)
        ocr_release_slot();
    return ocrOut[0] != '\0';
}

/* modFilter should return 1 if the current page must be indexed 0 if discarded */
#ifdef WIN32
extern __declspec(dllexport)
#endif
int modFilter(struct functArg *arg)
{
    IMAGE_META meta;
    char filename[512];
    char bestTitle[MAXDESCRIPTIONSIZE];
    char text[MAXPACKETBUFSIZE];
    char ocrText[MAXPACKETBUFSIZE];
    const char *descrSource = "none";

    if (!arg || !arg->hostInfo)
        return 0;

    if (arg->hostInfo->type != 4)
        return 1;

    memset(&meta, 0, sizeof(meta));
    if (!detect_image(arg, &meta))
        return 1;

    basename_from_path(arg->hostInfo, filename, sizeof(filename));
    memset(bestTitle, 0, sizeof(bestTitle));
    if (meta.title[0] != '\0')
        strncpy(bestTitle, meta.title, sizeof(bestTitle) - 1);
    else if (meta.description[0] != '\0')
        strncpy(bestTitle, meta.description, sizeof(bestTitle) - 1);
    else if (filename[0] != '\0')
        strncpy(bestTitle, filename, sizeof(bestTitle) - 1);

    memset(text, 0, sizeof(text));
    memset(ocrText, 0, sizeof(ocrText));
    append_field(text, sizeof(text), "image", filename);
    append_field(text, sizeof(text), "title", meta.title);
    append_field(text, sizeof(text), "description", meta.description);
    append_field(text, sizeof(text), "author", meta.author);
    append_field(text, sizeof(text), "software", meta.software);
    append_field(text, sizeof(text), "mime", meta.mime);
    append_number_field(text, sizeof(text), "width", meta.width);
    append_number_field(text, sizeof(text), "height", meta.height);
    if (run_ocr(arg, &meta, ocrText, sizeof(ocrText)))
        append_free_text(text, sizeof(text), "ocr", ocrText);

    if (meta.description[0] != '\0')
        descrSource = "meta";
    if (bestTitle[0] == '\0' && ocrText[0] != '\0')
    {
        snprintf(bestTitle, sizeof(bestTitle), "ocr: %.245s", ocrText);
        bestTitle[sizeof(bestTitle) - 1] = '\0';
        append_field(text, sizeof(text), "desc_source", "ocr");
        descrSource = "ocr";
    }

    if (bestTitle[0] != '\0')
    {
        strncpy(arg->hostInfo->Description, bestTitle, MAXDESCRIPTIONSIZE - 1);
        arg->hostInfo->Description[MAXDESCRIPTIONSIZE - 1] = '\0';
    }

    strncpy(arg->text, text, MAXPACKETBUFSIZE - 1);
    arg->text[MAXPACKETBUFSIZE - 1] = '\0';
    arg->textLength = strlen(arg->text);
    save_image_meta(arg, &meta, ocrText, descrSource);

    return 1;
}

#ifdef WIN32
extern __declspec(dllexport)
#endif
int modInitFilter(char *hostname, char *error)
{
    FILE *pF;
    char line[600];

    (void)hostname;
    if (error) error[0] = '\0';

    gTesseractPath[0] = '\0';
    strncpy(gTesseractLang, "eng", sizeof(gTesseractLang) - 1);
    gTesseractLang[sizeof(gTesseractLang) - 1] = '\0';
    gOcrEnabled = 0;
    gOcrMinChars = 8;
    gOcrMaxBytes = 3000000;
    gOcrMaxPixels = 50000000;
    gOcrTimeoutMs = 8000;
    gOcrThreadsMax = 1;
    gOcrDebugKeepTmp = 0;

    pF = ows_fopen_config("mod_image.conf", "r", NULL, 0);
    if (!pF)
        return 1;

    while (fgets(line, sizeof(line) - 1, pF))
    {
        char parsed[600];

        strncpy(parsed, line, sizeof(parsed) - 1);
        parsed[sizeof(parsed) - 1] = '\0';
        trim_inplace(parsed);
        if (parsed[0] == '\0' || parsed[0] == '#')
            continue;

        if (starts_with(parsed, "tesseract="))
        {
            strncpy(gTesseractPath, parsed + 10, sizeof(gTesseractPath) - 1);
            gTesseractPath[sizeof(gTesseractPath) - 1] = '\0';
        }
        else if (starts_with(parsed, "ocr="))
        {
            gOcrEnabled = atoi(parsed + 4) ? 1 : 0;
        }
        else if (starts_with(parsed, "lang="))
        {
            strncpy(gTesseractLang, parsed + 5, sizeof(gTesseractLang) - 1);
            gTesseractLang[sizeof(gTesseractLang) - 1] = '\0';
        }
        else if (starts_with(parsed, "ocr_min_chars="))
        {
            gOcrMinChars = atoi(parsed + 14);
            if (gOcrMinChars < 1) gOcrMinChars = 1;
            if (gOcrMinChars > 200) gOcrMinChars = 200;
        }
        else if (starts_with(parsed, "enable_ocr="))
        {
            gOcrEnabled = atoi(parsed + 11) ? 1 : 0;
        }
        else if (starts_with(parsed, "ocr_max_bytes="))
        {
            gOcrMaxBytes = (unsigned int)strtoul(parsed + 14, NULL, 10);
            if (gOcrMaxBytes < 1024) gOcrMaxBytes = 1024;
        }
        else if (starts_with(parsed, "ocr_max_pixels="))
        {
            gOcrMaxPixels = (unsigned int)strtoul(parsed + 15, NULL, 10);
            if (gOcrMaxPixels < 10000) gOcrMaxPixels = 10000;
        }
        else if (starts_with(parsed, "ocr_timeout_ms="))
        {
            gOcrTimeoutMs = (unsigned int)strtoul(parsed + 15, NULL, 10);
            if (gOcrTimeoutMs < 100) gOcrTimeoutMs = 100;
        }
        else if (starts_with(parsed, "ocr_threads_max="))
        {
            gOcrThreadsMax = (unsigned int)strtoul(parsed + 16, NULL, 10);
            if (gOcrThreadsMax < 1) gOcrThreadsMax = 1;
            if (gOcrThreadsMax > 8) gOcrThreadsMax = 8;
        }
        else if (starts_with(parsed, "ocr_debug_keep_tmp="))
        {
            gOcrDebugKeepTmp = atoi(parsed + 19) ? 1 : 0;
        }
    }
    fclose(pF);

    if (gOcrEnabled && gTesseractPath[0] == '\0')
    {
        strncpy(gTesseractPath, "tesseract", sizeof(gTesseractPath) - 1);
        gTesseractPath[sizeof(gTesseractPath) - 1] = '\0';
    }

    return 1;
}
