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>
435 lines
12 KiB
C
435 lines
12 KiB
C
/* Text blocks: list, take out glyphs, write new text (PDF-03). */
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include "ely_shim.h"
|
|
#include "mupdf/ucdn.h"
|
|
|
|
/* The page's text, read the same way every time. */
|
|
static fz_stext_page *read_text(fz_context *ctx, fz_document *doc, int number)
|
|
{
|
|
fz_stext_options opts = { 0 };
|
|
return fz_new_stext_page_from_page_number(ctx, doc, number, &opts);
|
|
}
|
|
|
|
/* The `index`th text block, or NULL. */
|
|
static fz_stext_block *nth_block(fz_stext_page *text, int index)
|
|
{
|
|
fz_stext_block *block;
|
|
for (block = text->first_block; block; block = block->next)
|
|
if (block->type == FZ_STEXT_BLOCK_TEXT && index-- == 0)
|
|
return block;
|
|
return NULL;
|
|
}
|
|
|
|
/* Glyph origins to take out, in user space. */
|
|
typedef struct
|
|
{
|
|
fz_point *at;
|
|
int *c;
|
|
int n;
|
|
int hit;
|
|
/* Page-wide: each target's block, and hits per block. */
|
|
int *owner;
|
|
int *hits;
|
|
/* The glyph each block last counted, so once each. */
|
|
int *seen;
|
|
int glyph;
|
|
} targets;
|
|
|
|
/* The glyphs of `block` with their user-space origins. */
|
|
static void block_targets(fz_context *ctx, fz_stext_block *block, fz_matrix to_user, targets *t)
|
|
{
|
|
fz_stext_line *line;
|
|
fz_stext_char *ch;
|
|
int n = 0;
|
|
for (line = block->u.t.first_line; line; line = line->next)
|
|
for (ch = line->first_char; ch; ch = ch->next)
|
|
n++;
|
|
t->at = fz_malloc_array(ctx, n + 1, fz_point);
|
|
t->c = fz_malloc_array(ctx, n + 1, int);
|
|
t->n = t->hit = 0;
|
|
for (line = block->u.t.first_line; line; line = line->next)
|
|
for (ch = line->first_char; ch; ch = ch->next)
|
|
if (!(ch->flags & FZ_STEXT_SYNTHETIC))
|
|
{
|
|
t->at[t->n] = fz_transform_point(ch->origin, to_user);
|
|
t->c[t->n++] = ch->c;
|
|
}
|
|
}
|
|
|
|
/* `c` as stext spells it (stext-device.c); ligatures split. */
|
|
static int spell(int c, int *out)
|
|
{
|
|
static const char *named[] = { "ff", "fi", "fl", "ffi", "ffl", "st", "st" };
|
|
int i, n = 0;
|
|
uint32_t lig[18];
|
|
if (c >= 0xFB00 && c <= 0xFB06)
|
|
{
|
|
for (i = 0; named[c - 0xFB00][i]; i++)
|
|
out[n++] = named[c - 0xFB00][i];
|
|
return n;
|
|
}
|
|
if ((c >= 0xfb00 && c <= 0xfdff) || (c >= 0xfe70 && c <= 0xfefc))
|
|
n = ucdn_compat_decompose((uint32_t)c, lig);
|
|
for (i = 0; i < n; i++)
|
|
out[i] = (int)lig[i];
|
|
if (n > 0)
|
|
return n;
|
|
out[0] = c;
|
|
return 1;
|
|
}
|
|
|
|
/* Targets from `i` one glyph spells: a ligature spells several. */
|
|
static int spelled(targets *t, int i, const int *ucs, int ucslen)
|
|
{
|
|
int j = 1;
|
|
if (ucslen < 2 || ucs[0] != t->c[i])
|
|
return 1;
|
|
while (j < ucslen && i + j < t->n && t->c[i + j] == ucs[j] && (!t->owner || t->owner[i + j] == t->owner[i]))
|
|
j++;
|
|
return j;
|
|
}
|
|
|
|
/* Removes a glyph drawn at one of the targets. */
|
|
static int take_glyph(fz_context *ctx, void *opaque, int *ucs, int ucslen, fz_matrix trm, fz_matrix ctm,
|
|
fz_rect bbox, int tr, float ca, float CA)
|
|
{
|
|
targets *t = opaque;
|
|
(void)bbox;
|
|
(void)tr;
|
|
(void)ca;
|
|
(void)CA;
|
|
fz_point at = fz_transform_point(fz_transform_point(fz_make_point(0, 0), trm), ctm);
|
|
/* MuPDF gives at most PDF_MRANGE_CAP; each spells to 18. */
|
|
int i, k, found = 0, spelt[18 * PDF_MRANGE_CAP], n = 0;
|
|
t->glyph++;
|
|
if (ucslen > PDF_MRANGE_CAP)
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a glyph with %d characters", ucslen);
|
|
/* Match what stext reads: ligature code points spelled out. */
|
|
for (k = 0; k < ucslen; k++)
|
|
n += spell(ucs[k], spelt + n);
|
|
ucs = spelt;
|
|
ucslen = n;
|
|
for (i = 0; i < t->n; i++)
|
|
{
|
|
int match = 0;
|
|
if (fabsf(at.x - t->at[i].x) > 0.5f || fabsf(at.y - t->at[i].y) > 0.5f)
|
|
continue;
|
|
for (k = 0; k < ucslen && !match; k++)
|
|
match = ucs[k] == t->c[i] || t->c[i] == FZ_REPLACEMENT_CHARACTER;
|
|
if (!match)
|
|
continue;
|
|
if (!t->owner)
|
|
{
|
|
t->hit += spelled(t, i, ucs, ucslen);
|
|
return 1;
|
|
}
|
|
/* Every block this glyph matches counts it once. */
|
|
if (t->seen[t->owner[i]] != t->glyph)
|
|
{
|
|
t->seen[t->owner[i]] = t->glyph;
|
|
t->hits[t->owner[i]] += spelled(t, i, ucs, ucslen);
|
|
}
|
|
found = 1;
|
|
}
|
|
t->hit += found;
|
|
return found;
|
|
}
|
|
|
|
/* Runs the glyph filter over the page; `dry` changes nothing. */
|
|
static void filter_glyphs(fz_context *ctx, pdf_document *pdf, pdf_page *page, targets *t, int dry)
|
|
{
|
|
pdf_sanitize_filter_options sopts = { 0 };
|
|
pdf_filter_factory list[2] = { { pdf_new_sanitize_filter, NULL }, { NULL, NULL } };
|
|
pdf_filter_options opts = { 0 };
|
|
sopts.opaque = t;
|
|
sopts.text_filter = take_glyph;
|
|
list[0].options = &sopts;
|
|
opts.filters = list;
|
|
opts.no_update = dry;
|
|
t->hit = 0;
|
|
pdf_filter_page_contents(ctx, pdf, page, &opts);
|
|
}
|
|
|
|
/* User space from the page space MuPDF reports in. */
|
|
fz_matrix ely_to_user(fz_context *ctx, pdf_page *page)
|
|
{
|
|
fz_matrix ctm;
|
|
pdf_page_transform(ctx, page, NULL, &ctm);
|
|
return fz_invert_matrix(ctm);
|
|
}
|
|
|
|
/* Whether all of `block` sits in the page's own content. */
|
|
static int reachable(fz_context *ctx, pdf_document *pdf, pdf_page *page, fz_stext_block *block)
|
|
{
|
|
targets t = { NULL, NULL, 0, 0, NULL, NULL, NULL, 0 };
|
|
int ok = 0;
|
|
fz_var(t.at);
|
|
fz_var(t.c);
|
|
pdf_begin_operation(ctx, pdf, "Look at text");
|
|
fz_try(ctx)
|
|
{
|
|
block_targets(ctx, block, ely_to_user(ctx, page), &t);
|
|
filter_glyphs(ctx, pdf, page, &t, 1);
|
|
ok = t.n > 0 && t.hit == t.n;
|
|
}
|
|
fz_always(ctx)
|
|
{
|
|
pdf_abandon_operation(ctx, pdf);
|
|
ely_forget_resources(ctx, pdf);
|
|
fz_free(ctx, t.at);
|
|
fz_free(ctx, t.c);
|
|
}
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
return ok;
|
|
}
|
|
|
|
/* Each text block's glyphs found in page content, one pass. */
|
|
static void reachable_all(fz_context *ctx, pdf_document *pdf, pdf_page *page, fz_stext_page *text, int n, int *ok)
|
|
{
|
|
targets t = { NULL, NULL, 0, 0, NULL, NULL, NULL, 0 };
|
|
int *count = NULL, all = 0, b;
|
|
fz_matrix to_user = ely_to_user(ctx, page);
|
|
fz_stext_block *block;
|
|
fz_stext_line *line;
|
|
fz_stext_char *ch;
|
|
fz_var(t.at);
|
|
fz_var(t.c);
|
|
fz_var(t.owner);
|
|
fz_var(t.hits);
|
|
fz_var(t.seen);
|
|
fz_var(count);
|
|
pdf_begin_operation(ctx, pdf, "Look at text");
|
|
fz_try(ctx)
|
|
{
|
|
for (block = text->first_block; block; block = block->next)
|
|
if (block->type == FZ_STEXT_BLOCK_TEXT)
|
|
for (line = block->u.t.first_line; line; line = line->next)
|
|
for (ch = line->first_char; ch; ch = ch->next)
|
|
all++;
|
|
t.at = fz_malloc_array(ctx, all + 1, fz_point);
|
|
t.c = fz_malloc_array(ctx, all + 1, int);
|
|
t.owner = fz_malloc_array(ctx, all + 1, int);
|
|
t.hits = fz_calloc(ctx, n + 1, sizeof(int));
|
|
t.seen = fz_calloc(ctx, n + 1, sizeof(int));
|
|
count = fz_calloc(ctx, n + 1, sizeof(int));
|
|
b = 0;
|
|
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)
|
|
for (ch = line->first_char; ch; ch = ch->next)
|
|
if (!(ch->flags & FZ_STEXT_SYNTHETIC))
|
|
{
|
|
t.at[t.n] = fz_transform_point(ch->origin, to_user);
|
|
t.c[t.n] = ch->c;
|
|
t.owner[t.n++] = b;
|
|
count[b]++;
|
|
}
|
|
b++;
|
|
}
|
|
filter_glyphs(ctx, pdf, page, &t, 1);
|
|
for (b = 0; b < n; b++)
|
|
ok[b] = count[b] > 0 && t.hits[b] == count[b];
|
|
}
|
|
fz_always(ctx)
|
|
{
|
|
pdf_abandon_operation(ctx, pdf);
|
|
ely_forget_resources(ctx, pdf);
|
|
fz_free(ctx, t.at);
|
|
fz_free(ctx, t.c);
|
|
fz_free(ctx, t.owner);
|
|
fz_free(ctx, t.hits);
|
|
fz_free(ctx, t.seen);
|
|
fz_free(ctx, count);
|
|
}
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
}
|
|
|
|
/* The text of `block`, lines on lines. */
|
|
static void block_text(fz_context *ctx, fz_buffer *buf, fz_stext_block *block)
|
|
{
|
|
fz_stext_line *line;
|
|
fz_stext_char *ch;
|
|
for (line = block->u.t.first_line; line; line = line->next)
|
|
{
|
|
if (line != block->u.t.first_line)
|
|
fz_append_byte(ctx, buf, '\n');
|
|
for (ch = line->first_char; ch; ch = ch->next)
|
|
fz_append_rune(ctx, buf, ch->c);
|
|
}
|
|
}
|
|
|
|
/* Horizontal lines only; turned text is not edited. */
|
|
static int upright(fz_stext_block *block)
|
|
{
|
|
fz_stext_line *line;
|
|
for (line = block->u.t.first_line; line; line = line->next)
|
|
if (line->wmode || fabsf(line->dir.x - 1) > 0.01f)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/* JSON of the page's text blocks. */
|
|
int ely_text_blocks(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
|
|
{
|
|
fz_stext_page *text = NULL;
|
|
pdf_page *page = NULL;
|
|
fz_buffer *buf = NULL, *words = NULL;
|
|
fz_output *out = NULL;
|
|
int *reached = NULL;
|
|
fz_var(text);
|
|
fz_var(page);
|
|
fz_var(buf);
|
|
fz_var(words);
|
|
fz_var(out);
|
|
fz_var(reached);
|
|
fz_try(ctx)
|
|
{
|
|
pdf_document *pdf = ely_pdf_of(ctx, doc);
|
|
fz_stext_block *block;
|
|
int first = 1, n = 0, at = 0;
|
|
text = read_text(ctx, doc, number);
|
|
page = pdf_load_page(ctx, pdf, number);
|
|
for (block = text->first_block; block; block = block->next)
|
|
n += block->type == FZ_STEXT_BLOCK_TEXT;
|
|
reached = fz_calloc(ctx, n + 1, sizeof(int));
|
|
reachable_all(ctx, pdf, page, text, n, reached);
|
|
buf = fz_new_buffer(ctx, 1024);
|
|
out = fz_new_output_with_buffer(ctx, buf);
|
|
fz_write_string(ctx, out, "[");
|
|
for (block = text->first_block; block; block = block->next)
|
|
{
|
|
fz_stext_char *ch;
|
|
int editable;
|
|
if (block->type != FZ_STEXT_BLOCK_TEXT)
|
|
continue;
|
|
ch = block->u.t.first_line->first_char;
|
|
editable = reached[at++];
|
|
editable = editable && upright(block);
|
|
fz_drop_buffer(ctx, words);
|
|
words = NULL;
|
|
words = fz_new_buffer(ctx, 256);
|
|
block_text(ctx, words, block);
|
|
fz_terminate_buffer(ctx, words);
|
|
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":",
|
|
first ? "" : ",", block->bbox.x0, block->bbox.y0, block->bbox.x1, block->bbox.y1);
|
|
ely_json_string(ctx, out, fz_string_from_buffer(ctx, words));
|
|
fz_write_string(ctx, out, ",\"font\":");
|
|
ely_json_string(ctx, out, fz_font_name(ctx, ch->font));
|
|
fz_write_printf(ctx, out, ",\"size\":%g,\"color\":%d,\"editable\":%s}",
|
|
ch->size, (int)(ch->argb & 0xffffff), editable ? "true" : "false");
|
|
first = 0;
|
|
}
|
|
fz_write_string(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_page(ctx, (fz_page *)page);
|
|
fz_drop_stext_page(ctx, text);
|
|
fz_free(ctx, reached);
|
|
}
|
|
fz_catch(ctx)
|
|
return ely_fail(ctx);
|
|
return 0;
|
|
}
|
|
|
|
/* Distinct baselines, and the gap between the first two. */
|
|
static void baselines(fz_stext_block *block, ely_shape *shape)
|
|
{
|
|
fz_stext_line *line;
|
|
float last = block->u.t.first_line->first_char->origin.y;
|
|
shape->lines = 1;
|
|
shape->gap = 0;
|
|
for (line = block->u.t.first_line->next; line; line = line->next)
|
|
{
|
|
float y = line->first_char->origin.y;
|
|
if (fabsf(y - last) <= 0.5f)
|
|
continue;
|
|
if (shape->lines == 1)
|
|
shape->gap = y - shape->first.y;
|
|
shape->lines++;
|
|
last = y;
|
|
}
|
|
/* Runs on one baseline: an ordinary line gap. */
|
|
if (shape->gap <= 0)
|
|
shape->gap = shape->size * 1.2f;
|
|
}
|
|
|
|
/* Reads block `index` and checks it; nothing changes. */
|
|
void ely_read_block(fz_context *ctx, fz_document *doc, pdf_page *page, int number, int index, const char *expected,
|
|
ely_shape *shape)
|
|
{
|
|
pdf_document *pdf = ely_pdf_of(ctx, doc);
|
|
fz_stext_page *text = NULL;
|
|
fz_buffer *words = NULL;
|
|
targets *t = fz_malloc_struct(ctx, targets);
|
|
shape->glyphs = t;
|
|
fz_var(text);
|
|
fz_var(words);
|
|
fz_try(ctx)
|
|
{
|
|
fz_stext_block *block;
|
|
fz_stext_char *ch;
|
|
text = read_text(ctx, doc, number);
|
|
block = nth_block(text, index);
|
|
if (!block)
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no text block %d", index);
|
|
words = fz_new_buffer(ctx, 256);
|
|
block_text(ctx, words, block);
|
|
fz_terminate_buffer(ctx, words);
|
|
if (strcmp(fz_string_from_buffer(ctx, words), expected))
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the text changed since it was read");
|
|
if (!upright(block) || !reachable(ctx, pdf, page, block))
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "this text is not in the page's own content");
|
|
block_targets(ctx, block, ely_to_user(ctx, page), t);
|
|
ch = block->u.t.first_line->first_char;
|
|
shape->font = fz_keep_font(ctx, ch->font);
|
|
shape->first = ch->origin;
|
|
shape->size = ch->size;
|
|
shape->rgb = (int)(ch->argb & 0xffffff);
|
|
shape->bbox = block->bbox;
|
|
baselines(block, shape);
|
|
}
|
|
fz_always(ctx)
|
|
{
|
|
fz_drop_buffer(ctx, words);
|
|
fz_drop_stext_page(ctx, text);
|
|
}
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
}
|
|
|
|
/* Takes out the glyphs `ely_read_block` found. */
|
|
void ely_remove_block(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_shape *shape)
|
|
{
|
|
targets *t = shape->glyphs;
|
|
filter_glyphs(ctx, pdf, page, t, 0);
|
|
if (t->hit != t->n)
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "%d of %d glyphs were found", t->hit, t->n);
|
|
}
|
|
|
|
void ely_drop_shape(fz_context *ctx, ely_shape *shape)
|
|
{
|
|
targets *t = shape->glyphs;
|
|
fz_drop_font(ctx, shape->font);
|
|
if (t)
|
|
{
|
|
fz_free(ctx, t->at);
|
|
fz_free(ctx, t->c);
|
|
fz_free(ctx, t);
|
|
}
|
|
shape->font = NULL;
|
|
shape->glyphs = NULL;
|
|
}
|