2009-12-20 20:58:26 +01:00
|
|
|
/*
|
2011-04-21 23:14:28 +02:00
|
|
|
* Copyright © 2009,2010 Red Hat, Inc.
|
2012-08-10 09:28:50 +02:00
|
|
|
* Copyright © 2010,2011,2012 Google, Inc.
|
2009-12-20 20:58:26 +01:00
|
|
|
*
|
2010-04-22 06:11:43 +02:00
|
|
|
* This is part of HarfBuzz, a text shaping library.
|
2009-12-20 20:58:26 +01:00
|
|
|
*
|
|
|
|
* Permission is hereby granted, without written agreement and without
|
|
|
|
* license or royalty fees, to use, copy, modify, and distribute this
|
|
|
|
* software and its documentation for any purpose, provided that the
|
|
|
|
* above copyright notice and the following two paragraphs appear in
|
|
|
|
* all copies of this software.
|
|
|
|
*
|
|
|
|
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
|
|
|
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
|
|
|
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
|
|
|
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
|
|
* DAMAGE.
|
|
|
|
*
|
|
|
|
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
|
|
|
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
|
|
|
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
|
|
|
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
|
|
*
|
|
|
|
* Red Hat Author(s): Behdad Esfahbod
|
2010-10-07 23:47:33 +02:00
|
|
|
* Google Author(s): Behdad Esfahbod
|
2009-12-20 20:58:26 +01:00
|
|
|
*/
|
|
|
|
|
2012-07-26 23:34:25 +02:00
|
|
|
#define HB_SHAPER ot
|
|
|
|
#define hb_ot_shaper_face_data_t hb_ot_layout_t
|
2012-07-27 08:29:32 +02:00
|
|
|
#define hb_ot_shaper_shape_plan_data_t hb_ot_shape_plan_t
|
2012-07-26 23:34:25 +02:00
|
|
|
#include "hb-shaper-impl-private.hh"
|
|
|
|
|
2010-10-09 01:18:16 +02:00
|
|
|
#include "hb-ot-shape-private.hh"
|
2012-08-02 15:38:28 +02:00
|
|
|
#include "hb-ot-shape-complex-private.hh"
|
2012-08-08 20:33:37 +02:00
|
|
|
#include "hb-ot-shape-fallback-private.hh"
|
2012-08-08 04:41:38 +02:00
|
|
|
#include "hb-ot-shape-normalize-private.hh"
|
2009-12-20 20:58:26 +01:00
|
|
|
|
2012-07-24 02:18:17 +02:00
|
|
|
#include "hb-ot-layout-private.hh"
|
2012-04-24 22:56:37 +02:00
|
|
|
#include "hb-set-private.hh"
|
2011-05-03 02:46:32 +02:00
|
|
|
|
2010-07-23 21:11:18 +02:00
|
|
|
|
2012-08-09 00:04:29 +02:00
|
|
|
static hb_tag_t common_features[] = {
|
2009-12-20 20:58:26 +01:00
|
|
|
HB_TAG('c','c','m','p'),
|
2012-04-16 21:55:13 +02:00
|
|
|
HB_TAG('l','i','g','a'),
|
2011-05-31 21:18:13 +02:00
|
|
|
HB_TAG('l','o','c','l'),
|
|
|
|
HB_TAG('m','a','r','k'),
|
|
|
|
HB_TAG('m','k','m','k'),
|
|
|
|
HB_TAG('r','l','i','g'),
|
|
|
|
};
|
|
|
|
|
2012-05-09 15:04:13 +02:00
|
|
|
|
2012-08-09 00:04:29 +02:00
|
|
|
static hb_tag_t horizontal_features[] = {
|
2011-05-31 21:18:13 +02:00
|
|
|
HB_TAG('c','a','l','t'),
|
2009-12-20 20:58:26 +01:00
|
|
|
HB_TAG('c','l','i','g'),
|
2010-05-21 19:06:35 +02:00
|
|
|
HB_TAG('c','u','r','s'),
|
2009-12-20 20:58:26 +01:00
|
|
|
HB_TAG('k','e','r','n'),
|
2012-08-08 03:12:49 +02:00
|
|
|
HB_TAG('r','c','l','t'),
|
2011-05-31 21:18:13 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Note:
|
|
|
|
* Technically speaking, vrt2 and vert are mutually exclusive.
|
|
|
|
* According to the spec, valt and vpal are also mutually exclusive.
|
|
|
|
* But we apply them all for now.
|
|
|
|
*/
|
2012-08-09 00:04:29 +02:00
|
|
|
static hb_tag_t vertical_features[] = {
|
2011-05-31 21:18:13 +02:00
|
|
|
HB_TAG('v','a','l','t'),
|
|
|
|
HB_TAG('v','e','r','t'),
|
|
|
|
HB_TAG('v','k','r','n'),
|
|
|
|
HB_TAG('v','p','a','l'),
|
|
|
|
HB_TAG('v','r','t','2'),
|
2009-12-20 20:58:26 +01:00
|
|
|
};
|
|
|
|
|
2012-04-15 02:23:58 +02:00
|
|
|
|
|
|
|
|
2009-12-20 20:58:26 +01:00
|
|
|
static void
|
2011-05-28 00:13:31 +02:00
|
|
|
hb_ot_shape_collect_features (hb_ot_shape_planner_t *planner,
|
2010-10-12 22:50:36 +02:00
|
|
|
const hb_segment_properties_t *props,
|
2011-05-28 00:13:31 +02:00
|
|
|
const hb_feature_t *user_features,
|
|
|
|
unsigned int num_user_features)
|
2009-12-20 20:58:26 +01:00
|
|
|
{
|
2012-08-02 16:07:58 +02:00
|
|
|
hb_ot_map_builder_t *map = &planner->map;
|
|
|
|
|
2010-10-12 22:00:21 +02:00
|
|
|
switch (props->direction) {
|
2010-05-29 02:21:47 +02:00
|
|
|
case HB_DIRECTION_LTR:
|
2013-02-14 17:05:56 +01:00
|
|
|
map->add_global_bool_feature (HB_TAG ('l','t','r','a'));
|
|
|
|
map->add_global_bool_feature (HB_TAG ('l','t','r','m'));
|
2010-05-29 02:21:47 +02:00
|
|
|
break;
|
|
|
|
case HB_DIRECTION_RTL:
|
2013-02-14 17:05:56 +01:00
|
|
|
map->add_global_bool_feature (HB_TAG ('r','t','l','a'));
|
2013-02-14 17:25:10 +01:00
|
|
|
map->add_feature (HB_TAG ('r','t','l','m'), 1, F_NONE);
|
2010-05-29 02:21:47 +02:00
|
|
|
break;
|
|
|
|
case HB_DIRECTION_TTB:
|
|
|
|
case HB_DIRECTION_BTT:
|
2011-03-16 18:53:32 +01:00
|
|
|
case HB_DIRECTION_INVALID:
|
2010-05-29 02:21:47 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2012-07-31 03:08:51 +02:00
|
|
|
if (planner->shaper->collect_features)
|
2012-08-02 15:38:28 +02:00
|
|
|
planner->shaper->collect_features (planner);
|
2010-05-29 02:37:06 +02:00
|
|
|
|
2013-02-15 12:22:26 +01:00
|
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH (common_features); i++)
|
|
|
|
map->add_global_bool_feature (common_features[i]);
|
2011-05-31 21:18:13 +02:00
|
|
|
|
|
|
|
if (HB_DIRECTION_IS_HORIZONTAL (props->direction))
|
2013-02-15 12:22:26 +01:00
|
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH (horizontal_features); i++)
|
2013-02-15 13:41:07 +01:00
|
|
|
map->add_feature (horizontal_features[i], 1, F_GLOBAL |
|
|
|
|
(horizontal_features[i] == HB_TAG('k','e','r','n') ?
|
|
|
|
F_HAS_FALLBACK : F_NONE));
|
2011-05-31 21:18:13 +02:00
|
|
|
else
|
2013-02-15 12:22:26 +01:00
|
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH (vertical_features); i++)
|
2013-02-15 13:41:07 +01:00
|
|
|
map->add_feature (vertical_features[i], 1, F_GLOBAL |
|
|
|
|
(vertical_features[i] == HB_TAG('v','k','r','n') ?
|
|
|
|
F_HAS_FALLBACK : F_NONE));
|
2011-05-31 21:18:13 +02:00
|
|
|
|
2012-07-31 03:08:51 +02:00
|
|
|
if (planner->shaper->override_features)
|
2012-08-02 15:38:28 +02:00
|
|
|
planner->shaper->override_features (planner);
|
2012-07-17 02:26:57 +02:00
|
|
|
|
2010-10-12 22:00:21 +02:00
|
|
|
for (unsigned int i = 0; i < num_user_features; i++) {
|
|
|
|
const hb_feature_t *feature = &user_features[i];
|
2013-02-14 17:25:10 +01:00
|
|
|
map->add_feature (feature->tag, feature->value,
|
|
|
|
(feature->start == 0 && feature->end == (unsigned int) -1) ?
|
|
|
|
F_GLOBAL : F_NONE);
|
2010-05-29 02:37:06 +02:00
|
|
|
}
|
2010-10-08 18:29:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-07-27 08:29:32 +02:00
|
|
|
/*
|
|
|
|
* shaper face data
|
|
|
|
*/
|
|
|
|
|
|
|
|
hb_ot_shaper_face_data_t *
|
|
|
|
_hb_ot_shaper_face_data_create (hb_face_t *face)
|
|
|
|
{
|
|
|
|
return _hb_ot_layout_create (face);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
_hb_ot_shaper_face_data_destroy (hb_ot_shaper_face_data_t *data)
|
|
|
|
{
|
|
|
|
_hb_ot_layout_destroy (data);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* shaper font data
|
|
|
|
*/
|
|
|
|
|
|
|
|
struct hb_ot_shaper_font_data_t {};
|
|
|
|
|
|
|
|
hb_ot_shaper_font_data_t *
|
|
|
|
_hb_ot_shaper_font_data_create (hb_font_t *font)
|
|
|
|
{
|
|
|
|
return (hb_ot_shaper_font_data_t *) HB_SHAPER_DATA_SUCCEEDED;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
_hb_ot_shaper_font_data_destroy (hb_ot_shaper_font_data_t *data)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* shaper shape_plan data
|
|
|
|
*/
|
|
|
|
|
|
|
|
hb_ot_shaper_shape_plan_data_t *
|
|
|
|
_hb_ot_shaper_shape_plan_data_create (hb_shape_plan_t *shape_plan,
|
|
|
|
const hb_feature_t *user_features,
|
|
|
|
unsigned int num_user_features)
|
|
|
|
{
|
2012-08-02 16:46:34 +02:00
|
|
|
hb_ot_shape_plan_t *plan = (hb_ot_shape_plan_t *) calloc (1, sizeof (hb_ot_shape_plan_t));
|
|
|
|
if (unlikely (!plan))
|
2012-07-27 08:29:32 +02:00
|
|
|
return NULL;
|
|
|
|
|
2012-08-02 15:24:35 +02:00
|
|
|
hb_ot_shape_planner_t planner (shape_plan);
|
2012-07-27 08:29:32 +02:00
|
|
|
|
2012-11-13 03:23:38 +01:00
|
|
|
planner.shaper = hb_ot_shape_complex_categorize (&planner);
|
2012-07-27 08:29:32 +02:00
|
|
|
|
|
|
|
hb_ot_shape_collect_features (&planner, &shape_plan->props, user_features, num_user_features);
|
|
|
|
|
2012-08-02 16:46:34 +02:00
|
|
|
planner.compile (*plan);
|
2012-07-27 08:29:32 +02:00
|
|
|
|
2012-08-02 16:46:34 +02:00
|
|
|
if (plan->shaper->data_create) {
|
|
|
|
plan->data = plan->shaper->data_create (plan);
|
|
|
|
if (unlikely (!plan->data))
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
return plan;
|
2012-07-27 08:29:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2012-08-02 16:46:34 +02:00
|
|
|
_hb_ot_shaper_shape_plan_data_destroy (hb_ot_shaper_shape_plan_data_t *plan)
|
2012-07-27 08:29:32 +02:00
|
|
|
{
|
2012-08-02 16:46:34 +02:00
|
|
|
if (plan->shaper->data_destroy)
|
|
|
|
plan->shaper->data_destroy (const_cast<void *> (plan->data));
|
|
|
|
|
|
|
|
plan->finish ();
|
2012-07-30 15:53:06 +02:00
|
|
|
|
2012-08-02 16:46:34 +02:00
|
|
|
free (plan);
|
2012-07-27 08:29:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* shaper
|
|
|
|
*/
|
|
|
|
|
2012-04-15 02:23:58 +02:00
|
|
|
struct hb_ot_shape_context_t
|
|
|
|
{
|
|
|
|
hb_ot_shape_plan_t *plan;
|
|
|
|
hb_font_t *font;
|
|
|
|
hb_face_t *face;
|
|
|
|
hb_buffer_t *buffer;
|
|
|
|
const hb_feature_t *user_features;
|
|
|
|
unsigned int num_user_features;
|
|
|
|
|
|
|
|
/* Transient stuff */
|
|
|
|
hb_direction_t target_direction;
|
|
|
|
};
|
|
|
|
|
2009-12-20 20:58:26 +01:00
|
|
|
|
|
|
|
|
2010-05-21 15:34:23 +02:00
|
|
|
/* Main shaper */
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
|
2010-05-21 15:34:23 +02:00
|
|
|
/* Prepare */
|
|
|
|
|
2012-04-15 02:23:58 +02:00
|
|
|
static void
|
|
|
|
hb_set_unicode_props (hb_buffer_t *buffer)
|
2010-11-03 21:37:24 +01:00
|
|
|
{
|
2011-07-21 17:34:59 +02:00
|
|
|
unsigned int count = buffer->len;
|
2012-04-12 16:06:52 +02:00
|
|
|
for (unsigned int i = 0; i < count; i++)
|
2012-05-09 15:04:13 +02:00
|
|
|
_hb_glyph_info_set_unicode_props (&buffer->info[i], buffer->unicode);
|
2010-11-03 21:37:24 +01:00
|
|
|
}
|
|
|
|
|
2012-09-02 02:38:45 +02:00
|
|
|
static void
|
|
|
|
hb_insert_dotted_circle (hb_buffer_t *buffer, hb_font_t *font)
|
|
|
|
{
|
2012-11-13 23:42:35 +01:00
|
|
|
if (!(buffer->flags & HB_BUFFER_FLAG_BOT) ||
|
2012-09-26 03:35:35 +02:00
|
|
|
_hb_glyph_info_get_general_category (&buffer->info[0]) !=
|
2012-09-02 02:38:45 +02:00
|
|
|
HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
|
|
|
|
return;
|
|
|
|
|
|
|
|
hb_codepoint_t dottedcircle_glyph;
|
|
|
|
if (!font->get_glyph (0x25CC, 0, &dottedcircle_glyph))
|
|
|
|
return;
|
|
|
|
|
|
|
|
hb_glyph_info_t dottedcircle;
|
|
|
|
dottedcircle.codepoint = 0x25CC;
|
|
|
|
_hb_glyph_info_set_unicode_props (&dottedcircle, buffer->unicode);
|
|
|
|
|
|
|
|
buffer->clear_output ();
|
|
|
|
|
|
|
|
buffer->idx = 0;
|
|
|
|
hb_glyph_info_t info = dottedcircle;
|
|
|
|
info.cluster = buffer->cur().cluster;
|
|
|
|
info.mask = buffer->cur().mask;
|
|
|
|
buffer->output_info (info);
|
|
|
|
while (buffer->idx < buffer->len)
|
|
|
|
buffer->next_glyph ();
|
|
|
|
|
|
|
|
buffer->swap_buffers ();
|
|
|
|
}
|
|
|
|
|
2010-05-21 15:34:23 +02:00
|
|
|
static void
|
2011-07-21 17:34:59 +02:00
|
|
|
hb_form_clusters (hb_buffer_t *buffer)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2011-07-21 17:34:59 +02:00
|
|
|
unsigned int count = buffer->len;
|
2010-05-21 15:34:23 +02:00
|
|
|
for (unsigned int i = 1; i < count; i++)
|
2012-09-06 20:25:48 +02:00
|
|
|
if (HB_UNICODE_GENERAL_CATEGORY_IS_MARK (_hb_glyph_info_get_general_category (&buffer->info[i])))
|
2012-06-09 02:40:02 +02:00
|
|
|
buffer->merge_clusters (i - 1, i + 1);
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
|
2010-10-06 05:00:05 +02:00
|
|
|
static void
|
2011-07-21 17:34:59 +02:00
|
|
|
hb_ensure_native_direction (hb_buffer_t *buffer)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2011-07-21 17:34:59 +02:00
|
|
|
hb_direction_t direction = buffer->props.direction;
|
2010-05-21 15:34:23 +02:00
|
|
|
|
2011-05-25 03:04:15 +02:00
|
|
|
/* TODO vertical:
|
|
|
|
* The only BTT vertical script is Ogham, but it's not clear to me whether OpenType
|
|
|
|
* Ogham fonts are supposed to be implemented BTT or not. Need to research that
|
|
|
|
* first. */
|
2011-07-21 17:34:59 +02:00
|
|
|
if ((HB_DIRECTION_IS_HORIZONTAL (direction) && direction != hb_script_get_horizontal_direction (buffer->props.script)) ||
|
2011-05-25 03:04:15 +02:00
|
|
|
(HB_DIRECTION_IS_VERTICAL (direction) && direction != HB_DIRECTION_TTB))
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2011-07-21 17:34:59 +02:00
|
|
|
hb_buffer_reverse_clusters (buffer);
|
|
|
|
buffer->props.direction = HB_DIRECTION_REVERSE (buffer->props.direction);
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Substitute */
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_mirror_chars (hb_ot_shape_context_t *c)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2010-12-07 22:22:02 +01:00
|
|
|
if (HB_DIRECTION_IS_FORWARD (c->target_direction))
|
2010-05-21 15:34:23 +02:00
|
|
|
return;
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
hb_unicode_funcs_t *unicode = c->buffer->unicode;
|
2010-10-13 21:54:06 +02:00
|
|
|
hb_mask_t rtlm_mask = c->plan->map.get_1_mask (HB_TAG ('r','t','l','m'));
|
2010-07-23 23:22:11 +02:00
|
|
|
|
2010-10-09 02:27:38 +02:00
|
|
|
unsigned int count = c->buffer->len;
|
2010-05-21 15:34:23 +02:00
|
|
|
for (unsigned int i = 0; i < count; i++) {
|
2012-08-01 23:13:10 +02:00
|
|
|
hb_codepoint_t codepoint = unicode->mirroring (c->buffer->info[i].codepoint);
|
2010-10-09 02:27:38 +02:00
|
|
|
if (likely (codepoint == c->buffer->info[i].codepoint))
|
2012-08-10 04:00:53 +02:00
|
|
|
c->buffer->info[i].mask |= rtlm_mask;
|
2010-05-21 18:58:20 +02:00
|
|
|
else
|
2010-10-09 02:27:38 +02:00
|
|
|
c->buffer->info[i].codepoint = codepoint;
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_shape_setup_masks (hb_ot_shape_context_t *c)
|
|
|
|
{
|
|
|
|
hb_ot_map_t *map = &c->plan->map;
|
|
|
|
|
|
|
|
hb_mask_t global_mask = map->get_global_mask ();
|
|
|
|
c->buffer->reset_masks (global_mask);
|
|
|
|
|
|
|
|
if (c->plan->shaper->setup_masks)
|
|
|
|
c->plan->shaper->setup_masks (c->plan, c->buffer, c->font);
|
|
|
|
|
|
|
|
for (unsigned int i = 0; i < c->num_user_features; i++)
|
|
|
|
{
|
|
|
|
const hb_feature_t *feature = &c->user_features[i];
|
|
|
|
if (!(feature->start == 0 && feature->end == (unsigned int)-1)) {
|
|
|
|
unsigned int shift;
|
|
|
|
hb_mask_t mask = map->get_mask (feature->tag, &shift);
|
|
|
|
c->buffer->set_masks (feature->value << shift, mask, feature->start, feature->end);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void
|
2012-08-10 04:33:32 +02:00
|
|
|
hb_ot_map_glyphs_fast (hb_buffer_t *buffer)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2012-08-10 04:33:32 +02:00
|
|
|
/* Normalization process sets up glyph_index(), we just copy it. */
|
|
|
|
unsigned int count = buffer->len;
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
|
|
|
buffer->info[i].codepoint = buffer->info[i].glyph_index();
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
|
2012-08-29 17:53:26 +02:00
|
|
|
static inline void
|
|
|
|
hb_synthesize_glyph_classes (hb_ot_shape_context_t *c)
|
|
|
|
{
|
|
|
|
unsigned int count = c->buffer->len;
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
|
|
|
c->buffer->info[i].glyph_props() = _hb_glyph_info_get_general_category (&c->buffer->info[i]) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK ?
|
2012-11-16 22:34:29 +01:00
|
|
|
HB_OT_LAYOUT_GLYPH_PROPS_MARK :
|
|
|
|
HB_OT_LAYOUT_GLYPH_PROPS_BASE_GLYPH;
|
2012-08-29 17:53:26 +02:00
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_substitute_default (hb_ot_shape_context_t *c)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2012-09-06 04:19:28 +02:00
|
|
|
if (c->plan->shaper->preprocess_text)
|
2012-08-12 00:34:13 +02:00
|
|
|
c->plan->shaper->preprocess_text (c->plan, c->buffer, c->font);
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
hb_ot_mirror_chars (c);
|
|
|
|
|
2012-08-10 04:33:32 +02:00
|
|
|
HB_BUFFER_ALLOCATE_VAR (c->buffer, glyph_index);
|
|
|
|
|
2012-11-16 21:39:23 +01:00
|
|
|
_hb_ot_shape_normalize (c->plan, c->buffer, c->font);
|
2012-08-10 03:58:07 +02:00
|
|
|
|
|
|
|
hb_ot_shape_setup_masks (c);
|
2011-07-28 22:48:43 +02:00
|
|
|
|
2012-09-02 01:20:41 +02:00
|
|
|
/* This is unfortunate to go here, but necessary... */
|
|
|
|
if (!hb_ot_layout_has_positioning (c->face))
|
|
|
|
_hb_ot_shape_fallback_position_recategorize_marks (c->plan, c->font, c->buffer);
|
|
|
|
|
2012-08-10 04:33:32 +02:00
|
|
|
hb_ot_map_glyphs_fast (c->buffer);
|
|
|
|
|
|
|
|
HB_BUFFER_DEALLOCATE_VAR (c->buffer, glyph_index);
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
2011-07-25 06:44:50 +02:00
|
|
|
hb_ot_substitute_complex (hb_ot_shape_context_t *c)
|
|
|
|
{
|
2012-08-02 14:36:40 +02:00
|
|
|
hb_ot_layout_substitute_start (c->font, c->buffer);
|
2012-07-31 01:30:01 +02:00
|
|
|
|
2012-08-01 04:48:38 +02:00
|
|
|
if (!hb_ot_layout_has_glyph_classes (c->face))
|
|
|
|
hb_synthesize_glyph_classes (c);
|
|
|
|
|
2012-09-06 04:19:28 +02:00
|
|
|
c->plan->substitute (c->font, c->buffer);
|
2011-07-25 06:44:50 +02:00
|
|
|
|
2012-08-02 14:36:40 +02:00
|
|
|
hb_ot_layout_substitute_finish (c->font, c->buffer);
|
2011-07-25 06:44:50 +02:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_substitute (hb_ot_shape_context_t *c)
|
|
|
|
{
|
|
|
|
hb_ot_substitute_default (c);
|
|
|
|
hb_ot_substitute_complex (c);
|
|
|
|
}
|
2010-05-21 15:34:23 +02:00
|
|
|
|
|
|
|
/* Position */
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_position_default (hb_ot_shape_context_t *c)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2012-07-31 01:30:01 +02:00
|
|
|
hb_ot_layout_position_start (c->font, c->buffer);
|
2010-05-21 15:34:23 +02:00
|
|
|
|
2010-10-06 05:00:05 +02:00
|
|
|
unsigned int count = c->buffer->len;
|
Adjust mark advance-width zeroing logic for Myanmar
Before, we were zeroing advance width of attached marks for
non-Indic scripts, and not doing it for Indic.
We have now three different behaviors, which seem to better
reflect what Uniscribe is doing:
- For Indic, no explicit zeroing happens whatsoever, which
is the same as before,
- For Myanmar, zero advance width of glyphs marked as marks
*in GDEF*, and do that *before* applying GPOS. This seems
to be what the new Win8 Myanmar shaper does,
- For everything else, zero advance width of glyphs that are
from General_Category=Mn Unicode characters, and do so
before applying GPOS. This seems to be what Uniscribe does
for Latin at least.
With these changes, positioning of all tests matches for Myanmar,
except for the glitch in Uniscribe not applying 'mark'. See preivous
commit.
2013-02-12 15:44:57 +01:00
|
|
|
for (unsigned int i = 0; i < count; i++)
|
|
|
|
{
|
2012-08-02 01:03:46 +02:00
|
|
|
c->font->get_glyph_advance_for_direction (c->buffer->info[i].codepoint,
|
|
|
|
c->buffer->props.direction,
|
|
|
|
&c->buffer->pos[i].x_advance,
|
|
|
|
&c->buffer->pos[i].y_advance);
|
|
|
|
c->font->subtract_glyph_origin_for_direction (c->buffer->info[i].codepoint,
|
|
|
|
c->buffer->props.direction,
|
|
|
|
&c->buffer->pos[i].x_offset,
|
|
|
|
&c->buffer->pos[i].y_offset);
|
Adjust mark advance-width zeroing logic for Myanmar
Before, we were zeroing advance width of attached marks for
non-Indic scripts, and not doing it for Indic.
We have now three different behaviors, which seem to better
reflect what Uniscribe is doing:
- For Indic, no explicit zeroing happens whatsoever, which
is the same as before,
- For Myanmar, zero advance width of glyphs marked as marks
*in GDEF*, and do that *before* applying GPOS. This seems
to be what the new Win8 Myanmar shaper does,
- For everything else, zero advance width of glyphs that are
from General_Category=Mn Unicode characters, and do so
before applying GPOS. This seems to be what Uniscribe does
for Latin at least.
With these changes, positioning of all tests matches for Myanmar,
except for the glitch in Uniscribe not applying 'mark'. See preivous
commit.
2013-02-12 15:44:57 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2013-02-13 11:57:24 +01:00
|
|
|
/* Zero'ing mark widths by GDEF (as used in Myanmar spec) happens
|
|
|
|
* *before* GPOS. */
|
Adjust mark advance-width zeroing logic for Myanmar
Before, we were zeroing advance width of attached marks for
non-Indic scripts, and not doing it for Indic.
We have now three different behaviors, which seem to better
reflect what Uniscribe is doing:
- For Indic, no explicit zeroing happens whatsoever, which
is the same as before,
- For Myanmar, zero advance width of glyphs marked as marks
*in GDEF*, and do that *before* applying GPOS. This seems
to be what the new Win8 Myanmar shaper does,
- For everything else, zero advance width of glyphs that are
from General_Category=Mn Unicode characters, and do so
before applying GPOS. This seems to be what Uniscribe does
for Latin at least.
With these changes, positioning of all tests matches for Myanmar,
except for the glitch in Uniscribe not applying 'mark'. See preivous
commit.
2013-02-12 15:44:57 +01:00
|
|
|
switch (c->plan->shaper->zero_width_marks)
|
|
|
|
{
|
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_GDEF:
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
|
|
|
if ((c->buffer->info[i].glyph_props() & HB_OT_LAYOUT_GLYPH_PROPS_MARK))
|
|
|
|
{
|
|
|
|
c->buffer->pos[i].x_advance = 0;
|
|
|
|
c->buffer->pos[i].y_advance = 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_NONE:
|
2013-02-13 11:57:24 +01:00
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_UNICODE:
|
Adjust mark advance-width zeroing logic for Myanmar
Before, we were zeroing advance width of attached marks for
non-Indic scripts, and not doing it for Indic.
We have now three different behaviors, which seem to better
reflect what Uniscribe is doing:
- For Indic, no explicit zeroing happens whatsoever, which
is the same as before,
- For Myanmar, zero advance width of glyphs marked as marks
*in GDEF*, and do that *before* applying GPOS. This seems
to be what the new Win8 Myanmar shaper does,
- For everything else, zero advance width of glyphs that are
from General_Category=Mn Unicode characters, and do so
before applying GPOS. This seems to be what Uniscribe does
for Latin at least.
With these changes, positioning of all tests matches for Myanmar,
except for the glitch in Uniscribe not applying 'mark'. See preivous
commit.
2013-02-12 15:44:57 +01:00
|
|
|
break;
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline bool
|
2011-07-25 06:44:50 +02:00
|
|
|
hb_ot_position_complex (hb_ot_shape_context_t *c)
|
|
|
|
{
|
2012-08-10 03:58:07 +02:00
|
|
|
bool ret = false;
|
2013-02-13 11:57:24 +01:00
|
|
|
unsigned int count = c->buffer->len;
|
2011-07-25 06:44:50 +02:00
|
|
|
|
2011-07-28 22:48:43 +02:00
|
|
|
if (hb_ot_layout_has_positioning (c->face))
|
|
|
|
{
|
|
|
|
/* Change glyph origin to what GPOS expects, apply GPOS, change it back. */
|
|
|
|
|
|
|
|
for (unsigned int i = 0; i < count; i++) {
|
2012-08-02 01:03:46 +02:00
|
|
|
c->font->add_glyph_origin_for_direction (c->buffer->info[i].codepoint,
|
|
|
|
HB_DIRECTION_LTR,
|
|
|
|
&c->buffer->pos[i].x_offset,
|
|
|
|
&c->buffer->pos[i].y_offset);
|
2011-07-28 22:48:43 +02:00
|
|
|
}
|
2011-07-25 06:44:50 +02:00
|
|
|
|
2012-08-02 16:07:58 +02:00
|
|
|
c->plan->position (c->font, c->buffer);
|
2011-07-25 06:44:50 +02:00
|
|
|
|
2011-07-28 22:48:43 +02:00
|
|
|
for (unsigned int i = 0; i < count; i++) {
|
2012-08-02 01:03:46 +02:00
|
|
|
c->font->subtract_glyph_origin_for_direction (c->buffer->info[i].codepoint,
|
|
|
|
HB_DIRECTION_LTR,
|
|
|
|
&c->buffer->pos[i].x_offset,
|
|
|
|
&c->buffer->pos[i].y_offset);
|
2011-07-28 22:48:43 +02:00
|
|
|
}
|
2011-08-02 20:06:51 +02:00
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
ret = true;
|
2012-08-08 04:41:38 +02:00
|
|
|
}
|
2012-08-01 05:08:25 +02:00
|
|
|
|
2013-02-13 11:57:24 +01:00
|
|
|
/* Zero'ing mark widths by Unicode happens
|
|
|
|
* *after* GPOS. */
|
|
|
|
switch (c->plan->shaper->zero_width_marks)
|
|
|
|
{
|
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_UNICODE:
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
|
|
|
if (_hb_glyph_info_get_general_category (&c->buffer->info[i]) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
|
|
|
|
{
|
|
|
|
c->buffer->pos[i].x_advance = 0;
|
|
|
|
c->buffer->pos[i].y_advance = 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_NONE:
|
|
|
|
case HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_GDEF:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
Adjust mark advance-width zeroing logic for Myanmar
Before, we were zeroing advance width of attached marks for
non-Indic scripts, and not doing it for Indic.
We have now three different behaviors, which seem to better
reflect what Uniscribe is doing:
- For Indic, no explicit zeroing happens whatsoever, which
is the same as before,
- For Myanmar, zero advance width of glyphs marked as marks
*in GDEF*, and do that *before* applying GPOS. This seems
to be what the new Win8 Myanmar shaper does,
- For everything else, zero advance width of glyphs that are
from General_Category=Mn Unicode characters, and do so
before applying GPOS. This seems to be what Uniscribe does
for Latin at least.
With these changes, positioning of all tests matches for Myanmar,
except for the glitch in Uniscribe not applying 'mark'. See preivous
commit.
2013-02-12 15:44:57 +01:00
|
|
|
hb_ot_layout_position_finish (c->font, c->buffer);
|
2011-07-25 06:44:50 +02:00
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
return ret;
|
2011-07-25 06:44:50 +02:00
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
static inline void
|
|
|
|
hb_ot_position (hb_ot_shape_context_t *c)
|
|
|
|
{
|
|
|
|
hb_ot_position_default (c);
|
|
|
|
|
|
|
|
hb_bool_t fallback = !hb_ot_position_complex (c);
|
|
|
|
|
2012-11-14 22:48:26 +01:00
|
|
|
if (fallback && c->plan->shaper->fallback_position)
|
|
|
|
_hb_ot_shape_fallback_position (c->plan, c->font, c->buffer);
|
2012-08-10 03:58:07 +02:00
|
|
|
|
|
|
|
if (HB_DIRECTION_IS_BACKWARD (c->buffer->props.direction))
|
|
|
|
hb_buffer_reverse (c->buffer);
|
|
|
|
|
2012-11-14 22:48:26 +01:00
|
|
|
/* Visual fallback goes here. */
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
if (fallback)
|
2013-02-21 21:23:39 +01:00
|
|
|
_hb_ot_shape_fallback_kern (c->plan, c->font, c->buffer);
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
|
|
|
|
/* Post-process */
|
|
|
|
|
2012-05-09 15:04:13 +02:00
|
|
|
static void
|
2012-11-13 23:42:35 +01:00
|
|
|
hb_ot_hide_default_ignorables (hb_ot_shape_context_t *c)
|
2012-05-09 15:04:13 +02:00
|
|
|
{
|
2012-11-13 23:42:35 +01:00
|
|
|
if (c->buffer->flags & HB_BUFFER_FLAG_PRESERVE_DEFAULT_IGNORABLES)
|
|
|
|
return;
|
|
|
|
|
2012-08-31 20:06:26 +02:00
|
|
|
hb_codepoint_t space = 0;
|
2012-05-09 15:04:13 +02:00
|
|
|
|
|
|
|
unsigned int count = c->buffer->len;
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
2012-07-24 02:18:17 +02:00
|
|
|
if (unlikely (!is_a_ligature (c->buffer->info[i]) &&
|
2012-10-26 01:32:54 +02:00
|
|
|
_hb_glyph_info_is_default_ignorable (&c->buffer->info[i])))
|
2012-08-31 20:06:26 +02:00
|
|
|
{
|
|
|
|
if (!space) {
|
|
|
|
/* We assume that the space glyph is not gid0. */
|
|
|
|
if (unlikely (!c->font->get_glyph (' ', 0, &space)) || !space)
|
|
|
|
return; /* No point! */
|
|
|
|
}
|
2012-05-09 15:04:13 +02:00
|
|
|
c->buffer->info[i].codepoint = space;
|
|
|
|
c->buffer->pos[i].x_advance = 0;
|
|
|
|
c->buffer->pos[i].y_advance = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-05-21 15:34:23 +02:00
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
/* Pull it all together! */
|
2010-05-21 15:34:23 +02:00
|
|
|
|
2010-10-06 05:00:05 +02:00
|
|
|
static void
|
2012-07-27 08:29:32 +02:00
|
|
|
hb_ot_shape_internal (hb_ot_shape_context_t *c)
|
2010-05-21 15:34:23 +02:00
|
|
|
{
|
2011-07-28 22:48:43 +02:00
|
|
|
c->buffer->deallocate_var_all ();
|
|
|
|
|
2010-10-12 21:35:45 +02:00
|
|
|
/* Save the original direction, we use it later. */
|
2010-12-07 22:22:02 +01:00
|
|
|
c->target_direction = c->buffer->props.direction;
|
2010-10-12 21:35:45 +02:00
|
|
|
|
2012-05-09 15:04:13 +02:00
|
|
|
HB_BUFFER_ALLOCATE_VAR (c->buffer, unicode_props0);
|
|
|
|
HB_BUFFER_ALLOCATE_VAR (c->buffer, unicode_props1);
|
2011-07-28 22:48:43 +02:00
|
|
|
|
2012-06-09 02:40:02 +02:00
|
|
|
c->buffer->clear_output ();
|
|
|
|
|
2012-05-09 15:04:13 +02:00
|
|
|
hb_set_unicode_props (c->buffer);
|
2012-09-02 02:38:45 +02:00
|
|
|
hb_insert_dotted_circle (c->buffer, c->font);
|
2011-07-22 22:15:32 +02:00
|
|
|
hb_form_clusters (c->buffer);
|
|
|
|
|
2011-07-21 17:34:59 +02:00
|
|
|
hb_ensure_native_direction (c->buffer);
|
2011-07-21 07:11:09 +02:00
|
|
|
|
2012-08-10 03:58:07 +02:00
|
|
|
hb_ot_substitute (c);
|
|
|
|
hb_ot_position (c);
|
2010-05-21 15:34:23 +02:00
|
|
|
|
2012-11-13 23:42:35 +01:00
|
|
|
hb_ot_hide_default_ignorables (c);
|
2012-05-09 15:04:13 +02:00
|
|
|
|
|
|
|
HB_BUFFER_DEALLOCATE_VAR (c->buffer, unicode_props1);
|
|
|
|
HB_BUFFER_DEALLOCATE_VAR (c->buffer, unicode_props0);
|
2011-07-28 22:48:43 +02:00
|
|
|
|
2010-12-07 22:22:02 +01:00
|
|
|
c->buffer->props.direction = c->target_direction;
|
2011-07-28 23:06:46 +02:00
|
|
|
|
|
|
|
c->buffer->deallocate_var_all ();
|
2010-10-06 05:00:05 +02:00
|
|
|
}
|
|
|
|
|
2010-10-12 22:57:47 +02:00
|
|
|
|
2011-08-05 04:31:05 +02:00
|
|
|
hb_bool_t
|
2012-07-27 04:05:39 +02:00
|
|
|
_hb_ot_shape (hb_shape_plan_t *shape_plan,
|
|
|
|
hb_font_t *font,
|
2012-04-12 20:53:53 +02:00
|
|
|
hb_buffer_t *buffer,
|
|
|
|
const hb_feature_t *features,
|
|
|
|
unsigned int num_features)
|
2010-10-06 05:00:05 +02:00
|
|
|
{
|
2012-07-27 08:29:32 +02:00
|
|
|
hb_ot_shape_context_t c = {HB_SHAPER_DATA_GET (shape_plan), font, font->face, buffer, features, num_features};
|
|
|
|
hb_ot_shape_internal (&c);
|
2011-08-05 04:31:05 +02:00
|
|
|
|
2012-06-06 02:35:40 +02:00
|
|
|
return true;
|
2010-05-21 15:34:23 +02:00
|
|
|
}
|
2012-04-24 22:56:37 +02:00
|
|
|
|
|
|
|
|
2012-11-16 03:39:46 +01:00
|
|
|
void
|
|
|
|
hb_ot_shape_plan_collect_lookups (hb_shape_plan_t *shape_plan,
|
|
|
|
hb_tag_t table_tag,
|
|
|
|
hb_set_t *lookup_indexes /* OUT */)
|
|
|
|
{
|
2012-11-16 23:08:05 +01:00
|
|
|
/* XXX Does the first part always succeed? */
|
2012-11-16 03:39:46 +01:00
|
|
|
HB_SHAPER_DATA_GET (shape_plan)->collect_lookups (table_tag, lookup_indexes);
|
|
|
|
}
|
2012-08-10 04:33:32 +02:00
|
|
|
|
2012-11-16 03:39:46 +01:00
|
|
|
|
|
|
|
/* TODO Move this to hb-ot-shape-normalize, make it do decompose, and make it public. */
|
|
|
|
static void
|
|
|
|
add_char (hb_font_t *font,
|
|
|
|
hb_unicode_funcs_t *unicode,
|
|
|
|
hb_bool_t mirror,
|
|
|
|
hb_codepoint_t u,
|
|
|
|
hb_set_t *glyphs)
|
2012-08-10 04:33:32 +02:00
|
|
|
{
|
2012-11-16 03:39:46 +01:00
|
|
|
hb_codepoint_t glyph;
|
|
|
|
if (font->get_glyph (u, 0, &glyph))
|
|
|
|
glyphs->add (glyph);
|
|
|
|
if (mirror)
|
|
|
|
{
|
|
|
|
hb_codepoint_t m = unicode->mirroring (u);
|
|
|
|
if (m != u && font->get_glyph (m, 0, &glyph))
|
|
|
|
glyphs->add (glyph);
|
|
|
|
}
|
2012-08-10 04:33:32 +02:00
|
|
|
}
|
|
|
|
|
2012-11-16 03:39:46 +01:00
|
|
|
|
2012-04-24 22:56:37 +02:00
|
|
|
void
|
|
|
|
hb_ot_shape_glyphs_closure (hb_font_t *font,
|
|
|
|
hb_buffer_t *buffer,
|
|
|
|
const hb_feature_t *features,
|
|
|
|
unsigned int num_features,
|
|
|
|
hb_set_t *glyphs)
|
|
|
|
{
|
|
|
|
hb_ot_shape_plan_t plan;
|
|
|
|
|
2012-11-16 03:39:46 +01:00
|
|
|
const char *shapers[] = {"ot", NULL};
|
|
|
|
hb_shape_plan_t *shape_plan = hb_shape_plan_create_cached (font->face, &buffer->props,
|
|
|
|
features, num_features, shapers);
|
2012-04-24 22:56:37 +02:00
|
|
|
|
2012-11-16 03:39:46 +01:00
|
|
|
bool mirror = hb_script_get_horizontal_direction (buffer->props.script) == HB_DIRECTION_RTL;
|
2012-04-24 22:56:37 +02:00
|
|
|
|
|
|
|
unsigned int count = buffer->len;
|
|
|
|
for (unsigned int i = 0; i < count; i++)
|
2012-11-16 03:39:46 +01:00
|
|
|
add_char (font, buffer->unicode, mirror, buffer->info[i].codepoint, glyphs);
|
|
|
|
|
|
|
|
hb_set_t lookups;
|
|
|
|
lookups.init ();
|
|
|
|
hb_ot_shape_plan_collect_lookups (shape_plan, HB_OT_TAG_GSUB, &lookups);
|
2012-04-24 22:56:37 +02:00
|
|
|
|
|
|
|
/* And find transitive closure. */
|
|
|
|
hb_set_t copy;
|
|
|
|
copy.init ();
|
|
|
|
do {
|
|
|
|
copy.set (glyphs);
|
2012-11-16 03:39:46 +01:00
|
|
|
for (hb_codepoint_t lookup_index = -1; hb_set_next (&lookups, &lookup_index);)
|
|
|
|
hb_ot_layout_lookup_substitute_closure (font->face, lookup_index, glyphs);
|
2012-11-16 01:15:42 +01:00
|
|
|
} while (!copy.is_equal (glyphs));
|
2012-07-27 08:29:32 +02:00
|
|
|
|
|
|
|
hb_shape_plan_destroy (shape_plan);
|
2012-04-24 22:56:37 +02:00
|
|
|
}
|