Files
ElyOffice/crates/ely-pdf-engine/shim/ely_ocr.c
T
ZacharyZhang-NYandClaude Opus 5.5 68b5af76e0 Windows and Linux build environments; check clean on both (E-01)
MuPDF builds with MSBuild on Windows (/MT, OpenSSL from vcpkg);
Git for Windows' MinGit stands in for the source build there. CEF
loads from where libcef lives and sets its API version first on
every platform. File identity on Windows compares volume and index.
The checkout and reference/ keep LF on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:53:56 -04:00

285 lines
9.8 KiB
C

/* OCR: recognise a page, add a hidden text layer (PDF-08). */
#include <string.h>
#include "ely_shim.h"
static const char to_unicode[] =
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n"
"/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n"
"/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n"
"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n"
"1 beginbfrange\n<0000> <FFFF> <0000>\nendbfrange\n"
"endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n";
/* Lines Tesseract reads on a page, page space, as JSON. */
int ely_ocr_page(fz_context *ctx, fz_document *doc, int number, const char *lang, const char *datadir, float dpi,
unsigned char **data, size_t *len)
{
fz_page *page = NULL;
fz_pixmap *pix = NULL;
fz_image *image = NULL;
fz_stext_page *text = NULL;
fz_device *sdev = NULL, *ocr = NULL;
fz_buffer *buf = NULL, *words = NULL;
fz_output *out = NULL;
fz_var(page);
fz_var(pix);
fz_var(image);
fz_var(text);
fz_var(sdev);
fz_var(ocr);
fz_var(buf);
fz_var(words);
fz_var(out);
fz_try(ctx)
{
float zoom = dpi / 72;
fz_rect area;
fz_stext_block *block;
fz_stext_line *line;
fz_stext_char *ch;
int first = 1;
if (dpi < 72 || dpi > 600)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a resolution of %g dpi", dpi);
page = fz_load_page(ctx, doc, number);
pix = fz_new_pixmap_from_page(ctx, page, fz_scale(zoom, zoom), fz_device_gray(ctx), 0);
image = fz_new_image_from_pixmap(ctx, pix, NULL);
area = fz_make_rect(0, 0, (float)pix->w, (float)pix->h);
text = fz_new_stext_page(ctx, area);
sdev = fz_new_stext_device(ctx, text, NULL);
ocr = fz_new_ocr_device(ctx, sdev, fz_identity, area, 0, lang, datadir, NULL, NULL);
fz_fill_image(ctx, ocr, image, fz_make_matrix(area.x1, 0, 0, area.y1, 0, 0), 1, fz_default_color_params);
fz_close_device(ctx, ocr);
fz_close_device(ctx, sdev);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
for (block = text->first_block; block; block = block->next)
{
if (block->type != FZ_STEXT_BLOCK_TEXT)
continue;
for (line = block->u.t.first_line; line; line = line->next)
{
fz_rect r = fz_transform_rect(line->bbox, fz_scale(1 / zoom, 1 / zoom));
if (!line->first_char)
continue;
fz_drop_buffer(ctx, words);
words = NULL;
words = fz_new_buffer(ctx, 128);
for (ch = line->first_char; ch; ch = ch->next)
fz_append_rune(ctx, words, ch->c);
fz_terminate_buffer(ctx, words);
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":", first ? "" : ",", r.x0, r.y0, r.x1, r.y1);
ely_json_string(ctx, out, fz_string_from_buffer(ctx, words));
fz_write_byte(ctx, out, '}');
first = 0;
}
}
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_buffer(ctx, words);
fz_drop_device(ctx, ocr);
fz_drop_device(ctx, sdev);
fz_drop_stext_page(ctx, text);
fz_drop_image(ctx, image);
fz_drop_pixmap(ctx, pix);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* A font with no glyphs; codes are UTF-16 units. */
static pdf_obj *glyphless(fz_context *ctx, pdf_document *pdf)
{
pdf_obj *font = NULL, *cid = NULL, *desc = NULL, *info, *bbox, *kids;
fz_buffer *cmap = NULL;
fz_var(font);
fz_var(cid);
fz_var(desc);
fz_var(cmap);
fz_try(ctx)
{
desc = pdf_add_new_dict(ctx, pdf, 8);
pdf_dict_put(ctx, desc, PDF_NAME(Type), PDF_NAME(FontDescriptor));
pdf_dict_put_name(ctx, desc, PDF_NAME(FontName), "GlyphLessFont");
pdf_dict_put_int(ctx, desc, PDF_NAME(Flags), 5);
bbox = pdf_dict_put_array(ctx, desc, PDF_NAME(FontBBox), 4);
pdf_array_push_int(ctx, bbox, 0), pdf_array_push_int(ctx, bbox, 0);
pdf_array_push_int(ctx, bbox, 500), pdf_array_push_int(ctx, bbox, 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(ItalicAngle), 0);
pdf_dict_put_int(ctx, desc, PDF_NAME(Ascent), 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(Descent), 0);
pdf_dict_put_int(ctx, desc, PDF_NAME(CapHeight), 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(StemV), 80);
cid = pdf_add_new_dict(ctx, pdf, 8);
pdf_dict_put(ctx, cid, PDF_NAME(Type), PDF_NAME(Font));
pdf_dict_put(ctx, cid, PDF_NAME(Subtype), PDF_NAME(CIDFontType2));
pdf_dict_put_name(ctx, cid, PDF_NAME(BaseFont), "GlyphLessFont");
info = pdf_dict_put_dict(ctx, cid, PDF_NAME(CIDSystemInfo), 3);
pdf_dict_put_text_string(ctx, info, PDF_NAME(Registry), "Adobe");
pdf_dict_put_text_string(ctx, info, PDF_NAME(Ordering), "Identity");
pdf_dict_put_int(ctx, info, PDF_NAME(Supplement), 0);
pdf_dict_put(ctx, cid, PDF_NAME(FontDescriptor), desc);
pdf_dict_put_int(ctx, cid, PDF_NAME(DW), 500);
pdf_dict_put(ctx, cid, PDF_NAME(CIDToGIDMap), PDF_NAME(Identity));
font = pdf_add_new_dict(ctx, pdf, 6);
pdf_dict_put(ctx, font, PDF_NAME(Type), PDF_NAME(Font));
pdf_dict_put(ctx, font, PDF_NAME(Subtype), PDF_NAME(Type0));
pdf_dict_put_name(ctx, font, PDF_NAME(BaseFont), "GlyphLessFont");
pdf_dict_put(ctx, font, PDF_NAME(Encoding), PDF_NAME(Identity_H));
kids = pdf_dict_put_array(ctx, font, PDF_NAME(DescendantFonts), 1);
pdf_array_push(ctx, kids, cid);
cmap = fz_new_buffer_from_copied_data(ctx, (const unsigned char *)to_unicode, strlen(to_unicode));
pdf_dict_put_drop(ctx, font, PDF_NAME(ToUnicode), pdf_add_stream(ctx, pdf, cmap, NULL, 0));
}
fz_always(ctx)
{
pdf_drop_obj(ctx, desc);
pdf_drop_obj(ctx, cid);
fz_drop_buffer(ctx, cmap);
}
fz_catch(ctx)
{
pdf_drop_obj(ctx, font);
fz_rethrow(ctx);
}
return font;
}
/* Drops the page's earlier Ely OCR layers. */
static void drop_layers(fz_context *ctx, pdf_obj *page)
{
pdf_obj *contents = pdf_dict_get(ctx, page, PDF_NAME(Contents));
int i;
for (i = pdf_array_len(ctx, contents) - 1; i >= 0; i--)
if (pdf_dict_gets(ctx, pdf_array_get(ctx, contents, i), "ElyOCR"))
pdf_array_delete(ctx, contents, i);
}
/* Throws unless every line is whole and in the BMP. */
static void check_layer(fz_context *ctx, fz_json *pages, int count)
{
int i, k;
for (i = 0; i < fz_json_array_length(ctx, pages); i++)
{
fz_json *spec = fz_json_array_get(ctx, pages, i);
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
fz_json *lines = ely_member(ctx, spec, "lines", FZ_JSON_ARRAY);
if (!page || page->u.number < 0 || page->u.number >= count || !lines)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a layer with no page or lines");
for (k = 0; k < fz_json_array_length(ctx, lines); k++)
{
fz_json *line = fz_json_array_get(ctx, lines, k);
fz_json *words = ely_member(ctx, line, "text", FZ_JSON_STRING);
const char *s;
int c;
ely_rect(ctx, ely_member(ctx, line, "rect", FZ_JSON_ARRAY), 0.5f);
if (!words || !words->u.string[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line with no text");
for (s = words->u.string; *s; )
{
s += fz_chartorune(&c, s);
if (c > 0xFFFF || c == FZ_REPLACEMENT_CHARACTER)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a character the layer cannot hold: U+%X", c);
}
}
}
}
/* One line: hidden text stretched over its box. */
static void write_line(fz_context *ctx, fz_buffer *buf, fz_json *line, fz_matrix to_user, const char *font)
{
fz_rect r = ely_rect(ctx, ely_member(ctx, line, "rect", FZ_JSON_ARRAY), 0.5f);
const char *s = ely_member(ctx, line, "text", FZ_JSON_STRING)->u.string, *p;
float size = r.y1 - r.y0;
int n = 0, c;
fz_matrix m;
for (p = s; *p; n++)
p += fz_chartorune(&c, p);
/* Each code is half an em wide. */
m = fz_concat(fz_make_matrix((r.x1 - r.x0) / (0.5f * n), 0, 0, -size, r.x0, r.y1 - 0.2f * size), to_user);
fz_append_printf(ctx, buf, "BT 3 Tr /%s 1 Tf %g %g %g %g %g %g Tm <", font, m.a, m.b, m.c, m.d, m.e, m.f);
for (p = s; *p; )
{
p += fz_chartorune(&c, p);
fz_append_printf(ctx, buf, "%04x", c);
}
fz_append_string(ctx, buf, "> Tj ET\n");
}
/* Hidden text on pages, earlier layers first dropped if asked. */
int ely_text_layer(fz_context *ctx, fz_document *doc, const char *json, int replace)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *font = NULL;
fz_buffer *buf = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(font);
fz_var(buf);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
fz_json *pages;
int i, k;
pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
pages = fz_parse_json(ctx, pool, json);
if (pages->type != FZ_JSON_ARRAY || fz_json_array_length(ctx, pages) == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no pages to lay text on");
check_layer(ctx, pages, pdf_count_pages(ctx, pdf));
pdf_begin_operation(ctx, pdf, "Add recognised text");
begun = 1;
for (i = 0; i < fz_json_array_length(ctx, pages); i++)
{
fz_json *spec = fz_json_array_get(ctx, pages, i);
fz_json *lines = ely_member(ctx, spec, "lines", FZ_JSON_ARRAY);
char name[32];
page = pdf_load_page(ctx, pdf, (int)ely_member(ctx, spec, "page", FZ_JSON_NUMBER)->u.number);
if (replace)
drop_layers(ctx, page->obj);
if (fz_json_array_length(ctx, lines) > 0)
{
if (!font)
font = glyphless(ctx, pdf);
ely_add_resource(ctx, page->obj, PDF_NAME(Font), "ElyOCR", font, name, sizeof name);
buf = fz_new_buffer(ctx, 1024);
/* Its own state, so later text stays seen. */
fz_append_string(ctx, buf, "q\n");
for (k = 0; k < fz_json_array_length(ctx, lines); k++)
write_line(ctx, buf, fz_json_array_get(ctx, lines, k), ely_to_user(ctx, page), name);
fz_append_string(ctx, buf, "Q\n");
pdf_dict_puts(ctx, ely_append_content(ctx, pdf, page->obj, buf), "ElyOCR", PDF_TRUE);
fz_drop_buffer(ctx, buf);
buf = NULL;
}
fz_drop_page(ctx, (fz_page *)page);
page = NULL;
}
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
pdf_drop_obj(ctx, font);
fz_drop_page(ctx, (fz_page *)page);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}