/* BASIC2026 portable PDF document and annotation inspection. */
typedef struct {
    char *data;
    size_t used;
    size_t capacity;
} B2026PdfTextBuffer;

typedef struct {
    int total;
    int comments;
    int links;
    int widgets;
    int attachments;
    int media;
    int other;
} B2026PdfAnnotationStats;

static int b2026_pdf_text_reserve(B2026PdfTextBuffer *b, size_t extra) {
    size_t need, cap; char *p;
    if (extra > SIZE_MAX - b->used - 1U) return 0;
    need = b->used + extra + 1U;
    if (need <= b->capacity) return 1;
    cap = b->capacity == 0U ? 128U : b->capacity;
    while (cap < need) { if (cap > SIZE_MAX / 2U) { cap = need; break; } cap *= 2U; }
    p = (char *)realloc(b->data, cap); if (p == NULL) return 0;
    b->data = p; b->capacity = cap; return 1;
}

static int b2026_pdf_text_append_bytes(B2026PdfTextBuffer *b, const char *s, size_t n) {
    if (!b2026_pdf_text_reserve(b, n)) return 0;
    if (n > 0U) memcpy(b->data + b->used, s, n);
    b->used += n; b->data[b->used] = '\0'; return 1;
}

static int b2026_pdf_text_append_char(B2026PdfTextBuffer *b, char c) {
    return b2026_pdf_text_append_bytes(b, &c, 1U);
}

static int b2026_pdf_text_append_codepoint(B2026PdfTextBuffer *b, unsigned int cp) {
    char out[8]; int n = utf8_encode_codepoint(cp, out);
    if (n <= 0) { n = utf8_encode_codepoint(0xFFFDU, out); }
    return n > 0 && b2026_pdf_text_append_bytes(b, out, (size_t)n);
}

static char *b2026_pdf_text_take(B2026PdfTextBuffer *b) {
    char *result;
    if (b->data == NULL) return b2026_strdup_simple("");
    result = b->data; b->data = NULL; b->used = b->capacity = 0U; return result;
}

static const unsigned char *b2026_pdf_find_key(const unsigned char *body, size_t n,
                                                const char *key, size_t *value_pos) {
    const unsigned char *q; size_t offset = 0U, key_len = strlen(key);
    while (offset < n && (q = b2026_pdf_memfind(body + offset, n - offset, key, key_len)) != NULL) {
        size_t pos = (size_t)(q - body);
        if (pos + key_len >= n || b2026_pdf_is_delim(body[pos + key_len])) {
            size_t p = pos + key_len; while (p < n && isspace(body[p])) ++p;
            if (value_pos != NULL) *value_pos = p; return q;
        }
        offset = pos + 1U;
    }
    return NULL;
}

static unsigned int b2026_pdf_doc_codepoint(unsigned int c) {
    static const unsigned short map_80_9f[32] = {
        0x2022,0x2020,0x2021,0x2026,0x2014,0x2013,0x0192,0x2044,
        0x2039,0x203A,0x2212,0x2030,0x201E,0x201C,0x201D,0x2018,
        0x2019,0x201A,0x2122,0xFB01,0xFB02,0x0141,0x0152,0x0160,
        0x0178,0x017D,0x0131,0x0142,0x0153,0x0161,0x017E,0xFFFD
    };
    if (c >= 0x80U && c <= 0x9FU) return map_80_9f[c - 0x80U];
    if (c == 0x18U) return 0x02D8U; if (c == 0x19U) return 0x02C7U;
    if (c == 0x1AU) return 0x02C6U; if (c == 0x1BU) return 0x02D9U;
    if (c == 0x1CU) return 0x02DDU; if (c == 0x1DU) return 0x02DBU;
    if (c == 0x1EU) return 0x02DAU; if (c == 0x1FU) return 0x02DCU;
    if (c == 0x7FU) return 0xFFFD; return c;
}

static char *b2026_pdf_decode_text_bytes(const unsigned char *bytes, size_t n) {
    B2026PdfTextBuffer out; size_t i = 0U; memset(&out, 0, sizeof(out));
    if (n >= 2U && bytes[0] == 0xFEU && bytes[1] == 0xFFU) {
        i = 2U;
        while (i + 1U < n) {
            unsigned int u = ((unsigned int)bytes[i] << 8) | bytes[i + 1U]; i += 2U;
            if (u >= 0xD800U && u <= 0xDBFFU && i + 1U < n) {
                unsigned int low = ((unsigned int)bytes[i] << 8) | bytes[i + 1U];
                if (low >= 0xDC00U && low <= 0xDFFFU) { i += 2U; u = 0x10000U + ((u - 0xD800U) << 10) + (low - 0xDC00U); }
                else u = 0xFFFDU;
            } else if (u >= 0xDC00U && u <= 0xDFFFU) u = 0xFFFDU;
            if (!b2026_pdf_text_append_codepoint(&out, u)) goto fail;
        }
    } else if (n >= 2U && bytes[0] == 0xFFU && bytes[1] == 0xFEU) {
        i = 2U;
        while (i + 1U < n) {
            unsigned int u = bytes[i] | ((unsigned int)bytes[i + 1U] << 8); i += 2U;
            if (u >= 0xD800U && u <= 0xDBFFU && i + 1U < n) {
                unsigned int low = bytes[i] | ((unsigned int)bytes[i + 1U] << 8);
                if (low >= 0xDC00U && low <= 0xDFFFU) { i += 2U; u = 0x10000U + ((u - 0xD800U) << 10) + (low - 0xDC00U); }
                else u = 0xFFFDU;
            } else if (u >= 0xDC00U && u <= 0xDFFFU) u = 0xFFFDU;
            if (!b2026_pdf_text_append_codepoint(&out, u)) goto fail;
        }
    } else if (n >= 3U && bytes[0] == 0xEFU && bytes[1] == 0xBBU && bytes[2] == 0xBFU) {
        if (!b2026_pdf_text_append_bytes(&out, (const char *)bytes + 3U, n - 3U)) goto fail;
    } else {
        for (i = 0U; i < n; ++i) if (!b2026_pdf_text_append_codepoint(&out, b2026_pdf_doc_codepoint(bytes[i]))) goto fail;
    }
    return b2026_pdf_text_take(&out);
fail:
    free(out.data); return NULL;
}

static char *b2026_pdf_parse_string_value(B2026PdfDocument *doc, const unsigned char *body,
                                          size_t n, size_t p, int depth) {
    B2026PdfTextBuffer raw; size_t i; memset(&raw, 0, sizeof(raw));
    if (depth > 8 || p >= n) return NULL;
    if (body[p] == '(') {
        int level = 1; ++p;
        while (p < n && level > 0) {
            unsigned char c = body[p++];
            if (c == '\\' && p < n) {
                c = body[p++];
                if (c == 'n') c = '\n'; else if (c == 'r') c = '\r'; else if (c == 't') c = '\t';
                else if (c == 'b') c = '\b'; else if (c == 'f') c = '\f';
                else if (c == '\r' || c == '\n') { if (c == '\r' && p < n && body[p] == '\n') ++p; continue; }
                else if (c >= '0' && c <= '7') {
                    int count = 1; unsigned int v = c - '0';
                    while (count < 3 && p < n && body[p] >= '0' && body[p] <= '7') { v = v * 8U + (body[p++] - '0'); ++count; }
                    c = (unsigned char)(v & 255U);
                }
                if (!b2026_pdf_text_append_char(&raw, (char)c)) goto fail;
            } else if (c == '(') { ++level; if (!b2026_pdf_text_append_char(&raw, '(')) goto fail; }
            else if (c == ')') { --level; if (level > 0 && !b2026_pdf_text_append_char(&raw, ')')) goto fail; }
            else if (!b2026_pdf_text_append_char(&raw, (char)c)) goto fail;
        }
        if (level != 0) goto fail;
    } else if (body[p] == '<' && p + 1U < n && body[p + 1U] != '<') {
        int high = -1; ++p;
        while (p < n && body[p] != '>') {
            int v; unsigned char c = body[p++]; if (isspace(c)) continue;
            if (c >= '0' && c <= '9') v = c - '0'; else if (c >= 'A' && c <= 'F') v = c - 'A' + 10;
            else if (c >= 'a' && c <= 'f') v = c - 'a' + 10; else goto fail;
            if (high < 0) high = v; else { char byte = (char)((high << 4) | v); if (!b2026_pdf_text_append_char(&raw, byte)) goto fail; high = -1; }
        }
        if (high >= 0) { char byte = (char)(high << 4); if (!b2026_pdf_text_append_char(&raw, byte)) goto fail; }
    } else if (isdigit(body[p])) {
        unsigned long number = 0UL, generation = 0UL; int index; char *end;
        number = strtoul((const char *)body + p, &end, 10); if (end == (char *)body + p) goto fail;
        p = (size_t)(end - (char *)body); while (p < n && isspace(body[p])) ++p;
        generation = strtoul((const char *)body + p, &end, 10); if (end == (char *)body + p) goto fail;
        p = (size_t)(end - (char *)body); while (p < n && isspace(body[p])) ++p;
        if (p >= n || body[p] != 'R' || number > INT_MAX || generation > INT_MAX) goto fail;
        index = b2026_pdf_object_lookup(doc, (int)number, (int)generation); if (index < 0) goto fail;
        p = 0U; while (p < doc->objects[index].body_length && isspace(doc->objects[index].body[p])) ++p;
        return b2026_pdf_parse_string_value(doc, doc->objects[index].body, doc->objects[index].body_length, p, depth + 1);
    } else goto fail;
    { char *result = b2026_pdf_decode_text_bytes((const unsigned char *)(raw.data != NULL ? raw.data : ""), raw.used); free(raw.data); return result; }
fail:
    free(raw.data); return NULL;
}

static char *b2026_pdf_dict_string(B2026PdfDocument *doc, const unsigned char *body,
                                   size_t n, const char *key) {
    size_t p; if (b2026_pdf_find_key(body, n, key, &p) == NULL) return NULL;
    return b2026_pdf_parse_string_value(doc, body, n, p, 0);
}

static char *b2026_pdf_dict_name(const unsigned char *body, size_t n, const char *key) {
    size_t p, start; char *out;
    if (b2026_pdf_find_key(body, n, key, &p) == NULL || p >= n || body[p] != '/') return NULL;
    start = ++p; while (p < n && !b2026_pdf_is_delim(body[p])) ++p;
    out = (char *)malloc(p - start + 1U); if (out == NULL) return NULL;
    memcpy(out, body + start, p - start); out[p - start] = '\0'; return out;
}

static int b2026_pdf_dict_real(const unsigned char *body, size_t n, const char *key, double *value) {
    size_t p; char *end;
    if (b2026_pdf_find_key(body, n, key, &p) == NULL) return 0;
    errno = 0; *value = strtod((const char *)body + p, &end);
    return end != (char *)body + p && errno != ERANGE && isfinite(*value);
}

static int b2026_pdf_number_array(const unsigned char *body, size_t n, const char *key,
                                  double *values, int max_values) {
    size_t p; int count = 0; char *end;
    if (b2026_pdf_find_key(body, n, key, &p) == NULL || p >= n || body[p] != '[') return 0;
    ++p;
    while (p < n && body[p] != ']' && count < max_values) {
        while (p < n && isspace(body[p])) ++p; if (p >= n || body[p] == ']') break;
        errno = 0; values[count] = strtod((const char *)body + p, &end);
        if (end == (char *)body + p || errno == ERANGE || !isfinite(values[count])) return -1;
        p = (size_t)(end - (char *)body); ++count;
    }
    return count;
}

static int b2026_pdf_entity(B2026PdfTextBuffer *out, const char *s, size_t n) {
    unsigned int cp = 0U; char temp[32], *end;
    if (n >= sizeof(temp)) return 0; memcpy(temp, s, n); temp[n] = '\0';
    if (strcmp(temp, "amp") == 0) cp = '&'; else if (strcmp(temp, "lt") == 0) cp = '<';
    else if (strcmp(temp, "gt") == 0) cp = '>'; else if (strcmp(temp, "quot") == 0) cp = '"';
    else if (strcmp(temp, "apos") == 0) cp = '\''; else if (strcmp(temp, "nbsp") == 0) cp = 0xA0U;
    else if (temp[0] == '#') { errno = 0; cp = (unsigned int)strtoul(temp + (temp[1] == 'x' || temp[1] == 'X' ? 2 : 1), &end, (temp[1] == 'x' || temp[1] == 'X') ? 16 : 10); if (errno == ERANGE || *end != '\0') return 0; }
    else return 0;
    return b2026_pdf_text_append_codepoint(out, cp);
}

static char *b2026_pdf_markup_to_text(const char *markup) {
    B2026PdfTextBuffer out; const char *p = markup != NULL ? markup : ""; int suppress = 0;
    memset(&out, 0, sizeof(out));
    while (*p != '\0') {
        if (*p == '<') {
            const char *end = strchr(p, '>'); size_t len; char tag[32]; size_t i = 0U, j;
            if (end == NULL) break; len = (size_t)(end - p - 1); j = 0U;
            while (j < len && (isspace((unsigned char)p[1 + j]) || p[1 + j] == '/')) ++j;
            while (j < len && i + 1U < sizeof(tag) && (isalnum((unsigned char)p[1 + j]) || p[1 + j] == ':')) tag[i++] = (char)tolower((unsigned char)p[1 + j++]);
            tag[i] = '\0';
            if (strcmp(tag, "script") == 0 || strcmp(tag, "style") == 0) suppress = p[1] == '/' ? 0 : 1;
            if (!suppress && (strcmp(tag, "br") == 0 || strcmp(tag, "p") == 0 || strcmp(tag, "div") == 0 || strcmp(tag, "li") == 0) && out.used > 0U && out.data[out.used - 1U] != '\n')
                if (!b2026_pdf_text_append_char(&out, '\n')) goto fail;
            p = end + 1; continue;
        }
        if (suppress) { ++p; continue; }
        if (*p == '&') {
            const char *semi = strchr(p + 1, ';');
            if (semi != NULL && semi - p < 32 && b2026_pdf_entity(&out, p + 1, (size_t)(semi - p - 1))) { p = semi + 1; continue; }
        }
        if (!b2026_pdf_text_append_char(&out, *p++)) goto fail;
    }
    while (out.used > 0U && isspace((unsigned char)out.data[out.used - 1U])) out.data[--out.used] = '\0';
    return b2026_pdf_text_take(&out);
fail:
    free(out.data); return NULL;
}

static char *b2026_pdf_stream_text(B2026PdfObject *o) {
    const unsigned char *stream, *payload, *endstream; size_t n; int decoded_length = 0; char *decoded;
    stream = b2026_pdf_memfind(o->body, o->body_length, "stream", 6U); if (stream == NULL) return NULL;
    payload = stream + 6U; if (payload < o->body + o->body_length && *payload == '\r') ++payload;
    if (payload < o->body + o->body_length && *payload == '\n') ++payload;
    endstream = b2026_pdf_memfind(payload, o->body_length - (size_t)(payload - o->body), "endstream", 9U); if (endstream == NULL) return NULL;
    n = (size_t)(endstream - payload); while (n > 0U && (payload[n - 1U] == '\r' || payload[n - 1U] == '\n')) --n;
    if (n > INT_MAX) return NULL;
    if (b2026_pdf_memfind(o->body, (size_t)(stream - o->body), "/FlateDecode", 12U) != NULL)
        decoded = stbi_zlib_decode_malloc((const char *)payload, (int)n, &decoded_length);
    else if (b2026_pdf_memfind(o->body, (size_t)(stream - o->body), "/Filter", 7U) == NULL) {
        decoded = (char *)malloc(n + 1U); if (decoded != NULL) { memcpy(decoded, payload, n); decoded[n] = '\0'; }
    } else return NULL;
    return decoded;
}

static char *b2026_pdf_xmp_text(B2026PdfDocument *doc) {
    int i;
    for (i = 0; i < doc->object_count; ++i) {
        B2026PdfObject *o = &doc->objects[i];
        if (b2026_pdf_dict_name_is(o->body, o->body_length, "/Type", "Metadata") ||
            b2026_pdf_memfind(o->body, o->body_length, "/Subtype /XML", 13U) != NULL) return b2026_pdf_stream_text(o);
    }
    return NULL;
}

static char *b2026_pdf_xml_field(const char *xml, const char *tag) {
    char open[80], close[80]; const char *a, *b, *gt; char *fragment, *text; size_t n;
    if (xml == NULL) return NULL;
    snprintf(open, sizeof(open), "<%s", tag); snprintf(close, sizeof(close), "</%s>", tag);
    a = strstr(xml, open); if (a == NULL) return NULL; gt = strchr(a, '>'); if (gt == NULL) return NULL;
    b = strstr(gt + 1, close); if (b == NULL) return NULL; n = (size_t)(b - (gt + 1));
    fragment = (char *)malloc(n + 1U); if (fragment == NULL) return NULL;
    memcpy(fragment, gt + 1, n); fragment[n] = '\0'; text = b2026_pdf_markup_to_text(fragment); free(fragment); return text;
}

static char *b2026_pdf_normalize_date(const char *value) {
    char out[64]; const char *p; int y=0,mo=1,d=1,h=0,mi=0,se=0; char sign = '\0'; int oh = 0, om = 0;
    if (value == NULL) return NULL; p = strncmp(value, "D:", 2U) == 0 ? value + 2 : value;
    /* XMP already uses ISO 8601.  Do not reinterpret its separators as signs. */
    if (strlen(p) >= 10U && p[4] == '-' && p[7] == '-') return b2026_strdup_simple(p);
    if (strlen(p) < 4U || sscanf(p, "%4d%2d%2d%2d%2d%2d", &y,&mo,&d,&h,&mi,&se) < 1) return b2026_strdup_simple(value);
    mo = strlen(p) >= 6U ? mo : 1; d = strlen(p) >= 8U ? d : 1; h = strlen(p) >= 10U ? h : 0; mi = strlen(p) >= 12U ? mi : 0; se = strlen(p) >= 14U ? se : 0;
    if (strlen(p) > 14U && (p[14] == '+' || p[14] == '-')) { sign = p[14]; (void)sscanf(p + 15, "%2d'%2d", &oh, &om); }
    if (strlen(p) > 14U && p[14] == 'Z') snprintf(out, sizeof(out), "%04d-%02d-%02dT%02d:%02d:%02dZ", y,mo,d,h,mi,se);
    else if (sign != '\0') snprintf(out, sizeof(out), "%04d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d", y,mo,d,h,mi,se,sign,oh,om);
    else snprintf(out, sizeof(out), "%04d-%02d-%02dT%02d:%02d:%02d", y,mo,d,h,mi,se);
    return b2026_strdup_simple(out);
}

static void b2026_pdf_hash_pair_number(const char *target, const char *jp, const char *en, double value) {
    hash_set_number(target, jp, value); hash_set_number(target, en, value);
}

static int b2026_pdf_inspect_fail(int code, const char *command, const char *path, const char *detail) {
    char message[4096];
    snprintf(message, sizeof(message), "%s failed: path=%s: %s",
             command, path != NULL ? path : "", detail != NULL ? detail : "unknown error / 不明なエラー");
    basic_set_error_logged(code, message, command, current_source_path, current_line, "PDF");
    b2026_try_append_log_line("__PDF_ERROR.log", message); return 0;
}

static int b2026_pdf_count_token(const unsigned char *data, size_t n, const char *token) {
    int count = 0; const unsigned char *p = data; size_t len = strlen(token);
    while ((p = b2026_pdf_memfind(p, n - (size_t)(p - data), token, len)) != NULL) { ++count; p += len; }
    return count;
}

static const char *b2026_pdf_annotation_category(const char *type, B2026PdfAnnotationStats *s) {
    static const char *comments[] = {"Text","FreeText","Highlight","Underline","StrikeOut","Squiggly","Stamp","Ink","Line","Square","Circle","Polygon","PolyLine","Caret"};
    size_t i; ++s->total;
    if (type != NULL && strcmp(type, "Link") == 0) { ++s->links; return "LINK"; }
    if (type != NULL && strcmp(type, "Widget") == 0) { ++s->widgets; return "WIDGET"; }
    if (type != NULL && strcmp(type, "FileAttachment") == 0) { ++s->attachments; return "FILEATTACHMENT"; }
    if (type != NULL && (strcmp(type,"Sound")==0 || strcmp(type,"Movie")==0 || strcmp(type,"Screen")==0 || strcmp(type,"RichMedia")==0 || strcmp(type,"3D")==0)) { ++s->media; return "MEDIA"; }
    for (i = 0U; type != NULL && i < sizeof(comments)/sizeof(comments[0]); ++i) if (strcmp(type, comments[i]) == 0) { ++s->comments; return "COMMENT"; }
    ++s->other; return "OTHER";
}

static void b2026_pdf_annotation_stats_to_hash(const char *target, const B2026PdfAnnotationStats *s) {
    b2026_pdf_hash_pair_number(target,"注釈", "hasAnnotations", s->total > 0 ? 1.0 : 0.0);
    b2026_pdf_hash_pair_number(target,"注釈数", "annotationCount", (double)s->total);
    b2026_pdf_hash_pair_number(target,"コメント注釈数", "commentAnnotationCount", (double)s->comments);
    b2026_pdf_hash_pair_number(target,"リンク注釈数", "linkAnnotationCount", (double)s->links);
    b2026_pdf_hash_pair_number(target,"フォーム注釈数", "widgetAnnotationCount", (double)s->widgets);
    b2026_pdf_hash_pair_number(target,"ファイル添付注釈数", "fileAttachmentAnnotationCount", (double)s->attachments);
    b2026_pdf_hash_pair_number(target,"メディア注釈数", "mediaAnnotationCount", (double)s->media);
    b2026_pdf_hash_pair_number(target,"その他注釈数", "otherAnnotationCount", (double)s->other);
}

static void b2026_pdf_annotation_key(char *out, size_t cap, int index, const char *field) {
    snprintf(out, cap, "%d,%s", index, field);
}

static const char *b2026_pdf_annotation_japanese_field(const char *field) {
    static const char *en[] = {"page","type","category","text","richText","author","subject","modified","created","id","parentId","replyType","left","bottom","right","top","quadPoints","quadPointCount","colorR","colorG","colorB","opacity","flags","status","stateModel"};
    static const char *jp[] = {"ページ","種類","分類","内容","リッチテキスト","作成者","件名","更新日時","作成日時","ID","親ID","返信種別","左","下","右","上","四角形座標","四角形座標数","赤","緑","青","不透明度","フラグ","状態","状態モデル"};
    size_t i;for(i=0U;i<sizeof(en)/sizeof(en[0]);++i)if(strcmp(field,en[i])==0)return jp[i];return NULL;
}

static void b2026_pdf_annotation_null_fields(const char *target, int index) {
    static const char *fields[] = {"page","type","category","text","richText","author","subject","modified","created","id","parentId","replyType","left","bottom","right","top","quadPoints","quadPointCount","colorR","colorG","colorB","opacity","flags","status","stateModel"};
    char key[96]; size_t i;
    for (i = 0U; i < sizeof(fields)/sizeof(fields[0]); ++i) { const char *jp=b2026_pdf_annotation_japanese_field(fields[i]);b2026_pdf_annotation_key(key,sizeof(key),index,fields[i]); hash_set_number(target,key,null_value());if(jp!=NULL){b2026_pdf_annotation_key(key,sizeof(key),index,jp);hash_set_number(target,key,null_value());} }
}

static void b2026_pdf_annotation_string_field(const char *target, int index, const char *field, const char *value) {
    char key[96];const char *jp; if (value == NULL) return; b2026_pdf_annotation_key(key,sizeof(key),index,field); hash_set_string(target,key,value);jp=b2026_pdf_annotation_japanese_field(field);if(jp!=NULL){b2026_pdf_annotation_key(key,sizeof(key),index,jp);hash_set_string(target,key,value);}
}

static void b2026_pdf_annotation_number_field(const char *target, int index, const char *field, double value) {
    char key[96];const char *jp; b2026_pdf_annotation_key(key,sizeof(key),index,field); hash_set_number(target,key,value);jp=b2026_pdf_annotation_japanese_field(field);if(jp!=NULL){b2026_pdf_annotation_key(key,sizeof(key),index,jp);hash_set_number(target,key,value);}
}

static int b2026_pdf_annotation_one(B2026PdfDocument *doc, const unsigned char *body, size_t n,
                                    int object_index, int page, const char *target, int detail,
                                    int *detail_index, B2026PdfAnnotationStats *stats) {
    char *type = b2026_pdf_dict_name(body,n,"/Subtype"), *text = NULL, *rich = NULL, *author = NULL, *subject = NULL;
    char *modified = NULL, *created = NULL, *id = NULL, *reply = NULL, *status = NULL, *state_model = NULL;
    const char *category; double rect[4], color[4], opacity, flags; int rect_count, color_count;
    category = b2026_pdf_annotation_category(type, stats);
    if (!detail) { free(type); return 1; }
    ++*detail_index; b2026_pdf_annotation_null_fields(target, *detail_index);
    b2026_pdf_annotation_number_field(target,*detail_index,"page",(double)page);
    b2026_pdf_annotation_string_field(target,*detail_index,"type",type != NULL ? type : "UNKNOWN");
    b2026_pdf_annotation_string_field(target,*detail_index,"category",category);
    text = b2026_pdf_dict_string(doc,body,n,"/Contents"); rich = b2026_pdf_dict_string(doc,body,n,"/RC");
    if (rich != NULL) { char *plain = b2026_pdf_markup_to_text(rich); free(rich); rich = plain; }
    if ((text == NULL || *text == '\0') && rich != NULL) { free(text); text = b2026_strdup_simple(rich); }
    author = b2026_pdf_dict_string(doc,body,n,"/T"); subject = b2026_pdf_dict_string(doc,body,n,"/Subj");
    modified = b2026_pdf_dict_string(doc,body,n,"/M"); created = b2026_pdf_dict_string(doc,body,n,"/CreationDate");
    if(modified!=NULL){char *normalized=b2026_pdf_normalize_date(modified);free(modified);modified=normalized;}
    if(created!=NULL){char *normalized=b2026_pdf_normalize_date(created);free(created);created=normalized;}
    id = b2026_pdf_dict_string(doc,body,n,"/NM"); reply = b2026_pdf_dict_name(body,n,"/RT");
    status = b2026_pdf_dict_string(doc,body,n,"/State"); if (status == NULL) status = b2026_pdf_dict_name(body,n,"/State");
    state_model = b2026_pdf_dict_string(doc,body,n,"/StateModel"); if (state_model == NULL) state_model = b2026_pdf_dict_name(body,n,"/StateModel");
    b2026_pdf_annotation_string_field(target,*detail_index,"text",text);
    b2026_pdf_annotation_string_field(target,*detail_index,"richText",rich);
    b2026_pdf_annotation_string_field(target,*detail_index,"author",author);
    b2026_pdf_annotation_string_field(target,*detail_index,"subject",subject);
    b2026_pdf_annotation_string_field(target,*detail_index,"modified",modified);
    b2026_pdf_annotation_string_field(target,*detail_index,"created",created);
    b2026_pdf_annotation_string_field(target,*detail_index,"id",id);
    b2026_pdf_annotation_string_field(target,*detail_index,"replyType",reply);
    b2026_pdf_annotation_string_field(target,*detail_index,"status",status);
    b2026_pdf_annotation_string_field(target,*detail_index,"stateModel",state_model);
    { int pn,pg,parent_index; if (b2026_pdf_named_ref(body,n,"/IRT",&pn,&pg) && (parent_index=b2026_pdf_object_lookup(doc,pn,pg))>=0) { char *parent_id=b2026_pdf_dict_string(doc,doc->objects[parent_index].body,doc->objects[parent_index].body_length,"/NM"); char fallback[40]; if(parent_id==NULL){snprintf(fallback,sizeof(fallback),"%d %d R",pn,pg);parent_id=b2026_strdup_simple(fallback);} b2026_pdf_annotation_string_field(target,*detail_index,"parentId",parent_id);free(parent_id);} }
    rect_count = b2026_pdf_number_array(body,n,"/Rect",rect,4);
    if (rect_count == 4) { double unit=1.0; int page_obj=doc->page_objects[page-1]; if (b2026_pdf_page_direct_number(doc,page_obj,"/UserUnit",&unit)<=0) unit=1.0; b2026_pdf_annotation_number_field(target,*detail_index,"left",fmin(rect[0],rect[2])*unit); b2026_pdf_annotation_number_field(target,*detail_index,"bottom",fmin(rect[1],rect[3])*unit); b2026_pdf_annotation_number_field(target,*detail_index,"right",fmax(rect[0],rect[2])*unit); b2026_pdf_annotation_number_field(target,*detail_index,"top",fmax(rect[1],rect[3])*unit); }
    color_count = b2026_pdf_number_array(body,n,"/C",color,4);
    if (color_count == 1) { b2026_pdf_annotation_number_field(target,*detail_index,"colorR",color[0]); b2026_pdf_annotation_number_field(target,*detail_index,"colorG",color[0]); b2026_pdf_annotation_number_field(target,*detail_index,"colorB",color[0]); }
    else if (color_count >= 3) { b2026_pdf_annotation_number_field(target,*detail_index,"colorR",color[0]); b2026_pdf_annotation_number_field(target,*detail_index,"colorG",color[1]); b2026_pdf_annotation_number_field(target,*detail_index,"colorB",color[2]); }
    if (b2026_pdf_dict_real(body,n,"/CA",&opacity)) b2026_pdf_annotation_number_field(target,*detail_index,"opacity",opacity);
    if (b2026_pdf_dict_real(body,n,"/F",&flags)) b2026_pdf_annotation_number_field(target,*detail_index,"flags",flags);
    { double points[256]; int count=b2026_pdf_number_array(body,n,"/QuadPoints",points,256); if(count>0){B2026PdfTextBuffer q;int i;memset(&q,0,sizeof(q));for(i=0;i<count;++i){char num[48];snprintf(num,sizeof(num),"%s%.17g",i==0?"":",",points[i]);if(!b2026_pdf_text_append_bytes(&q,num,strlen(num))){free(q.data);break;}}if(q.data!=NULL){b2026_pdf_annotation_string_field(target,*detail_index,"quadPoints",q.data);free(q.data);}b2026_pdf_annotation_number_field(target,*detail_index,"quadPointCount",(double)(count/2));} }
    free(type); free(text); free(rich); free(author); free(subject); free(modified); free(created); free(id); free(reply); free(status); free(state_model); (void)object_index; return 1;
}

static int b2026_pdf_annotations_for_page(B2026PdfDocument *doc, int page, const char *target,
                                          int detail, int *detail_index, B2026PdfAnnotationStats *stats,
                                          unsigned char *seen) {
    B2026PdfObject *po; const unsigned char *array; size_t p, end, n; int an,ag,array_index=-1;
    po=&doc->objects[doc->page_objects[page-1]];
    if (b2026_pdf_find_key(po->body,po->body_length,"/Annots",&p)==NULL) return 1;
    array=po->body;n=po->body_length;
    if(p<n && isdigit(array[p])){if(!b2026_pdf_named_ref(po->body,po->body_length,"/Annots",&an,&ag))return 0;array_index=b2026_pdf_object_lookup(doc,an,ag);if(array_index<0)return 0;array=doc->objects[array_index].body;n=doc->objects[array_index].body_length;p=0U;while(p<n&&isspace(array[p]))++p;}
    if(p>=n||array[p]!='[')return 0;end=p+1U;{int level=1;while(end<n&&level>0){if(array[end]=='[')++level;else if(array[end]==']')--level;++end;}if(level!=0)return 0;}
    ++p;
    while(p<end){while(p<end&&isspace(array[p]))++p;if(p>=end||array[p]==']')break;
        if(p+1U<end&&array[p]=='<'&&array[p+1U]=='<'){size_t start=p,scan=p+2U;int level=1;while(scan<end&&level>0){if(scan+1U<end&&array[scan]=='<'&&array[scan+1U]=='<'){++level;scan+=2U;}else if(scan+1U<end&&array[scan]=='>'&&array[scan+1U]=='>'){--level;scan+=2U;}else ++scan;}if(level!=0)return 0;if(!b2026_pdf_annotation_one(doc,array+start,scan-start,-1,page,target,detail,detail_index,stats))return 0;p=scan;continue;}
        if(isdigit(array[p])){char *e;long number=strtol((const char*)array+p,&e,10),generation;size_t q=(size_t)(e-(char*)array);while(q<end&&isspace(array[q]))++q;generation=strtol((const char*)array+q,&e,10);if(e!=(char*)array+q){q=(size_t)(e-(char*)array);while(q<end&&isspace(array[q]))++q;if(q<end&&array[q]=='R'&&number>0&&number<=INT_MAX&&generation>=0&&generation<=INT_MAX){int index=b2026_pdf_object_lookup(doc,(int)number,(int)generation);if(index<0)return 0;if(!seen[index]){seen[index]=1;if(!b2026_pdf_annotation_one(doc,doc->objects[index].body,doc->objects[index].body_length,index,page,target,detail,detail_index,stats))return 0;}p=q+1U;continue;}}}
        ++p;
    }
    (void)array_index; return 1;
}

static int b2026_pdf_collect_annotations(B2026PdfDocument *doc, int requested_page, const char *target,
                                         int detail, B2026PdfAnnotationStats *stats, int *detail_count) {
    unsigned char *seen=(unsigned char*)calloc((size_t)doc->object_count,1U);int first=1,last=doc->page_count,p;
    if(seen==NULL)return 0;memset(stats,0,sizeof(*stats));*detail_count=0;
    if(requested_page>0)first=last=requested_page;
    for(p=first;p<=last;++p)if(!b2026_pdf_annotations_for_page(doc,p,target,detail,detail_count,stats,seen)){free(seen);return 0;}
    free(seen);return 1;
}

static void b2026_pdf_info_defaults(const char *target) {
    static const char *numeric[] = {"pages","fileSize","encrypted","signed","hasForm","hasAttachments","hasJavaScript","hasBookmarks","hasPageLabels","tagged","hasXMP","hasObjectStreams","incrementalUpdates"};
    static const char *strings[] = {"version","headerVersion","title","author","subject","keywords","creator","producer","creationDate","modDate","metadataSource","xrefType"};
    size_t i; for(i=0U;i<sizeof(numeric)/sizeof(numeric[0]);++i)hash_set_number(target,numeric[i],null_value());
    for(i=0U;i<sizeof(strings)/sizeof(strings[0]);++i)hash_set_number(target,strings[i],null_value());
}

static void b2026_pdf_info_aliases(const char *target) {
    static const char *jp[] = {"版","ヘッダー版","題名","著者","件名","キーワード","作成ソフト","生成ソフト","作成日時","更新日時","メタデータ元","総ページ数","ファイルサイズ","暗号化","電子署名","フォーム","添付ファイル","JavaScript","しおり","ページラベル","タグ付き","XMP","オブジェクトストリーム","増分更新回数","相互参照形式"};
    static const char *en[] = {"version","headerVersion","title","author","subject","keywords","creator","producer","creationDate","modDate","metadataSource","pages","fileSize","encrypted","signed","hasForm","hasAttachments","hasJavaScript","hasBookmarks","hasPageLabels","tagged","hasXMP","hasObjectStreams","incrementalUpdates","xrefType"};
    HashVariable *hash_var=ensure_local_hash(get_hash_frame(target),target); size_t i;
    for(i=0U;i<sizeof(en)/sizeof(en[0]);++i){HashEntry *e=find_hash_entry(hash_var,en[i]);if(e!=NULL){if(e->value.type!=TYPE_NUMBER){char *copy=b2026_strdup_simple(e->value.val.blob.data!=NULL?e->value.val.blob.data:"");if(copy!=NULL){hash_set_string(target,jp[i],copy);free(copy);}}else{double number=e->value.val.number;hash_set_number(target,jp[i],number);}}}
}

static int b2026_pdf_has_xref_table(const unsigned char *data, size_t n) {
    size_t i;
    for(i=0U;i+4U<=n;++i)if(memcmp(data+i,"xref",4U)==0){
        int line_start=(i==0U||data[i-1U]=='\n'||data[i-1U]=='\r');
        int after=(i+4U==n||isspace(data[i+4U]));
        if(line_start&&after)return 1;
    }
    return 0;
}

static void b2026_pdf_effective_version(B2026PdfDocument *doc, char *out, size_t cap) {
    int i; char *catalog_version=NULL;
    snprintf(out,cap,"%d.%d",doc->version_major,doc->version_minor);
    if(doc->root_object>=0&&doc->root_object<doc->object_count)catalog_version=b2026_pdf_dict_name(doc->objects[doc->root_object].body,doc->objects[doc->root_object].body_length,"/Version");
    if(catalog_version!=NULL&&*catalog_version!='\0')snprintf(out,cap,"%s",catalog_version);
    free(catalog_version);
}

static void b2026_pdf_info_metadata(B2026PdfDocument *doc, const char *target) {
    static const char *keys[] = {"/Title","/Author","/Subject","/Keywords","/Creator","/Producer","/CreationDate","/ModDate"};
    static const char *outkeys[] = {"title","author","subject","keywords","creator","producer","creationDate","modDate"};
    static const char *xmpkeys[] = {"dc:title","dc:creator","dc:description","pdf:Keywords","xmp:CreatorTool","pdf:Producer","xmp:CreateDate","xmp:ModifyDate"};
    char *info_values[8]={0},*xmp_values[8]={0},*xmp=b2026_pdf_xmp_text(doc);int i,info_any=0,xmp_any=0;
    if(doc->info_object>=0){B2026PdfObject*o=&doc->objects[doc->info_object];for(i=0;i<8;++i){info_values[i]=b2026_pdf_dict_string(doc,o->body,o->body_length,keys[i]);if(info_values[i]!=NULL)info_any=1;}}
    if(xmp!=NULL)for(i=0;i<8;++i){xmp_values[i]=b2026_pdf_xml_field(xmp,xmpkeys[i]);if(xmp_values[i]!=NULL&&*xmp_values[i]!='\0')xmp_any=1;}
    for(i=0;i<8;++i){char*value=(xmp_values[i]!=NULL&&*xmp_values[i]!='\0')?xmp_values[i]:info_values[i];char*normalized=NULL;if(value!=NULL&&(i==6||i==7))normalized=b2026_pdf_normalize_date(value);hash_set_string(target,outkeys[i],normalized!=NULL?normalized:(value!=NULL?value:""));if(value==NULL)hash_set_number(target,outkeys[i],null_value());free(normalized);free(info_values[i]);free(xmp_values[i]);}
    hash_set_string(target,"metadataSource",xmp_any?(info_any?"MIXED":"XMP"):(info_any?"INFO":"NONE"));free(xmp);
}

static int b2026_pdf_inspect(const char *target_name, const char *operation, int argc, const B2026PdfArg *args) {
    HashVariable *target;B2026PdfDocument doc;const char*path,*command;int rc,page=0,clamped,detail=0,has_page;B2026PdfAnnotationStats stats;int detail_count=0;
    if(target_name==NULL||*target_name=='\0')return b2026_pdf_inspect_fail(4150,"PDF.INFO","","result must be assigned to an associative array / 結果は連想配列へ代入してください");
    target=ensure_local_hash(get_hash_frame(target_name),target_name);clear_hash_entries(target);
    command=strcmp(operation,"INFO")==0?"PDF.INFO":(strcmp(operation,"ANNOTATIONINFO")==0?"PDF.ANNOTATIONINFO":"PDF.GETANNOTATIONS");
    if(argc<1||argc>2||args[0].kind!=1||args[0].text==NULL||(strcmp(operation,"INFO")==0&&argc!=1))
        return b2026_pdf_inspect_fail(4151,command,"","invalid PDF inspection arguments / PDF情報取得の引数が正しくありません");
    path=args[0].text;has_page=argc==2;if(has_page&&!b2026_pdf_arg_integer(&args[1],&page))return b2026_pdf_inspect_fail(4152,command,path,"page must be a finite integer / ページ番号は有限の整数で指定してください");
    rc=b2026_pdf_load(&doc,path,0);
    if(rc==-2&&strcmp(operation,"INFO")==0){char version[16];snprintf(version,sizeof(version),"%d.%d",doc.version_major,doc.version_minor);b2026_pdf_info_defaults(target_name);b2026_pdf_hash_pair_number(target_name,"取得","ok",1.0);b2026_pdf_hash_pair_number(target_name,"一部取得","partial",1.0);hash_set_string(target_name,"version",version);hash_set_string(target_name,"headerVersion",version);hash_set_number(target_name,"fileSize",(double)doc.length);hash_set_number(target_name,"encrypted",1.0);b2026_pdf_info_aliases(target_name);b2026_pdf_log_warning("暗号化PDFのため文書情報を部分取得しました。 / Encrypted PDF information was only partially available.");b2026_pdf_document_free(&doc);return 1;}
    if(rc<=0){b2026_pdf_document_free(&doc);return b2026_pdf_inspect_fail(4153,command,path,b2026_pdf_load_error(rc));}
    if(has_page){if(b2026_pdf_clamp_page(page,doc.page_count,&clamped))b2026_pdf_log_warning("注釈を調べるページ番号を有効な範囲へ調整しました。 / Annotation page number was adjusted.");page=clamped;}
    detail=strcmp(operation,"GETANNOTATIONS")==0;
    if(!b2026_pdf_collect_annotations(&doc,page,target_name,detail,&stats,&detail_count)){b2026_pdf_document_free(&doc);clear_hash_entries(target);return b2026_pdf_inspect_fail(4154,command,path,"annotation structure is invalid / 注釈構造が不正です");}
    b2026_pdf_hash_pair_number(target_name,"取得","ok",1.0);b2026_pdf_hash_pair_number(target_name,"一部取得","partial",0.0);b2026_pdf_hash_pair_number(target_name,"総ページ数","pages",(double)doc.page_count);
    if(page>0)b2026_pdf_hash_pair_number(target_name,"ページ","page",(double)page);else b2026_pdf_hash_pair_number(target_name,"ページ","page",null_value());
    b2026_pdf_annotation_stats_to_hash(target_name,&stats);
    if(detail){b2026_pdf_hash_pair_number(target_name,"注釈数","count",(double)detail_count);}
    if(strcmp(operation,"INFO")==0){char version[16],header[16];int starts,xref_table,xref_stream;snprintf(header,sizeof(header),"%d.%d",doc.version_major,doc.version_minor);b2026_pdf_effective_version(&doc,version,sizeof(version));b2026_pdf_info_defaults(target_name);b2026_pdf_hash_pair_number(target_name,"取得","ok",1.0);b2026_pdf_hash_pair_number(target_name,"一部取得","partial",0.0);hash_set_string(target_name,"version",version);hash_set_string(target_name,"headerVersion",header);hash_set_number(target_name,"pages",(double)doc.page_count);hash_set_number(target_name,"fileSize",(double)doc.length);hash_set_number(target_name,"encrypted",0.0);hash_set_number(target_name,"signed",b2026_pdf_memfind(doc.bytes,doc.length,"/ByteRange",10U)!=NULL?1.0:0.0);hash_set_number(target_name,"hasForm",b2026_pdf_memfind(doc.bytes,doc.length,"/AcroForm",9U)!=NULL?1.0:0.0);hash_set_number(target_name,"hasAttachments",b2026_pdf_memfind(doc.bytes,doc.length,"/EmbeddedFiles",14U)!=NULL?1.0:0.0);hash_set_number(target_name,"hasJavaScript",(b2026_pdf_memfind(doc.bytes,doc.length,"/JavaScript",11U)!=NULL||b2026_pdf_memfind(doc.bytes,doc.length,"/JS",3U)!=NULL)?1.0:0.0);hash_set_number(target_name,"hasBookmarks",b2026_pdf_memfind(doc.bytes,doc.length,"/Outlines",9U)!=NULL?1.0:0.0);hash_set_number(target_name,"hasPageLabels",b2026_pdf_memfind(doc.bytes,doc.length,"/PageLabels",11U)!=NULL?1.0:0.0);hash_set_number(target_name,"tagged",b2026_pdf_memfind(doc.bytes,doc.length,"/StructTreeRoot",15U)!=NULL?1.0:0.0);hash_set_number(target_name,"hasXMP",(b2026_pdf_memfind(doc.bytes,doc.length,"/Type /Metadata",15U)!=NULL||b2026_pdf_memfind(doc.bytes,doc.length,"/Subtype /XML",13U)!=NULL)?1.0:0.0);hash_set_number(target_name,"hasObjectStreams",b2026_pdf_memfind(doc.bytes,doc.length,"/ObjStm",7U)!=NULL?1.0:0.0);starts=b2026_pdf_count_token(doc.bytes,doc.length,"startxref");hash_set_number(target_name,"incrementalUpdates",(double)(starts>0?starts-1:0));xref_table=b2026_pdf_has_xref_table(doc.bytes,doc.length);xref_stream=b2026_pdf_memfind(doc.bytes,doc.length,"/Type /XRef",11U)!=NULL;hash_set_string(target_name,"xrefType",xref_table&&xref_stream?"HYBRID":(xref_stream?"STREAM":"TABLE"));b2026_pdf_info_metadata(&doc,target_name);b2026_pdf_annotation_stats_to_hash(target_name,&stats);b2026_pdf_info_aliases(target_name);}
    b2026_pdf_document_free(&doc);return 1;
}
