/* * Copyright (c) 2024, Alliance for Open Media. All rights reserved. * * This source code is subject to the terms of the BSD 2 Clause License and * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License * was not distributed with this source code in the LICENSE file, you can * obtain it at www.aomedia.org/license/software. If the Alliance for Open * Media Patent License 1.0 was not distributed with this source code in the * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
*/
#define RETURN_IF_FALSE(A) \ do { \ if (!(A)) { \ returnfalse; \
} \
} while (0)
constexpr int kMaxNumSpatialLayers = 4;
// Removes comments and trailing spaces from the line. void cleanup_line(std::string &line) { // Remove everything after the first '#'.
std::size_t comment_pos = line.find('#'); if (comment_pos != std::string::npos) {
line.resize(comment_pos);
} // Remove spaces at the end of the line. while (!line.empty() && line.back() == ' ') {
line.resize(line.length() - 1);
}
}
// Finds the indentation level of the line, and sets 'has_list_prefix' to true // if the line has a '-' indicating a new item in a list. void get_indent(const std::string &line, int *indent, bool *has_list_prefix) {
*indent = 0;
*has_list_prefix = false; while (
*indent < static_cast<int>(line.length()) &&
(line[*indent] == ' ' || line[*indent] == '\t' || line[*indent] == '-')) { if (line[*indent] == '-') {
*has_list_prefix = true;
}
++(*indent);
}
}
class ParsedValue { public: enumclass Type { kNone, kInteger, kFloatingPoint };
bool ValueAsFloatingPoint(int line_idx, double *v) { if (type_ == Type::kNone) {
fprintf(
stderr, "No value found where floating point value was expected at line %d\n",
line_idx); returnfalse;
}
*v = (type_ == Type::kFloatingPoint) ? double_value_
: static_cast<double>(int_value_); returntrue;
}
template <typename T> bool IntegerValueInRange(int64_t min, int64_t max, int line_idx, T *v) { switch (type_) { case Type::kInteger: if (int_value_ < min || int_value_ > max) {
fprintf(stderr, "Integer value %" PRId64 " out of range [%" PRId64 ", %" PRId64 "] at line %d\n",
int_value_, min, max, line_idx); returnfalse;
}
*v = static_cast<T>(int_value_); returntrue; case Type::kFloatingPoint:
fprintf(stderr, "Floating point value found where integer was expected at line " "%d\n",
line_idx); returnfalse; case Type::kNone: default:
fprintf(stderr, "No value found where integer was expected at line %d\n",
line_idx); returnfalse;
}
}
/* * Parses the next line from the file, skipping empty lines. * Returns false if the end of the file was reached, or if the line was indented * less than 'min_indent', meaning that parsing should go back to the previous * function in the stack. * * 'min_indent' is the minimum indentation expected for the next line. * 'is_list' must be true if the line is allowed to contain list items ('-'). * 'indent' MUST be initialized to -1 before the first call, and is then set to * the indentation of the line. * 'has_list_prefix' is set to true if the line starts a new list item with '-'. * 'line_idx' is set to the index of the last line read. * 'field_name' is set to the field name if the line contains a colon, or to an * empty string otherwise. * 'value' is set to the value on the line if present. * In case of syntax error, 'syntax_error' is set to true and the function * returns false.
*/ bool parse_line(std::ifstream &file, int min_indent, bool is_list, int *indent, bool *has_list_prefix, int *line_idx, std::string *field_name,
ParsedValue *value, bool *syntax_error) {
*field_name = "";
*syntax_error = false;
value->Clear();
std::string line;
std::ifstream::pos_type prev_file_position; constint prev_indent = *indent; while (prev_file_position = file.tellg(), std::getline(file, line)) {
cleanup_line(line);
get_indent(line, indent, has_list_prefix);
line = line.substr(*indent); // skip indentation // If the line is indented less than 'min_indent', it belongs to the outer // object, and parsing should go back to the previous function in the stack. if (!line.empty() &&
(*indent < min_indent || (prev_indent > 0 && *indent < prev_indent))) { // Undo reading the last line. if (!file.seekg(prev_file_position, std::ios::beg)) {
fprintf(stderr, "Failed to seek to previous file position\n");
*syntax_error = true; returnfalse;
} returnfalse;
}
++(*line_idx); if (line.empty()) continue;
if (prev_indent >= 0 && prev_indent != *indent) {
fprintf(stderr, "Error: Bad indentation at line %d\n", *line_idx);
*syntax_error = true; returnfalse;
} if (*has_list_prefix && !is_list) {
fprintf(stderr, "Error: Unexpected list item at line %d\n", *line_idx);
*syntax_error = true; returnfalse;
}
std::string value_str = line;
size_t colon_pos = line.find(':'); if (colon_pos != std::string::npos) {
*field_name = line.substr(0, colon_pos);
value_str = line.substr(colon_pos + 1);
} if (!value_str.empty()) { char *endptr; if (line.find('.') != std::string::npos) {
value->SetFloatingPointValue(strtod(value_str.c_str(), &endptr)); if (*endptr != '\0') {
fprintf(stderr, "Error: Failed to parse floating point value from '%s' at " "line %d\n",
value_str.c_str(), *line_idx);
*syntax_error = true; returnfalse;
}
} else {
value->SetIntegerValue(strtol(value_str.c_str(), &endptr, 10)); if (*endptr != '\0') {
fprintf(stderr, "Error: Failed to parse integer from '%s' at line %d\n",
value_str.c_str(), *line_idx);
*syntax_error = true; returnfalse;
}
}
} returntrue;
} returnfalse; // Reached the end of the file.
}
template <typename T> bool parse_integer_list(std::ifstream &file, int min_indent, int *line_idx,
std::vector<T> *result) { bool has_list_prefix; int indent = -1;
std::string field_name;
ParsedValue value; bool syntax_error; while (parse_line(file, min_indent, /*is_list=*/true, &indent,
&has_list_prefix, line_idx, &field_name, &value,
&syntax_error)) { if (!field_name.empty()) {
fprintf(
stderr, "Error: Unexpected field name '%s' at line %d, expected a number\n",
field_name.c_str(), *line_idx); returnfalse;
} elseif (!has_list_prefix) {
fprintf(stderr, "Error: Missing list prefix '-' at line %d\n", *line_idx); returnfalse;
} else {
T v;
RETURN_IF_FALSE(value.IntegerValueInRange( static_cast<int64_t>(std::numeric_limits<T>::min()), static_cast<int64_t>(std::numeric_limits<T>::max()), *line_idx, &v));
result->push_back(v);
}
} if (syntax_error) returnfalse; returntrue;
}
bool parse_multilayer_layer_alpha(std::ifstream &file, int min_indent, int *line_idx, AlphaInformation *alpha_info) { bool has_list_prefix; int indent = -1;
std::string field_name;
ParsedValue value; bool syntax_error;
*alpha_info = {}; while (parse_line(file, min_indent, /*is_list=*/false, &indent,
&has_list_prefix, line_idx, &field_name, &value,
&syntax_error)) { if (field_name == "alpha_use_idc") {
RETURN_IF_FALSE(value.IntegerValueInRange( /*min=*/0, /*max=*/7, *line_idx, &alpha_info->alpha_use_idc));
} elseif (field_name == "alpha_bit_depth") {
RETURN_IF_FALSE(value.IntegerValueInRange( /*min=*/8, /*max=*/15, *line_idx, &alpha_info->alpha_bit_depth));
} elseif (field_name == "alpha_clip_idc") {
RETURN_IF_FALSE(value.IntegerValueInRange(/*min=*/0, /*max=*/3, *line_idx,
&alpha_info->alpha_clip_idc));
} elseif (field_name == "alpha_incr_flag") {
RETURN_IF_FALSE(value.IntegerValueInRange(/*min=*/0, /*max=*/1, *line_idx,
&alpha_info->alpha_incr_flag));
} elseif (field_name == "alpha_transparent_value") { // At this point we may not have parsed 'alpha_bit_depth' yet, so the // exact range is checked later.
RETURN_IF_FALSE(value.IntegerValueInRange(
std::numeric_limits<uint16_t>::min(),
std::numeric_limits<uint16_t>::max(), *line_idx,
&alpha_info->alpha_transparent_value));
} elseif (field_name == "alpha_opaque_value") { // At this point we may not have parsed 'alpha_bit_depth' yet, so the // exact range is checked later.
RETURN_IF_FALSE(value.IntegerValueInRange(
std::numeric_limits<uint16_t>::min(),
std::numeric_limits<uint16_t>::max(), *line_idx,
&alpha_info->alpha_opaque_value));
} elseif (field_name == "alpha_color_description") {
ColorProperties color;
RETURN_IF_FALSE(parse_color_properties(file, indent, line_idx, &color));
alpha_info->alpha_color_description = value_present(color);
} elseif (field_name == "label_type_id") {
RETURN_IF_FALSE(
parse_integer_list<uint16_t>(file, /*min_indent=*/indent + 1,
line_idx, &alpha_info->label_type_id));
} else {
fprintf(stderr, "Error: Unknown field '%s' at line %d\n",
field_name.c_str(), *line_idx); returnfalse;
}
} if (syntax_error) returnfalse;
// Validation. if (alpha_info->alpha_bit_depth == 0) {
fprintf(stderr, "Error: alpha_bit_depth must be specified (in range [8, 15]) for " "alpha info\n"); returnfalse;
} constint alpha_max = (1 << (alpha_info->alpha_bit_depth + 1)) - 1; if (alpha_info->alpha_transparent_value > alpha_max) {
fprintf(stderr, "Error: alpha_transparent_value %d out of range [0, %d]\n",
alpha_info->alpha_transparent_value, alpha_max); returnfalse;
} if (alpha_info->alpha_opaque_value > alpha_max) {
fprintf(stderr, "Error: alpha_opaque_value %d out of range [0, %d]\n",
alpha_info->alpha_opaque_value, alpha_max); returnfalse;
} if ((!alpha_info->label_type_id.empty()) &&
(alpha_info->alpha_use_idc != ALPHA_SEGMENTATION)) {
fprintf(stderr, "Error: label_type_id can only be set if alpha_use_idc is %d\n",
ALPHA_SEGMENTATION); returnfalse;
} constint alpha_range = (std::abs(alpha_info->alpha_opaque_value -
alpha_info->alpha_transparent_value) +
1); if (!alpha_info->label_type_id.empty() && static_cast<int>(alpha_info->label_type_id.size()) != alpha_range) {
fprintf(stderr, "Error: if present, label_type_id size must be " "equal to the range of alpha values between " "alpha_transparent_value and alpha_opaque_value (expected " "%d values, found %d values)\n",
alpha_range, static_cast<int>(alpha_info->label_type_id.size())); returnfalse;
} if (alpha_info->alpha_color_description.second &&
(alpha_info->alpha_use_idc != ALPHA_STRAIGHT)) {
fprintf(stderr, "Error: alpha_color_description can only be set if alpha_use_idc " "is %d\n",
ALPHA_STRAIGHT); returnfalse;
} returntrue;
}
// Validation. if (depth_info->depth_representation_type == 3 &&
depth_info->depth_nonlinear_precision == 0) {
fprintf(stderr, "Error: depth_nonlinear_precision must be specified (in range [8, " "23]) when " "depth_representation_type is 3\n"); returnfalse;
} if ((depth_info->depth_representation_type == 3) !=
(!depth_info->depth_nonlinear_representation_model.empty())) {
fprintf(stderr, "Error: depth_nonlinear_representation_model must be set if and " "only if depth_representation_type is 3\n"); returnfalse;
} const uint32_t depth_max = (1 << depth_info->depth_nonlinear_precision) - 1; for (uint32_t v : depth_info->depth_nonlinear_representation_model) { if (v > depth_max) {
fprintf(stderr, "Error: depth_nonlinear_representation_model value %d out of " "range [0, %d]\n",
v, depth_max); returnfalse;
}
}
returntrue;
}
bool validate_layer(const LayerMetadata &layer, bool layer_has_alpha, bool layer_has_depth) { if (layer_has_alpha != (layer.layer_type == MULTILAYER_LAYER_TYPE_ALPHA &&
layer.layer_metadata_scope >= SCOPE_GLOBAL)) {
fprintf(stderr, "Error: alpha info must be set if and only if layer_type is " "%d and layer_metadata_scpoe is >= %d\n",
MULTILAYER_LAYER_TYPE_ALPHA, SCOPE_GLOBAL); returnfalse;
} if (layer_has_depth != (layer.layer_type == MULTILAYER_LAYER_TYPE_DEPTH &&
layer.layer_metadata_scope >= SCOPE_GLOBAL)) {
fprintf(stderr, "Error: depth info must be set if and only if layer_type is " "%d and layer_metadata_scpoe is >= %d\n",
MULTILAYER_LAYER_TYPE_DEPTH, SCOPE_GLOBAL); returnfalse;
} returntrue;
}
bool parse_multilayer_layer_metadata(std::ifstream &file, int min_indent, int *line_idx,
std::vector<LayerMetadata> &layers) { bool has_list_prefix; int indent = -1;
std::string field_name;
ParsedValue value; bool syntax_error; bool layer_has_alpha = false; bool layer_has_depth = false; while (parse_line(file, min_indent, /*is_list=*/true, &indent,
&has_list_prefix, line_idx, &field_name, &value,
&syntax_error)) { if (has_list_prefix) { // Start of a new layer. if (layers.size() >= kMaxNumSpatialLayers) {
fprintf(stderr, "Error: Too many layers at line %d, the maximum is %d\n",
*line_idx, kMaxNumSpatialLayers); returnfalse;
}
// Validate the previous layer. if (!layers.empty()) {
validate_layer(layers.back(), layer_has_alpha, layer_has_depth);
} if (layers.size() == 1 && layers.back().layer_color_description.second) {
fprintf(stderr, "Error: layer_color_description cannot be specified for the " "first layer\n"); returnfalse;
}
layers.push_back({});
layer_has_alpha = false;
layer_has_depth = false;
} if (layers.empty()) {
fprintf(stderr, "Error: Missing list prefix '-' at line %d\n", *line_idx); returnfalse;
}
bool validate_multilayer_metadata(const MultilayerMetadata &multilayer) { if (multilayer.layers.empty()) {
fprintf(stderr, "Error: No layers found, there must be at least one\n"); returnfalse;
} if (multilayer.layers.size() > 4) {
fprintf(stderr, "Error: Too many layers, found %d, max 4\n", static_cast<int>(multilayer.layers.size())); returnfalse;
}
for (int i = 0; i < static_cast<int>(multilayer.layers.size()); ++i) { const LayerMetadata &layer = multilayer.layers[i]; switch (multilayer.use_case) { case MULTILAYER_USE_CASE_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_STEREO: case MULTILAYER_USE_CASE_STEREO_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_STEREO_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_444_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_444_GLOBAL_DEPTH: if (layer.layer_metadata_scope != SCOPE_GLOBAL) {
fprintf(
stderr, "Error: for use_case %d, all layers must have scope %d, found %d " "instead for layer %d (zero-based index)\n",
multilayer.use_case, SCOPE_GLOBAL, layer.layer_metadata_scope, i); returnfalse;
} break; default: break;
} switch (multilayer.use_case) { case MULTILAYER_USE_CASE_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_ALPHA: case MULTILAYER_USE_CASE_DEPTH: case MULTILAYER_USE_CASE_444_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_444_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_444: case MULTILAYER_USE_CASE_420_444: if (!same_view_type) {
fprintf(stderr, "Error: for use_case %d, all layers must have the same view " "type, found different view_type for layer %d (zero-based " "index)\n",
multilayer.use_case, i); returnfalse;
} default: break;
} if (layer.layer_type != MULTILAYER_LAYER_TYPE_UNSPECIFIED) switch (multilayer.use_case) { case MULTILAYER_USE_CASE_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_ALPHA: case MULTILAYER_USE_CASE_STEREO_GLOBAL_ALPHA: case MULTILAYER_USE_CASE_STEREO_ALPHA: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE &&
layer.layer_type != MULTILAYER_LAYER_TYPE_ALPHA) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d or " "%d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE,
MULTILAYER_LAYER_TYPE_ALPHA, layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_DEPTH: case MULTILAYER_USE_CASE_STEREO_GLOBAL_DEPTH: case MULTILAYER_USE_CASE_STEREO_DEPTH: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE &&
layer.layer_type != MULTILAYER_LAYER_TYPE_DEPTH) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d or " "%d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE,
MULTILAYER_LAYER_TYPE_DEPTH, layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_STEREO: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d, " "found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE,
layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_444_GLOBAL_ALPHA: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_1 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_2 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_3 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_ALPHA) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d, " "%d, %d, or %d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE_1,
MULTILAYER_LAYER_TYPE_TEXTURE_2,
MULTILAYER_LAYER_TYPE_TEXTURE_3,
MULTILAYER_LAYER_TYPE_ALPHA, layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_444_GLOBAL_DEPTH: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_1 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_2 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_3 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_DEPTH) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d, " "%d, %d, or %d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE_1,
MULTILAYER_LAYER_TYPE_TEXTURE_2,
MULTILAYER_LAYER_TYPE_TEXTURE_3,
MULTILAYER_LAYER_TYPE_DEPTH, layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_444: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_1 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_2 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_3) {
fprintf(
stderr, "Error: for use_case %d, all layers must be of type %d, %d, or " "%d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE_1,
MULTILAYER_LAYER_TYPE_TEXTURE_2,
MULTILAYER_LAYER_TYPE_TEXTURE_3, layer.layer_type, i); returnfalse;
} break; case MULTILAYER_USE_CASE_420_444: if (layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_1 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_2 &&
layer.layer_type != MULTILAYER_LAYER_TYPE_TEXTURE_3) {
fprintf(stderr, "Error: for use_case %d, all layers must be of type %d, " "%d, %d, or %d, found %d for layer %d (zero-based index)\n",
multilayer.use_case, MULTILAYER_LAYER_TYPE_TEXTURE,
MULTILAYER_LAYER_TYPE_TEXTURE_1,
MULTILAYER_LAYER_TYPE_TEXTURE_2,
MULTILAYER_LAYER_TYPE_TEXTURE_3, layer.layer_type, i); returnfalse;
} break; default: break;
} if (layer.layer_dependency_idc >= (1 << i)) {
fprintf(stderr, "Error: layer_dependency_idc of layer %d (zero-based index) must " "be in [0, %d], found %d for layer %d (zero-based index)\n",
i, (1 << i) - 1, layer.layer_dependency_idc, i); returnfalse;
} if ((layer.layer_type == MULTILAYER_LAYER_TYPE_ALPHA ||
layer.layer_type == MULTILAYER_LAYER_TYPE_DEPTH) &&
layer.layer_color_description.second) {
fprintf(stderr, "Error: alpha or depth layers cannot have " "layer_color_description for layer %d (zero-based index)\n",
i); returnfalse;
}
} returntrue;
}
} // namespace
double depth_representation_element_to_double( const DepthRepresentationElement &e) { // Let x be a variable that is computed using four variables s, e, m, and n, // as follows: If e is greater than 0 and less than 127, x is set equal to // (−1)^s*2^(e−31) * (1+m÷2^n). // Otherwise (e is equal to 0), x is set equal to (−1)^s*2^−(30+n)*m. if (e.exponent > 0) { return (e.sign_flag ? -1 : 1) * std::pow(2.0, e.exponent - 31) *
(1 + static_cast<double>(e.mantissa) /
(static_cast<int64_t>(1) << e.mantissa_len));
} else { return (e.sign_flag ? -1 : 1) * e.mantissa *
std::pow(2.0, -30 + e.mantissa_len);
}
}
bool double_to_depth_representation_element( double v, DepthRepresentationElement *element) { constdouble orig = v; if (v == 0.0) {
*element = { 0, 0, 0, 1 }; returntrue;
} constbool sign = v < 0.0; if (sign) {
v = -v;
} int exp = 0; if (v >= 1.0) { while (v >= 2.0) {
++exp;
v /= 2;
}
} else { while (v < 1.0) {
++exp;
v *= 2.0;
}
exp = -exp;
} if ((exp + 31) <= 0 || (exp + 31) > 126) {
fprintf(stderr, "Error: Floating point value %f out of range (too large or too " "small)\n",
orig); returnfalse;
}
assert(v >= 1.0 && v < 2.0);
v -= 1.0;
uint32_t mantissa = 0;
uint8_t mantissa_len = 0;
constexpr uint8_t kMaxMantissaLen = 32; do { constint bit = (v >= 0.5);
mantissa = (mantissa << 1) + bit;
v -= bit * 0.5;
++mantissa_len;
v *= 2.0;
} while (mantissa_len < kMaxMantissaLen && v > 0.0);
*element = { sign, static_cast<uint8_t>(exp + 31), mantissa_len, mantissa }; returntrue;
}
bool parse_multilayer_file(constchar *metadata_path,
MultilayerMetadata *multilayer) {
std::ifstream file(metadata_path); if (!file.is_open()) {
fprintf(stderr, "Error: Failed to open %s\n", metadata_path); returnfalse;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.