Impressum hb-ot-var-common.hh
Sprache: C
/*
* Copyright © 2021 Google , Inc .
*
* This is part of HarfBuzz , a text shaping library .
*
* 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 .
*
*/
#ifndef HB_OT_VAR_COMMON_HH
#define HB_OT_VAR_COMMON_HH
#include "hb-ot-layout-common.hh"
#include "hb-alloc-pool.hh"
#include "hb-priority-queue.hh"
#include "hb-subset-instancer-iup.hh"
namespace OT {
using rebase_tent_result_scratch_t = hb_pair_t<rebase_tent_result_t, rebase_tent_resu
lt_t>;
/* https://docs.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats#tuplevariationheader */
struct TupleVariationHeader
{
friend struct tuple_delta_t;
size_t get_size (unsigned axis_count_times_2) const
{
// This function is super hot in mega-var-fonts with hundreds of masters.
unsigned ti = tupleIndex;
if (unlikely ((ti & (TupleIndex::EmbeddedPeakTuple | TupleIndex::IntermediateRegion))))
{
unsigned count = ((ti & TupleIndex::EmbeddedPeakTuple) != 0 ) + ((ti & TupleIndex::IntermediateRegion) != 0 ) * 2 ;
return min_size + count * axis_count_times_2;
}
return min_size;
}
unsigned get_data_size () const { return varDataSize; }
const TupleVariationHeader &get_next (unsigned axis_count_times_2) const
{ return StructAtOffset<TupleVariationHeader> (this , get_size (axis_count_times_2)); }
bool unpack_axis_tuples (unsigned axis_count,
const hb_array_t<const F2DOT14> shared_tuples,
const hb_map_t *axes_old_index_tag_map,
hb_hashmap_t<hb_tag_t, Triple>& axis_tuples /* OUT */) const
{
const F2DOT14 *peak_tuple = nullptr;
if (has_peak ())
peak_tuple = get_peak_tuple (axis_count);
else
{
unsigned int index = get_index ();
if (unlikely ((index + 1 ) * axis_count > shared_tuples.length))
return false ;
peak_tuple = shared_tuples.sub_array (axis_count * index, axis_count).arrayZ;
}
const F2DOT14 *start_tuple = nullptr;
const F2DOT14 *end_tuple = nullptr;
bool has_interm = has_intermediate ();
if (has_interm)
{
start_tuple = get_start_tuple (axis_count);
end_tuple = get_end_tuple (axis_count);
}
for (unsigned i = 0 ; i < axis_count; i++)
{
float peak = peak_tuple[i].to_float ();
if (peak == 0 .f) continue ;
hb_tag_t *axis_tag;
if (!axes_old_index_tag_map->has (i, &axis_tag))
return false ;
float start, end;
if (has_interm)
{
start = start_tuple[i].to_float ();
end = end_tuple[i].to_float ();
}
else
{
start = hb_min (peak, 0 .f);
end = hb_max (peak, 0 .f);
}
axis_tuples.set (*axis_tag, Triple ((double ) start, (double ) peak, (double ) end));
}
return true ;
}
HB_ALWAYS_INLINE
double calculate_scalar (hb_array_t<const int > coords, unsigned int coord_count,
const hb_array_t<const F2DOT14> shared_tuples,
hb_scalar_cache_t *shared_tuple_scalar_cache = nullptr) const
{
unsigned tuple_index = tupleIndex;
const F2DOT14 *peak_tuple;
bool has_interm = tuple_index & TupleIndex::IntermediateRegion; // Inlined for performance
if (unlikely (tuple_index & TupleIndex::EmbeddedPeakTuple)) // Inlined for performance
{
peak_tuple = get_peak_tuple (coord_count);
shared_tuple_scalar_cache = nullptr;
}
else
{
unsigned int index = tuple_index & TupleIndex::TupleIndexMask; // Inlined for performance
float scalar;
if (shared_tuple_scalar_cache &&
shared_tuple_scalar_cache->get (index, &scalar))
{
if (has_interm && (scalar != 0 && scalar != 1 .f))
shared_tuple_scalar_cache = nullptr;
else
return (double ) scalar;
}
if (unlikely ((index + 1 ) * coord_count > shared_tuples.length))
return 0 .0 ;
peak_tuple = shared_tuples.arrayZ + (coord_count * index);
}
const F2DOT14 *start_tuple = nullptr;
const F2DOT14 *end_tuple = nullptr;
if (has_interm)
{
start_tuple = get_start_tuple (coord_count);
end_tuple = get_end_tuple (coord_count);
}
double scalar = 1 .0 ;
#ifndef HB_OPTIMIZE_SIZE
#if HB_FAST_NUM_ACCESS
bool skip = coord_count >= 16 ;
#endif
#endif
for (unsigned int i = 0 ; i < coord_count; i++)
{
#ifndef HB_OPTIMIZE_SIZE
#if HB_FAST_NUM_ACCESS
if (skip)
{
while (i + 4 <= coord_count && * (HBUINT64LE *) &peak_tuple[i] == 0 )
i += 4 ;
while (i < coord_count && peak_tuple[i].to_int () == 0 )
i += 1 ;
if (i >= coord_count)
break ;
}
#endif
#endif
int peak = peak_tuple[i].to_int ();
if (!peak) continue ;
int v = coords[i];
if (!v) { scalar = 0 .0 ; break ; }
if (v == peak) continue ;
if (has_interm)
{
shared_tuple_scalar_cache = nullptr;
int start = start_tuple[i].to_int ();
int end = end_tuple[i].to_int ();
if (unlikely (start > peak || peak > end ||
(start < 0 && end > 0 && peak))) continue ;
if (v < start || v > end) { scalar = 0 .0 ; break ; }
if (v < peak)
{ if (peak != start) scalar *= (double ) (v - start) / (peak - start); }
else
{ if (peak != end) scalar *= (double ) (end - v) / (end - peak); }
}
else if (v < hb_min (0 , peak) || v > hb_max (0 , peak)) { scalar = 0 .0 ; break ; }
else
scalar *= (double ) v / peak;
}
if (shared_tuple_scalar_cache)
shared_tuple_scalar_cache->set (get_index (), scalar);
return scalar;
}
bool has_peak () const { return tupleIndex & TupleIndex::EmbeddedPeakTuple; }
bool has_intermediate () const { return tupleIndex & TupleIndex::IntermediateRegion; }
bool has_private_points () const { return tupleIndex & TupleIndex::PrivatePointNumbers; }
unsigned get_index () const { return tupleIndex & TupleIndex::TupleIndexMask; }
protected :
struct TupleIndex : HBUINT16
{
enum Flags {
EmbeddedPeakTuple = 0 x8000u,
IntermediateRegion = 0 x4000u,
PrivatePointNumbers = 0 x2000u,
TupleIndexMask = 0 x0FFFu
};
TupleIndex& operator = (uint16_t i) { HBUINT16::operator = (i); return *this ; }
DEFINE_SIZE_STATIC (2 );
};
hb_array_t<const F2DOT14> get_all_tuples (unsigned axis_count) const
{ return StructAfter<UnsizedArrayOf<F2DOT14>> (tupleIndex).as_array ((has_peak () + has_intermediate () * 2 ) * axis_count); }
const F2DOT14* get_all_tuples_base (unsigned axis_count) const
{ return StructAfter<UnsizedArrayOf<F2DOT14>> (tupleIndex).arrayZ; }
const F2DOT14* get_peak_tuple (unsigned axis_count) const
{ return get_all_tuples_base (axis_count); }
const F2DOT14* get_start_tuple (unsigned axis_count) const
{ return get_all_tuples_base (axis_count) + has_peak () * axis_count; }
const F2DOT14* get_end_tuple (unsigned axis_count) const
{ return get_all_tuples_base (axis_count) + has_peak () * axis_count + axis_count; }
HBUINT16 varDataSize; /* The size in bytes of the serialized
* data for this tuple variation table. */
TupleIndex tupleIndex; /* A packed field. The high 4 bits are flags (see below).
The low 12 bits are an index into a shared tuple
records array. */
/* UnsizedArrayOf<F2DOT14> peakTuple - optional */
/* Peak tuple record for this tuple variation table — optional,
* determined by flags in the tupleIndex value .
*
* Note that this must always be included in the 'cvar' table. */
/* UnsizedArrayOf<F2DOT14> intermediateStartTuple - optional */
/* Intermediate start tuple record for this tuple variation table — optional,
determined by flags in the tupleIndex value. */
/* UnsizedArrayOf<F2DOT14> intermediateEndTuple - optional */
/* Intermediate end tuple record for this tuple variation table — optional,
* determined by flags in the tupleIndex value. */
public :
DEFINE_SIZE_MIN (4 );
};
struct optimize_scratch_t
{
iup_scratch_t iup;
hb_vector_t<bool > opt_indices;
hb_vector_t<int > rounded_x_deltas;
hb_vector_t<int > rounded_y_deltas;
hb_vector_t<float > opt_deltas_x;
hb_vector_t<float > opt_deltas_y;
hb_vector_t<unsigned char > opt_point_data;
hb_vector_t<unsigned char > opt_deltas_data;
hb_vector_t<unsigned char > point_data;
hb_vector_t<unsigned char > deltas_data;
hb_vector_t<int > rounded_deltas;
};
struct tuple_delta_t
{
static constexpr bool realloc_move = true ; // Watch out when adding new members!
public :
hb_hashmap_t<hb_tag_t, Triple> axis_tuples;
/* indices_length = point_count, indice[i] = 1 means point i is referenced */
hb_vector_t<bool > indices;
hb_vector_t<float > deltas_x;
/* empty for cvar tuples */
hb_vector_t<float > deltas_y;
/* compiled data: header and deltas
* compiled point data is saved in a hashmap within tuple_variations_t cause
* some point sets might be reused by different tuple variations */
hb_vector_t<unsigned char > compiled_tuple_header;
hb_vector_t<unsigned char > compiled_deltas;
hb_vector_t<F2DOT14> compiled_peak_coords;
hb_vector_t<F2DOT14> compiled_interm_coords;
tuple_delta_t (hb_alloc_pool_t *pool = nullptr) {}
tuple_delta_t (const tuple_delta_t& o) = default ;
tuple_delta_t& operator = (const tuple_delta_t& o) = default ;
friend void swap (tuple_delta_t& a, tuple_delta_t& b) noexcept
{
hb_swap (a.axis_tuples, b.axis_tuples);
hb_swap (a.indices, b.indices);
hb_swap (a.deltas_x, b.deltas_x);
hb_swap (a.deltas_y, b.deltas_y);
hb_swap (a.compiled_tuple_header, b.compiled_tuple_header);
hb_swap (a.compiled_deltas, b.compiled_deltas);
hb_swap (a.compiled_peak_coords, b.compiled_peak_coords);
}
tuple_delta_t (tuple_delta_t&& o) noexcept : tuple_delta_t ()
{ hb_swap (*this , o); }
tuple_delta_t& operator = (tuple_delta_t&& o) noexcept
{
hb_swap (*this , o);
return *this ;
}
void copy_from (const tuple_delta_t& o, hb_alloc_pool_t *pool = nullptr)
{
axis_tuples = o.axis_tuples;
indices.duplicate_vector_from_pool (pool, o.indices);
deltas_x.duplicate_vector_from_pool (pool, o.deltas_x);
deltas_y.duplicate_vector_from_pool (pool, o.deltas_y);
compiled_tuple_header.duplicate_vector_from_pool (pool, o.compiled_tuple_header);
compiled_deltas.duplicate_vector_from_pool (pool, o.compiled_deltas);
compiled_peak_coords.duplicate_vector_from_pool (pool, o.compiled_peak_coords);
compiled_interm_coords.duplicate_vector_from_pool (pool, o.compiled_interm_coords);
}
void remove_axis (hb_tag_t axis_tag)
{ axis_tuples.del (axis_tag); }
bool set_tent (hb_tag_t axis_tag, Triple tent)
{ return axis_tuples.set (axis_tag, tent); }
tuple_delta_t& operator += (const tuple_delta_t& o)
{
unsigned num = indices.length;
for (unsigned i = 0 ; i < num; i++)
{
if (indices.arrayZ[i])
{
if (o.indices.arrayZ[i])
{
deltas_x[i] += o.deltas_x[i];
if (deltas_y && o.deltas_y)
deltas_y[i] += o.deltas_y[i];
}
}
else
{
if (!o.indices.arrayZ[i]) continue ;
indices.arrayZ[i] = true ;
deltas_x[i] = o.deltas_x[i];
if (deltas_y && o.deltas_y)
deltas_y[i] = o.deltas_y[i];
}
}
return *this ;
}
tuple_delta_t& operator *= (float scalar)
{
if (scalar == 1 .0 f)
return *this ;
unsigned num = indices.length;
if (deltas_y)
for (unsigned i = 0 ; i < num; i++)
{
if (!indices.arrayZ[i]) continue ;
deltas_x[i] *= scalar;
deltas_y[i] *= scalar;
}
else
for (unsigned i = 0 ; i < num; i++)
{
if (!indices.arrayZ[i]) continue ;
deltas_x[i] *= scalar;
}
return *this ;
}
void change_tuple_var_axis_limit (hb_tag_t axis_tag, Triple axis_limit,
TripleDistances axis_triple_distances,
hb_vector_t<tuple_delta_t>& out,
rebase_tent_result_scratch_t &scratch,
hb_alloc_pool_t *pool = nullptr)
{
// May move *this out.
out.reset ();
Triple *tent;
if (!axis_tuples.has (axis_tag, &tent))
{
out.push (std::move (*this ));
return ;
}
if ((tent->minimum < 0 .0 && tent->maximum > 0 .0 ) ||
!(tent->minimum <= tent->middle && tent->middle <= tent->maximum))
return ;
if (tent->middle == 0 .0 )
{
out.push (std::move (*this ));
return ;
}
if (!axis_limit.is_point () &&
!(-1 .0 <= axis_limit.minimum &&
axis_limit.minimum <= axis_limit.middle &&
axis_limit.middle <= axis_limit.maximum &&
axis_limit.maximum <= +1 .0 ))
return ;
rebase_tent_result_t &solutions = scratch.first;
rebase_tent (*tent, axis_limit, axis_triple_distances, solutions, scratch.second);
for (unsigned i = 0 ; i < solutions.length; i++)
{
auto &t = solutions.arrayZ[i];
tuple_delta_t new_var;
if (i < solutions.length - 1 )
new_var.copy_from (*this , pool);
else
new_var = std::move (*this );
if (t.second == Triple ())
new_var.remove_axis (axis_tag);
else
new_var.set_tent (axis_tag, t.second);
new_var *= t.first;
out.push (std::move (new_var));
}
}
bool compile_coords (const hb_map_t& axes_index_map,
const hb_map_t& axes_old_index_tag_map,
hb_alloc_pool_t *pool= nullptr)
{
unsigned cur_axis_count = axes_index_map.get_population ();
if (pool)
{
if (unlikely (!compiled_peak_coords.allocate_from_pool (pool, cur_axis_count)))
return false ;
}
else if (unlikely (!compiled_peak_coords.resize (cur_axis_count)))
return false ;
hb_array_t<F2DOT14> start_coords, end_coords;
unsigned orig_axis_count = axes_old_index_tag_map.get_population ();
unsigned j = 0 ;
for (unsigned i = 0 ; i < orig_axis_count; i++)
{
if (!axes_index_map.has (i))
continue ;
hb_tag_t axis_tag = axes_old_index_tag_map.get (i);
Triple *coords = nullptr;
if (axis_tuples.has (axis_tag, &coords))
{
float min_val = coords->minimum;
float val = coords->middle;
float max_val = coords->maximum;
compiled_peak_coords.arrayZ[j].set_float (val);
if (min_val != hb_min (val, 0 .f) || max_val != hb_max (val, 0 .f))
{
if (!compiled_interm_coords)
{
if (pool)
{
if (unlikely (!compiled_interm_coords.allocate_from_pool (pool, 2 * cur_axis_count)))
return false ;
}
else if (unlikely (!compiled_interm_coords.resize (2 * cur_axis_count)))
return false ;
start_coords = compiled_interm_coords.as_array ().sub_array (0 , cur_axis_count);
end_coords = compiled_interm_coords.as_array ().sub_array (cur_axis_count);
for (unsigned k = 0 ; k < j; k++)
{
signed peak = compiled_peak_coords.arrayZ[k].to_int ();
if (!peak) continue ;
start_coords.arrayZ[k].set_int (hb_min (peak, 0 ));
end_coords.arrayZ[k].set_int (hb_max (peak, 0 ));
}
}
}
if (compiled_interm_coords)
{
start_coords.arrayZ[j].set_float (min_val);
end_coords.arrayZ[j].set_float (max_val);
}
}
j++;
}
return !compiled_peak_coords.in_error () && !compiled_interm_coords.in_error ();
}
/* deltas should be compiled already before we compile tuple
* variation header cause we need to fill in the size of the
* serialized data for this tuple variation */
bool compile_tuple_var_header (const hb_map_t& axes_index_map,
unsigned points_data_length,
const hb_map_t& axes_old_index_tag_map,
const hb_hashmap_t<const hb_vector_t<F2DOT14>*, unsigned >* shared_tuples_idx_map,
hb_alloc_pool_t *pool = nullptr)
{
/* compiled_deltas could be empty after iup delta optimization, we can skip
* compiling this tuple and return true */
if (!compiled_deltas) return true ;
unsigned cur_axis_count = axes_index_map.get_population ();
/* allocate enough memory: 1 peak + 2 intermediate coords + fixed header size */
unsigned alloc_len = 3 * cur_axis_count * (F2DOT14::static_size) + 4 ;
if (unlikely (!compiled_tuple_header.allocate_from_pool (pool, alloc_len, false ))) return false ;
unsigned flag = 0 ;
/* skip the first 4 header bytes: variationDataSize+tupleIndex */
F2DOT14* p = reinterpret_cast <F2DOT14 *> (compiled_tuple_header.begin () + 4 );
F2DOT14* end = reinterpret_cast <F2DOT14 *> (compiled_tuple_header.end ());
hb_array_t<F2DOT14> coords (p, end - p);
if (!shared_tuples_idx_map)
compile_coords (axes_index_map, axes_old_index_tag_map); // non-gvar tuples do not have compiled coords yet
/* encode peak coords */
unsigned peak_count = 0 ;
unsigned *shared_tuple_idx;
if (shared_tuples_idx_map &&
shared_tuples_idx_map->has (&compiled_peak_coords, &shared_tuple_idx))
{
flag = *shared_tuple_idx;
}
else
{
peak_count = encode_peak_coords(coords, flag);
if (!peak_count) return false ;
}
/* encode interim coords, it's optional so returned num could be 0 */
unsigned interim_count = encode_interm_coords (coords.sub_array (peak_count), flag);
/* pointdata length = 0 implies "use shared points" */
if (points_data_length)
flag |= TupleVariationHeader::TupleIndex::PrivatePointNumbers;
unsigned serialized_data_size = points_data_length + compiled_deltas.length;
TupleVariationHeader *o = reinterpret_cast <TupleVariationHeader *> (compiled_tuple_header.begin ());
o->varDataSize = serialized_data_size;
o->tupleIndex = flag;
unsigned total_header_len = 4 + (peak_count + interim_count) * (F2DOT14::static_size);
compiled_tuple_header.shrink_back_to_pool (pool, total_header_len);
return true ;
}
unsigned encode_peak_coords (hb_array_t<F2DOT14> peak_coords,
unsigned & flag) const
{
hb_memcpy (&peak_coords[0 ], &compiled_peak_coords[0 ], compiled_peak_coords.length * sizeof (compiled_peak_coords[0 ]));
flag |= TupleVariationHeader::TupleIndex::EmbeddedPeakTuple;
return compiled_peak_coords.length;
}
/* if no need to encode intermediate coords, then just return p */
unsigned encode_interm_coords (hb_array_t<F2DOT14> coords,
unsigned & flag) const
{
if (compiled_interm_coords)
{
hb_memcpy (&coords[0 ], &compiled_interm_coords[0 ], compiled_interm_coords.length * sizeof (compiled_interm_coords[0 ]));
flag |= TupleVariationHeader::TupleIndex::IntermediateRegion;
}
return compiled_interm_coords.length;
}
bool compile_deltas (hb_vector_t<int > &rounded_deltas_scratch,
hb_alloc_pool_t *pool = nullptr)
{ return compile_deltas (indices, deltas_x, deltas_y, compiled_deltas, rounded_deltas_scratch, pool); }
static bool compile_deltas (hb_array_t<const bool > point_indices,
hb_array_t<const float > x_deltas,
hb_array_t<const float > y_deltas,
hb_vector_t<unsigned char > &compiled_deltas, /* OUT */
hb_vector_t<int > &rounded_deltas, /* scratch */
hb_alloc_pool_t *pool = nullptr)
{
if (unlikely (!rounded_deltas.resize_dirty (point_indices.length)))
return false ;
unsigned j = 0 ;
for (unsigned i = 0 ; i < point_indices.length; i++)
{
if (!point_indices[i]) continue ;
rounded_deltas.arrayZ[j++] = (int ) roundf (x_deltas.arrayZ[i]);
}
rounded_deltas.resize (j);
if (!rounded_deltas) return true ;
/* Allocate enough memory: this is the correct bound:
* Worst case scenario is that each delta has to be encoded in 4 bytes , and there
* are runs of 64 items each . Any delta encoded in less than 4 bytes ( 2 , 1 , or 0 )
* is still smaller than the 4 - byte encoding even with their control byte .
* The initial 2 is to handle length==0, for both x and y deltas. */
unsigned alloc_len = 2 + 4 * rounded_deltas.length + (rounded_deltas.length + 63 ) / 64 ;
if (y_deltas)
alloc_len *= 2 ;
if (unlikely (!compiled_deltas.allocate_from_pool (pool, alloc_len, false ))) return false ;
unsigned encoded_len = compile_deltas (compiled_deltas, rounded_deltas);
if (y_deltas)
{
/* reuse the rounded_deltas vector, check that y_deltas have the same num of deltas as x_deltas */
unsigned j = 0 ;
for (unsigned idx = 0 ; idx < point_indices.length; idx++)
{
if (!point_indices[idx]) continue ;
int rounded_delta = (int ) roundf (y_deltas.arrayZ[idx]);
if (j >= rounded_deltas.length) return false ;
rounded_deltas[j++] = rounded_delta;
}
if (j != rounded_deltas.length) return false ;
encoded_len += compile_deltas (compiled_deltas.as_array ().sub_array (encoded_len), rounded_deltas);
}
compiled_deltas.shrink_back_to_pool (pool, encoded_len);
return true ;
}
static unsigned compile_deltas (hb_array_t<unsigned char > encoded_bytes,
hb_array_t<const int > deltas)
{
return TupleValues::compile_unsafe (deltas, encoded_bytes);
}
bool calc_inferred_deltas (const contour_point_vector_t& orig_points,
hb_vector_t<unsigned > &scratch)
{
unsigned point_count = orig_points.length;
if (point_count != indices.length)
return false ;
unsigned ref_count = 0 ;
hb_vector_t<unsigned > &end_points = scratch.reset ();
for (unsigned i = 0 ; i < point_count; i++)
{
ref_count += indices.arrayZ[i];
if (orig_points.arrayZ[i].is_end_point)
end_points.push (i);
}
/* all points are referenced, nothing to do */
if (ref_count == point_count)
return true ;
if (unlikely (end_points.in_error ())) return false ;
hb_bit_set_t inferred_idxes;
unsigned start_point = 0 ;
for (unsigned end_point : end_points)
{
/* Check the number of unreferenced points in a contour. If no unref points or no ref points, nothing to do. */
unsigned unref_count = 0 ;
for (unsigned i = start_point; i < end_point + 1 ; i++)
unref_count += indices.arrayZ[i];
unref_count = (end_point - start_point + 1 ) - unref_count;
unsigned j = start_point;
if (unref_count == 0 || unref_count > end_point - start_point)
goto no_more_gaps;
for (;;)
{
/* Locate the next gap of unreferenced points between two referenced points prev and next.
* Note that a gap may wrap around at left ( start_point ) and / or at right ( end_point ) .
*/
unsigned int prev, next, i;
for (;;)
{
i = j;
j = next_index (i, start_point, end_point);
if (indices.arrayZ[i] && !indices.arrayZ[j]) break ;
}
prev = j = i;
for (;;)
{
i = j;
j = next_index (i, start_point, end_point);
if (!indices.arrayZ[i] && indices.arrayZ[j]) break ;
}
next = j;
/* Infer deltas for all unref points in the gap between prev and next */
i = prev;
for (;;)
{
i = next_index (i, start_point, end_point);
if (i == next) break ;
deltas_x.arrayZ[i] = infer_delta ((double ) orig_points.arrayZ[i].x,
(double ) orig_points.arrayZ[prev].x,
(double ) orig_points.arrayZ[next].x,
(double ) deltas_x.arrayZ[prev], (double ) deltas_x.arrayZ[next]);
deltas_y.arrayZ[i] = infer_delta ((double ) orig_points.arrayZ[i].y,
(double ) orig_points.arrayZ[prev].y,
(double ) orig_points.arrayZ[next].y,
(double ) deltas_y.arrayZ[prev], (double ) deltas_y.arrayZ[next]);
inferred_idxes.add (i);
if (--unref_count == 0 ) goto no_more_gaps;
}
}
no_more_gaps:
start_point = end_point + 1 ;
}
for (unsigned i = 0 ; i < point_count; i++)
{
/* if points are not referenced and deltas are not inferred, set to 0.
* reference all points for gvar */
if ( !indices[i])
{
if (!inferred_idxes.has (i))
{
deltas_x.arrayZ[i] = 0 .0 ;
deltas_y.arrayZ[i] = 0 .0 ;
}
indices[i] = true ;
}
}
return true ;
}
bool optimize (const contour_point_vector_t& contour_points,
bool is_composite,
optimize_scratch_t &scratch,
double tolerance = 0 .5 + 1 e-10 )
{
unsigned count = contour_points.length;
if (deltas_x.length != count ||
deltas_y.length != count)
return false ;
hb_vector_t<bool > &opt_indices = scratch.opt_indices.reset ();
hb_vector_t<int > &rounded_x_deltas = scratch.rounded_x_deltas;
hb_vector_t<int > &rounded_y_deltas = scratch.rounded_y_deltas;
if (unlikely (!rounded_x_deltas.resize_dirty (count) ||
!rounded_y_deltas.resize_dirty (count)))
return false ;
for (unsigned i = 0 ; i < count; i++)
{
rounded_x_deltas.arrayZ[i] = (int ) roundf (deltas_x.arrayZ[i]);
rounded_y_deltas.arrayZ[i] = (int ) roundf (deltas_y.arrayZ[i]);
}
if (!iup_delta_optimize (contour_points, rounded_x_deltas, rounded_y_deltas, opt_indices, scratch.iup, tolerance))
return false ;
unsigned ref_count = 0 ;
for (bool ref_flag : opt_indices)
ref_count += ref_flag;
if (ref_count == count) return true ;
hb_vector_t<float > &opt_deltas_x = scratch.opt_deltas_x.reset ();
hb_vector_t<float > &opt_deltas_y = scratch.opt_deltas_y.reset ();
bool is_comp_glyph_wo_deltas = (is_composite && ref_count == 0 );
if (is_comp_glyph_wo_deltas)
{
if (unlikely (!opt_deltas_x.resize (count) ||
!opt_deltas_y.resize (count)))
return false ;
opt_indices.arrayZ[0 ] = true ;
for (unsigned i = 1 ; i < count; i++)
opt_indices.arrayZ[i] = false ;
}
hb_vector_t<unsigned char > &opt_point_data = scratch.opt_point_data.reset ();
if (!compile_point_set (opt_indices, opt_point_data))
return false ;
hb_vector_t<unsigned char > &opt_deltas_data = scratch.opt_deltas_data.reset ();
if (!compile_deltas (opt_indices,
is_comp_glyph_wo_deltas ? opt_deltas_x : deltas_x,
is_comp_glyph_wo_deltas ? opt_deltas_y : deltas_y,
opt_deltas_data,
scratch.rounded_deltas))
return false ;
hb_vector_t<unsigned char > &point_data = scratch.point_data.reset ();
if (!compile_point_set (indices, point_data))
return false ;
hb_vector_t<unsigned char > &deltas_data = scratch.deltas_data.reset ();
if (!compile_deltas (indices, deltas_x, deltas_y, deltas_data, scratch.rounded_deltas))
return false ;
if (opt_point_data.length + opt_deltas_data.length < point_data.length + deltas_data.length)
{
indices = std::move (opt_indices);
if (is_comp_glyph_wo_deltas)
{
deltas_x = std::move (opt_deltas_x);
deltas_y = std::move (opt_deltas_y);
}
}
return !indices.in_error () && !deltas_x.in_error () && !deltas_y.in_error ();
}
static bool compile_point_set (const hb_vector_t<bool > &point_indices,
hb_vector_t<unsigned char >& compiled_points /* OUT */)
{
unsigned num_points = 0 ;
for (bool i : point_indices)
if (i) num_points++;
/* when iup optimization is enabled, num of referenced points could be 0 */
if (!num_points) return true ;
unsigned indices_length = point_indices.length;
/* If the points set consists of all points in the glyph, it's encoded with a
* single zero byte */
if (num_points == indices_length)
return compiled_points.resize (1 );
/* allocate enough memories: 2 bytes for count + 3 bytes for each point */
unsigned num_bytes = 2 + 3 *num_points;
if (unlikely (!compiled_points.resize_dirty (num_bytes)))
return false ;
unsigned pos = 0 ;
/* binary data starts with the total number of reference points */
if (num_points < 0 x80)
compiled_points.arrayZ[pos++] = num_points;
else
{
compiled_points.arrayZ[pos++] = ((num_points >> 8 ) | 0 x80);
compiled_points.arrayZ[pos++] = num_points & 0 xFF;
}
const unsigned max_run_length = 0 x7F;
unsigned i = 0 ;
unsigned last_value = 0 ;
unsigned num_encoded = 0 ;
while (i < indices_length && num_encoded < num_points)
{
unsigned run_length = 0 ;
unsigned header_pos = pos;
compiled_points.arrayZ[pos++] = 0 ;
bool use_byte_encoding = false ;
bool new_run = true ;
while (i < indices_length && num_encoded < num_points &&
run_length <= max_run_length)
{
// find out next referenced point index
while (i < indices_length && !point_indices[i])
i++;
if (i >= indices_length) break ;
unsigned cur_value = i;
unsigned delta = cur_value - last_value;
if (new_run)
{
use_byte_encoding = (delta <= 0 xFF);
new_run = false ;
}
if (use_byte_encoding && delta > 0 xFF)
break ;
if (use_byte_encoding)
compiled_points.arrayZ[pos++] = delta;
else
{
compiled_points.arrayZ[pos++] = delta >> 8 ;
compiled_points.arrayZ[pos++] = delta & 0 xFF;
}
i++;
last_value = cur_value;
run_length++;
num_encoded++;
}
if (use_byte_encoding)
compiled_points.arrayZ[header_pos] = run_length - 1 ;
else
compiled_points.arrayZ[header_pos] = (run_length - 1 ) | 0 x80;
}
return compiled_points.resize_dirty (pos);
}
static double infer_delta (double target_val, double prev_val, double next_val, double prev_delta, double next_delta)
{
if (prev_val == next_val)
return (prev_delta == next_delta) ? prev_delta : 0 .0 ;
else if (target_val <= hb_min (prev_val, next_val))
return (prev_val < next_val) ? prev_delta : next_delta;
else if (target_val >= hb_max (prev_val, next_val))
return (prev_val > next_val) ? prev_delta : next_delta;
double r = (target_val - prev_val) / (next_val - prev_val);
return prev_delta + r * (next_delta - prev_delta);
}
static unsigned int next_index (unsigned int i, unsigned int start, unsigned int end)
{ return (i >= end) ? start : (i + 1 ); }
};
template <typename OffType = HBUINT16>
struct TupleVariationData
{
bool sanitize (hb_sanitize_context_t *c) const
{
TRACE_SANITIZE (this );
// here check on min_size only, TupleVariationHeader and var data will be
// checked while accessing through iterator.
return_trace (c->check_struct (this ));
}
size_t get_size (unsigned axis_count_times_2) const
{
unsigned total_size = min_size;
unsigned count = tupleVarCount.get_count ();
const TupleVariationHeader *tuple_var_header = &(get_tuple_var_header());
for (unsigned i = 0 ; i < count; i++)
{
total_size += tuple_var_header->get_size (axis_count_times_2) + tuple_var_header->get_data_size ();
tuple_var_header = &tuple_var_header->get_next (axis_count_times_2);
}
return total_size;
}
const TupleVariationHeader &get_tuple_var_header (void ) const
{ return StructAfter<TupleVariationHeader> (data); }
struct tuple_iterator_t;
struct tuple_variations_t
{
hb_vector_t<tuple_delta_t> tuple_vars;
private :
/* referenced point set->compiled point data map */
hb_hashmap_t<const hb_vector_t<bool >*, hb_vector_t<unsigned char >> point_data_map;
/* referenced point set-> count map, used in finding shared points */
hb_hashmap_t<const hb_vector_t<bool >*, unsigned > point_set_count_map;
/* empty for non-gvar tuples.
* shared_points_bytes is a pointer to some value in the point_data_map ,
* which will be freed during map destruction . Save it for serialization , so
* no need to do find_shared_points () again */
hb_vector_t<unsigned char > *shared_points_bytes = nullptr;
/* total compiled byte size as TupleVariationData format, initialized to 0 */
unsigned compiled_byte_size = 0 ;
bool needs_padding = false ;
/* for gvar iup delta optimization: whether this is a composite glyph */
bool is_composite = false ;
public :
tuple_variations_t () = default ;
tuple_variations_t (const tuple_variations_t&) = delete ;
tuple_variations_t& operator =(const tuple_variations_t&) = delete ;
tuple_variations_t (tuple_variations_t&&) = default ;
tuple_variations_t& operator =(tuple_variations_t&&) = default ;
~tuple_variations_t () = default ;
explicit operator bool () const { return bool (tuple_vars); }
unsigned get_var_count () const
{
unsigned count = 0 ;
/* when iup delta opt is enabled, compiled_deltas could be empty and we
* should skip this tuple */
for (auto & tuple: tuple_vars)
if (tuple.compiled_deltas) count++;
if (shared_points_bytes && shared_points_bytes->length)
count |= TupleVarCount::SharedPointNumbers;
return count;
}
unsigned get_compiled_byte_size () const
{ return compiled_byte_size; }
bool create_from_tuple_var_data (tuple_iterator_t iterator,
unsigned tuple_var_count,
unsigned point_count,
bool is_gvar,
const hb_map_t *axes_old_index_tag_map,
const hb_vector_t<unsigned > &shared_indices,
const hb_array_t<const F2DOT14> shared_tuples,
hb_alloc_pool_t *pool = nullptr,
bool is_composite_glyph = false )
{
hb_vector_t<unsigned > private_indices;
hb_vector_t<int > deltas_x;
hb_vector_t<int > deltas_y;
do
{
const HBUINT8 *p = iterator.get_serialized_data ();
unsigned int length = iterator.current_tuple->get_data_size ();
if (unlikely (!iterator.var_data_bytes.check_range (p, length)))
return false ;
hb_hashmap_t<hb_tag_t, Triple> axis_tuples;
if (!iterator.current_tuple->unpack_axis_tuples (iterator.get_axis_count (), shared_tuples, axes_old_index_tag_map, axis_tuples)
|| axis_tuples.is_empty ())
return false ;
private_indices.reset ();
bool has_private_points = iterator.current_tuple->has_private_points ();
const HBUINT8 *end = p + length;
if (has_private_points &&
!TupleVariationData::decompile_points (p, private_indices, end))
return false ;
const hb_vector_t<unsigned > &indices = has_private_points ? private_indices : shared_indices;
bool apply_to_all = (indices.length == 0 );
unsigned num_deltas = apply_to_all ? point_count : indices.length;
if (unlikely (!deltas_x.resize_dirty (num_deltas) ||
!TupleVariationData::decompile_deltas (p, deltas_x, end)))
return false ;
if (is_gvar)
{
if (unlikely (!deltas_y.resize_dirty (num_deltas) ||
!TupleVariationData::decompile_deltas (p, deltas_y, end)))
return false ;
}
tuple_delta_t var;
var.axis_tuples = std::move (axis_tuples);
if (unlikely (!var.indices.allocate_from_pool (pool, point_count) ||
!var.deltas_x.allocate_from_pool (pool, point_count, false )))
return false ;
if (is_gvar && unlikely (!var.deltas_y.allocate_from_pool (pool, point_count, false )))
return false ;
for (unsigned i = 0 ; i < num_deltas; i++)
{
unsigned idx = apply_to_all ? i : indices[i];
if (idx >= point_count) continue ;
var.indices[idx] = true ;
var.deltas_x[idx] = deltas_x[i];
if (is_gvar)
var.deltas_y[idx] = deltas_y[i];
}
tuple_vars.push (std::move (var));
} while (iterator.move_to_next ());
is_composite = is_composite_glyph;
return true ;
}
bool create_from_item_var_data (const VarData &var_data,
const hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>>& regions,
const hb_map_t& axes_old_index_tag_map,
unsigned & item_count,
const hb_inc_bimap_t* inner_map = nullptr)
{
/* NULL offset, to keep original varidx valid, just return */
if (&var_data == &Null (VarData))
return true ;
unsigned num_regions = var_data.get_region_index_count ();
if (!tuple_vars.alloc (num_regions)) return false ;
item_count = inner_map ? inner_map->get_population () : var_data.get_item_count ();
if (!item_count) return true ;
unsigned row_size = var_data.get_row_size ();
const HBUINT8 *delta_bytes = var_data.get_delta_bytes ();
for (unsigned r = 0 ; r < num_regions; r++)
{
/* In VarData, deltas are organized in rows, convert them into
* column(region) based tuples, resize deltas_x first */
tuple_delta_t tuple;
if (!tuple.deltas_x.resize_dirty (item_count) ||
!tuple.indices.resize_dirty (item_count))
return false ;
for (unsigned i = 0 ; i < item_count; i++)
{
tuple.indices.arrayZ[i] = true ;
tuple.deltas_x.arrayZ[i] = var_data.get_item_delta_fast (inner_map ? inner_map->backward (i) : i,
r, delta_bytes, row_size);
}
unsigned region_index = var_data.get_region_index (r);
if (region_index >= regions.length) return false ;
tuple.axis_tuples = regions.arrayZ[region_index];
tuple_vars.push (std::move (tuple));
}
return !tuple_vars.in_error ();
}
private :
static int _cmp_axis_tag (const void *pa, const void *pb)
{
const hb_tag_t *a = (const hb_tag_t*) pa;
const hb_tag_t *b = (const hb_tag_t*) pb;
return (int )(*a) - (int )(*b);
}
bool change_tuple_variations_axis_limits (const hb_hashmap_t<hb_tag_t, Triple>& normalized_axes_location,
const hb_hashmap_t<hb_tag_t, TripleDistances>& axes_triple_distances,
hb_alloc_pool_t *pool = nullptr)
{
/* sort axis_tag/axis_limits, make result deterministic */
hb_vector_t<hb_tag_t> axis_tags;
if (!axis_tags.alloc (normalized_axes_location.get_population ()))
return false ;
for (auto t : normalized_axes_location.keys ())
axis_tags.push (t);
// Reused vectors for reduced malloc pressure.
rebase_tent_result_scratch_t scratch;
hb_vector_t<tuple_delta_t> out;
axis_tags.qsort (_cmp_axis_tag);
for (auto axis_tag : axis_tags)
{
Triple *axis_limit;
if (!normalized_axes_location.has (axis_tag, &axis_limit))
return false ;
TripleDistances axis_triple_distances{1 .0 , 1 .0 };
if (axes_triple_distances.has (axis_tag))
axis_triple_distances = axes_triple_distances.get (axis_tag);
hb_vector_t<tuple_delta_t> new_vars;
for (tuple_delta_t& var : tuple_vars)
{
// This may move var out.
var.change_tuple_var_axis_limit (axis_tag, *axis_limit, axis_triple_distances, out, scratch, pool);
if (!out) continue ;
unsigned new_len = new_vars.length + out.length;
if (unlikely (!new_vars.alloc (new_len, false )))
return false ;
for (unsigned i = 0 ; i < out.length; i++)
new_vars.push (std::move (out[i]));
}
tuple_vars = std::move (new_vars);
}
return true ;
}
/* merge tuple variations with overlapping tents, if iup delta optimization
* is enabled, add default deltas to contour_points */
bool merge_tuple_variations (contour_point_vector_t* contour_points = nullptr)
{
hb_vector_t<tuple_delta_t> new_vars;
// The pre-allocation is essential for address stability of pointers
// we store in the hashmap.
if (unlikely (!new_vars.alloc (tuple_vars.length)))
return false ;
hb_hashmap_t<const hb_hashmap_t<hb_tag_t, Triple>*, unsigned > m;
for (tuple_delta_t& var : tuple_vars)
{
/* if all axes are pinned, drop the tuple variation */
if (var.axis_tuples.is_empty ())
{
/* if iup_delta_optimize is enabled, add deltas to contour coords */
if (contour_points && !contour_points->add_deltas (var.deltas_x,
var.deltas_y,
var.indices))
return false ;
continue ;
}
unsigned *idx;
if (m.has (&(var.axis_tuples), &idx))
{
new_vars[*idx] += var;
}
else
{
auto *new_var = new_vars.push ();
if (unlikely (new_vars.in_error ()))
return false ;
hb_swap (*new_var, var);
if (unlikely (!m.set (&(new_var->axis_tuples), new_vars.length - 1 )))
return false ;
}
}
m.fini (); // Just in case, since it points into new_vars data.
// Shouldn't be necessary though, since we only move new_vars, not its
// contents.
tuple_vars = std::move (new_vars);
return true ;
}
/* compile all point set and store byte data in a point_set->hb_bytes_t hashmap,
* also update point_set - > count map , which will be used in finding shared
* point set*/
bool compile_all_point_sets ()
{
for (const auto & tuple: tuple_vars)
{
const hb_vector_t<bool >* points_set = &(tuple.indices);
if (point_data_map.has (points_set))
{
unsigned *count;
if (unlikely (!point_set_count_map.has (points_set, &count) ||
!point_set_count_map.set (points_set, (*count) + 1 )))
return false ;
continue ;
}
hb_vector_t<unsigned char > compiled_point_data;
if (!tuple_delta_t::compile_point_set (*points_set, compiled_point_data))
return false ;
if (!point_data_map.set (points_set, std::move (compiled_point_data)) ||
!point_set_count_map.set (points_set, 1 ))
return false ;
}
return true ;
}
/* find shared points set which saves most bytes */
void find_shared_points ()
{
unsigned max_saved_bytes = 0 ;
for (const auto & _ : point_data_map.iter_ref ())
{
const hb_vector_t<bool >* points_set = _.first;
unsigned data_length = _.second.length;
if (!data_length) continue ;
unsigned *count;
if (unlikely (!point_set_count_map.has (points_set, &count) ||
*count <= 1 ))
{
shared_points_bytes = nullptr;
return ;
}
unsigned saved_bytes = data_length * ((*count) -1 );
if (saved_bytes > max_saved_bytes)
{
max_saved_bytes = saved_bytes;
shared_points_bytes = &(_.second);
}
}
}
bool calc_inferred_deltas (const contour_point_vector_t& contour_points,
hb_vector_t<unsigned > &scratch)
{
for (tuple_delta_t& var : tuple_vars)
if (!var.calc_inferred_deltas (contour_points, scratch))
return false ;
return true ;
}
bool iup_optimize (const contour_point_vector_t& contour_points,
optimize_scratch_t &scratch)
{
for (tuple_delta_t& var : tuple_vars)
{
if (!var.optimize (contour_points, is_composite, scratch))
return false ;
}
return true ;
}
public :
bool instantiate (const hb_hashmap_t<hb_tag_t, Triple>& normalized_axes_location,
const hb_hashmap_t<hb_tag_t, TripleDistances>& axes_triple_distances,
optimize_scratch_t &scratch,
hb_alloc_pool_t *pool = nullptr,
contour_point_vector_t* contour_points = nullptr,
bool optimize = false )
{
if (!tuple_vars) return true ;
if (!change_tuple_variations_axis_limits (normalized_axes_location, axes_triple_distances, pool))
return false ;
/* compute inferred deltas only for gvar */
if (contour_points)
{
hb_vector_t<unsigned > scratch;
if (!calc_inferred_deltas (*contour_points, scratch))
return false ;
}
/* if iup delta opt is on, contour_points can't be null */
if (optimize && !contour_points)
return false ;
if (!merge_tuple_variations (optimize ? contour_points : nullptr))
return false ;
if (optimize && !iup_optimize (*contour_points, scratch)) return false ;
return !tuple_vars.in_error ();
}
bool compile_bytes (const hb_map_t& axes_index_map,
const hb_map_t& axes_old_index_tag_map,
bool use_shared_points,
bool is_gvar = false ,
const hb_hashmap_t<const hb_vector_t<F2DOT14>*, unsigned >* shared_tuples_idx_map = nullptr,
hb_alloc_pool_t *pool = nullptr)
{
// return true for empty glyph
if (!tuple_vars)
return true ;
// compile points set and store data in hashmap
if (!compile_all_point_sets ())
return false ;
/* total compiled byte size as TupleVariationData format, initialized to its
* min_size: 4 */
compiled_byte_size += 4 ;
if (use_shared_points)
{
find_shared_points ();
if (shared_points_bytes)
compiled_byte_size += shared_points_bytes->length;
}
hb_vector_t<int > rounded_deltas_scratch;
// compile delta and tuple var header for each tuple variation
for (auto & tuple: tuple_vars)
{
const hb_vector_t<bool >* points_set = &(tuple.indices);
hb_vector_t<unsigned char > *points_data;
if (unlikely (!point_data_map.has (points_set, &points_data)))
return false ;
/* when iup optimization is enabled, num of referenced points could be 0
* and thus the compiled points bytes is empty , we should skip compiling
* this tuple */
if (!points_data->length)
continue ;
if (!tuple.compile_deltas (rounded_deltas_scratch, pool))
return false ;
unsigned points_data_length = (points_data != shared_points_bytes) ? points_data->length : 0 ;
if (!tuple.compile_tuple_var_header (axes_index_map, points_data_length, axes_old_index_tag_map,
shared_tuples_idx_map,
pool))
return false ;
compiled_byte_size += tuple.compiled_tuple_header.length + points_data_length + tuple.compiled_deltas.length;
}
if (is_gvar && (compiled_byte_size % 2 ))
{
needs_padding = true ;
compiled_byte_size += 1 ;
}
return true ;
}
bool serialize_var_headers (hb_serialize_context_t *c, unsigned & total_header_len) const
{
TRACE_SERIALIZE (this );
for (const auto & tuple: tuple_vars)
{
tuple.compiled_tuple_header.as_array ().copy (c);
if (c->in_error ()) return_trace (false );
total_header_len += tuple.compiled_tuple_header.length;
}
return_trace (true );
}
bool serialize_var_data (hb_serialize_context_t *c, bool is_gvar) const
{
TRACE_SERIALIZE (this );
if (is_gvar && shared_points_bytes)
{
hb_ubytes_t s (shared_points_bytes->arrayZ, shared_points_bytes->length);
s.copy (c);
}
for (const auto & tuple: tuple_vars)
{
const hb_vector_t<bool >* points_set = &(tuple.indices);
hb_vector_t<unsigned char > *point_data;
if (!point_data_map.has (points_set, &point_data))
return_trace (false );
if (!is_gvar || point_data != shared_points_bytes)
{
hb_ubytes_t s (point_data->arrayZ, point_data->length);
s.copy (c);
}
tuple.compiled_deltas.as_array ().copy (c);
if (c->in_error ()) return_trace (false );
}
/* padding for gvar */
if (is_gvar && needs_padding)
{
HBUINT8 pad;
pad = 0 ;
if (!c->embed (pad)) return_trace (false );
}
return_trace (true );
}
};
struct tuple_iterator_t
{
unsigned get_axis_count () const { return axis_count; }
void init (hb_bytes_t var_data_bytes_, unsigned int axis_count_, const void *table_base_)
{
var_data_bytes = var_data_bytes_;
var_data = var_data_bytes_.as<TupleVariationData> ();
tuples_left = var_data->tupleVarCount.get_count ();
axis_count = axis_count_;
axis_count_times_2 = axis_count_ * 2 ;
current_tuple = &var_data->get_tuple_var_header ();
data_offset = 0 ;
table_base = table_base_;
}
bool get_shared_indices (hb_vector_t<unsigned int > &shared_indices /* OUT */)
{
if (var_data->has_shared_point_numbers ())
{
const HBUINT8 *base = &(table_base+var_data->data);
const HBUINT8 *p = base;
if (!decompile_points (p, shared_indices, (const HBUINT8 *) (var_data_bytes.arrayZ + var_data_bytes.length))) return false ;
data_offset = p - base;
}
return true ;
}
bool is_valid ()
{
if (unlikely (tuples_left <= 0 ))
return false ;
current_tuple_size = TupleVariationHeader::min_size;
if (unlikely (!var_data_bytes.check_end ((const char *) current_tuple + current_tuple_size)))
return false ;
current_tuple_size = current_tuple->get_size (axis_count_times_2);
if (unlikely (!var_data_bytes.check_end ((const char *) current_tuple + current_tuple_size)))
return false ;
return true ;
}
HB_ALWAYS_INLINE
bool move_to_next ()
{
data_offset += current_tuple->get_data_size ();
current_tuple = &StructAtOffset<TupleVariationHeader> (current_tuple, current_tuple_size);
tuples_left--;
return is_valid ();
}
// TODO: Make it return (sanitized) hb_bytes_t
const HBUINT8 *get_serialized_data () const
{ return &(table_base+var_data->data) + data_offset; }
private :
signed tuples_left;
const TupleVariationData *var_data;
unsigned int axis_count;
unsigned int axis_count_times_2;
unsigned int data_offset;
unsigned int current_tuple_size;
const void *table_base;
public :
hb_bytes_t var_data_bytes;
const TupleVariationHeader *current_tuple;
};
static bool get_tuple_iterator (hb_bytes_t var_data_bytes, unsigned axis_count,
const void *table_base,
hb_vector_t<unsigned int > &shared_indices /* OUT */,
tuple_iterator_t *iterator /* OUT */)
{
iterator->init (var_data_bytes, axis_count, table_base);
if (!iterator->get_shared_indices (shared_indices))
return false ;
return iterator->is_valid ();
}
bool has_shared_point_numbers () const { return tupleVarCount.has_shared_point_numbers (); }
static bool decompile_points (const HBUINT8 *&p /* IN/OUT */,
hb_vector_t<unsigned int > &points /* OUT */,
const HBUINT8 *end)
{
enum packed_point_flag_t
{
POINTS_ARE_WORDS = 0 x80,
POINT_RUN_COUNT_MASK = 0 x7F
};
if (unlikely (p + 1 > end)) return false ;
unsigned count = *p++;
if (count & POINTS_ARE_WORDS)
{
if (unlikely (p + 1 > end)) return false ;
count = ((count & POINT_RUN_COUNT_MASK) << 8 ) | *p++;
}
if (unlikely (!points.resize_dirty (count))) return false ;
unsigned n = 0 ;
unsigned i = 0 ;
while (i < count)
{
if (unlikely (p + 1 > end)) return false ;
unsigned control = *p++;
unsigned run_count = (control & POINT_RUN_COUNT_MASK) + 1 ;
unsigned stop = i + run_count;
if (unlikely (stop > count)) return false ;
if (control & POINTS_ARE_WORDS)
{
if (unlikely (p + run_count * HBUINT16::static_size > end)) return false ;
for (; i < stop; i++)
{
n += *(const HBUINT16 *)p;
points.arrayZ[i] = n;
p += HBUINT16::static_size;
}
}
else
{
if (unlikely (p + run_count > end)) return false ;
for (; i < stop; i++)
{
n += *p++;
points.arrayZ[i] = n;
}
}
}
return true ;
}
template <typename T>
HB_ALWAYS_INLINE
static bool decompile_deltas (const HBUINT8 *&p /* IN/OUT */,
hb_vector_t<T> &deltas /* IN/OUT */,
const HBUINT8 *end,
bool consume_all = false ,
unsigned start = 0 )
{
return TupleValues::decompile (p, deltas, end, consume_all, start);
}
bool has_data () const { return tupleVarCount; }
bool decompile_tuple_variations (unsigned point_count,
bool is_gvar,
tuple_iterator_t iterator,
const hb_map_t *axes_old_index_tag_map,
const hb_vector_t<unsigned > &shared_indices,
const hb_array_t<const F2DOT14> shared_tuples,
tuple_variations_t& tuple_variations, /* OUT */
hb_alloc_pool_t *pool = nullptr,
bool is_composite_glyph = false ) const
{
return tuple_variations.create_from_tuple_var_data (iterator, tupleVarCount,
point_count, is_gvar,
axes_old_index_tag_map,
shared_indices,
shared_tuples,
pool,
is_composite_glyph);
}
bool serialize (hb_serialize_context_t *c,
bool is_gvar,
const tuple_variations_t& tuple_variations) const
{
TRACE_SERIALIZE (this );
/* empty tuple variations, just return and skip serialization. */
if (!tuple_variations) return_trace (true );
auto *out = c->start_embed (this );
if (unlikely (!c->extend_min (out))) return_trace (false );
if (!c->check_assign (out->tupleVarCount, tuple_variations.get_var_count (),
HB_SERIALIZE_ERROR_INT_OVERFLOW)) return_trace (false );
unsigned total_header_len = 0 ;
if (!tuple_variations.serialize_var_headers (c, total_header_len))
return_trace (false );
unsigned data_offset = min_size + total_header_len;
if (!is_gvar) data_offset += 4 ;
if (!c->check_assign (out->data, data_offset, HB_SERIALIZE_ERROR_INT_OVERFLOW)) return_trace (false );
return tuple_variations.serialize_var_data (c, is_gvar);
}
protected :
struct TupleVarCount : HBUINT16
{
friend struct tuple_variations_t;
bool has_shared_point_numbers () const { return ((*this ) & SharedPointNumbers); }
unsigned int get_count () const { return (*this ) & CountMask; }
TupleVarCount& operator = (uint16_t i) { HBUINT16::operator = (i); return *this ; }
explicit operator bool () const { return get_count (); }
protected :
enum Flags
{
SharedPointNumbers= 0 x8000u,
CountMask = 0 x0FFFu
};
public :
DEFINE_SIZE_STATIC (2 );
};
TupleVarCount tupleVarCount; /* A packed field. The high 4 bits are flags, and the
* low 12 bits are the number of tuple variation tables
* for this glyph . The number of tuple variation tables
* can be any number between 1 and 4095 . * /
OffsetTo < HBUINT8 , OffType >
data ; / * Offset from the start of the base table
* to the serialized data . * /
/ * TupleVariationHeader tupleVariationHeaders [ ] * / / * Array of tuple variation headers . * /
public :
DEFINE_SIZE_MIN ( 2 + OffType : : static_size ) ;
} ;
/ / TODO : Move tuple_variations_t to outside of TupleVariationData
using tuple_variations_t = TupleVariationData < HBUINT16 > : : tuple_variations_t ;
struct item_variations_t
{
using region_t = const hb_hashmap_t < hb_tag_t , Triple > * ;
private :
/ * each subtable is decompiled into a tuple_variations_t , in which all tuples
* have the same num of deltas ( rows ) * /
hb_vector_t < tuple_variations_t > vars ;
/ * num of retained rows for each subtable , there ' re 2 cases when var_data is empty :
* 1 . retained item_count is zero
* 2 . regions is empty and item_count is non - zero .
* when converting to tuples , both will be dropped because the tuple is empty ,
* however , we need to retain 2 . as all - zero rows to keep original varidx
* valid , so we need a way to remember the num of rows for each subtable * /
hb_vector_t < unsigned > var_data_num_rows ;
/ * original region list , decompiled from item varstore , used when rebuilding
* region list after instantiation * /
hb_vector_t < hb_hashmap_t < hb_tag_t , Triple > > orig_region_list ;
/ * region list : vector of Regions , maintain the original order for the regions
* that existed before instantiate ( ) , append the new regions at the end .
* Regions are stored in each tuple already , save pointers only .
* When converting back to item varstore , unused regions will be pruned * /
hb_vector_t < region_t > region_list ;
/ * region - > idx map after instantiation and pruning unused regions * /
hb_hashmap_t < region_t , unsigned > region_map ;
/ * all delta rows after instantiation * /
hb_vector_t < hb_vector_t < int > > delta_rows ;
/ * final optimized vector of encoding objects used to assemble the varstore * /
hb_vector_t < delta_row_encoding_t > encodings ;
/ * old varidxes - > new var_idxes map * /
hb_map_t varidx_map ;
/ * has long words * /
bool has_long = false ;
public :
bool has_long_word ( ) const
{ return has_long ; }
const hb_vector_t < region_t > & get_region_list ( ) const
{ return region_list ; }
const hb_vector_t < delta_row_encoding_t > & get_vardata_encodings ( ) const
{ return encodings ; }
const hb_map_t & get_varidx_map ( ) const
{ return varidx_map ; }
bool add_vardata_encoding_for_testing ( hb_vector_t < const hb_vector_t < int > * > & & rows ,
unsigned num_cols )
{
encodings . push ( delta_row_encoding_t ( std : : move ( rows ) , num_cols ) ) ;
return ! encodings . in_error ( ) ;
}
bool compile_varidx_map_for_testing ( const hb_hashmap_t < unsigned , const hb_vector_t < int > * > & front_mapping )
{ return compile_varidx_map ( front_mapping ) ; }
bool instantiate ( const ItemVariationStore & varStore ,
const hb_subset_plan_t * plan ,
bool optimize = true ,
bool use_no_variation_idx = true ,
const hb_array_t < const hb_inc_bimap_t > inner_maps = hb_array_t < const hb_inc_bimap_t > ( ) )
{
if ( ! create_from_item_varstore ( varStore , plan - > axes_old_index_tag_map , inner_maps ) )
return false ;
if ( ! instantiate_tuple_vars ( plan - > axes_location , plan - > axes_triple_distances ) )
return false ;
return as_item_varstore ( optimize , use_no_variation_idx ) ;
}
/ * keep below APIs public only for unit test : test - item - varstore * /
bool create_from_item_varstore ( const ItemVariationStore & varStore ,
const hb_map_t & axes_old_index_tag_map ,
const hb_array_t < const hb_inc_bimap_t > inner_maps = hb_array_t < const hb_inc_bimap_t > ( ) )
{
const VarRegionList & regionList = varStore . get_region_list ( ) ;
if ( ! regionList . get_var_regions ( axes_old_index_tag_map , orig_region_list ) )
return false ;
unsigned num_var_data = varStore . get_sub_table_count ( ) ;
if ( inner_maps & & inner_maps . length ! = num_var_data ) return false ;
if ( ! vars . alloc ( num_var_data ) | |
! var_data_num_rows . alloc ( num_var_data ) ) return false ;
for ( unsigned i = 0 ; i < num_var_data ; i + + )
{
if ( inner_maps & & ! inner_maps . arrayZ [ i ] . get_population ( ) )
continue ;
tuple_variations_t var_data_tuples ;
unsigned item_count = 0 ;
if ( ! var_data_tuples . create_from_item_var_data ( varStore . get_sub_table ( i ) ,
orig_region_list ,
axes_old_index_tag_map ,
item_count ,
inner_maps ? & ( inner_maps . arrayZ [ i ] ) : nullptr ) )
return false ;
var_data_num_rows . push ( item_count ) ;
vars . push ( std : : move ( var_data_tuples ) ) ;
}
return ! vars . in_error ( ) & & ! var_data_num_rows . in_error ( ) & & vars . length = = var_data_num_rows . length ;
}
bool instantiate_tuple_vars ( const hb_hashmap_t < hb_tag_t , Triple > & normalized_axes_location ,
const hb_hashmap_t < hb_tag_t , TripleDistances > & axes_triple_distances )
{
optimize_scratch_t scratch ;
for ( tuple_variations_t & tuple_vars : vars )
if ( ! tuple_vars . instantiate ( normalized_axes_location , axes_triple_distances , scratch ) )
return false ;
if ( ! build_region_list ( ) ) return false ;
return true ;
}
bool build_region_list ( )
{
/ * scan all tuples and collect all unique regions , prune unused regions * /
hb_hashmap_t < region_t , unsigned > all_regions ;
hb_hashmap_t < region_t , unsigned > used_regions ;
/ * use a vector when inserting new regions , make result deterministic * /
hb_vector_t < region_t > all_unique_regions ;
for ( const tuple_variations_t & sub_table : vars )
{
for ( const tuple_delta_t & tuple : sub_table . tuple_vars )
{
region_t r = & ( tuple . axis_tuples ) ;
if ( ! used_regions . has ( r ) )
{
bool all_zeros = true ;
for ( float d : tuple . deltas_x )
{
int delta = ( int ) roundf ( d ) ;
if ( delta ! = 0 )
{
all_zeros = false ;
break ;
}
}
if ( ! all_zeros )
{
if ( ! used_regions . set ( r , 1 ) )
return false ;
}
}
if ( all_regions . has ( r ) )
continue ;
if ( ! all_regions . set ( r , 1 ) )
return false ;
all_unique_regions . push ( r ) ;
}
}
/ * regions are empty means no variation data , return true * /
if ( ! all_regions | | ! all_unique_regions ) return true ;
if ( ! region_list . alloc ( all_regions . get_population ( ) ) )
return false ;
unsigned idx = 0 ;
/ * append the original regions that pre - existed * /
for ( const auto & r : orig_region_list )
{
if ( ! all_regions . has ( & r ) | | ! used_regions . has ( & r ) )
continue ;
region_list . push ( & r ) ;
if ( ! region_map . set ( & r , idx ) )
return false ;
all_regions . del ( & r ) ;
idx + + ;
}
/ * append the new regions at the end * /
for ( const auto & r : all_unique_regions )
{
if ( ! all_regions . has ( r ) | | ! used_regions . has ( r ) )
continue ;
region_list . push ( r ) ;
if ( ! region_map . set ( r , idx ) )
return false ;
all_regions . del ( r ) ;
idx + + ;
}
return ( ! region_list . in_error ( ) ) & & ( ! region_map . in_error ( ) ) ;
}
/ * main algorithm ported from fonttools VarStore_optimize ( ) method , optimize
* varstore by default * /
struct combined_gain_idx_tuple_t
{
uint64_t encoded ;
combined_gain_idx_tuple_t ( ) = default ;
combined_gain_idx_tuple_t ( unsigned gain , unsigned i , unsigned j )
: encoded ( ( uint64_t ( 0 xFFFFFF - gain ) < < 40 ) | ( uint64_t ( i ) < < 20 ) | uint64_t ( j ) )
{
assert ( gain < 0 xFFFFFF ) ;
assert ( i < 0 xFFFFFFF & & j < 0 xFFFFFFF ) ;
}
bool operator < ( const combined_gain_idx_tuple_t & o )
{
return encoded < o . encoded ;
}
bool operator < = ( const combined_gain_idx_tuple_t & o )
{
return encoded < = o . encoded ;
}
unsigned idx_1 ( ) const { return ( encoded > > 20 ) & 0 xFFFFF ; } ;
unsigned idx_2 ( ) const { return encoded & 0 xFFFFF ; } ;
} ;
bool as_item_varstore ( bool optimize = true , bool use_no_variation_idx = true )
{
/ * return true if no variation data * /
if ( ! region_list ) return true ;
unsigned num_cols = region_list . length ;
/ * pre - alloc a 2 D vector for all sub_table ' s VarData rows * /
unsigned total_rows = 0 ;
for ( unsigned major = 0 ; major < var_data_num_rows . length ; major + + )
total_rows + = var_data_num_rows [ major ] ;
if ( ! delta_rows . resize ( total_rows ) ) return false ;
/ * init all rows to [ 0 ] * num_cols * /
for ( unsigned i = 0 ; i < total_rows ; i + + )
if ( ! ( delta_rows [ i ] . resize ( num_cols ) ) ) return false ;
/ * old VarIdxes - > full encoding_row mapping * /
hb_hashmap_t < unsigned , const hb_vector_t < int > * > front_mapping ;
unsigned start_row = 0 ;
hb_vector_t < delta_row_encoding_t > encoding_objs ;
/ * delta_rows map , used for filtering out duplicate rows * /
hb_vector_t < const hb_vector_t < int > * > major_rows ;
hb_hashmap_t < const hb_vector_t < int > * , unsigned > delta_rows_map ;
for ( unsigned major = 0 ; major < vars . length ; major + + )
{
/ * deltas are stored in tuples ( column based ) , convert them back into items
* ( row based ) delta * /
const tuple_variations_t & tuples = vars [ major ] ;
unsigned num_rows = var_data_num_rows [ major ] ;
if ( ! num_rows ) continue ;
for ( const tuple_delta_t & tuple : tuples . tuple_vars )
{
if ( tuple . deltas_x . length ! = num_rows )
return false ;
/ * skip unused regions * /
unsigned * col_idx ;
if ( ! region_map . has ( & ( tuple . axis_tuples ) , & col_idx ) )
continue ;
for ( unsigned i = 0 ; i < num_rows ; i + + )
{
int rounded_delta = roundf ( tuple . deltas_x [ i ] ) ;
delta_rows [ start_row + i ] [ * col_idx ] + = rounded_delta ;
has_long | = rounded_delta < - 65536 | | rounded_delta > 65535 ;
}
}
major_rows . reset ( ) ;
for ( unsigned minor = 0 ; minor < num_rows ; minor + + )
{
const hb_vector_t < int > & row = delta_rows [ start_row + minor ] ;
if ( use_no_variation_idx )
{
bool all_zeros = true ;
for ( int delta : row )
{
if ( delta ! = 0 )
{
all_zeros = false ;
break ;
}
}
if ( all_zeros )
continue ;
}
if ( ! front_mapping . set ( ( major < < 16 ) + minor , & row ) )
return false ;
if ( delta_rows_map . has ( & row ) )
continue ;
delta_rows_map . set ( & row , 1 ) ;
major_rows . push ( & row ) ;
}
if ( major_rows )
encoding_objs . push ( delta_row_encoding_t ( std : : move ( major_rows ) , num_cols ) ) ;
start_row + = num_rows ;
}
/ * return directly if no optimization , maintain original VariationIndex so
* varidx_map would be empty * /
if ( ! optimize )
{
encodings = std : : move ( encoding_objs ) ;
return ! encodings . in_error ( ) ;
}
/ * NOTE : Fonttools instancer always optimizes VarStore from scratch . This
* is too costly for large fonts . So , instead , we retain the encodings of
* the original VarStore , and just try to combine them if possible . This
* is a compromise between optimization and performance and practically
* works very well . * /
/ / This produces slightly smaller results in some cases .
encoding_objs . qsort ( ) ;
/ * main algorithm : repeatedly pick 2 best encodings to combine , and combine them * /
using item_t = hb_priority_queue_t < combined_gain_idx_tuple_t > : : item_t ;
hb_vector_t < item_t > queue_items ;
unsigned num_todos = encoding_objs . length ;
for ( unsigned i = 0 ; i < num_todos ; i + + )
{
for ( unsigned j = i + 1 ; j < num_todos ; j + + )
{
int combining_gain = encoding_objs . arrayZ [ i ] . gain_from_merging ( encoding_objs . arrayZ [ j ] ) ;
if ( combining_gain > 0 )
queue_items . push ( item_t ( combined_gain_idx_tuple_t ( combining_gain , i , j ) , 0 ) ) ;
}
}
hb_priority_queue_t < combined_gain_idx_tuple_t > queue ( std : : move ( queue_items ) ) ;
hb_bit_set_t removed_todo_idxes ;
while ( queue )
{
auto t = queue . pop_minimum ( ) . first ;
unsigned i = t . idx_1 ( ) ;
unsigned j = t . idx_2 ( ) ;
if ( removed_todo_idxes . has ( i ) | | removed_todo_idxes . has ( j ) )
continue ;
delta_row_encoding_t & encoding = encoding_objs . arrayZ [ i ] ;
delta_row_encoding_t & other_encoding = encoding_objs . arrayZ [ j ] ;
removed_todo_idxes . add ( i ) ;
removed_todo_idxes . add ( j ) ;
encoding . merge ( other_encoding ) ;
for ( unsigned idx = 0 ; idx < encoding_objs . length ; idx + + )
{
if ( removed_todo_idxes . has ( idx ) ) continue ;
const delta_row_encoding_t & obj = encoding_objs . arrayZ [ idx ] ;
/ / In the unlikely event that the same encoding exists already , combine it .
if ( obj . width = = encoding . width & & obj . chars = = encoding . chars )
{
/ / This is straight port from fonttools algorithm . I added this branch there
/ / because I thought it can happen . But looks like we never get in here in
/ / practice . I ' m not confident enough to remove it though ; in theory it can
/ / happen . I think it ' s just that our tests are not extensive enough to hit
/ / this path .
for ( const auto & row : obj . items )
encoding . add_row ( row ) ;
removed_todo_idxes . add ( idx ) ;
continue ;
}
int combined_gain = encoding . gain_from_merging ( obj ) ;
if ( combined_gain > 0 )
queue . insert ( combined_gain_idx_tuple_t ( combined_gain , idx , encoding_objs . length ) , 0 ) ;
}
auto moved_encoding = std : : move ( encoding ) ;
encoding_objs . push ( moved_encoding ) ;
}
int num_final_encodings = ( int ) encoding_objs . length - ( int ) removed_todo_idxes . get_population ( ) ;
if ( num_final_encodings < = 0 ) return false ;
if ( ! encodings . alloc ( num_final_encodings ) ) return false ;
for ( unsigned i = 0 ; i < encoding_objs . length ; i + + )
{
if ( removed_todo_idxes . has ( i ) ) continue ;
encodings . push ( std : : move ( encoding_objs . arrayZ [ i ] ) ) ;
}
return compile_varidx_map ( front_mapping ) ;
}
private :
/ * compile varidx_map for one VarData subtable ( index specified by major ) * /
bool compile_varidx_map ( const hb_hashmap_t < unsigned , const hb_vector_t < int > * > & front_mapping )
{
/ * full encoding_row - > new VarIdxes mapping * /
hb_hashmap_t < const hb_vector_t < int > * , unsigned > back_mapping ;
hb_vector_t < delta_row_encoding_t > split_encodings ;
for ( unsigned i = 0 ; i < encodings . length ; i + + )
{
delta_row_encoding_t & encoding = encodings [ i ] ;
/ * just sanity check , this shouldn ' t happen * /
if ( encoding . is_empty ( ) )
return false ;
unsigned num_rows = encoding . items . length ;
unsigned num_cols = encoding . chars . length ;
/ * sort rows , make result deterministic * /
encoding . items . qsort ( _ cmp_row ) ;
for ( unsigned start = 0 ; start < num_rows ; start + = 0 xFFFFu )
{
unsigned chunk_len = hb_min ( num_rows - start , 0 xFFFFu ) ;
hb_vector_t < const hb_vector_t < int > * > rows ;
if ( ! rows . alloc ( chunk_len ) )
return false ;
unsigned major = split_encodings . length ;
for ( unsigned minor = 0 ; minor < chunk_len ; minor + + )
{
const hb_vector_t < int > * row = encoding . items . arrayZ [ start + minor ] ;
rows . push ( row ) ;
if ( ! back_mapping . set ( row , ( major < < 16 ) + minor ) )
return false ;
}
split_encodings . push ( delta_row_encoding_t ( std : : move ( rows ) , num_cols ) ) ;
}
}
encodings = std : : move ( split_encodings ) ;
if ( encodings . in_error ( ) | | back_mapping . in_error ( ) )
return false ;
for ( auto _ : front_mapping . iter ( ) )
{
unsigned old_varidx = _ . first ;
unsigned * new_varidx ;
if ( back_mapping . has ( _ . second , & new_varidx ) )
varidx_map . set ( old_varidx , * new_varidx ) ;
else
varidx_map . set ( old_varidx , HB_OT_LAYOUT_NO_VARIATIONS_INDEX ) ;
}
return ! varidx_map . in_error ( ) ;
}
static int _ cmp_row ( const void * pa , const void * pb )
{
/ * compare pointers of vectors ( const hb_vector_t < int > * ) that represent a row * /
const hb_vector_t < int > * * a = ( const hb_vector_t < int > * * ) pa ;
const hb_vector_t < int > * * b = ( const hb_vector_t < int > * * ) pb ;
for ( unsigned i = 0 ; i < ( * b ) - > length ; i + + )
{
int va = ( * a ) - > arrayZ [ i ] ;
int vb = ( * b ) - > arrayZ [ i ] ;
if ( va ! = vb )
return va < vb ? - 1 : 1 ;
}
return 0 ;
}
} ;
} / * namespace OT * /
# endif / * HB_OT_VAR_COMMON_HH * /
Messung V0.5 in Prozent C=94 H=95 G=94
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.45Angebot
¤
*Eine klare Vorstellung vom Zielzustand
2026-10-11
Neuigkeiten
Aktuelles
Motto des Tages
Open Source Software
Jenseits des Üblichen ....
Besucherstatistik
Statistik
#Sources=1127926
#Domains=2039723