/* * DrawingTracer.cc * * Created on: 2014-6-15 * Author: duanyao */ #include "GfxFont.h" #include "util/math.h" #include "DrawingTracer.h" #if !ENABLE_SVG #warning "Cairo is disabled because ENABLE_SVG is off, --correct-text-visibility has limited functionality." #endif static constexpr bool DT_DEBUG = false; namespace pdf2htmlEX { DrawingTracer::DrawingTracer(const Param & param): param(param) #if ENABLE_SVG , cairo(nullptr) #endif { } DrawingTracer::~DrawingTracer() { finish(); } void DrawingTracer::reset(GfxState *state) { if (!param.correct_text_visibility) return; finish(); #if ENABLE_SVG // pbox is defined in device space, which is affected by zooming; // We want to trace in page space which is stable, so invert pbox by ctm. double pbox[] { 0, 0, state->getPageWidth(), state->getPageHeight() }; Matrix ctm, ictm; state->getCTM(&ctm); ctm.invertTo(&ictm); tm_transform_bbox(ictm.m, pbox); cairo_rectangle_t page_box { pbox[0], pbox[1], pbox[2] - pbox[0], pbox[3] - pbox[1] }; cairo_surface_t * surface = cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, &page_box); cairo = cairo_create(surface); if (DT_DEBUG) printf("DrawingTracer::reset:page bbox:[%f,%f,%f,%f]\n",pbox[0], pbox[1], pbox[2], pbox[3]); #endif } void DrawingTracer::finish() { #if ENABLE_SVG if (cairo) { cairo_destroy(cairo); cairo = nullptr; } #endif } // Poppler won't inform us its initial CTM, and the initial CTM is affected by zoom level. // OutputDev::clip() may be called before OutputDev::updateCTM(), so we can't rely on GfxState::getCTM(), // and should trace ctm changes ourself (via cairo). void DrawingTracer::update_ctm(GfxState *state, double m11, double m12, double m21, double m22, double m31, double m32) { if (!param.correct_text_visibility) return; #if ENABLE_SVG cairo_matrix_t matrix; matrix.xx = m11; matrix.yx = m12; matrix.xy = m21; matrix.yy = m22; matrix.x0 = m31; matrix.y0 = m32; cairo_transform(cairo, &matrix); if (DT_DEBUG) { cairo_matrix_t mat; cairo_get_matrix(cairo, &mat); printf("DrawingTracer::update_ctm:ctm:[%f,%f,%f,%f,%f,%f]\n", mat.xx, mat.yx, mat.xy, mat.yy, mat.x0, mat.y0); } #endif } void DrawingTracer::clip(GfxState * state, bool even_odd) { if (!param.correct_text_visibility) return; #if ENABLE_SVG do_path(state, state->getPath()); cairo_set_fill_rule(cairo, even_odd? CAIRO_FILL_RULE_EVEN_ODD : CAIRO_FILL_RULE_WINDING); cairo_clip (cairo); if (DT_DEBUG) { double cbox[4]; cairo_clip_extents(cairo, cbox, cbox + 1, cbox + 2, cbox + 3); printf("DrawingTracer::clip:extents:[%f,%f,%f,%f]\n", cbox[0],cbox[1],cbox[2],cbox[3]); } #endif } void DrawingTracer::clip_to_stroke_path(GfxState * state) { if (!param.correct_text_visibility) return; // TODO cairo_stroke_to_path() ? } void DrawingTracer::save() { if (!param.correct_text_visibility) return; #if ENABLE_SVG cairo_save(cairo); if (DT_DEBUG) printf("DrawingTracer::save\n"); #endif } void DrawingTracer::restore() { if (!param.correct_text_visibility) return; #if ENABLE_SVG cairo_restore(cairo); if (DT_DEBUG) printf("DrawingTracer::restore\n"); #endif } void DrawingTracer::do_path(GfxState * state, GfxPath * path) { #if ENABLE_SVG //copy from CairoOutputDev::doPath GfxSubpath *subpath; int i, j; double x, y; cairo_new_path(cairo); if (DT_DEBUG) printf("DrawingTracer::do_path:new_path\n"); for (i = 0; i < path->getNumSubpaths(); ++i) { subpath = path->getSubpath(i); if (subpath->getNumPoints() > 0) { x = subpath->getX(0); y = subpath->getY(0); cairo_move_to(cairo, x, y); if (DT_DEBUG) printf("DrawingTracer::do_path:move_to[%f,%f]\n",x,y); j = 1; while (j < subpath->getNumPoints()) { if (subpath->getCurve(j)) { x = subpath->getX(j+2); y = subpath->getY(j+2); cairo_curve_to(cairo, subpath->getX(j), subpath->getY(j), subpath->getX(j+1), subpath->getY(j+1), x, y); if (DT_DEBUG) printf("DrawingTracer::do_path:curve_to[%f,%f]\n",x,y); j += 3; } else { x = subpath->getX(j); y = subpath->getY(j); cairo_line_to(cairo, x, y); if (DT_DEBUG) printf("DrawingTracer::do_path:line_to[%f,%f]\n",x,y); ++j; } } if (subpath->isClosed()) { cairo_close_path (cairo); if (DT_DEBUG) printf("DrawingTracer::do_path:close\n"); } } } #endif } void DrawingTracer::stroke(GfxState * state) { #if ENABLE_SVG if (!param.correct_text_visibility) return; if (DT_DEBUG) printf("DrawingTracer::stroke\n"); cairo_set_line_width(cairo, state->getLineWidth()); // GfxPath is broken into steps, each step makes up a cairo path and its bbox is used for covering test. // TODO // 1. path steps that are not vertical or horizontal lines may still falsely "cover" many chars, // can we slice those steps further? // 2. if the line width is small, can we just ignore the path? // 3. line join feature can't be retained. We use line-cap-square to minimize the problem that // some chars actually covered by a line join are missed. However chars covered by a acute angle // with line-join-miter may be still recognized as not covered. cairo_set_line_cap(cairo, CAIRO_LINE_CAP_SQUARE); GfxPath * path = state->getPath(); for (int i = 0; i < path->getNumSubpaths(); ++i) { GfxSubpath * subpath = path->getSubpath(i); if (subpath->getNumPoints() <= 0) continue; double x = subpath->getX(0); double y = subpath->getY(0); //p: loop cursor; j: next point index int p =1, j = 1; int n = subpath->getNumPoints(); while (p <= n) { cairo_new_path(cairo); cairo_move_to(cairo, x, y); if (subpath->getCurve(j)) { x = subpath->getX(j+2); y = subpath->getY(j+2); cairo_curve_to(cairo, subpath->getX(j), subpath->getY(j), subpath->getX(j+1), subpath->getY(j+1), x, y); p += 3; } else { x = subpath->getX(j); y = subpath->getY(j); cairo_line_to(cairo, x, y); ++p; } if (DT_DEBUG) printf("DrawingTracer::stroke:new box:\n"); double sbox[4]; cairo_stroke_extents(cairo, sbox, sbox + 1, sbox + 2, sbox + 3); if (sbox[0] != sbox[2] && sbox[1] != sbox[3]) draw_non_char_bbox(state, sbox); else if (DT_DEBUG) printf("DrawingTracer::stroke:zero box!\n"); if (p == n) { if (subpath->isClosed()) j = 0; // if sub path is closed, go back to starting point else break; } else j = p; } } #endif } void DrawingTracer::fill(GfxState * state, bool even_odd) { if (!param.correct_text_visibility) return; #if ENABLE_SVG do_path(state, state->getPath()); //cairo_fill_extents don't take fill rule into account. //cairo_set_fill_rule (cairo, even_odd? CAIRO_FILL_RULE_EVEN_ODD : CAIRO_FILL_RULE_WINDING); double fbox[4]; cairo_fill_extents(cairo, fbox, fbox + 1, fbox + 2, fbox + 3); draw_non_char_bbox(state, fbox); #endif } void DrawingTracer::draw_non_char_bbox(GfxState * state, double * bbox) { #if ENABLE_SVG double cbox[4]; cairo_clip_extents(cairo, cbox, cbox + 1, cbox + 2, cbox + 3); if(bbox_intersect(cbox, bbox, bbox)) #endif { transform_bbox_by_ctm(bbox, state); if (DT_DEBUG) printf("DrawingTracer::draw_non_char_bbox:[%f,%f,%f,%f]\n", bbox[0],bbox[1],bbox[2],bbox[3]); if (on_non_char_drawn) on_non_char_drawn(bbox); } } void DrawingTracer::draw_char_bbox(GfxState * state, double * bbox) { #if ENABLE_SVG // Note: even if 4 corners of the char are all in or all out of the clip area, // it could still be partially clipped. // TODO better solution? int pt_in = 0; if (cairo_in_clip(cairo, bbox[0], bbox[1])) ++pt_in; if (cairo_in_clip(cairo, bbox[2], bbox[3])) ++pt_in; if (cairo_in_clip(cairo, bbox[2], bbox[1])) ++pt_in; if (cairo_in_clip(cairo, bbox[0], bbox[3])) ++pt_in; if (pt_in == 0) { transform_bbox_by_ctm(bbox); if(on_char_clipped) on_char_clipped(bbox, false); } else { if (pt_in < 4) { double cbox[4]; cairo_clip_extents(cairo, cbox, cbox + 1, cbox + 2, cbox + 3); bbox_intersect(cbox, bbox, bbox); } transform_bbox_by_ctm(bbox); if (pt_in < 4) { if(on_char_clipped) on_char_clipped(bbox, true); } else { if (on_char_drawn) on_char_drawn(bbox); } } #else transform_bbox_by_ctm(bbox, state); if (on_char_drawn) on_char_drawn(bbox); #endif if (DT_DEBUG) printf("DrawingTracer::draw_char_bbox:[%f,%f,%f,%f]\n",bbox[0],bbox[1],bbox[2],bbox[3]); } void DrawingTracer::draw_image(GfxState *state) { if (!param.correct_text_visibility) return; double bbox[4] {0, 0, 1, 1}; draw_non_char_bbox(state, bbox); } void DrawingTracer::draw_char(GfxState *state, double x, double y, double ax, double ay) { if (!param.correct_text_visibility) return; Matrix tm, itm; memcpy(tm.m, state->getTextMat(), sizeof(tm.m)); double cx = state->getCurX(), cy = state->getCurY(), fs = state->getFontSize(), ry = state->getRise(), h = state->getHorizScaling(); //cx and cy has been transformed by text matrix, we need to reverse them. tm.invertTo(&itm); double char_cx, char_cy; itm.transform(cx, cy, &char_cx, &char_cy); //TODO Vertical? Currently vertical/type3 chars are treated as non-chars. double char_m[6] {fs * h, 0, 0, fs, char_cx + x, char_cy + y + ry}; double final_m[6]; tm_multiply(final_m, tm.m, char_m); auto font = state->getFont(); double bbox[4] {0, 0, ax, ay}; double desc = font->getDescent(), asc = font->getAscent(); if (font->getWMode() == 0) { bbox[1] += desc; bbox[3] += asc; } else {//TODO Vertical? } tm_transform_bbox(final_m, bbox); draw_char_bbox(state, bbox); } void DrawingTracer::transform_bbox_by_ctm(double * bbox, GfxState * state) { #if ENABLE_SVG cairo_matrix_t mat; cairo_get_matrix(cairo, &mat); double mat_a[6] {mat.xx, mat.yx, mat.xy, mat.yy, mat.x0, mat.y0}; tm_transform_bbox(mat_a, bbox); #else tm_transform_bbox(state->getCTM(), bbox); #endif } } /* namespace pdf2htmlEX */