diff options
Diffstat (limited to 'libview/ev-page-accessible.c')
-rw-r--r-- | libview/ev-page-accessible.c | 685 |
1 files changed, 684 insertions, 1 deletions
diff --git a/libview/ev-page-accessible.c b/libview/ev-page-accessible.c index 83bb80d8..fa1f3e02 100644 --- a/libview/ev-page-accessible.c +++ b/libview/ev-page-accessible.c @@ -22,7 +22,9 @@ #include <config.h> +#include <glib/gi18n-lib.h> #include "ev-page-accessible.h" +#include "ev-view-private.h" struct _EvPageAccessiblePrivate { EvViewAccessible *view_accessible; @@ -35,8 +37,10 @@ enum { PROP_PAGE, }; +static void ev_page_accessible_text_iface_init (AtkTextIface *iface); -G_DEFINE_TYPE (EvPageAccessible, ev_page_accessible, ATK_TYPE_OBJECT) +G_DEFINE_TYPE_WITH_CODE (EvPageAccessible, ev_page_accessible, ATK_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (ATK_TYPE_TEXT, ev_page_accessible_text_iface_init)) gint ev_page_accessible_get_page (EvPageAccessible *page_accessible) @@ -140,6 +144,666 @@ ev_page_accessible_class_init (EvPageAccessibleClass *klass) } +EvView * +ev_page_accessible_get_view (EvPageAccessible *page_accessible) +{ + g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (page_accessible), NULL); + + return EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (page_accessible->priv->view_accessible))); +} + +/* ATs expect to be able to identify sentence boundaries based on content. Valid, + * content-based boundaries may be present at the end of a newline, for instance + * at the end of a heading within a document. Thus being able to distinguish hard + * returns from soft returns is necessary. However, the text we get from Poppler + * for non-tagged PDFs has "\n" inserted at the end of each line resulting in a + * broken accessibility implementation w.r.t. sentences. + */ +static gboolean +treat_as_soft_return (EvView *view, + gint page, + PangoLogAttr *log_attrs, + gint offset) +{ + EvRectangle *areas = NULL; + guint n_areas = 0; + gdouble line_spacing, this_line_height, next_word_width; + EvRectangle *this_line_start; + EvRectangle *this_line_end; + EvRectangle *next_line_start; + EvRectangle *next_line_end; + EvRectangle *next_word_end; + gint prev_offset, next_offset; + + + if (!log_attrs[offset].is_white) + return FALSE; + + ev_page_cache_get_text_layout (view->page_cache, page, &areas, &n_areas); + if (n_areas <= offset + 1) + return FALSE; + + prev_offset = offset - 1; + next_offset = offset + 1; + + /* In wrapped text, the character at the start of the next line starts a word. + * Examples where this condition might fail include bullets and images. But it + * also includes things like "(", so also check the next character. + */ + if (!log_attrs[next_offset].is_word_start && + (next_offset + 1 >= n_areas || !log_attrs[next_offset + 1].is_word_start)) + return FALSE; + + /* In wrapped text, the chars on either side of the newline have very similar heights. + * Examples where this condition might fail include a newline at the end of a heading, + * and a newline at the end of a paragraph that is followed by a heading. + */ + this_line_end = areas + prev_offset; + next_line_start = areas + next_offset;; + + this_line_height = this_line_end->y2 - this_line_end->y1; + if (ABS (this_line_height - (next_line_start->y2 - next_line_start->y1)) > 0.25) + return FALSE; + + /* If there is significant white space between this line and the next, odds are this + * is not a soft return in wrapped text. Lines within a typical paragraph are at most + * double-spaced. If the spacing is more than that, assume a hard return is present. + */ + line_spacing = next_line_start->y1 - this_line_end->y2; + if (line_spacing - this_line_height > 1) + return FALSE; + + /* Lines within a typical paragraph have *reasonably* similar x1 coordinates. But + * we cannot count on them being nearly identical. Examples where indentation can + * be present in wrapped text include indenting the first line of the paragraph, + * and hanging indents (e.g. in the works cited within an academic paper). So we'll + * be somewhat tolerant here. + */ + for ( ; prev_offset > 0 && !log_attrs[prev_offset].is_mandatory_break; prev_offset--); + this_line_start = areas + prev_offset; + if (ABS (this_line_start->x1 - next_line_start->x1) > 20) + return FALSE; + + /* Ditto for x2, but this line might be short due to a wide word on the next line. */ + for ( ; next_offset < n_areas && !log_attrs[next_offset].is_word_end; next_offset++); + next_word_end = areas + next_offset; + next_word_width = next_word_end->x2 - next_line_start->x1; + + for ( ; next_offset < n_areas && !log_attrs[next_offset + 1].is_mandatory_break; next_offset++); + next_line_end = areas + next_offset; + if (next_line_end->x2 - (this_line_end->x2 + next_word_width) > 20) + return FALSE; + + return TRUE; +} + +static gchar * +ev_page_accessible_get_substring (AtkText *text, + gint start_offset, + gint end_offset) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + gchar *substring, *normalized; + const gchar* page_text; + + if (!view->page_cache) + return NULL; + + page_text = ev_page_cache_get_text (view->page_cache, self->priv->page); + start_offset = MAX (0, start_offset); + if (end_offset < 0 || end_offset > g_utf8_strlen (page_text, -1)) + end_offset = strlen (page_text); + + substring = g_utf8_substring (page_text, start_offset, end_offset); + normalized = g_utf8_normalize (substring, -1, G_NORMALIZE_NFKC); + g_free (substring); + + return normalized; +} + +static gchar * +ev_page_accessible_get_text (AtkText *text, + gint start_pos, + gint end_pos) +{ + return ev_page_accessible_get_substring (text, start_pos, end_pos); +} + +static gunichar +ev_page_accessible_get_character_at_offset (AtkText *text, + gint offset) +{ + gchar *string; + gunichar unichar; + + string = ev_page_accessible_get_substring (text, offset, offset + 1); + unichar = g_utf8_get_char (string); + g_free(string); + + return unichar; +} + +static void +ev_page_accessible_get_range_for_boundary (AtkText *text, + AtkTextBoundary boundary_type, + gint offset, + gint *start_offset, + gint *end_offset) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + gint start = 0; + gint end = 0; + PangoLogAttr *log_attrs = NULL; + gulong n_attrs; + + if (!view->page_cache) + return; + + ev_page_cache_get_text_log_attrs (view->page_cache, self->priv->page, &log_attrs, &n_attrs); + if (!log_attrs) + return; + + switch (boundary_type) { + case ATK_TEXT_BOUNDARY_CHAR: + start = offset; + end = offset + 1; + break; + case ATK_TEXT_BOUNDARY_WORD_START: + for (start = offset; start >= 0 && !log_attrs[start].is_word_start; start--); + for (end = offset + 1; end <= n_attrs && !log_attrs[end].is_word_start; end++); + break; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + for (start = offset; start >= 0; start--) { + if (log_attrs[start].is_mandatory_break && treat_as_soft_return (view, self->priv->page, log_attrs, start - 1)) + continue; + if (log_attrs[start].is_sentence_start) + break; + } + for (end = offset + 1; end <= n_attrs; end++) { + if (log_attrs[end].is_mandatory_break && treat_as_soft_return (view, self->priv->page, log_attrs, end - 1)) + continue; + if (log_attrs[end].is_sentence_start) + break; + } + break; + case ATK_TEXT_BOUNDARY_LINE_START: + for (start = offset; start >= 0 && !log_attrs[start].is_mandatory_break; start--); + for (end = offset + 1; end <= n_attrs && !log_attrs[end].is_mandatory_break; end++); + break; + default: + /* The "END" boundary types are deprecated */ + break; + } + + *start_offset = start; + *end_offset = end; +} + +static gchar * +ev_page_accessible_get_text_at_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + gchar *retval; + + ev_page_accessible_get_range_for_boundary (text, boundary_type, offset, start_offset, end_offset); + retval = ev_page_accessible_get_substring (text, *start_offset, *end_offset); + + /* If newlines appear inside the text of a sentence (i.e. between the start and + * end offsets returned by ev_page_accessible_get_substring), it interferes with + * the prosody of text-to-speech based-solutions such as a screen reader because + * speech synthesizers tend to pause after the newline char as if it were the end + * of the sentence. + */ + if (boundary_type == ATK_TEXT_BOUNDARY_SENTENCE_START) + g_strdelimit (retval, "\n", ' '); + + return retval; +} + +static gint +ev_page_accessible_get_caret_offset (AtkText *text) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + + if (self->priv->page == view->cursor_page && view->caret_enabled) + return view->cursor_offset; + + return -1; +} + +static gboolean +ev_page_accessible_set_caret_offset (AtkText *text, gint offset) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + + ev_view_set_caret_cursor_position (view, + self->priv->page, + offset); + + return TRUE; +} + +static gint +ev_page_accessible_get_character_count (AtkText *text) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + gint retval; + + retval = g_utf8_strlen (ev_page_cache_get_text (view->page_cache, self->priv->page), -1); + + return retval; +} + +static gboolean +get_selection_bounds (EvView *view, + EvViewSelection *selection, + gint *start_offset, + gint *end_offset) +{ + cairo_rectangle_int_t rect; + gint start, end; + + if (!selection->covered_region || cairo_region_is_empty (selection->covered_region)) + return FALSE; + + cairo_region_get_rectangle (selection->covered_region, 0, &rect); + start = _ev_view_get_caret_cursor_offset_at_doc_point (view, + selection->page, + rect.x / view->scale, + (rect.y + (rect.height / 2)) / view->scale); + if (start == -1) + return FALSE; + + cairo_region_get_rectangle (selection->covered_region, + cairo_region_num_rectangles (selection->covered_region) - 1, + &rect); + end = _ev_view_get_caret_cursor_offset_at_doc_point (view, + selection->page, + (rect.x + rect.width) / view->scale, + (rect.y + (rect.height / 2)) / view->scale); + if (end == -1) + return FALSE; + + *start_offset = start; + *end_offset = end; + + return TRUE; +} + +static gint +ev_page_accessible_get_n_selections (AtkText *text) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + gint n_selections = 0; + GList *l; + + if (!EV_IS_SELECTION (view->document) || !view->selection_info.selections) + return 0; + + for (l = view->selection_info.selections; l != NULL; l = l->next) { + EvViewSelection *selection = (EvViewSelection *)l->data; + + if (selection->page != self->priv->page) + continue; + + n_selections = 1; + break; + } + + return n_selections; +} + +static gchar * +ev_page_accessible_get_selection (AtkText *text, + gint selection_num, + gint *start_pos, + gint *end_pos) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + gchar *selected_text = NULL; + gchar *normalized_text = NULL; + GList *l; + + *start_pos = -1; + *end_pos = -1; + + if (selection_num != 0) + return NULL; + + if (!EV_IS_SELECTION (view->document) || !view->selection_info.selections) + return NULL; + + for (l = view->selection_info.selections; l != NULL; l = l->next) { + EvViewSelection *selection = (EvViewSelection *)l->data; + gint start, end; + + if (selection->page != self->priv->page) + continue; + + if (get_selection_bounds (view, selection, &start, &end) && start != end) { + EvPage *page; + + page = ev_document_get_page (view->document, selection->page); + + ev_document_doc_mutex_lock (); + selected_text = ev_selection_get_selected_text (EV_SELECTION (view->document), + page, + selection->style, + &(selection->rect)); + + ev_document_doc_mutex_unlock (); + + g_object_unref (page); + + *start_pos = start; + *end_pos = end; + } + + break; + } + + if (selected_text) { + normalized_text = g_utf8_normalize (selected_text, -1, G_NORMALIZE_NFKC); + g_free (selected_text); + } + + return normalized_text; +} + +static AtkAttributeSet * +add_attribute (AtkAttributeSet *attr_set, + AtkTextAttribute attr_type, + gchar *attr_value) +{ + AtkAttribute *attr = g_new (AtkAttribute, 1); + + attr->name = g_strdup (atk_text_attribute_get_name (attr_type)); + attr->value = attr_value; + + return g_slist_prepend (attr_set, attr); +} + +static AtkAttributeSet * +get_run_attributes (PangoAttrList *attrs, + const gchar *text, + gint offset, + gint *start_offset, + gint *end_offset) +{ + AtkAttributeSet *atk_attr_set = NULL; + PangoAttrString *pango_string; + PangoAttrInt *pango_int; + PangoAttrColor *pango_color; + PangoAttrIterator *iter; + gint i, start, end; + gboolean has_attrs = FALSE; + glong text_length; + gchar *attr_value; + + text_length = g_utf8_strlen (text, -1); + if (offset < 0 || offset >= text_length) + return NULL; + + /* Check if there are attributes for the offset, + * and set the attributes range if positive */ + iter = pango_attr_list_get_iterator (attrs); + i = g_utf8_offset_to_pointer (text, offset) - text; + + do { + pango_attr_iterator_range (iter, &start, &end); + if (i >= start && i < end) { + *start_offset = g_utf8_pointer_to_offset (text, text + start); + if (end == G_MAXINT) /* Last iterator */ + end = text_length; + *end_offset = g_utf8_pointer_to_offset (text, text + end); + has_attrs = TRUE; + } + } while (!has_attrs && pango_attr_iterator_next (iter)); + + if (!has_attrs) { + pango_attr_iterator_destroy (iter); + return NULL; + } + + /* Create the AtkAttributeSet from the Pango attributes */ + pango_string = (PangoAttrString *) pango_attr_iterator_get (iter, PANGO_ATTR_FAMILY); + if (pango_string) { + attr_value = g_strdup (pango_string->value); + atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_FAMILY_NAME, attr_value); + } + + pango_int = (PangoAttrInt *) pango_attr_iterator_get (iter, PANGO_ATTR_SIZE); + if (pango_int) { + attr_value = g_strdup_printf ("%i", pango_int->value / PANGO_SCALE); + atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_SIZE, attr_value); + } + + pango_int = (PangoAttrInt *) pango_attr_iterator_get (iter, PANGO_ATTR_UNDERLINE); + if (pango_int) { + atk_attr_set = add_attribute (atk_attr_set, + ATK_TEXT_ATTR_UNDERLINE, + g_strdup (atk_text_attribute_get_value (ATK_TEXT_ATTR_UNDERLINE, + pango_int->value))); + } + + pango_color = (PangoAttrColor *) pango_attr_iterator_get (iter, PANGO_ATTR_FOREGROUND); + if (pango_color) { + attr_value = g_strdup_printf ("%u,%u,%u", + pango_color->color.red, + pango_color->color.green, + pango_color->color.blue); + atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_FG_COLOR, attr_value); + } + + pango_attr_iterator_destroy (iter); + + return atk_attr_set; +} + +static AtkAttributeSet* +ev_page_accessible_get_run_attributes (AtkText *text, + gint offset, + gint *start_offset, + gint *end_offset) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + PangoAttrList *attrs; + const gchar *page_text; + + if (offset < 0) + return NULL; + + if (!view->page_cache) + return NULL; + + page_text = ev_page_cache_get_text (view->page_cache, self->priv->page); + if (!page_text) + return NULL; + + attrs = ev_page_cache_get_text_attrs (view->page_cache, self->priv->page); + if (!attrs) + return NULL; + + return get_run_attributes (attrs, page_text, offset, start_offset, end_offset); +} + +static AtkAttributeSet* +ev_page_accessible_get_default_attributes (AtkText *text) +{ + /* No default attributes */ + return NULL; +} + +static void +ev_page_accessible_get_character_extents (AtkText *text, + gint offset, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coords) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + GtkWidget *toplevel; + EvRectangle *areas = NULL; + EvRectangle *doc_rect; + guint n_areas = 0; + gint x_widget, y_widget; + GdkRectangle view_rect; + + if (!view->page_cache) + return; + + ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas); + if (!areas || offset >= n_areas) + return; + + doc_rect = areas + offset; + _ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, doc_rect, &view_rect); + view_rect.x -= view->scroll_x; + view_rect.y -= view->scroll_y; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + gtk_widget_translate_coordinates (GTK_WIDGET (view), toplevel, 0, 0, &x_widget, &y_widget); + view_rect.x += x_widget; + view_rect.y += y_widget; + + if (coords == ATK_XY_SCREEN) { + gint x_window, y_window; + + gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window); + view_rect.x += x_window; + view_rect.y += y_window; + } + + *x = view_rect.x; + *y = view_rect.y; + *width = view_rect.width; + *height = view_rect.height; +} + +static gint +ev_page_accessible_get_offset_at_point (AtkText *text, + gint x, + gint y, + AtkCoordType coords) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + GtkWidget *toplevel; + EvRectangle *areas = NULL; + EvRectangle *rect = NULL; + guint n_areas = 0; + guint i; + gint x_widget, y_widget; + gint offset=-1; + GdkPoint view_point; + gdouble doc_x, doc_y; + GtkBorder border; + GdkRectangle page_area; + + if (!view->page_cache) + return -1; + + ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas); + if (!areas) + return -1; + + view_point.x = x; + view_point.y = y; + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + gtk_widget_translate_coordinates (GTK_WIDGET (self), toplevel, 0, 0, &x_widget, &y_widget); + view_point.x -= x_widget; + view_point.y -= y_widget; + + if (coords == ATK_XY_SCREEN) { + gint x_window, y_window; + + gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window); + view_point.x -= x_window; + view_point.y -= y_window; + } + + ev_view_get_page_extents (view, self->priv->page, &page_area, &border); + _ev_view_transform_view_point_to_doc_point (view, &view_point, &page_area, &doc_x, &doc_y); + + for (i = 0; i < n_areas; i++) { + rect = areas + i; + if (doc_x >= rect->x1 && doc_x <= rect->x2 && + doc_y >= rect->y1 && doc_y <= rect->y2) + offset = i; + } + + return offset; +} + +/* ATK allows for multiple, non-contiguous selections within a single AtkText + * object. Unless and until Evince supports this, selection numbers are ignored. + */ +static gboolean +ev_page_accessible_remove_selection (AtkText *text, + gint selection_num) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + + if (ev_view_get_has_selection (view)) { + _ev_view_clear_selection (view); + return TRUE; + } + + return FALSE; +} + +static gboolean +ev_page_accessible_set_selection (AtkText *text, + gint selection_num, + gint start_pos, + gint end_pos) +{ + EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text); + EvView *view = ev_page_accessible_get_view (self); + EvRectangle *areas = NULL; + guint n_areas = 0; + GdkRectangle start_rect, end_rect; + GdkPoint start_point, end_point; + + ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas); + if (start_pos < 0 || end_pos >= n_areas) + return FALSE; + + _ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + start_pos, &start_rect); + _ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + end_pos - 1, &end_rect); + start_point.x = start_rect.x; + start_point.y = start_rect.y; + end_point.x = end_rect.x + end_rect.width; + end_point.y = end_rect.y + end_rect.height; + _ev_view_set_selection (view, &start_point, &end_point); + + return TRUE; +} + +static gboolean +ev_page_accessible_add_selection (AtkText *text, + gint start_pos, + gint end_pos) +{ + return ev_page_accessible_set_selection (text, 0, start_pos, end_pos); + +} + static void ev_page_accessible_init (EvPageAccessible *page) { @@ -148,6 +812,25 @@ ev_page_accessible_init (EvPageAccessible *page) page->priv = G_TYPE_INSTANCE_GET_PRIVATE (page, EV_TYPE_PAGE_ACCESSIBLE, EvPageAccessiblePrivate); } +static void +ev_page_accessible_text_iface_init (AtkTextIface *iface) +{ + iface->get_text = ev_page_accessible_get_text; + iface->get_text_at_offset = ev_page_accessible_get_text_at_offset; + iface->get_character_at_offset = ev_page_accessible_get_character_at_offset; + iface->get_caret_offset = ev_page_accessible_get_caret_offset; + iface->set_caret_offset = ev_page_accessible_set_caret_offset; + iface->get_character_count = ev_page_accessible_get_character_count; + iface->get_n_selections = ev_page_accessible_get_n_selections; + iface->get_selection = ev_page_accessible_get_selection; + iface->remove_selection = ev_page_accessible_remove_selection; + iface->add_selection = ev_page_accessible_add_selection; + iface->get_run_attributes = ev_page_accessible_get_run_attributes; + iface->get_default_attributes = ev_page_accessible_get_default_attributes; + iface->get_character_extents = ev_page_accessible_get_character_extents; + iface->get_offset_at_point = ev_page_accessible_get_offset_at_point; +} + EvPageAccessible * ev_page_accessible_new (EvViewAccessible *view_accessible, gint page) |