Files
ElyOffice/crates/ely-pdf-engine/shim/ely_ocr.c
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -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, pdf, 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;
}