diff --git a/.ref/.gitignore b/.ref/.gitignore new file mode 100644 index 0000000000..c2c027fec1 --- /dev/null +++ b/.ref/.gitignore @@ -0,0 +1 @@ +local \ No newline at end of file diff --git a/.ref/css/README.md b/.ref/css/README.md new file mode 100644 index 0000000000..d0155a2075 --- /dev/null +++ b/.ref/css/README.md @@ -0,0 +1,30 @@ +# CSS Spec references + +This directory contains reference materials used to implement a browser-grade CSS cascading module. +The goal is to achieve fully spec-accurate `initial` and `inherited` behavior for every CSS property, matching real-world engines like Chromium Blink. + +## [`css_properties.json5`](./css_properties.json5) + +`css_properties.json5` is a **direct extract of Chromium Blink’s CSS property database**, located at: + +[`third_party/blink/renderer/core/css/css_properties.json5`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/css/css_properties.json5) + +This file is Blink’s _single source of truth_ for all CSS property metadata, including: + +- **Property names** (canonical) +- **Inheritance behavior** (`inherited: true/false`) +- **Initial values** (`default_value: ...`) +- **Type information** (`type_name`, value converter) +- **Longhand/shorthand relationships** +- **Animation/interpolation flags** +- **Parsing behavior flags** +- **Extra property-specific metadata** + +Blink uses this file to generate: + +- `ComputedStyleBase` fields +- `ComputedStyleInitialValues` +- CSS parser logic +- Style builder code + +This means the file embeds the **actual behavior used by Chrome** when resolving CSS cascade and computed style. diff --git a/.ref/css/css_properties.json5 b/.ref/css/css_properties.json5 new file mode 100644 index 0000000000..aaba3d8d5b --- /dev/null +++ b/.ref/css/css_properties.json5 @@ -0,0 +1,9775 @@ +// https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/css/css_properties.json5 +// Copyright 2017 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +{ +// This file specifies all the CSS properties we support and the necessary +// information for our code generation. The various supported arguments +// are described below with example usage + parameters: { + // - alias_for: "other-property" + // Properties specifying alias_for should be virtually identical to the + // properties they alias. Minor parsing differences are allowed as long as + // the CSSValues created are of the same format of the aliased property. + alias_for: { + }, + // - alternative_of: "other-property" + // + // Makes the the property an "alternative" of another property. + // An alternative property has a separate CSSProperty class which (based on + // runtime flags) is used internally in place of the main CSSProperty class. + // This makes it possible to e.g. switch a property definition from a + // longhand to a shorthand at runtime. + // + // When parsing text (e.g. "animation") into a CSSPropertyID, the + // alternative will be chosen if it is enabled. Otherwise, the main + // property will be chosen. + // + // A main property may only have a single alternative property. It is + // however possible to have an alternative of an alternative, in which case + // the chain is followed. In other words, we choose the "innermost" + // alternative that's enabled. + // + // Note that an alternative property ignores any runtime_flag on the main + // property. + alternative_of: { + valid_type: "str", + }, + // - longhands: ["property", "other-property"] + // The property is a shorthand for several other properties. + longhands: { + }, + // - property_methods: ["method1", "method2"] + // List of methods that are implemented in the CSSProperty for this + // property. + property_methods: { + default: [], + valid_type: "list", + valid_values: [ + "ParseSingleValue", + "ParseShorthand", + "CSSValueFromComputedStyleInternal", + "ColorIncludingFallback", + "InitialValue" + ], + }, + // Suppresses code generation for the specified style builder functions. + // This allows us to provide hand-written style builder functions in cases + // where it's needed. + style_builder_custom_functions: { + default: [], + valid_type: "list", + valid_values: [ + "initial", + "inherit", + "value", + ], + }, + // Affects how the style building functions are generated. + // + // Several property groups (e.g. color properties) deviate from the default + // style builder application, yet there are enough of these properties that + // we want to generate code for them rather than having manually written + // style builder functions. + style_builder_template: { + valid_values: [ + "animation", + "auto", + "background_layer", + "border_image", + "color", + "counter", + "empty", + "grid", + // The legacy template means that regular code generation should not be + // be performed, and that the property is hard-coded in + // style_builder_functions.cc.tmpl. + "legacy", + "mask_box", + "mask_layer", + "transition", + "visited_color", + ], + }, + // Additional arguments to 'style_builder_template' may be provided here. + style_builder_template_args: { + default: {}, + valid_type: "dict" + }, + // - is_descriptor + // Whether it is a CSS descriptor. Descriptors define the characteristics of + // an at-rule. E.g. @font-face is an at-rule, and src is a valid descriptor + // for @font-face. Descriptors and CSS properties with the same name are + // handled together in this file. + // TODO(crbug.com/752745): Don't use CSSPropertyID for descriptors. + // - is_property + // Whether it is a CSS property. If this is false then is_descriptor must be + // true. + is_descriptor: { + default: false, + valid_type: "bool", + }, + is_property: { + default: true, + valid_type: "bool", + }, + // - independent + // This property affects only one field on ComputedStyle, and can be set + // directly during inheritance instead of forcing a recalc. + // StyleResolver and StyleAdjuster are not invoked when these properties + // are changed on a parent. Recalcs only happen if at least one + // non-independent inherited property is changed in the parent. + independent: { + default: false, + valid_type: "bool", + }, + // - semi_independent_variable + // This property affects to the {Inherited, NonInherited}Variable data fields so that we + // can assume that the custom properties might not depend on any other property. We can + // handle these properties so that they are excluded from the shared Inherited/NohInherited + // logic, like the Equal and inheritance functions. + semi_independent_variable: { + default: false, + valid_type: "bool", + }, + // - affected_by_all + // The affected_by_all flag indicates whether a change to the CSS property + // "all" affects this property. + // c.f. https://drafts.csswg.org/css-cascade/#all-shorthand + // Descriptors (is_property: false) are never affected by changes to the + // all property. + affected_by_all: { + default: true, + valid_type: "bool", + }, + // - interpolable + // The interpolable flag indicates whether a property can be animated + // smoothly. If this flag is set, the property should also be added to the + // switch statements in CSSPropertyEquality and InterpolationTypesMap. + interpolable: { + default: false, + valid_type: "bool", + }, + // - inherited + // The property will inherit by default if no value is specified, typically + // mentioned in specifications as "Inherited: yes" + inherited: { + default: false, + valid_type: "bool", + }, + // - compositable + // The property can be animated by the compositor + compositable: { + default: false, + valid_type: "bool", + }, + // - computable + // + // Whether or not a property appears on CSSStyleDeclaration. + // + // By default a property is computable if it's all of the following: + // + // - Not an alias + // - A property (as opposed to a descriptor) + // - A longhand + // + // Otherwise the property is (by default) _not_ computable. + // + // If an explicit true/false value is provided, this overrides the default, + // and the property unconditionally becomes computable/not-computable + // according to the value specified. + // + // Internal properties (-internal-*) are never computable, and using this + // flag on internal properties is an error. + computable: { + valid_type: "bool", + }, + // - includes_currentcolor + // + // Whether or not a property's value potentially includes currentcolor. + // + // This is used to determine if we need to invalidate the property when the + // currentcolor is changed. + includes_currentcolor: { + valid_type: "bool", + default: false, + }, + // - runtime_flag + // The name of the flag on RuntimeEnabledFeatures + // (e.g. "CSSOverscrollBehavior") that conditionally enables the + // property. + // This doesn't currently work with alias_for. + runtime_flag: { + valid_type: "str", + }, + // - field_group + // Name of the group that this field belongs to. Fields in the same group + // are stored together as a nested class inside ComputedStyle and + // dynamically allocated on use. + // Leave this out if the field is stored directly on ComputedStyle. + // If you want to auto group this property use: field_group: "*[->subgroup]" + // If you use the auto grouping function check if your property is in + // css_properties_ranking.json5 + // - If yes, only provide: field_group: "*" + // - If no, you can specify a subgroup following the asterisk: + // field_group: "*[->subgroup]" + field_group: { + valid_type: "str", + }, + // - field_size + // Number of bits needed to store this field. + field_size: { + valid_type: "int", + }, + // - field_template + // Affects how the interface to this field is generated. + // TODO(sashab, meade): Remove this once TypedOM types are specified for + // every property, since this value can be inferred from that. + field_template: { + valid_values: [ + // Field is stored as an enum and has a initial/getter/setter/resetter. + // If include_paths is empty, we would also generate the corresponding + // enum definition in ComputedStyleConstants.h. + "keyword", + // Field can take on any subset of values from a list of keywords. + "multi_keyword", + // Semantically equivalent to keyword, but the type is represented as a + // bit flag field as with multi_keyword as a performance optimization + // for matching multiple values. + "bitset_keyword", + // Field stores a primitive value like int/bool. The type is specified + // by type_name. The interface has a initial/getter/setter/resetter. + "primitive", + // Field is stored as a bool, whose default value is false + // and can only be set to true. Has a initial/getter/setter. + "monotonic_flag", + // A derived flag is derived from other information on ComputedStyle. + // It has no setters, and is instead calculated on first access by + // the function specified by 'derived_from'. + // + // Derived flags must be marked as 'mutable', and can not have a + // 'field_group' (i.e. must exist on the top level of ComputedStyle). + // + // See computed_style_extra_fields.json5 for examples of derived flags. + "derived_flag", + // Field has type specified at type_name and has a getter/setter. + // Also has a setter taking an rvalue reference. Cannot be packed. + "external", + // Field is stored as a wrapper_pointer_name to a class. + "pointer", + // Preset "length" for external and Length class + // This preset represents alias templates that will be replace by + // entries in CSSFieldAlias.json5. + "<[a-z]+>" + ], + }, + // - anchor_mode + // Determines whether or not anchor() / anchor-size() queries are allowed + // in the relevant property. + // + // If omitted, no anchor queries are allowed. + // + // See also AnchorScope::Mode. + anchor_mode: { + valid_values: [ + // anchor() / anchor-size() + "left", + "right", + "top", + "bottom", + // anchor-size() + "width", + "height", + ] + }, + // When specified on a property/field, this will generate code within + // ComputedStyleBase::FieldInvalidationDiff to check if the property/field + // has changed, and if so set a flag indicating this. + // + // Example usage: + // if (field_diff & kBorderRadius) { + // diff.SetBorderRadiusChanged(); + // } + // + // The diff can also be used to "guard" against more expensive checks, e.g: + // if ((field_diff & kOutline) && !OutlineVisuallyEqual(other)) { + // return true; + // } + // + // This is to be **only** used within ComputedStyle::VisualInvalidationDiff + // and will generally be more efficient than comparing fields directly. + invalidate: { + default: [], + valid_type: "list", + valid_values: [ + "accent-color", + "background", + "background-color", + "blend-mode", + "border-image", + "border-outline-visited-color", + "border-radius", + "border-shape", + "border-visual", + "border-width", + "clip", + "clip-path", + "color", + "compositing", + "corner-shape", + "currentcolor", + "filter-data", + "gap-decorations", + "has-transform", + "inset", + "layout", + "margin", + "mask", + "opacity", + "outline", + "out-of-flow", + "paint", + "reshape", + "scroll-anchor", + "scrollbar-color", + "scrollbar-style", + "stroke", + "text-decoration", + "transform-data", + "transform-other", + "transform-property", + "visibility", + "visual-overflow", + "z-index", + ], + }, + // Valid for field_template:derived_flag only. This specifies the function + // on ComputedStyle used to calculate the flag. + derived_from: { + valid_type: "str", + }, + // - include_paths: ["path/to/file1.h", "path/to/file2.h"] + // List of files containing the definitions of types in 'type_name'. Each of + // these files will appear as a #include in ComputedStyleBase.h. For + // example, if the type_name is 'Vector', include_paths should be + // ["third_party/blink/renderer/platform/wtf/vector.h", + // "third_party/blink/renderer/platform/wtf/text/wtf_string.h"] + include_paths: { + default: [], + }, + // Name of the pointer type that wraps this field (e.g. scoped_refptr). + wrapper_pointer_name: { + valid_type: "str", + valid_values: ["scoped_refptr", "Member", "std::unique_ptr"], + }, + // - keywords: ["keyword1", "keyword2"] + // This specifies all valid keyword values for the property. + // TODO(sashab): Once all properties are represented here, delete + // CSSValueKeywords.in and use this list instead. + keywords: { + default: [], + }, + // - default_value: "keyword-value" + // This specifies the default value for this field. + // - for keyword fields, this is the initial keyword + // - for other fields, this is a string containg the C++ expression + // that is used to initialise the field. + default_value: { + }, + // Flags which go into CSSOMTypes: + // - typedom_types: ["Keyword", "Type", "OtherType"] + // The property can take types specified in typedom_types for CSS Typed OM. + // - separator + // The property supports a list of values, and when there is more than one, + // it is separated with this character. + typedom_types: { + default: [], + valid_type: "list", + valid_values: [ + "Angle", + "Flex", + "Frequency", + "Keyword", + "Length", + "Number", + "Percentage", + "Position", + "Resolution", + "Time", + "Transform", + "Unparsed", + "Image" + ], + }, + separator: { + valid_values: [",", " ", "/"], + }, + // The remaining arguments are used for the StyleBuilder and allow us to + // succinctly describe how to apply properties. When default handlers are + // not sufficient, we should prefer to use converter, and failing that + // define custom property handlers in CSSProperty subclasses. We should only + // use style_builder_functions.tmpl to define handlers when there are + // multiple properties requiring the same handling, but converter doesn't + // suffice. + // - font + // The default property handlers call into the FontBuilder instead of + // setting values directly onto the ComputedStyle + font: { + default: false, + valid_type: "bool", + }, + // - name_for_methods: "BlendMode" + // Tweaks how we choose defaults for getter, setter, initial and type_name. + // For example, setting this to BlendMode will make us use a setter of + // SetBlendMode. Note that 'name_for_methods' also determines the name + // of the generated field on ComputedStyle. + // - initial + // The static function to invoke on ComputedStyleInitialValues + // or FontBuilder to retrieve the initial value. + // Defaults to e.g. InitialBorderBottomLeft. + // - getter + // The ComputedStyle getter, defaults to e.g. BorderBottomLeft + // - setter + // The ComputedStyle setter, defaults to e.g. GetBorderBottomLeft + // - type_name + // The computed type for the property. Only required for the default value + // application, defaults to e.g. EDisplay + name_for_methods: { + }, + initial: { + }, + getter: { + }, + setter: { + }, + type_name: { + }, + // - computed_style_protected_functions + // + // Any function specified in the list will be generated with protected + // visibility. This is useful if the default-generated getter function is + // typically not what clients want to use. + // + // For example, the Clear getter is protected to force clients to take + // TextDirection into account. + computed_style_protected_functions: { + default: [], + valid_type: "list", + valid_values: ["getter", "setter", "resetter"], + }, + // - computed_style_custom_functions + // + // Any function specified in the list will be generated with protected + // visibility and an "Internal" suffix. A custom accessor (with the suffix- + // less name) must be manually provided on ComputedStyle. This is useful for + // e.g. properties that have special behavior that affects the computed + // value of the property. + // + // For example, the computed value of border-left-width magically becomes + // zero if border-left-style is none or hidden. The generated code can not + // express this, hence a custom one is specified. + // + // Any custom function automatically gets protected visiblity, and therefore + // it is not valid to specify a function as both custom and explicitly + // protected (using computed_style_protected_functions). + computed_style_custom_functions: { + default: [], + valid_type: "list", + valid_values: ["initial", "getter", "setter", "resetter"], + }, + // - converter: "ConvertRadius" + // The StyleBuilder will call the specified function on + // StyleBuilderConverter to convert a CSSValue to an appropriate platform + // value + converter: { + }, + // - logical_property_group: used for properties that depend on writing-mode + // and/or text-direction (e.g. css-logical), and for their physical counterparts. + // Represents the "logical property group" described by css-logical + // (https://drafts.csswg.org/css-logical/#logical-property-group). + logical_property_group: { + // A name identifying the logical property group. All logical and physical + // properties in the same group should have the same name. + // + // In terms of code generation, each value corresponds to 2 functions in + // CSSDirectionAwareResolver. E.g. a value of "foo-bar" would correspond to: + // - CSSDirectionAwareResolver::LogicalFooBarMapping(), containing the + // properties of the group with a flow-relative mapping logic. + // - CSSDirectionAwareResolver::PhysicalFooBarMapping(), containing the + // properties of the group with a physical mapping logic. + name: { + valid_type: "str", + valid_values: ["border", "border-color", "border-radius", + "border-style", "border-width", "contain-intrinsic-size", + "inset", "margin", "max-size", "min-size", "overflow", + "padding", "scroll-margin", "scroll-padding", "size", + "visited-border-color"], + }, + // The name of the mapping function used to convert between equivalent + // logical and physical properties within the same group. Corresponds to + // a function in CSSDirectionAwareResolver. E.g. a value of "baz" + // corresponds to CSSDirectionAwareResolver::ResolveBaz(...). + // + // Also identifies the mapping logic of the group + // (https://drafts.csswg.org/css-logical-1/#mapping-logic) + resolver: { + valid_type: "str", + valid_values: [ + // Mapping logic: flow-relative (logical) + "block", "inline", + "block-start", "block-end", "inline-start", "inline-end", + "start-start", "start-end", "end-start", "end-end", + // Mapping logic: physical + "vertical", "horizontal", + "top", "bottom", "left", "right", + "top-left", "top-right", "bottom-right", "bottom-left", + ], + }, + }, + // - surrogate_for: "other-property" + // + // A surrogate is a property which acts like another property. Unlike an + // alias (which is resolved as parse-time), a surrogate exists alongside + // the original in the parsed rule, and in the cascade. + // + // However, surrogates modify the same fields on ComputedStyle. Examples of + // surrogates are: + // + // * -webkit-writing-mode (surrogate of writing-mode) + // * inline-size (surrogate for width, or height) + // * All css-logical properties in general + // + // Note that for properties that use logical_property_group, + // 'surrogate_for' should not be set, as the mapping is determined at + // run-time (depending og e.g. 'direction'). + surrogate_for: { + valid_type: "str", + }, + // - priority: 1 + // The priority level for computing the property. Properties with the same + // priority level are grouped and computed in alphabetical order. + // Anything above zero are designated "high priority" and done before + // certain operations, like updating fonts. (Most high-priority properties + // are 1; 2 and higher are used only in special circumstances.) This mechanism + // is primarily useful for properties that influence other properties, + // like line-height influencing lh units. Negative values are not used. + priority: { + default: 0, + valid_type: "int", + }, + // - layout_dependent + // The resolved value used for getComputedStyle() depends on layout for this + // property, which means we may need to update layout to return the correct + // value from getComputedStyle(). Setting this to true will override + // IsLayoutDependentProperty() to return true and require a custom + // IsLayoutDependent() which typically checks for LayoutObject existence and + // type. + layout_dependent: { + default: false, + valid_type: "bool", + }, + // - visited_property_for: "other-property" + // CSS properties that are allowed in :visited selectors each have an + // internal "companion" property with the visited value. For privacy reasons + // CSSOM APIs must return computed values as if links aren't visited, but + // for rendering purposes we need the value with the :visited rules applied. + // + // This means that the regular property (e.g. background-color) represents + // the value as seen by CSSOM, and the -internal-visited counterpart (e.g. + // -internal-visited-background-color) represents the same property as seen + // by painting. + visited_property_for: { + valid_type: "str", + }, + // - valid_for_first_letter: true + // + // https://drafts.csswg.org/css-pseudo-4/#first-letter-styling + valid_for_first_letter: { + default: false, + valid_type: "bool", + }, + // - valid_for_first_line: true + // + // https://drafts.csswg.org/css-pseudo-4/#first-line-styling + valid_for_first_line: { + default: false, + valid_type: "bool", + }, + // - valid_for_cue: true + // + // https://w3c.github.io/webvtt/#the-cue-pseudo-element + valid_for_cue: { + default: false, + valid_type: "bool", + }, + // - valid_for_marker: true + // + // https://drafts.csswg.org/css-pseudo-4/#marker-pseudo + valid_for_marker: { + default: false, + valid_type: "bool", + }, + // - valid_for_highlight: true + // + // https://drafts.csswg.org/css-pseudo-4/#highlight-styling + valid_for_highlight: { + default: false, + valid_type: "bool", + }, + // Applicable @page properties and descriptors. + valid_for_page_context: { + default: false, + valid_type: "bool", + }, + // - is_border + // The property, when used by the author, will disable any native + // appearance on UI elements. + is_border: { + default: false, + valid_type: "bool", + }, + // - is_background + // The property, when used by the author, will disable any native + // appearance on UI elements. + is_background: { + default: false, + valid_type: "bool", + }, + // - is_border_radius + // The property, when used by the author, will disable any native + // appearance on UI elements. + is_border_radius: { + default: false, + valid_type: "bool", + }, + // - is_highlight_colors + // The property participates in paired cascade, such that when encountered + // in highlight styles, we make all other highlight color properties default + // to initial, rather than the UA default. + // https://drafts.csswg.org/css-pseudo-4/#highlight-cascade + is_highlight_colors: { + default: false, + valid_type: "bool", + }, + // - is_visited_highlight_colors + // Like the previous one but for visited internal properties. + is_visited_highlight_colors: { + default: false, + valid_type: "bool", + }, + // - is_animation_property + // The property is a longhand of the 'animation' or 'transition' shorthands. + is_animation_property: { + default: false, + valid_type: "bool", + }, + // Whether changing this property is so independent that we can apply + // a change to them incrementally on top of the old style. This happens + // only when inline style is changed. Conceptually, this could be a blocklist, + // but being conservative, we have chosen to make it an allowlist. + // The properties with known issue are explicitly marked as false, + // so changing the default from false to true _should_ have no ill effects, + // but bugs are of course possible. (There is a DCHECK verifying that we + // computed the correct style when this optimization is in effect.) + // + // Since animations can affect pretty much anything else, and we don't + // support their interactions anyway (see CanApplyInlineStyleIncrementally()), + // animation properties are also never marked as supporting incremental style. + // This is verified in validate_property(). + supports_incremental_style: { + default: false, + valid_type: "bool", + }, + // If false, this property is known to cause problems if setting + // it on an element's inline style will cause problems with computing + // that element's style incrementally. + // + // NOTE: Setting false here is probably indicative of a bug. Long-term, + // we should fix all of these and remove the flag. + idempotent: { + default: true, + valid_type: "bool", + }, + // If true, this property will accept a CSSNumericLiteralValue + // (created by a fast-path parser), with no restrictions on range. + // (NaN and infinities will be sent through the normal ParseSingleValue + // path.) A typical case is the properties that can accept an alpha value; + // percent values will take the slow paths, but simple numbers will be sent + // directly through. + accepts_numeric_literal: { + default: false, + valid_type: "bool", + }, + // If true, then the ComputedStyle field for this property overlaps with + // another property. + // + // Overlapping properties are *partially* overlapping, or do otherwise not + // have compatible or interchangeable values with each other. + overlapping: { + default: false, + valid_type: "bool", + }, + // Like 'overlapping', but set on -webkit-prefixed properties that should + // ultimately be removed. + // + // Note that properties that are legacy_overlapping are also overlapping + // (i.e. legacy_overlapping:true implies overlapping:true). + legacy_overlapping: { + default: false, + valid_type: "bool", + }, + // - valid_for_keyframe: true + // + // Whether the property can be used in @keyframes. + // https://www.w3.org/TR/css-animations-1/#typedef-keyframe-block + valid_for_keyframe: { + default: true, + valid_type: "bool", + }, + // Whether the property can be applied to elements. + // See https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style + valid_for_permission_element: { + default: false, + valid_type: "bool", + }, + // Whether the property can be applied to ::permission-icon elements. + // See https://github.com/WICG/PEPC/blob/main/explainer.md#locking-the-pepc-style + valid_for_permission_icon: { + default: false, + valid_type: "bool", + }, + // - valid_for_position_try: true + // + // Whether the property can be used in a @position-try rule + // https://drafts.csswg.org/css-anchor-1/#fallback-rule + valid_for_position_try: { + default: false, + valid_type: "bool", + }, + // - affected_by_zoom: true + // + // Whether or not the computed value of this property is affected by + // the effective zoom factor. Generally, all computed values that contain + // a blink::Length are affected by zoom. + // + // Setting this flag to 'true' will change the inheritance behavior + // (Longhand::ApplyInherit) to effectively "rezoom" the inherited value. + // + // https://github.com/w3c/csswg-drafts/issues/9397 + affected_by_zoom: { + default: false, + valid_type: "bool", + }, + // - highlight_style_comes_from_originating_element: true + // + // ::highlight pseudo styles have different inheritance rules, where almost + // all properties (even non-inherited ones) come from the parent style. + // A couple of styles and fields are different in that they come from the + // originating element's style instead, and those are marked as 'true' here. + highlight_style_comes_from_originating_element: { + default: false, + valid_type: "bool", + }, + }, + // Members in the data objects should appear in the same order as in the + // parameters object above + data: [ + // Properties with StyleBuilder handling + // Animation Priority properties + { + name: "animation-composition", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + keywords: ["replace", "add", "accumulate"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "Composition", + }, + typedom_types: ["Keyword"], + separator: ",", + include_paths: ["third_party/blink/renderer/core/animation/effect_model.h"], + default_value: "EffectModel::kCompositeReplace", + type_name: "EffectModel::CompositeOperation", + valid_for_marker: true, + }, + { + name: "animation-delay", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "DelayStart", + }, + typedom_types: ["Time"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-direction", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + keywords: ["normal", "reverse", "alternate", "alternate-reverse"], + typedom_types: ["Keyword"], + separator: ",", + style_builder_template: "animation", + style_builder_template_args: { + attribute: "Direction", + }, + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-duration", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + separator: ",", + style_builder_template: "animation", + style_builder_template_args: { + attribute: "Duration", + }, + typedom_types: ["Time"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-fill-mode", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "FillMode", + }, + keywords: ["none", "forwards", "backwards", "both"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-iteration-count", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + keywords: ["infinite"], + separator: ",", + style_builder_template: "animation", + style_builder_template_args: { + attribute: "IterationCount", + }, + keywords: ["infinite"], + typedom_types: ["Keyword", "Number"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + // TODO(futhark): Set the TreeScope on CSSAnimationData. + name: "animation-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "Name", + }, + keywords: ["none"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-play-state", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "PlayState", + }, + keywords: ["running", "paused"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-range-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "RangeStart", + }, + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-range-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "RangeEnd", + }, + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-timeline", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "Timeline", + }, + keywords: ["none", "auto"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "animation-timing-function", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimingFunction", + }, + keywords: [ + "linear", + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "jump-both", + "jump-end", + "jump-none", + "jump-start", + "step-start", + "step-end" + ], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "animation-trigger", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "external", + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TriggerAttachments", + }, + keywords: ["none"], + type_name: "HeapVector>", + include_paths: ["third_party/blink/renderer/core/style/style_trigger_attachment.h"], + default_value: "{ nullptr }", + separator: ",", + valid_for_marker: true, + runtime_flag: "AnimationTrigger", + }, + { + name: "timeline-trigger-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "external", + // TODO: maybe define an animation_trigger template? + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerName", + }, + default_value: "nullptr", + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + converter: "ConvertTimelineTriggerName", + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "timeline-trigger-range-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerRangeStart", + }, + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "timeline-trigger-range-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerRangeEnd", + }, + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "timeline-trigger-exit-range-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerExitRangeStart", + }, + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "timeline-trigger-exit-range-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerExitRangeEnd", + }, + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "timeline-trigger-source", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "animation", + style_builder_template_args: { + attribute: "TimelineTriggerSource", + }, + keywords: ["none", "auto"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + runtime_flag: "TimelineTrigger", + }, + { + name: "transition-delay", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "transition", + style_builder_template_args: { + attribute: "DelayStart", + }, + typedom_types: ["Time"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "transition-duration", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + typedom_types: ["Keyword", "Time"], + separator: ",", + style_builder_template: "transition", + style_builder_template_args: { + attribute: "Duration", + }, + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "transition-property", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "transition", + style_builder_template_args: { + attribute: "Property", + }, + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "transition-behavior", + keywords: ["normal", "allow-discrete"], + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + typedom_types: ["Keyword"], + separator: ",", + style_builder_template: "transition", + style_builder_template_args: { + attribute: "Behavior", + }, + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "transition-timing-function", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "transition", + style_builder_template_args: { + attribute: "TimingFunction", + }, + keywords: [ + "linear", + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "jump-both", + "jump-end", + "jump-none", + "jump-start", + "step-start", + "step-end"], + typedom_types: ["Keyword"], + separator: ",", + valid_for_marker: true, + is_animation_property: true, + // Animation properites are never incremental. + supports_incremental_style: false, + }, + // High Priority and all other font properties. + // Other properties can depend upon high priority properties + // (e.g. font-size / ems) + { + name: "color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + // color isn't strictly independent of all other properties; + // it determines currentColor, which in turn can affect the used value of + // other properties (such as border colors, stops in gradients, etc.). + // However, changes to color generally also trigger paint invalidation, + // and paint invalidation resolves the color anew. (For the special case + // of gradient stops, we have logic within ComputedStyle::AdjustDiffForBackgroundVisuallyEqual + // that forces paint invalidation, recomputing the gradient and repainting + // the element.) + independent: true, + field_group: "inherited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kBlack)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + style_builder_custom_functions: ["initial", "inherit", "value"], + priority: 1, + keywords: ["currentcolor"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_highlight: true, + is_highlight_colors: true, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["accent-color", "border-visual", "color", "currentcolor", "outline"], + }, + { + name: "direction", + property_methods: ["CSSValueFromComputedStyleInternal"], + affected_by_all: false, + inherited: true, + field_template: "keyword", + include_paths: ["third_party/blink/renderer/platform/text/text_direction.h"], + keywords: ["ltr", "rtl"], + typedom_types: ["Keyword"], + default_value: "ltr", + type_name: "TextDirection", + style_builder_custom_functions: ["value"], + priority: 1, + valid_for_marker: true, + valid_for_page_context: true, + invalidate: ["reshape"], + }, + { + name: "font-family", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + inherited: true, + font: true, + name_for_methods: "FamilyDescription", + type_name: "FontDescription::FamilyDescription", + style_builder_custom_functions: ["initial", "inherit"], + converter: "ConvertFontFamily", + priority: 1, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_page_context: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-kerning", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "Kerning", + converter: "ConvertFontKerning", + type_name: "FontDescription::Kerning", + priority: 1, + keywords: ["auto", "normal", "none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-optical-sizing", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "FontOpticalSizing", + converter: "ConvertFontOpticalSizing", + type_name: "OpticalSizing", + priority: 1, + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-palette", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + interpolable: true, + inherited: true, + font: true, + converter: "ConvertFontPalette", + type_name: "FontPalette", + priority: 1, + keywords: ["normal", "light", "dark"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + valid_for_page_context: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-size", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + font: true, + name_for_methods: "Size", + getter: "GetSize", + converter: "ConvertFontSize", + priority: 1, + keywords: ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large", "larger", "smaller", "-webkit-xxx-large"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-size-adjust", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + runtime_flag: "CSSFontSizeAdjust", + font: true, + name_for_methods: "SizeAdjust", + converter: "ConvertFontSizeAdjust", + priority: 1, + keywords: ["none", "ex-height", "cap-height", "ch-width", "ic-width", "ic-height", "from-font"], + typedom_types: ["Keyword", "Number"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + valid_for_page_context: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-stretch", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + interpolable: true, + inherited: true, + font: true, + name_for_methods: "Stretch", + converter: "ConvertFontStretch", + priority: 1, + keywords: [ + "normal", "ultra-condensed", "extra-condensed", "condensed", + "semi-condensed", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded" + ], + typedom_types: ["Keyword", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + interpolable: true, + inherited: true, + font: true, + name_for_methods: "Style", + converter: "ConvertFontStyle", + priority: 1, + keywords: ["normal", "italic", "oblique"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-variant-ligatures", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantLigatures", + type_name: "VariantLigatures", + converter: "ConvertFontVariantLigatures", + priority: 1, + keywords: [ + "normal", "none", "common-ligatures", "no-common-ligatures", + "discretionary-ligatures", "no-discretionary-ligatures", + "historical-ligatures", "no-historical-ligatures", "contextual", + "no-contextual" + ], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-variant-caps", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantCaps", + converter: "ConvertFontVariantCaps", + priority: 1, + keywords: [ + "normal", "small-caps", "all-small-caps", "petite-caps", + "all-petite-caps", "unicase", "titling-caps" + ], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-variant-east-asian", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantEastAsian", + converter: "ConvertFontVariantEastAsian", + priority: 1, + keywords: [ + "normal", "jis78", "jis83", "jis90", "jis04", "simplified", + "traditional", "full-width", "proportional-width", "ruby" + ], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-variant-numeric", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantNumeric", + converter: "ConvertFontVariantNumeric", + priority: 1, + keywords: [ + "normal", "lining-nums", "oldstyle-nums", "proportional-nums", + "tabular-nums", "diagonal-fractions", "stacked-fractions", "ordinal", + "slashed-zero" + ], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-variant-alternates", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + type_name: "FontVariantAlternates", + name_for_methods: "FontVariantAlternates", + converter: "ConvertFontVariantAlternates", + priority: 1, + keywords: [ + "normal", + ], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-weight", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + interpolable: true, + inherited: true, + font: true, + name_for_methods: "Weight", + converter: "ConvertFontWeight", + priority: 1, + keywords: ["normal", "bold", "bolder", "lighter"], + typedom_types: ["Keyword", "Number"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "font-synthesis-weight", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "FontSynthesisWeight", + type_name: "FontDescription::FontSynthesisWeight", + priority: 1, + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + }, + { + name: "font-synthesis-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "FontSynthesisStyle", + type_name: "FontDescription::FontSynthesisStyle", + priority: 1, + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + }, + { + name: "font-synthesis-small-caps", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "FontSynthesisSmallCaps", + type_name: "FontDescription::FontSynthesisSmallCaps", + priority: 1, + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + }, + { + name: "font-feature-settings", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + inherited: true, + font: true, + name_for_methods: "FeatureSettings", + converter: "ConvertFontFeatureSettings", + priority: 1, + keywords: ["normal"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + computable: false, + // See comment on font. + supports_incremental_style: false, + valid_for_permission_element: true, + }, + { + name: "font-variation-settings", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + interpolable: true, + inherited: true, + font: true, + name_for_methods: "VariationSettings", + converter: "ConvertFontVariationSettings", + priority: 1, + keywords: ["normal"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + computable: false, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-language-override", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + runtime_flag: "FontLanguageOverride", + priority: 1, + keywords: ["normal"], + typedom_types: ["Keyword"], + converter: "ConvertFontLanguageOverride", + }, + { + name: "font-variant-emoji", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantEmoji", + type_name: "FontDescription::FontVariantEmoji", + converter: "ConvertFontVariantEmoji", + priority: 1, + keywords: ["normal", "text", "emoji", "unicode"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + supports_incremental_style: false, + }, + { + name: "font-variant-position", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + name_for_methods: "VariantPosition", + type_name: "FontDescription::FontVariantPosition", + converter: "ConvertFontVariantPosition", + priority: 1, + keywords: ["normal", "sub", "super"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "-webkit-font-smoothing", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + type_name: "FontSmoothingMode", + priority: 1, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_permission_element: true, + }, + { + name: "forced-color-adjust", + field_group: "*", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + runtime_flag: "ForcedColors", + field_template: "keyword", + // Affects the computed value of color when it is inherited and + // forced-color- adjust is set to preserve-parent-color. + priority: 2, + keywords: ["auto", "none", "preserve-parent-color"], + typedom_types: ["Keyword"], + default_value: "auto", + computable: false, + valid_for_permission_element: true, + highlight_style_comes_from_originating_element: true, + }, + { + name: "field-sizing", + field_group: "visual", + field_template: "keyword", + property_methods: ["CSSValueFromComputedStyleInternal"], + keywords: ["fixed", "content"], + default_value: "fixed", + typedom_types: ["Keyword"], + invalidate: ["layout"], + }, + { + name: "-webkit-locale", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + style_builder_custom_functions: ["value"], + priority: 1, + }, + { + name: "math-depth", + default_value: 0, + field_group: "*", + field_template: "primitive", + inherited: true, + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["value"], + type_name: "short", + typedom_types: ["Number"], + // Affects the computed value of 'font-size', hence needs to happen before + // high-priority properties. + priority: 2, + }, + { + name: "text-orientation", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["sideways", "mixed", "upright"], + typedom_types: ["Keyword"], + default_value: "mixed", + getter: "GetTextOrientation", + style_builder_custom_functions: ["initial", "inherit", "value"], + priority: 1, + computable: false, + invalidate: ["layout", "paint"], + valid_for_marker: true, + }, + { + name: "-webkit-text-orientation", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + type_name: "TextOrientation", + priority: 1, + surrogate_for: "text-orientation", + valid_for_marker: true, + }, + { + name: "writing-mode", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "keyword", + include_paths: ["third_party/blink/renderer/platform/text/writing_mode.h"], + keywords: ["horizontal-tb", "vertical-rl", "vertical-lr", + "sideways-rl", "sideways-lr"], + typedom_types: ["Keyword"], + default_value: "horizontal-tb", + type_name: "WritingMode", + style_builder_custom_functions: ["initial", "inherit", "value"], + priority: 1, + valid_for_page_context: true, + // Incremental code does not call DidChangeWritingMode(), which influences + // the font. + supports_incremental_style: false, + invalidate: ["layout", "paint"], + highlight_style_comes_from_originating_element: true, + }, + { + name: "-webkit-writing-mode", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + type_name: "WritingMode", + priority: 1, + surrogate_for: "writing-mode", + }, + { + name: "text-rendering", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + type_name: "TextRenderingMode", + keywords: ["auto", "optimizespeed", "optimizelegibility", "geometricprecision"], + typedom_types: ["Keyword"], + priority: 1, + valid_for_permission_element: true, + }, + { + name: "zoom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "visual", + field_template: "primitive", + default_value: "1.0", + type_name: "float", + style_builder_custom_functions: ["initial", "inherit", "value"], + priority: 1, + // Setting zoom affects the _EffectiveZoom_, which in turns affects every px value + // stored on ComputedStyle; see CSSToLengthConversionData::ZoomedComputedPixels. + supports_incremental_style: false, + valid_for_permission_element: true, + }, + { + name: "accent-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_auto_color.h"], + type_name: "StyleAutoColor", + computed_style_protected_functions: ["getter"], + keywords: ["auto", "currentcolor"], + typedom_types: ["Keyword"], + converter: "ConvertStyleAutoColor", + default_value: "StyleAutoColor::AutoColor()", + computable: true, + invalidate: ["accent-color"], + includes_currentcolor: true, + }, + { + name: "align-content", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_content_alignment_data.h"], + default_value: "StyleContentAlignmentData(ContentPosition::kNormal, ContentDistributionType::kDefault, OverflowAlignment::kDefault)", + type_name: "StyleContentAlignmentData", + converter: "ConvertContentAlignmentData", + invalidate: ["layout"], + }, + { + name: "align-items", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_self_alignment_data.h"], + default_value: "StyleSelfAlignmentData(ItemPosition::kNormal, OverflowAlignment::kDefault)", + type_name: "StyleSelfAlignmentData", + converter: "ConvertSelfOrDefaultAlignmentData", + invalidate: ["layout"], + }, + { + name: "alignment-baseline", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "svg", + field_template: "keyword", + keywords: ["auto", "baseline", "alphabetic", "ideographic", "middle", + "central", "mathematical", "before-edge", "text-before-edge", + "after-edge", "text-after-edge", "hanging"], + typedom_types: ["Keyword"], + default_value: "auto", + invalidate: ["layout", "paint"], + }, + { + name: "align-self", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_self_alignment_data.h"], + default_value: "StyleSelfAlignmentData(ItemPosition::kAuto, OverflowAlignment::kDefault)", + type_name: "StyleSelfAlignmentData", + converter: "ConvertSelfOrDefaultAlignmentData", + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "anchor-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + include_paths: ["third_party/blink/renderer/core/style/scoped_css_name.h"], + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + default_value: "nullptr", + field_group: "*", + field_template: "external", + converter: "ConvertAnchorName", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "anchor-scope", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + include_paths: ["third_party/blink/renderer/core/style/style_anchor_scope.h"], + type_name: "StyleAnchorScope", + default_value: "StyleAnchorScope()", + field_group: "*", + field_template: "external", + converter: "ConvertAnchorScope", + keywords: ["none", "all"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "aspect-ratio", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + keywords: ["auto"], + default_value: "StyleAspectRatio(EAspectRatioType::kAuto, gfx::SizeF())", + type_name: "StyleAspectRatio", + converter: "ConvertAspectRatio", + include_paths: ["third_party/blink/renderer/core/style/style_aspect_ratio.h"], + computable: false, + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "backdrop-filter", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/filter_operations.h"], + default_value: "FilterOperations()", + type_name: "FilterOperations", + computed_style_custom_functions: ["initial"], + style_builder_custom_functions: ["value"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["compositing"], + includes_currentcolor: true, + }, + { + name: "backface-visibility", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["visible", "hidden"], + typedom_types: ["Keyword"], + default_value: "visible", + invalidate: ["compositing"], + }, + { + name: "background-attachment", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: ["scroll", "fixed", "local"], + typedom_types: ["Keyword"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Attachment", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-blend-mode", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: [ + "normal", "multiply", "screen", "overlay", "darken", "lighten", + "color-dodge", "color-burn", "hard-light", "soft-light", "difference", + "exclusion", "hue", "saturation", "color", "luminosity" + ], + typedom_types: ["Keyword"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "BlendMode", + fill_type_getter: "GetBlendMode", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + is_background: false, + valid_for_page_context: true, + }, + { + name: "background-clip", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: ["border-box", "padding-box", "content-box", "text"], + typedom_types: ["Keyword"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Clip", + }, + style_builder_custom_functions: ["value"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + compositable: true, + field_group: "background", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kTransparent)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialBackgroundColor", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + is_background: true, + is_highlight_colors: true, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["background-color"], + }, + { + name: "background-image", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + keywords: ["auto", "none"], + typedom_types: ["Keyword", "Image"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Image", + fill_type_getter: "GetImage", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + supports_incremental_style: true, + valid_for_page_context: true, + includes_currentcolor: true, + }, + { + name: "background-origin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: ["border-box", "padding-box", "content-box"], + typedom_types: ["Keyword"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Origin", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-position-x", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "PositionX", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + computable: false, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-position-y", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "PositionY", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + computable: false, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-repeat", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Repeat", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-size", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + keywords: ["auto", "cover", "contain"], + typedom_types: ["Keyword", "Length", "Percentage"], + separator: " ", + style_builder_template: "background_layer", + style_builder_template_args: { + fill_type: "Size", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + is_background: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "baseline-shift", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->svgmisc", + field_template: "external", + type_name: "Length", + default_value: "Length::Fixed()", + style_builder_custom_functions: ["inherit", "value"], + keywords: ["baseline", "sub", "super"], + typedom_types: ["Keyword", "Percentage", "Length"], + invalidate: ["layout", "paint"], + }, + { + name: "baseline-source", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + default_value: "auto", + keywords: ["auto", "first", "last"], + typedom_types: ["Keyword"], + invalidate: ["layout"], + }, + { + name: "border-bottom-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-color", + resolver: "bottom", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-visual"], + }, + { + name: "border-bottom-left-radius", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_size.h"], + default_value: "LengthSize(Length::Fixed(0), Length::Fixed(0))", + type_name: "LengthSize", + converter: "ConvertRadius", + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + is_border: true, + is_border_radius: true, + logical_property_group: { + name: "border-radius", + resolver: "bottom-left", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "border-bottom-right-radius", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_size.h"], + default_value: "LengthSize(Length::Fixed(0), Length::Fixed(0))", + type_name: "LengthSize", + converter: "ConvertRadius", + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + is_border: true, + is_border_radius: true, + logical_property_group: { + name: "border-radius", + resolver: "bottom-right", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "border-bottom-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "EBorderStyle", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-style", + resolver: "bottom", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + }, + { + name: "border-bottom-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "external", + keywords: ["thin", "medium", "thick"], + default_value: "3", + typedom_types: ["Keyword", "Length"], + type_name: "int", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertBorderWidth", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-width", + resolver: "bottom", + }, + // Overlaps with -webkit-border-image. + overlapping: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + affected_by_zoom: true, + }, + { + name: "border-collapse", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: ["separate", "collapse"], + typedom_types: ["Keyword"], + default_value: "separate", + invalidate: ["layout", "paint"], + }, + { + name: "border-image-outset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + typedom_types: ["Length", "Number"], + style_builder_template: "border_image", + style_builder_template_args: { + modifier_type: "Outset", + }, + valid_for_first_letter: true, + is_border: true, + // Overlaps with -webkit-border-image. + overlapping: true, + }, + { + name: "border-image-repeat", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + keywords: ["stretch", "repeat", "round", "space"], + typedom_types: ["Keyword"], + style_builder_template: "border_image", + style_builder_template_args: { + modifier_type: "Repeat", + }, + valid_for_first_letter: true, + is_border: true, + // Overlaps with -webkit-border-image. + overlapping: true, + }, + { + name: "border-image-slice", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + typedom_types: ["Number", "Percentage"], + style_builder_template: "border_image", + style_builder_template_args: { + modifier_type: "Slice", + }, + valid_for_first_letter: true, + is_border: true, + // Overlaps with -webkit-border-image. + overlapping: true, + }, + { + name: "border-image-source", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + keywords: ["none"], + typedom_types: ["Keyword", "Image"], + style_builder_custom_functions: ["value"], + valid_for_first_letter: true, + is_border: true, + // Overlaps with -webkit-border-image. + overlapping: true, + includes_currentcolor: true, + }, + { + name: "border-image-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage", "Number"], + style_builder_template: "border_image", + style_builder_template_args: { + modifier_type: "Width", + }, + valid_for_first_letter: true, + is_border: true, + // Overlaps with -webkit-border-image. + overlapping: true, + }, + { + name: "border-left-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-color", + resolver: "left", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-visual"], + }, + { + name: "border-left-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "EBorderStyle", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-style", + resolver: "left", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + }, + { + name: "border-left-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "external", + keywords: ["thin", "medium", "thick"], + default_value: "3", + typedom_types: ["Keyword", "Length"], + type_name: "int", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertBorderWidth", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-width", + resolver: "left", + }, + // Overlaps with -webkit-border-image. + overlapping: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + affected_by_zoom: true, + }, + { + name: "border-right-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-color", + resolver: "right", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-visual"], + }, + { + name: "border-right-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "EBorderStyle", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-style", + resolver: "right", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + }, + { + name: "border-right-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "external", + keywords: ["thin", "medium", "thick"], + default_value: "3", + typedom_types: ["Keyword", "Length"], + type_name: "int", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertBorderWidth", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-width", + resolver: "right", + }, + // Overlaps with -webkit-border-image. + overlapping: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + affected_by_zoom: true, + }, + { + name: "border-top-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-color", + resolver: "top", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-visual"], + }, + { + name: "border-top-left-radius", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_size.h"], + default_value: "LengthSize(Length::Fixed(0), Length::Fixed(0))", + type_name: "LengthSize", + converter: "ConvertRadius", + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + is_border: true, + is_border_radius: true, + logical_property_group: { + name: "border-radius", + resolver: "top-left", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "border-top-right-radius", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_size.h"], + default_value: "LengthSize(Length::Fixed(0), Length::Fixed(0))", + type_name: "LengthSize", + converter: "ConvertRadius", + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + is_border: true, + is_border_radius: true, + logical_property_group: { + name: "border-radius", + resolver: "top-right", + }, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "border-top-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "EBorderStyle", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-style", + resolver: "top", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + }, + { + name: "border-top-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "external", + keywords: ["thin", "medium", "thick"], + default_value: "3", + typedom_types: ["Keyword", "Length"], + type_name: "int", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertBorderWidth", + valid_for_first_letter: true, + is_border: true, + logical_property_group: { + name: "border-width", + resolver: "top", + }, + // Overlaps with -webkit-border-image. + overlapping: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["border-width", "border-visual"], + affected_by_zoom: true, + }, + { + name: "border-shape", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSBorderShape", + interpolable: true, + compositable: false, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_border_shape.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "StyleBorderShape", + converter: "ConvertBorderShape", + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["border-visual", "paint", "visual-overflow", "border-shape"], + }, + { + name: "bottom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "surround", + field_template: "", + keywords: ["auto"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + anchor_mode: "bottom", + logical_property_group: { + name: "inset", + resolver: "bottom", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["inset", "out-of-flow", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "box-decoration-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + keywords: ["slice", "clone"], + default_value: "slice", + invalidate: ["layout", "paint"], + }, + { + name: "box-shadow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/shadow_list.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "ShadowList", + converter: "ConvertShadowList", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_permission_element: true, + invalidate: ["paint", "visual-overflow"], + includes_currentcolor: true, + }, + { + name: "box-sizing", + property_methods: ["CSSValueFromComputedStyleInternal"], + // NOTE: Naturally fits into field_group: "box", but is so commonly set + // that is is better to have it at the root. + field_template: "keyword", + keywords: ["content-box", "border-box"], + typedom_types: ["Keyword"], + default_value: "content-box", + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["layout"], + }, + { + name: "break-after", + property_methods: ["CSSValueFromComputedStyleInternal"], + // Storage for this property also covers these legacy properties: + // page-break-after, -webkit-column-break-after + field_template: "keyword", + field_group: "*", + keywords: [ + "auto", "avoid", "avoid-column", "avoid-page", "column", "left", "page", + "recto", "right", "verso" + ], + typedom_types: ["Keyword"], + default_value: "auto", + type_name: "EBreakBetween", + invalidate: ["layout"], + }, + { + name: "break-before", + property_methods: ["CSSValueFromComputedStyleInternal"], + // Storage for this property also covers these legacy properties: + // page-break-before, -webkit-column-break-before + field_template: "keyword", + field_group: "*", + keywords: [ + "auto", "avoid", "avoid-column", "avoid-page", "column", "left", "page", + "recto", "right", "verso" + ], + typedom_types: ["Keyword"], + default_value: "auto", + type_name: "EBreakBetween", + invalidate: ["layout"], + }, + { + name: "break-inside", + property_methods: ["CSSValueFromComputedStyleInternal"], + // Storage for this property also covers these legacy properties: + // page-break-inside, -webkit-column-break-inside + field_template: "keyword", + field_group: "*", + keywords: ["auto", "avoid", "avoid-column", "avoid-page"], + typedom_types: ["Keyword"], + default_value: "auto", + invalidate: ["layout"], + }, + { + name: "buffered-rendering", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "svg", + field_template: "keyword", + keywords: ["auto", "dynamic", "static"], + default_value: "auto", + }, + { + name: "caption-side", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: ["top", "bottom"], + typedom_types: ["Keyword"], + default_value: "top", + invalidate: ["layout", "paint"], + }, + { + name: "caret-animation", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "keyword", + keywords: ["auto", "manual"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSCaretAnimation", + }, + { + name: "caret-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_auto_color.h"], + default_value: "StyleAutoColor::AutoColor()", + type_name: "StyleAutoColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleAutoColor", + keywords: ["auto", "currentcolor"], + typedom_types: ["Keyword"], + invalidate: ["color"], + includes_currentcolor: true, + }, + { + name: "caret-shape", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "keyword", + keywords: ["auto", "bar", "block", "underscore"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSCaretShape", + }, + { + name: "clear", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + computed_style_protected_functions: ["getter"], + keywords: ["none", "left", "right", "both", "inline-start", "inline-end"], + typedom_types: ["Keyword"], + default_value: "none", + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "clip", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "visual", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_box.h"], + default_value: "LengthBox()", + type_name: "LengthBox", + computed_style_custom_functions: ["setter"], + style_builder_template: "auto", + converter: "ConvertClip", + keywords: ["auto"], + typedom_types: ["Keyword"], + invalidate: ["clip"], + }, + { + name: "clip-path", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/clip_path_operation.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "ClipPathOperation", + computed_style_custom_functions: ["getter", "setter"], + converter: "ConvertClipPath", + keywords: ["border-box", "padding-box", "content-box", "margin-box", "fill-box", "stroke-box", "view-box", "none"], + typedom_types: ["Keyword"], + invalidate: ["clip-path"], + }, + { + name: "clip-rule", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + // TODO(fs): Convert this to a keyword (requires enum massage). + field_template: "primitive", + field_size: 1, + include_paths: ["third_party/blink/renderer/platform/geometry/path_types.h"], + type_name: "WindRule", + keywords: ["nonzero", "evenodd"], + default_value: "RULE_NONZERO", + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "color-interpolation", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + type_name: "EColorInterpolation", + keywords: ["auto", "srgb", "linearrgb"], + default_value: "srgb", + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "color-interpolation-filters", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + type_name: "EColorInterpolation", + keywords: ["auto", "srgb", "linearrgb"], + default_value: "linearrgb", + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "color-rendering", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + keywords: ["auto", "optimizespeed", "optimizequality"], + default_value: "auto", + typedom_types: ["Keyword"], + }, + { + name: "color-scheme", + field_group: "*", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_custom_functions: ["initial", "inherit", "value"], + inherited: true, + include_paths: ["third_party/blink/public/mojom/frame/color_scheme.mojom-blink.h"], + type_name: "Vector", + default_value: "Vector()", + field_template: "external", + computable: false, + // Affects the computed value of 'color', hence needs to happen before + // high-priority properties. + priority: 2, + valid_for_permission_element: true, + }, + { + name: "column-fill", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["balance", "auto"], + default_value: "balance", + getter: "GetColumnFill", + typedom_types: ["Keyword"], + computable: false, + invalidate: ["layout", "paint"], + }, + { + name: "contain", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_size: 6, + field_template: "primitive", + default_value: "kContainsNone", + name_for_methods: "Contain", + type_name: "unsigned", + converter: "ConvertFlags", + keywords: ["none", "strict", "content", "size", "layout", "style", "paint", "inline-size", "block-size", "view-transition"], + typedom_types: ["Keyword"], + computable: false, + invalidate: ["layout"], + }, + { + name: "contain-intrinsic-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_intrinsic_length.h"], + keywords: ["none"], + default_value: "StyleIntrinsicLength()", + type_name: "StyleIntrinsicLength", + converter: "ConvertIntrinsicDimension", + valid_for_permission_element: true, + invalidate: ["layout", "scroll-anchor"], + }, + { + name: "contain-intrinsic-height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_intrinsic_length.h"], + keywords: ["none"], + default_value: "StyleIntrinsicLength()", + type_name: "StyleIntrinsicLength", + converter: "ConvertIntrinsicDimension", + valid_for_permission_element: true, + invalidate: ["layout", "scroll-anchor"], + }, + { + name: "container-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + default_value: "nullptr", + field_group: "*", + field_template: "external", + converter: "ConvertContainerName", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + }, + { + name: "container-type", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: ["normal", "inline-size", "size", "scroll-state", "anchored"], + field_group: "*", + field_size: 4, + field_template: "primitive", + default_value: "kContainerTypeNormal", + type_name: "unsigned", + converter: "ConvertFlags", + typedom_types: ["Keyword"], + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "content", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/content_data.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + separator: ",", + type_name: "ContentData", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["initial", "inherit", "value"], + valid_for_marker: true, + valid_for_page_context: true, + supports_incremental_style: true, + }, + { + name: "corner-bottom-left-shape", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + default_value: "Superellipse::Round()", + include_paths: ["third_party/blink/renderer/core/style/superellipse.h"], + keywords: ["notch", "scoop", "bevel", "round", "squircle", "square"], + type_name: "Superellipse", + converter: "ConvertCornerShape", + valid_for_first_letter: true, + logical_property_group: { + name: "corner-shape", + resolver: "bottom-left", + }, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "corner-bottom-right-shape", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + default_value: "Superellipse::Round()", + include_paths: ["third_party/blink/renderer/core/style/superellipse.h"], + keywords: ["notch", "scoop", "bevel", "round", "squircle", "square"], + type_name: "Superellipse", + converter: "ConvertCornerShape", + valid_for_first_letter: true, + logical_property_group: { + name: "corner-shape", + resolver: "bottom-right", + }, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "corner-top-left-shape", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + default_value: "Superellipse::Round()", + include_paths: ["third_party/blink/renderer/core/style/superellipse.h"], + keywords: ["notch", "scoop", "bevel", "round", "squircle", "square"], + type_name: "Superellipse", + converter: "ConvertCornerShape", + valid_for_first_letter: true, + logical_property_group: { + name: "corner-shape", + resolver: "top-left", + }, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "corner-top-right-shape", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "surround", + field_template: "external", + default_value: "Superellipse::Round()", + include_paths: ["third_party/blink/renderer/core/style/superellipse.h"], + keywords: ["notch", "scoop", "bevel", "round", "squircle", "square"], + type_name: "Superellipse", + converter: "ConvertCornerShape", + valid_for_first_letter: true, + logical_property_group: { + name: "corner-shape", + resolver: "top-right", + }, + valid_for_page_context: true, + invalidate: ["border-radius", "paint"], + }, + { + name: "counter-increment", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "counter", + style_builder_template_args: { + action: "Increment", + }, + keywords: ["none"], + typedom_types: ["Keyword"], + computable: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "counter-reset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "counter", + style_builder_template_args: { + action: "Reset", + }, + keywords: ["none"], + typedom_types: ["Keyword"], + computable: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "counter-set", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "counter", + style_builder_template_args: { + action: "Set", + }, + keywords: ["none"], + typedom_types: ["Keyword"], + computable: false, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "cursor", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "inherited", + inherited: true, + independent: true, + field_template: "keyword", + keywords: [ + "auto", "default", "none", "context-menu", "help", "pointer", + "progress", "wait", "cell", "crosshair", "text", "vertical-text", + "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", + "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", + "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", + "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", + "zoom-out", "grab", "grabbing" + ], + default_value: "auto", + style_builder_custom_functions: ["initial", "inherit", "value"], + typedom_types: ["Keyword"], + valid_for_marker: true, + valid_for_permission_element: true, + }, + { + name: "cx", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Fixed()", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "cy", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Fixed()", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "d", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/style_path.h"], + wrapper_pointer_name: "Member", + type_name: "StylePath", + default_value: "nullptr", + converter: "ConvertPathOrNone", + keywords: ["none"], + typedom_types: ["Keyword"], + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "display", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + keywords: [ + "inline", "block", "list-item", "inline-block", "table", "inline-table", + "table-row-group", "table-header-group", "table-footer-group", + "table-row", "table-column-group", "table-column", "table-cell", + "table-caption", "-webkit-box", "-webkit-inline-box", "flex", + "inline-flex", "grid", "inline-grid", "contents", "flow-root", "none", + "flow", "math", "ruby", "ruby-text", "grid-lanes", "inline-grid-lanes" + ], + typedom_types: ["Keyword"], + style_builder_custom_functions: ["initial", "inherit", "value"], + // In general many things are tweaked after-the-fact based on display/float/position + // (e.g. OriginalDisplay is based on display, and setting float can cause blockification), + // so we turn off incremental style for all them all. + supports_incremental_style: false, + valid_for_permission_element: true, + valid_for_permission_icon: true, + invalidate: ["layout", "paint"], + }, + { + name: "dominant-baseline", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + keywords: ["auto", "alphabetic", "ideographic", "middle", "central", "mathematical", "hanging", + "use-script", "no-change", "reset-size", "text-after-edge", "text-before-edge"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "empty-cells", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["show", "hide"], + typedom_types: ["Keyword"], + default_value: "show", + invalidate: ["layout", "paint"], + }, + { + name: "fill", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + includes_currentcolor: true, + field_group: "svginherited->fill", + field_template: "external", + type_name: "SVGPaint", + include_paths: ["third_party/blink/renderer/core/style/svg_paint.h"], + default_value: "SVGPaint::CreateInitialBlack()", + name_for_methods: "FillPaint", + converter: "ConvertSVGPaint", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialFillPaint", + }, + style_builder_custom_functions: ["value"], + valid_for_highlight: true, + valid_for_permission_icon: true, + invalidate: ["paint"], + }, + { + name: "fill-opacity", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->fill", + field_template: "primitive", + type_name: "float", + default_value: "1", + converter: "ConvertAlpha", + typedom_types: ["Number"], + accepts_numeric_literal: true, + invalidate: ["paint"], + }, + { + name: "fill-rule", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + // TODO(fs): Convert this to a keyword (requires enum massage). + field_template: "primitive", + field_size: 1, + include_paths: ["third_party/blink/renderer/platform/geometry/path_types.h"], + type_name: "WindRule", + keywords: ["nonzero", "evenodd"], + default_value: "RULE_NONZERO", + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "filter", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/filter_operations.h"], + default_value: "FilterOperations()", + type_name: "FilterOperations", + computed_style_custom_functions: ["initial"], + style_builder_custom_functions: ["value"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["filter-data"], + includes_currentcolor: true, + }, + { + name: "flex-basis", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "", + default_value: "Length::Auto()", + converter: "ConvertLengthSizing", + typedom_types: ["Keyword", "Length", "Percentage"], + keywords: ["auto", "fit-content", "min-content", "max-content", "content"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "flex-direction", + property_methods: ["CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "keyword", + typedom_types: ["Keyword"], + keywords: ["row", "row-reverse", "column", "column-reverse"], + default_value: "row", + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "flex-grow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + typedom_types: ["Number"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "flex-shrink", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "1.0f", + type_name: "float", + typedom_types: ["Number"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "flex-wrap", + property_methods: ["InitialValue", "ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_flex_wrap_data.h"], + default_value: "StyleFlexWrapData(FlexWrapMode::kNowrap)", + type_name: "StyleFlexWrapData", + converter: "ConvertFlexWrapData", + typedom_types: ["Keyword"], + keywords: ["nowrap", "wrap", "wrap-reverse", "balance"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "float", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + computed_style_protected_functions: ["getter"], + keywords: ["none", "left", "right", "inline-start", "inline-end"], + typedom_types: ["Keyword"], + default_value: "none", + name_for_methods: "Floating", + type_name: "EFloat", + valid_for_first_letter: true, + // See comment on display. + supports_incremental_style: false, + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "flood-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "svg->svgmisc", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kBlack)", + type_name: "StyleColor", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialFloodColor", + }, + converter: "ConvertStyleColor", + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + supports_incremental_style: true, + invalidate: ["paint"], + }, + { + name: "flood-opacity", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->svgmisc", + field_template: "primitive", + type_name: "float", + default_value: "1", + converter: "ConvertAlpha", + typedom_types: ["Number"], + supports_incremental_style: true, + accepts_numeric_literal: true, + invalidate: ["paint"], + }, + { + name: "grid-auto-columns", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_track_list.h"], + default_value: "GridTrackList(GridTrackSize(Length::Auto()))", + type_name: "GridTrackList", + converter: "ConvertGridTrackSizeList", + keywords: ["auto", "min-content", "max-content"], + typedom_types: ["Keyword", "Length", "Percentage", "Flex"], + separator: " ", + invalidate: ["layout", "paint"], + }, + { + name: "grid-auto-flow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_size: 4, // FIXME: Make this use "kGridAutoFlowBits". + field_template: "primitive", + default_value: "kAutoFlowRow", + type_name: "GridAutoFlow", + computed_style_custom_functions: ["getter"], + converter: "ConvertGridAutoFlow", + keywords: ["row", "column"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "grid-auto-rows", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_track_list.h"], + default_value: "GridTrackList(GridTrackSize(Length::Auto()))", + type_name: "GridTrackList", + converter: "ConvertGridTrackSizeList", + keywords: ["auto", "min-content", "max-content"], + typedom_types: ["Keyword", "Length", "Percentage", "Flex"], + separator: " ", + invalidate: ["layout", "paint"], + }, + { + name: "grid-column-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_position.h"], + default_value: "GridPosition()", + type_name: "GridPosition", + keywords: ["auto"], + typedom_types: ["Keyword"], + converter: "ConvertGridPosition", + invalidate: ["layout", "paint"], + }, + { + name: "grid-column-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_position.h"], + default_value: "GridPosition()", + type_name: "GridPosition", + keywords: ["auto"], + typedom_types: ["Keyword"], + converter: "ConvertGridPosition", + invalidate: ["layout", "paint"], + }, + { + name: "grid-lanes-fill", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["normal", "reverse"], + typedom_types: ["Keyword"], + default_value: "normal", + invalidate: ["layout", "paint"], + runtime_flag: "CSSMasonryLayout", + }, + { + name: "grid-row-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_position.h"], + default_value: "GridPosition()", + type_name: "GridPosition", + keywords: ["auto"], + typedom_types: ["Keyword"], + converter: "ConvertGridPosition", + invalidate: ["layout", "paint"], + }, + { + name: "grid-row-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/grid_position.h"], + default_value: "GridPosition()", + type_name: "GridPosition", + keywords: ["auto"], + typedom_types: ["Keyword"], + converter: "ConvertGridPosition", + invalidate: ["layout", "paint"], + }, + { + name: "grid-template-areas", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/computed_grid_template_areas.h"], + type_name: "ComputedGridTemplateAreas", + wrapper_pointer_name: "Member", + default_value: "nullptr", + keywords: ["none"], + typedom_types: ["Keyword"], + converter: "ConvertGridTemplateAreas", + invalidate: ["layout", "paint"], + }, + { + name: "grid-template-columns", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/computed_grid_track_list.h"], + interpolable: true, + default_value: "nullptr", + wrapper_pointer_name: "Member", + type_name: "ComputedGridTrackList", + converter: "ConvertGridTrackList", + getter: "SpecifiedGridTemplateColumns", + style_builder_custom_functions: ["inherit"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "grid-template-rows", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/computed_grid_track_list.h"], + interpolable: true, + default_value: "nullptr", + wrapper_pointer_name: "Member", + type_name: "ComputedGridTrackList", + converter: "ConvertGridTrackList", + getter: "SpecifiedGridTemplateRows", + style_builder_custom_functions: ["inherit"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + keywords: ["auto", "fit-content", "min-content", "max-content"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthSizing", + anchor_mode: "height", + logical_property_group: { + name: "size", + resolver: "vertical", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "hyphenate-limit-chars", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "external", + keywords: ["auto"], + type_name: "StyleHyphenateLimitChars", + default_value: "StyleHyphenateLimitChars()", + include_paths: ["third_party/blink/renderer/core/style/style_hyphenate_limit_chars.h"], + converter: "ConvertHyphenateLimitChars", + invalidate: ["layout", "paint"], + }, + { + name: "interest-delay", + runtime_flag: "HTMLInterestForAttribute", + longhands: [ + "interest-delay-start", "interest-delay-end" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: true, + }, + { + name: "interest-delay-start", + runtime_flag: "HTMLInterestForAttribute", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + type_name: "StyleInterestDelay", + default_value: "StyleInterestDelay()", + interpolable: true, + include_paths: ["third_party/blink/renderer/core/style/style_interest_delay.h"], + converter: "ConvertInterestDelayValue", + supports_incremental_style: true, + }, + { + runtime_flag: "HTMLInterestForAttribute", + name: "interest-delay-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + type_name: "StyleInterestDelay", + default_value: "StyleInterestDelay()", + interpolable: true, + include_paths: ["third_party/blink/renderer/core/style/style_interest_delay.h"], + converter: "ConvertInterestDelayValue", + supports_incremental_style: true, + }, + { + name: "hyphens", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["none", "manual", "auto"], + default_value: "manual", + type_name: "Hyphens", + typedom_types: ["Keyword"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "image-rendering", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: [ + "auto", "optimizespeed", "optimizequality", + "-webkit-optimize-contrast", "pixelated" + ], + typedom_types: ["Keyword"], + default_value: "auto", + invalidate: ["paint"], + }, + { + name: "image-orientation", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "primitive", + field_size: 1, + type_name: "RespectImageOrientationEnum", + default_value: "kRespectImageOrientation", + converter: "ConvertImageOrientation", + include_paths: [ + "third_party/blink/renderer/platform/graphics/image_orientation.h" + ], + invalidate: ["layout", "paint"], + }, + { + name: "dynamic-range-limit", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + runtime_flag: "CSSDynamicRangeLimit", + converter: "ConvertDynamicRangeLimit", + type_name: "DynamicRangeLimit", + field_group: "*", + field_template: "external", + keywords: ["standard", "no-limit", "constrained"], + typedom_types: ["Keyword"], + include_paths: ["third_party/blink/renderer/platform/graphics/graphics_context_types.h"], + default_value: "DynamicRangeLimit(cc::PaintFlags::DynamicRangeLimit::kHigh)", + invalidate: ["paint"], + }, + { + name: "initial-letter", + converter: "ConvertInitialLetter", + default_value: "StyleInitialLetter()", + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_initial_letter.h"], + inherited: false, + keywords: ["drop", "normal", "raise"], + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + type_name: "StyleInitialLetter", + valid_for_first_letter: true, + invalidate: ["reshape", "layout", "paint"], + }, + { + name: "interactivity", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + independent: true, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "inert"], + typedom_types: ["Keyword"], + default_value: "auto", + invalidate: [], + }, + { + name: "interpolate-size", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["numeric-only", "allow-keywords"], + typedom_types: ["Keyword"], + default_value: "numeric-only", + invalidate: [], + }, + { + name: "isolation", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["auto", "isolate"], + typedom_types: ["Keyword"], + default_value: "auto", + valid_for_permission_element: true, + invalidate: ["paint"], + }, + { + name: "justify-content", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_content_alignment_data.h"], + default_value: "StyleContentAlignmentData(ContentPosition::kNormal, ContentDistributionType::kDefault, OverflowAlignment::kDefault)", + type_name: "StyleContentAlignmentData", + converter: "ConvertContentAlignmentData", + invalidate: ["layout"], + }, + { + name: "justify-items", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_self_alignment_data.h"], + default_value: "StyleSelfAlignmentData(ItemPosition::kLegacy, OverflowAlignment::kDefault)", + type_name: "StyleSelfAlignmentData", + converter: "ConvertSelfOrDefaultAlignmentData", + invalidate: ["layout"], + }, + { + name: "justify-self", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_self_alignment_data.h"], + default_value: "StyleSelfAlignmentData(ItemPosition::kAuto, OverflowAlignment::kDefault)", + type_name: "StyleSelfAlignmentData", + converter: "ConvertSelfOrDefaultAlignmentData", + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "left", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "surround", + field_template: "", + keywords: ["auto"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + anchor_mode: "left", + logical_property_group: { + name: "inset", + resolver: "left", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["inset", "out-of-flow", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "letter-spacing", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + keywords: ["normal"], + field_group: "inherited", + field_template: "", + default_value: "Length::Fixed()", + getter: "ComputedLetterSpacing", + converter: "ConvertSpacing", + computed_style_custom_functions: ["getter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + valid_for_permission_element: true, + valid_for_page_context: true, + affected_by_zoom: true, + }, + { + name: "lighting-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "svg->svgmisc", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kWhite)", + type_name: "StyleColor", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialLightingColor", + }, + converter: "ConvertStyleColor", + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "line-height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "inherited", + field_template: "", + default_value: "Length::Auto()", + getter: "SpecifiedLineHeight", + computed_style_custom_functions: ["getter"], + converter: "ConvertLineHeight", + keywords: ["normal"], + typedom_types: ["Keyword", "Length", "Number", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_page_context: true, + affected_by_zoom: true, + invalidate: ["layout", "paint"], + // The font and line height are necessary to correctly resolve font relative + // units. + highlight_style_comes_from_originating_element: true, + }, + { + name: "list-style-image", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_image.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + typedom_types: ["Keyword", "Image"], + type_name: "StyleImage", + style_builder_custom_functions: ["value"], + keywords: ["none"], + invalidate: ["layout", "paint"], + affected_by_zoom: true, + includes_currentcolor: true, + }, + { + name: "list-style-position", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: ["outside", "inside"], + typedom_types: ["Keyword"], + default_value: "outside", + invalidate: ["layout", "paint"], + }, + { + name: "list-style-type", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/keywords.h", + "third_party/blink/renderer/core/style/list_style_type_data.h"], + wrapper_pointer_name: "Member", + default_value: "ListStyleTypeData::CreateCounterStyle(keywords::kDisc, nullptr)", + type_name: "ListStyleTypeData", + keywords: [ + "disc", "circle", "square", "disclosure-open", "disclosure-closed", + "decimal", "none" + ], + style_builder_custom_functions: ["value"], + invalidate: ["layout", "paint"], + }, + { + name: "margin-bottom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertQuirkyLength", + computed_style_custom_functions: ["setter"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_position_try: true, + logical_property_group: { + name: "margin", + resolver: "bottom", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["margin", "out-of-flow", "scroll-anchor"], + anchor_mode: "height", + }, + { + name: "margin-left", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertQuirkyLength", + computed_style_custom_functions: ["setter"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_position_try: true, + logical_property_group: { + name: "margin", + resolver: "left", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["margin", "out-of-flow", "scroll-anchor"], + anchor_mode: "width", + }, + { + name: "margin-right", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertQuirkyLength", + computed_style_custom_functions: ["setter"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_position_try: true, + logical_property_group: { + name: "margin", + resolver: "right", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["margin", "out-of-flow", "scroll-anchor"], + anchor_mode: "width", + }, + { + name: "margin-top", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertQuirkyLength", + computed_style_custom_functions: ["setter"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_position_try: true, + logical_property_group: { + name: "margin", + resolver: "top", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["margin", "out-of-flow", "scroll-anchor"], + anchor_mode: "height", + }, + { + name: "marker-end", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited->resources", + field_template: "pointer", + type_name: "StyleSVGResource", + include_paths: ["third_party/blink/renderer/core/style/style_svg_resource.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + name_for_methods: "MarkerEndResource", + style_builder_custom_functions: ["value"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "marker-mid", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited->resources", + field_template: "pointer", + type_name: "StyleSVGResource", + include_paths: ["third_party/blink/renderer/core/style/style_svg_resource.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + name_for_methods: "MarkerMidResource", + style_builder_custom_functions: ["value"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "marker-start", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited->resources", + field_template: "pointer", + type_name: "StyleSVGResource", + include_paths: ["third_party/blink/renderer/core/style/style_svg_resource.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + name_for_methods: "MarkerStartResource", + style_builder_custom_functions: ["value"], + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "mask-type", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "svg", + field_template: "keyword", + keywords: ["luminance", "alpha"], + typedom_types: ["Keyword"], + default_value: "luminance", + invalidate: ["paint"], + }, + { + name: "masonry-direction", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + typedom_types: ["Keyword"], + keywords: ["row", "row-reverse", "column", "column-reverse"], + default_value: "column", + invalidate: ["layout", "paint"], + runtime_flag: "CSSMasonryLayout", + }, + { + name: "item-tolerance", + include_paths: ["third_party/blink/renderer/core/style/item_tolerance.h"], + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + default_value: "ItemTolerance(CSSValueID::kNormal)", + type_name: "ItemTolerance", + converter: "ConvertItemTolerance", + typedom_types: ["Length", "Percentage", "Keyword"], + keywords: ["normal", "infinite"], + invalidate: ["layout", "paint"], + runtime_flag: "CSSMasonryLayout", + }, + { + name: "math-shift", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + inherited: true, + keywords: ["normal", "compact"], + typedom_types: ["Keyword"], + default_value: "normal", + }, + { + name: "math-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + inherited: true, + keywords: ["normal", "compact"], + typedom_types: ["Keyword"], + default_value: "normal", + }, + { + name: "max-height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "", + default_value: "Length::None()", + converter: "ConvertLengthMaxSizing", + anchor_mode: "height", + keywords: ["none"], + typedom_types: ["Keyword", "Length", "Percentage"], + logical_property_group: { + name: "max-size", + resolver: "vertical", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "max-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "", + default_value: "Length::None()", + converter: "ConvertLengthMaxSizing", + anchor_mode: "width", + keywords: ["none"], + typedom_types: ["Keyword", "Length", "Percentage"], + logical_property_group: { + name: "max-size", + resolver: "horizontal", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "min-height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "", + default_value: "Length()", + converter: "ConvertLengthSizing", + anchor_mode: "height", + typedom_types: ["Length", "Percentage"], + logical_property_group: { + name: "min-size", + resolver: "vertical", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "min-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "", + default_value: "Length()", + converter: "ConvertLengthSizing", + anchor_mode: "width", + typedom_types: ["Length", "Percentage"], + logical_property_group: { + name: "min-size", + resolver: "horizontal", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "mix-blend-mode", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + include_paths: ["third_party/blink/renderer/platform/graphics/blend_mode.h"], + keywords: [ + "normal", "multiply", "screen", "overlay", "darken", "lighten", + "color-dodge", "color-burn", "hard-light", "soft-light", "difference", + "exclusion", "hue", "saturation", "color", "luminosity", "plus-lighter" + ], + typedom_types: ["Keyword"], + default_value: "normal", + name_for_methods: "BlendMode", + type_name: "BlendMode", + invalidate: ["blend-mode", "paint"], + }, + { + name: "object-fit", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["fill", "contain", "cover", "none", "scale-down"], + typedom_types: ["Keyword"], + default_value: "fill", + getter: "GetObjectFit", + invalidate: ["paint"], + }, + { + name: "object-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_point.h"], + default_value: "LengthPoint(Length::Percent(50.0), Length::Percent(50.0))", + type_name: "LengthPoint", + converter: "ConvertPosition", + typedom_types: ["Keyword", "Position"], + invalidate: ["paint"], + }, + { + name: "object-view-box", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/basic_shapes.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "BasicShape", + converter: "ConvertObjectViewBox", + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "offset-anchor", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_point.h"], + default_value: "LengthPoint(Length::Auto(), Length::Auto())", + type_name: "LengthPoint", + converter: "ConvertPositionOrAuto", + keywords: ["auto"], + typedom_types: ["Keyword", "Position"], + invalidate: ["transform-data", "transform-other"], + }, + { + name: "offset-distance", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + converter: "ConvertLength", + typedom_types: ["Length", "Percentage"], + invalidate: ["transform-data", "transform-other"], + }, + { + name: "offset-path", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/offset_path_operation.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "OffsetPathOperation", + converter: "ConvertOffsetPath", + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["has-transform", "transform-data", "transform-other"], + }, + { + name: "offset-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_point.h"], + default_value: "LengthPoint(Length::None(), Length::None())", + type_name: "LengthPoint", + converter: "ConvertOffsetPosition", + keywords: ["auto", "normal"], + typedom_types: ["Keyword", "Position"], + invalidate: ["has-transform", "transform-data", "transform-other"], + }, + { + name: "offset-rotate", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_offset_rotation.h"], + default_value: "StyleOffsetRotation(0, OffsetRotationType::kAuto)", + type_name: "StyleOffsetRotation", + converter: "ConvertOffsetRotate", + keywords: ["auto", "reverse"], + typedom_types: ["Keyword", "Angle"], + invalidate: ["transform-data", "transform-other"], + }, + { + name: "opacity", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "svg", // Not SVG, but frequently used with it. + field_template: "primitive", + default_value: "1.0", + type_name: "float", + computed_style_custom_functions: ["setter"], + typedom_types: ["Number"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + supports_incremental_style: true, + accepts_numeric_literal: true, + invalidate: ["opacity"], + }, + { + name: "order", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "0", + type_name: "int", + typedom_types: ["Number"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + // This property is used for testing with origin trial intergration only. + // It should never be web-exposed. + name: "origin-trial-test-property", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + default_value: "normal", + keywords: ["normal", "none"], + typedom_types: ["Keyword"], + runtime_flag: "OriginTrialsSampleAPI", + computable: false, + }, + { + name: "orphans", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "primitive", + computed_style_custom_functions: ["setter"], + default_value: "2", + type_name: "short", + typedom_types: ["Number"], + valid_for_permission_element: true, + invalidate: ["layout"], + }, + { + name: "outline-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_cue: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["outline"], + }, + { + name: "outline-offset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/layout_unit.h"], + default_value: "LayoutUnit()", + type_name: "LayoutUnit", + converter: "ConvertLayoutUnit", + typedom_types: ["Length"], + valid_for_cue: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["outline"], + }, + { + name: "outline-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "EBorderStyle", + style_builder_custom_functions: ["initial", "inherit", "value"], + valid_for_cue: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["outline"], + }, + { + name: "outline-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + default_value: "3", + type_name: "int", + computed_style_custom_functions: ["getter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertBorderWidth", + keywords: ["thin", "medium", "thick"], + typedom_types: ["Keyword", "Length"], + valid_for_cue: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["outline"], + }, + { + name: "overflow-anchor", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_template: "keyword", + keywords: [ + "visible", "none", "auto" + ], + typedom_types: ["Keyword"], + default_value: "auto", + valid_for_permission_element: true, + }, + { + name: "overflow-wrap", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["normal", "break-word", "anywhere"], + default_value: "normal", + typedom_types: ["Keyword"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "overflow-inline", + logical_property_group: { + name: "overflow", + resolver: "inline", + }, + // See comment on overflow-x. + supports_incremental_style: false, + }, + { + name: "overflow-block", + logical_property_group: { + name: "overflow", + resolver: "block", + }, + // See comment on overflow-x. + supports_incremental_style: false, + }, + { + name: "overflow-clip-margin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_template: "external", + field_group: "box", + include_paths: ["third_party/blink/renderer/core/style/style_overflow_clip_margin.h"], + keywords: ["border-box", "content-box", "padding-box"], + separator: " ", + default_value: "std::nullopt", + type_name: "std::optional", + converter: "ConvertOverflowClipMargin", + invalidate: ["layout"], + }, + { + name: "overflow-x", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: [ + "visible", "hidden", "scroll", "auto", "overlay", "clip" + ], + typedom_types: ["Keyword"], + default_value: "visible", + // This is to allow us to gather metrics when overflow is explicitly set. + style_builder_custom_functions: ["initial", "inherit", "value"], + type_name: "EOverflow", + logical_property_group: { + name: "overflow", + resolver: "horizontal", + }, + // Overflow has special semantics; overflowY can influence overflowX. + // But StyleAdjuster::AdjustOverflow() only does this properly if overflowX + // is set to the initial value (it cannot distinguish between an explicitly + // set overflowX, and one that was already adjusted in a previous pass). + // Modifying overflow frequently should not be common, so we take it out. + supports_incremental_style: false, + idempotent: false, + invalidate: ["layout", "paint"], + }, + { + name: "overflow-y", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: [ + "visible", "hidden", "scroll", "auto", "overlay", "clip" + ], + typedom_types: ["Keyword"], + default_value: "visible", + // This is to allow us to gather metrics when overflow is explicitly set. + style_builder_custom_functions: ["initial", "inherit", "value"], + type_name: "EOverflow", + logical_property_group: { + name: "overflow", + resolver: "vertical", + }, + // See comment on overflow-x. + supports_incremental_style: false, + idempotent: false, + invalidate: ["layout", "paint"], + }, + { + name: "overscroll-area", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + include_paths: ["third_party/blink/renderer/core/style/scoped_css_name.h"], + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + default_value: "nullptr", + field_group: "*", + field_template: "external", + converter: "ConvertOverscrollArea", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + invalidate: ["layout"], + runtime_flag: "CSSOverscrollGestures", + }, + { + name: "overscroll-behavior-inline", + logical_property_group: { + name: "overscroll-behavior", + resolver: "inline", + }, + valid_for_permission_element: true, + }, + { + name: "overscroll-behavior-block", + logical_property_group: { + name: "overscroll-behavior", + resolver: "block", + }, + valid_for_permission_element: true, + }, + { + name: "overscroll-behavior-x", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + field_group: "*", + keywords: ["auto", "contain", "none"], + default_value: "auto", + type_name: "EOverscrollBehavior", + typedom_types: ["Keyword"], + logical_property_group: { + name: "overscroll-behavior", + resolver: "horizontal", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "overscroll-behavior-y", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + field_group: "*", + keywords: ["auto", "contain", "none"], + default_value: "auto", + type_name: "EOverscrollBehavior", + typedom_types: ["Keyword"], + logical_property_group: { + name: "overscroll-behavior", + resolver: "vertical", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "overscroll-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + include_paths: ["third_party/blink/renderer/core/style/scoped_css_name.h"], + type_name: "ScopedCSSName", + wrapper_pointer_name: "Member", + default_value: "nullptr", + field_group: "*", + field_template: "external", + converter: "ConvertOverscrollPosition", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + runtime_flag: "CSSOverscrollGestures", + }, + { + name: "padding-bottom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertLength", + computed_style_custom_functions: ["setter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_page_context: true, + logical_property_group: { + name: "padding", + resolver: "bottom", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint", "scroll-anchor"], + }, + { + name: "padding-left", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertLength", + computed_style_custom_functions: ["setter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_page_context: true, + logical_property_group: { + name: "padding", + resolver: "left", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint", "scroll-anchor"], + }, + { + name: "padding-right", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertLength", + computed_style_custom_functions: ["setter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_page_context: true, + logical_property_group: { + name: "padding", + resolver: "right", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint", "scroll-anchor"], + }, + { + name: "padding-top", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + default_value: "Length::Fixed()", + converter: "ConvertLength", + computed_style_custom_functions: ["setter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_page_context: true, + logical_property_group: { + name: "padding", + resolver: "top", + }, + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint", "scroll-anchor"], + }, + { + name: "page", + field_group: "*", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + converter: "ConvertPage", + type_name: "AtomicString", + default_value: "AtomicString()", + field_template: "external", + keywords: ["auto"], + typedom_types: ["Keyword"], + computable: false, + valid_for_permission_element: true, + }, + { + name: "page-orientation", + is_descriptor: true, + field_template: "primitive", + field_group: "*", + type_name: "PageOrientation", + field_size: 2, + default_value: "PageOrientation::kUpright", + include_paths: ["third_party/blink/public/common/css/page_orientation.h"], + computable: false, + valid_for_page_context: true, + }, + { + name: "paint-order", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "primitive", + field_size: 3, + type_name: "EPaintOrder", + default_value: "kPaintOrderNormal", + converter: "ConvertPaintOrder", + keywords: ["normal", "fill", "stroke", "markers"], + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "perspective", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "-1.0", + type_name: "float", + converter: "ConvertPerspective", + keywords: ["none"], + typedom_types: ["Keyword", "Length"], + invalidate: ["transform-other"], + }, + { + name: "perspective-origin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length_point.h"], + default_value: "LengthPoint(Length::Percent(50.0), Length::Percent(50.0))", + type_name: "LengthPoint", + converter: "ConvertPosition", + typedom_types: ["Position"], + // Overlaps with -webkit-perspective-origin-[x,y]. + overlapping: true, + invalidate: ["transform-other"], + }, + { + name: "pointer-events", + property_methods: ["CSSValueFromComputedStyleInternal"], + computed_style_protected_functions: ["getter"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: [ + "none", "auto", "stroke", "fill", "painted", "visible", "visiblestroke", + "visiblefill", "visiblepainted", "bounding-box", "all" + ], + typedom_types: ["Keyword"], + default_value: "auto", + }, + { + name: "position", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: [ + "static", "relative", "absolute", "fixed", "sticky" + ], + typedom_types: ["Keyword"], + default_value: "static", + getter: "GetPosition", + computed_style_custom_functions: ["getter"], + // See comment on display. + supports_incremental_style: false, + // @position-try-styling rely on this being high-priority, so that + // declarations from @position-try blocks can be applied conditionally + // based on whether or not we're out-of-flow positioned. Also, it needs to + // have a priority higher than position-area which is already at priority:1. + priority: 2, + valid_for_permission_element: true, + invalidate: ["clip", "layout", "scroll-anchor"], + }, + { + name: "position-anchor", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal" ], + style_builder_custom_functions: ["initial", "inherit", "value"], + include_paths: ["third_party/blink/renderer/core/style/style_position_anchor.h"], + type_name: "StylePositionAnchor", + default_value: "StylePositionAnchor::Initial()", + field_group: "*", + field_template: "external", + converter: "ConvertPositionAnchor", + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + valid_for_position_try: true, + // Needs to be applied before position-area which in turn needs to be applied + // before inset properties. + priority: 2, + invalidate: ["layout", "paint"], + }, + { + name: "position-try-fallbacks", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["value"], + field_group: "*", + field_template: "external", + keywords: ["none", "flip-block", "flip-inline", "flip-start"], + typedom_types: ["Keyword"], + include_paths: ["third_party/blink/renderer/core/style/position_try_fallbacks.h"], + wrapper_pointer_name: "Member", + type_name: "PositionTryFallbacks", + default_value: "nullptr", + invalidate: ["layout", "paint"], + }, + { + name: "position-try-order", + property_methods: ["CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "*", + field_template: "keyword", + keywords: ["normal", "most-width", "most-height", "most-block-size", "most-inline-size"], + typedom_types: ["Keyword"], + default_value: "normal", + invalidate: ["layout", "paint"], + }, + { + name: "position-visibility", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_size: 3, + field_template: "primitive", + default_value: "InitialPositionVisibilityKeyword()", + getter: "GetPositionVisibility", + type_name: "PositionVisibility", + converter: "ConvertPositionVisibility", + // TODO(crbug.com/332933527): Support anchors-valid. + keywords: ["always", "anchors-visible", "no-overflow"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "print-color-adjust", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: false, // Actually true, but setting it to false saves a precious bit in ComputedStyleBase. + inherited: true, + field_template: "keyword", + keywords: ["economy", "exact"], + default_value: "economy", + valid_for_permission_element: true, + invalidate: ["paint"], + }, + { + name: "quotes", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/platform/text/quotes_data.h"], + wrapper_pointer_name: "scoped_refptr", + default_value: "nullptr", + type_name: "QuotesData", + converter: "ConvertQuotes", + keywords: ["auto", "none"], + typedom_types: ["Keyword"], + computable: false, + valid_for_page_context: true, + invalidate: ["layout", "paint"], + }, + { + name: "content-visibility", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: ["visible", "auto", "hidden"], + default_value: "visible", + typedom_types: ["Keyword"], + computable: false, + invalidate: ["layout"], + }, + { + name: "reading-flow", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["normal", "flex-visual", "flex-flow", "grid-rows", "grid-columns", "grid-order", "source-order"], + typedom_types: ["Keyword"], + default_value: "normal", + invalidate: ["layout"], + }, + { + name: "reading-order", + interpolable: true, + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + typedom_types: ["Number"], + default_value: "0", + type_name: "int", + invalidate: ["layout"], + valid_for_permission_element: true, + }, + { + name: "resize", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + computed_style_protected_functions: ["getter"], + style_builder_custom_functions: ["value"], + keywords: ["none", "both", "horizontal", "vertical", "block", "inline"], + typedom_types: ["Keyword"], + default_value: "none", + invalidate: ["paint"], + }, + { + name: "right", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "surround", + field_template: "", + keywords: ["auto"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + anchor_mode: "right", + logical_property_group: { + name: "inset", + resolver: "right", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["inset", "out-of-flow", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "r", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Fixed()", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "rx", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Auto()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "ry", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Auto()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + supports_incremental_style: true, + invalidate: ["layout", "paint"], + }, + { + name: "scroll-target-group", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["none", "auto"], + typedom_types: ["Keyword"], + default_value: "none", + runtime_flag: "CSSScrollTargetGroup", + }, + { + name: "scroll-marker-group", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "pointer", + typedom_types: ["Keyword"], + default_value: "nullptr", + runtime_flag: "CSSPseudoScrollMarkers", + include_paths: ["third_party/blink/renderer/core/style/scroll_marker_group.h"], + wrapper_pointer_name: "Member", + type_name: "ScrollMarkerGroup", + converter: "ConvertScrollMarkerGroup", + typedom_types: ["Keyword"], + }, + { + interpolable: true, + name: "scrollbar-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + name_for_methods: "ScrollbarColor", + type_name: "StyleScrollbarColor", + converter: "ConvertScrollbarColor", + keywords: ["auto"], + field_group: "*", + field_template: "pointer", + default_value: "nullptr", + wrapper_pointer_name: "Member", + include_paths: ["third_party/blink/renderer/core/style/style_scrollbar_color.h"], + invalidate: ["scrollbar-style", "scrollbar-color"], + runtime_flag: "ScrollbarColor", + }, + { + name: "scrollbar-gutter", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: false, + field_size: 4, + field_template: "primitive", + default_value: "kScrollbarGutterAuto", + name_for_methods: "ScrollbarGutter", + type_name: "unsigned", + converter: "ConvertScrollbarGutter", + keywords: [ + "auto", "stable", "both-edges" + ], + typedom_types: ["Keyword"], + invalidate: ["layout"], + field_group: "*", + }, + { + name: "scrollbar-width", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "thin", "none"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint", "scrollbar-style"], + runtime_flag: "ScrollbarWidth", + }, + { + name: "scroll-behavior", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_size: 2, // FIXME: Convert this to a keyword field + field_template: "primitive", + include_paths: ["third_party/blink/public/mojom/scroll/scroll_enums.mojom-blink.h"], + default_value: "mojom::blink::ScrollBehavior::kAuto", + type_name: "mojom::blink::ScrollBehavior", + keywords: ["auto", "smooth"], + typedom_types: ["Keyword"], + }, + { + name: "scroll-initial-target", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + type_name: "EScrollInitialTarget", + default_value: "none", + keywords: ["none", "nearest"], + invalidate: ["layout"], + runtime_flag: "CSSScrollInitialTarget", + }, + { + name: "scroll-margin-block-end", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "scroll-margin", + resolver: "block-end", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "scroll-margin-block-start", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "scroll-margin", + resolver: "block-start", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "scroll-margin-bottom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + converter: "ConvertComputedLength", + typedom_types: ["Keyword", "Length"], + logical_property_group: { + name: "scroll-margin", + resolver: "bottom", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-margin-inline-end", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "scroll-margin", + resolver: "inline-end", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "scroll-margin-inline-start", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "scroll-margin", + resolver: "inline-start", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "scroll-margin-left", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + converter: "ConvertComputedLength", + typedom_types: ["Keyword", "Length"], + logical_property_group: { + name: "scroll-margin", + resolver: "left", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-margin-right", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + converter: "ConvertComputedLength", + typedom_types: ["Keyword", "Length"], + logical_property_group: { + name: "scroll-margin", + resolver: "right", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-margin-top", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + converter: "ConvertComputedLength", + typedom_types: ["Keyword", "Length"], + logical_property_group: { + name: "scroll-margin", + resolver: "top", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-block-end", + property_methods: ["ParseSingleValue"], + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + type_name: "Length", + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "block-end", + }, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-block-start", + property_methods: ["ParseSingleValue"], + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + type_name: "Length", + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "block-start", + }, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-bottom", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "", + default_value: "Length()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "bottom", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-inline-end", + property_methods: ["ParseSingleValue"], + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + type_name: "Length", + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "inline-end", + }, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-inline-start", + property_methods: ["ParseSingleValue"], + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + type_name: "Length", + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "inline-start", + }, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-left", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "", + default_value: "Length()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "left", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-right", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "", + default_value: "Length()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "right", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-padding-top", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "", + default_value: "Length()", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + logical_property_group: { + name: "scroll-padding", + resolver: "top", + }, + computable: false, + valid_for_permission_element: true, + }, + { + name: "scroll-snap-align", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["cc/input/scroll_snap_data.h"], + default_value: "cc::ScrollSnapAlign()", + getter: "GetScrollSnapAlign", + type_name: "cc::ScrollSnapAlign", + converter: "ConvertSnapAlign", + keywords: ["none", "start", "end", "center"], + typedom_types: ["Keyword"], + computable: false, + }, + { + name: "scroll-snap-stop", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: ["normal", "always"], + default_value: "normal", + typedom_types: ["Keyword"], + computable: false, + }, + { + name: "scroll-snap-type", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["cc/input/scroll_snap_data.h"], + default_value: "cc::ScrollSnapType()", + getter: "GetScrollSnapType", + type_name: "cc::ScrollSnapType", + converter: "ConvertSnapType", + keywords: ["none", "x", "y", "block", "inline", "both", "mandatory", "proximity"], + typedom_types: ["Keyword"], + computable: false, + }, + { + name: "scroll-timeline-axis", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "misc->timeline", + field_template: "external", + default_value: "Vector()", + type_name: "Vector", + converter: "ConvertViewTimelineAxis", + separator: ",", + }, + { + name: "scroll-timeline-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "misc->timeline", + field_template: "external", + default_value: "nullptr", + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + converter: "ConvertViewTimelineName", + separator: ",", + }, + { + name: "shape-image-threshold", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "0.0", + type_name: "float", + computed_style_custom_functions: ["setter"], + typedom_types: ["Number"], + accepts_numeric_literal: true, + }, + { + name: "shape-margin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + converter: "ConvertLength", + keywords: ["none"], + typedom_types: ["Length", "Percentage"], + invalidate: ["layout", "paint"], + }, + { + name: "shape-outside", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/shape_value.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + typedom_types: ["Keyword", "Image"], + type_name: "ShapeValue", + computed_style_custom_functions: ["getter"], + converter: "ConvertShapeValue", + keywords: ["none"], + invalidate: ["paint"], + }, + { + name: "shape-rendering", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + keywords: ["auto", "optimizespeed", "crispedges", "geometricprecision"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "size", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["initial", "inherit", "value"], + computable: false, + valid_for_page_context: true, + }, + { + name: "speak", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: [ + "none", "normal", "spell-out", "digits", "literal-punctuation", + "no-punctuation" + ], + default_value: "normal", + }, + { + name: "stop-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "svg->stop", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kBlack)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialStopColor", + }, + converter: "ConvertStyleColor", + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + invalidate: ["paint"], + }, + { + name: "stop-opacity", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->stop", + field_template: "primitive", + type_name: "float", + default_value: "1", + converter: "ConvertAlpha", + typedom_types: ["Number"], + accepts_numeric_literal: true, + invalidate: ["paint"], + }, + { + name: "stroke", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + includes_currentcolor: true, + field_group: "svginherited->stroke", + field_template: "external", + type_name: "SVGPaint", + include_paths: ["third_party/blink/renderer/core/style/svg_paint.h"], + default_value: "SVGPaint::CreateInitial()", + name_for_methods: "StrokePaint", + converter: "ConvertSVGPaint", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialStrokePaint", + }, + style_builder_custom_functions: ["value"], + valid_for_highlight: true, + valid_for_permission_icon: true, + invalidate: ["paint", "stroke"], + }, + { + name: "stroke-dasharray", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->stroke", + field_template: "pointer", + type_name: "SVGDashArray", + include_paths: ["third_party/blink/renderer/core/style/svg_dash_array.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + name_for_methods: "StrokeDashArray", + converter: "ConvertStrokeDasharray", + keywords: ["none"], + typedom_types: ["Keyword"], + invalidate: ["paint", "stroke", "border-shape"], + affected_by_zoom: true, + }, + { + name: "stroke-dashoffset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->stroke", + field_template: "external", + type_name: "Length", + default_value: "Length::Fixed()", + name_for_methods: "StrokeDashOffset", + converter: "ConvertLength", + typedom_types: ["Length", "Percentage"], + invalidate: ["paint"], + affected_by_zoom: true, + }, + { + name: "stroke-linecap", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited->stroke", + field_template: "primitive", + field_size: 2, + type_name: "LineCap", + default_value: "kButtCap", + name_for_methods: "CapStyle", + keywords: ["butt", "round", "square"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint", "border-shape"], + }, + { + name: "stroke-linejoin", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited->stroke", + field_template: "primitive", + field_size: 2, + type_name: "LineJoin", + default_value: "kMiterJoin", + name_for_methods: "JoinStyle", + keywords: ["miter", "bevel", "round"], + typedom_types: ["Keyword"], + invalidate: ["layout", "paint", "border-shape"], + }, + { + name: "stroke-miterlimit", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->stroke", + field_template: "primitive", + type_name: "float", + default_value: "4", + name_for_methods: "StrokeMiterLimit", + typedom_types: ["Number"], + invalidate: ["layout", "paint", "border-shape"], + }, + { + name: "stroke-opacity", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->stroke", + field_template: "primitive", + type_name: "float", + default_value: "1", + converter: "ConvertAlpha", + typedom_types: ["Number"], + accepts_numeric_literal: true, + invalidate: ["paint"], + }, + { + name: "stroke-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "svginherited->stroke", + field_template: "external", + type_name: "UnzoomedLength", + include_paths: ["third_party/blink/renderer/core/style/unzoomed_length.h"], + default_value: "UnzoomedLength(Length::Fixed(1))", + converter: "ConvertUnzoomedLength", + typedom_types: ["Length", "Percentage"], + valid_for_highlight: true, + valid_for_permission_icon: true, + invalidate: ["layout", "paint", "border-shape"], + affected_by_zoom: true, + }, + { + name: "table-layout", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: [ + "auto", "fixed" + ], + typedom_types: ["Keyword"], + default_value: "auto", + invalidate: ["layout", "paint"], + }, + { + name: "tab-size", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/text/tab_size.h"], + default_value: "TabSize(8)", + getter: "GetTabSize", + type_name: "TabSize", + converter: "ConvertLengthOrTabSpaces", + computed_style_custom_functions: ["setter"], + typedom_types: ["Number", "Length"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "text-align", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: false, + inherited: true, + field_template: "keyword", + keywords: [ + "left", "right", "center", "justify", "-webkit-left", "-webkit-right", + "-webkit-center", "start", "end", "match-parent" + ], + typedom_types: ["Keyword"], + default_value: "start", + getter: "GetTextAlign", + style_builder_custom_functions: ["value"], + valid_for_page_context: true, + invalidate: ["layout", "paint"], + }, + { + name: "text-align-last", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "start", "end", "left", "right", "center", "justify", + "match-parent" + ], + default_value: "auto", + typedom_types: ["Keyword"], + valid_for_page_context: true, + invalidate: ["layout", "paint"], + }, + { + name: "text-anchor", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "svginherited", + field_template: "keyword", + keywords: ["start", "middle", "end"], + default_value: "start", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "text-autospace", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["no-autospace", "normal"], + default_value: "no-autospace", + typedom_types: ["Keyword"], + invalidate: ["reshape"], + }, + { + name: "text-box", + longhands: ["text-box-trim", "text-box-edge"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "text-box-edge", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "primitive", + field_size: 6, // Sync with `TextBoxEdge::kBits`. + include_paths: ["third_party/blink/renderer/core/style/text_box_edge.h"], + default_value: "TextBoxEdge()", + type_name: "TextBoxEdge", + converter: "ConvertTextBoxEdge", + invalidate: ["layout"], + }, + { + name: "text-box-trim", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "box", + field_template: "keyword", + default_value: "none", + keywords: ["none", "trim-start", "trim-end", "trim-both"], + typedom_types: ["Keyword"], + invalidate: ["layout"], + }, + { + name: "text-combine-upright", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["none", "all"], + typedom_types: ["Keyword"], + default_value: "none", + name_for_methods: "TextCombine", + valid_for_marker: true, + computable: false, + invalidate: ["layout", "paint"], + }, + { + name: "text-decoration-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + supports_incremental_style: true, + valid_for_page_context: true, + invalidate: ["color"], + }, + { + name: "text-decoration-line", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "visual", + field_template: "multi_keyword", + keywords: ["none", "underline", "overline", "line-through", "blink", "spelling-error", "grammar-error"], + typedom_types: ["Keyword"], + default_value: "none", + type_name: "TextDecorationLine", + converter: "ConvertFlags", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + valid_for_page_context: true, + invalidate: ["text-decoration"], + }, + { + name: "text-decoration-skip-ink", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["none", "auto"], + typedom_types: ["Keyword"], + default_value: "auto", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_highlight: true, + valid_for_page_context: true, + invalidate: ["text-decoration"], + }, + { + name: "text-decoration-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["solid", "double", "dotted", "dashed", "wavy"], + typedom_types: ["Keyword"], + default_value: "solid", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + valid_for_page_context: true, + invalidate: ["text-decoration"], + }, + { + name: "text-decoration-thickness", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + include_paths: ["third_party/blink/renderer/core/style/text_decoration_thickness.h"], + inherited: false, + field_group: "*", + field_template: "external", + type_name: "TextDecorationThickness", + default_value: "TextDecorationThickness(Length::Auto())", + converter: "ConvertTextDecorationThickness", + keywords: ["auto", "from-font"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_highlight: true, + computable: false, + valid_for_page_context: true, + invalidate: ["text-decoration"], + }, + { + name: "text-indent", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "", + default_value: "Length::Fixed()", + style_builder_custom_functions: ["value"], + typedom_types: ["Length", "Percentage"], + valid_for_page_context: true, + invalidate: ["layout", "paint"], + affected_by_zoom: true, + }, + { + name: "text-justify", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + field_size: 2, + type_name: "TextJustify", + default_value: "TextJustify::kAuto", + include_paths: ["third_party/blink/renderer/platform/text/text_justify.h"], + inherited: true, + keywords: ["auto", "none", "inter-word", "inter-character"], + typedom_types: ["Keyword"], + invalidate: ["layout"], + runtime_flag: "CssTextJustify", + }, + { + name: "text-overflow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/text_overflow_data.h"], + default_value: "TextOverflowData(TextOverflowData::Type::kClip)", + type_name: "TextOverflowData", + converter: "ConvertTextOverflow", + keywords: ["clip", "ellipsis"], + typedom_types: ["Keyword"], + valid_for_page_context: true, + invalidate: ["layout", "paint"], + }, + { + name: "text-shadow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/shadow_list.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "ShadowList", + converter: "ConvertShadowList", + keywords: ["none"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_highlight: true, + valid_for_page_context: true, + invalidate: ["layout", "paint"], + affected_by_zoom: true, + includes_currentcolor: true, + }, + { + name: "text-size-adjust", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/text_size_adjust.h"], + style_builder_custom_functions: ["initial", "inherit", "value"], + default_value: "TextSizeAdjust::AdjustAuto()", + getter: "GetTextSizeAdjust", + // Affects font-size during style building and needs to apply before it. + priority: 2, + type_name: "TextSizeAdjust", + converter: "ConvertTextSizeAdjust", + keywords: ["none", "auto"], + typedom_types: ["Keyword", "Percentage"], + // Affects font-size during style building. Changes to anything related to + // fonts require that we call style.UpdateFont(), which the incremental + // path does not do. + supports_incremental_style: false, + invalidate: ["layout", "paint"], + }, + { + name: "text-spacing", + longhands: ["text-autospace", "text-spacing-trim"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSTextSpacing", + }, + { + name: "text-spacing-trim", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + font: true, + priority: 1, + type_name: "TextSpacingTrim", + include_paths: ["third_party/blink/renderer/platform/fonts/shaping/text_spacing_trim.h"], + keywords: ["normal", "space-all", "space-first", "trim-start"], + default_value: "TextSpacingTrim::kInitial", + typedom_types: ["Keyword"], + valid_for_permission_element: true, + }, + { + name: "text-transform", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: ["capitalize", "uppercase", "lowercase", "full-width", "none", "math-auto"], + typedom_types: ["Keyword"], + default_value: "none", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + valid_for_page_context: true, + invalidate: ["reshape"], + valid_for_permission_element: true, + }, + { + name: "text-underline-offset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "", + default_value: "Length()", + name_for_methods: "TextUnderlineOffset", + converter: "ConvertTextUnderlineOffset", + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + computable: false, + valid_for_highlight: true, + valid_for_page_context: true, + invalidate: ["text-decoration"], + affected_by_zoom: true, + }, + { + name: "text-underline-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_size: 4, + field_template: "primitive", + default_value: "TextUnderlinePosition::kAuto", + getter: "GetTextUnderlinePosition", + type_name: "TextUnderlinePosition", + converter: "ConvertTextUnderlinePosition", + keywords: ["auto", "from-font", "under", "left", "right"], + typedom_types: ["Keyword"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_page_context: true, + invalidate: ["text-decoration"], + }, + { + name: "timeline-scope", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "misc->timeline", + field_template: "external", + default_value: "nullptr", + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + converter: "ConvertTimelineScope", + separator: ",", + }, + { + name: "top", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "surround", + field_template: "", + keywords: ["auto"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthOrAuto", + anchor_mode: "top", + logical_property_group: { + name: "inset", + resolver: "top", + }, + supports_incremental_style: true, + valid_for_position_try: true, + valid_for_permission_element: true, + invalidate: ["inset", "out-of-flow", "scroll-anchor"], + affected_by_zoom: true, + }, + { + name: "overlay", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["none", "auto"], + default_value: "none", + typedom_types: ["Keyword"], + }, + { + name: "touch-action", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_size: 8, // FIXME: Make this use "kTouchActionBits". + field_template: "primitive", + include_paths: ["third_party/blink/renderer/platform/graphics/touch_action.h"], + default_value: "TouchAction::kAuto", + type_name: "TouchAction", + converter: "ConvertFlags", + keywords: ["auto", "none", "pan-x", "pan-left", "pan-right", "pan-y", "pan-up", "pan-down", "pinch-zoom", "manipulation"], + typedom_types: ["Keyword"], + }, + { + name: "transform", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + layout_dependent: true, + field_group: "svg", // Not SVG, but frequently used with it. + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/transforms/transform_operations.h"], + keywords: ["none"], + default_value: "EmptyTransformOperations()", + typedom_types: ["Keyword", "Transform"], + type_name: "TransformOperations", + converter: "ConvertTransformOperations", + supports_incremental_style: true, + invalidate: ["has-transform", "transform-data", "transform-property"], + }, + { + name: "transform-box", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: ["content-box", "border-box", "fill-box", "stroke-box", "view-box"], + default_value: "view-box", + typedom_types: ["Keyword"], + computable: false, + supports_incremental_style: true, + invalidate: ["transform-data", "transform-other"], + }, + { + name: "transform-origin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "svg", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/transform_origin.h"], + default_value: "TransformOrigin(Length::Percent(50.0), Length::Percent(50.0), 0)", + getter: "GetTransformOrigin", + type_name: "TransformOrigin", + converter: "ConvertTransformOrigin", + supports_incremental_style: true, + // Overlaps with -webkit-transform-origin-[x,y,z]. + overlapping: true, + invalidate: ["transform-data", "transform-other"], + }, + { + name: "transform-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["flat", "preserve-3d"], + default_value: "flat", + typedom_types: ["Keyword"], + name_for_methods: "TransformStyle3D", + supports_incremental_style: true, + }, + { + name: "translate", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + layout_dependent: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/platform/transforms/translate_transform_operation.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "TranslateTransformOperation", + converter: "ConvertTranslate", + supports_incremental_style: true, + invalidate: ["has-transform", "transform-data", "transform-other"], + }, + { + name: "rotate", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/platform/transforms/rotate_transform_operation.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "RotateTransformOperation", + converter: "ConvertRotate", + supports_incremental_style: true, + invalidate: ["has-transform", "transform-data", "transform-other"], + }, + { + name: "scale", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + compositable: true, + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/platform/transforms/scale_transform_operation.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "ScaleTransformOperation", + converter: "ConvertScale", + supports_incremental_style: true, + invalidate: ["has-transform", "transform-data", "transform-other"], + }, + { + name: "unicode-bidi", + property_methods: ["CSSValueFromComputedStyleInternal"], + affected_by_all: false, + field_template: "keyword", + include_paths: ["third_party/blink/renderer/platform/text/unicode_bidi.h"], + keywords: [ + "normal", "embed", "bidi-override", "isolate", "plaintext", + "isolate-override" + ], + typedom_types: ["Keyword"], + default_value: "normal", + type_name: "UnicodeBidi", + valid_for_marker: true, + invalidate: ["reshape"], + }, + { + name: "vector-effect", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "svg", + field_template: "keyword", + keywords: ["none", "non-scaling-stroke"], + typedom_types: ["Keyword"], + default_value: "none", + invalidate: ["layout", "paint"], + }, + { + name: "vertical-align", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_custom_functions: ["inherit", "value"], + typedom_types: ["Keyword", "Length", "Percentage"], + keywords: ["baseline", "sub", "super", "text-top", "text-bottom", "middle"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_page_context: true, + valid_for_permission_element: true, + }, + { + name: "view-timeline-axis", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "misc->timeline", + field_template: "external", + default_value: "Vector()", + type_name: "Vector", + converter: "ConvertViewTimelineAxis", + separator: ",", + }, + { + name: "view-timeline-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "misc->timeline", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/animation/timeline_inset.h"], + default_value: "Vector()", + type_name: "Vector", + converter: "ConvertViewTimelineInset", + separator: ",", + }, + { + name: "view-timeline-name", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + field_group: "misc->timeline", + field_template: "external", + default_value: "nullptr", + type_name: "ScopedCSSNameList", + wrapper_pointer_name: "Member", + converter: "ConvertViewTimelineName", + separator: ",", + }, + { + name: "view-transition-class", + field_group: "*", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + converter: "ConvertViewTransitionClass", + type_name: "ScopedCSSNameList", + default_value: "nullptr", + wrapper_pointer_name: "Member", + field_template: "external", + keywords: ["none"], + typedom_types: ["Keyword"], + }, + { + name: "view-transition-group", + field_group: "*", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + converter: "ConvertViewTransitionGroup", + type_name: "StyleViewTransitionGroup", + default_value: "StyleViewTransitionGroup::Normal()", + include_paths: ["third_party/blink/renderer/core/style/style_view_transition_group.h"], + field_template: "external", + keywords: ["normal", "contain", "nearest"], + typedom_types: ["Keyword"], + runtime_flag: "NestedViewTransition", + }, + { + name: "view-transition-name", + field_group: "*", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + converter: "ConvertViewTransitionName", + type_name: "StyleViewTransitionName", + default_value: "nullptr", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/style_view_transition_name.h"], + keywords: ["none", "auto"], + typedom_types: ["Keyword"], + wrapper_pointer_name: "Member", + }, + { + name: "visibility", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + interpolable: true, + inherited: true, + field_template: "keyword", + keywords: ["visible", "hidden", "collapse"], + typedom_types: ["Keyword"], + default_value: "visible", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["paint", "visibility"], + }, + { + name: "x", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Fixed()", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "y", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "svg->geometry", + field_template: "", + default_value: "Length::Fixed()", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + supports_incremental_style: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "appearance", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_size: 5, + field_template: "primitive", + include_paths: ["third_party/blink/renderer/platform/theme_types.h"], + computed_style_protected_functions: ["getter"], + default_value: "AppearanceValue::kNone", + type_name: "AppearanceValue", + // appearance needs to be computed before + // -internal-auto-base() can be resolved. + priority: 1, + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-appearance", + alias_for: "appearance", + }, + { + name: "app-region", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["none", "drag", "no-drag"], + default_value: "none", + name_for_methods: "DraggableRegionMode", + style_builder_custom_functions: ["initial", "inherit", "value"], + invalidate: ["layout"], + }, + { + name: "-webkit-app-region", + alias_for: "app-region", + }, + { + name: "-webkit-border-horizontal-spacing", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "inherited", + field_template: "primitive", + default_value: "0", + name_for_methods: "HorizontalBorderSpacing", + type_name: "short", + converter: "ConvertComputedLength", + valid_for_first_letter: true, + invalidate: ["layout", "paint"], + affected_by_zoom: true, + }, + { + name: "-webkit-border-image", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["value"], + valid_for_first_letter: true, + affected_by_all: false, + // Overlaps with border-image. + legacy_overlapping: true, + }, + { + name: "-webkit-border-vertical-spacing", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "inherited", + field_template: "primitive", + default_value: "0", + name_for_methods: "VerticalBorderSpacing", + type_name: "short", + converter: "ConvertComputedLength", + valid_for_first_letter: true, + invalidate: ["layout", "paint"], + affected_by_zoom: true, + }, + // For valid values of box-align see + // http://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#alignment + { + name: "-webkit-box-align", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["stretch", "start", "center", "end", "baseline"], + default_value: "stretch", + type_name: "EBoxAlignment", + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-decoration-break", + surrogate_for: "box-decoration-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + keywords: ["slice", "clone"], + }, + { + name: "-webkit-box-direction", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_template: "keyword", + keywords: ["normal", "reverse"], + default_value: "normal", + computed_style_protected_functions: ["getter"], + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-flex", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + accepts_numeric_literal: true, + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-ordinal-group", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "1", + type_name: "unsigned", + computed_style_custom_functions: ["setter"], + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-orient", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["horizontal", "vertical"], + default_value: "horizontal", + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-pack", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["start", "center", "end", "justify"], + default_value: "start", + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-box-reflect", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "pointer", + include_paths: ["third_party/blink/renderer/core/style/style_reflection.h"], + wrapper_pointer_name: "Member", + default_value: "nullptr", + type_name: "StyleReflection", + converter: "ConvertBoxReflect", + invalidate: ["filter-data"], + includes_currentcolor: true, + }, + { + name: "column-count", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "1", + type_name: "unsigned short", + computed_style_custom_functions: ["setter"], + style_builder_template: "auto", + keywords: ["auto"], + typedom_types: ["Keyword", "Number"], + invalidate: ["layout", "paint"], + }, + { + name: "column-gap", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + default_value: "std::nullopt", + type_name: "std::optional", + converter: "ConvertGapLength", + keywords: ["normal"], + typedom_types: ["Keyword", "Length", "Percentage"], + invalidate: ["layout", "paint"], + }, + { + name: "row-gap", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/geometry/length.h"], + default_value: "std::nullopt", + type_name: "std::optional", + converter: "ConvertGapLength", + keywords: ["normal"], + typedom_types: ["Keyword", "Length", "Percentage"], + invalidate: ["layout", "paint"], + }, + { + name: "gap-rule-overlap", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["row-over-column", "column-over-row"], + default_value: "row-over-column", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["none", "spanning-item", "intersection"], + default_value: "spanning-item", + type_name: "RuleBreak", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["none", "spanning-item", "intersection"], + default_value: "spanning-item", + type_name: "RuleBreak", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-edge-end-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-edge-end-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-edge-start-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-edge-start-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Fixed(0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-interior-end-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Percent(-50.0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-interior-end-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Percent(-50.0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-interior-start-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Percent(-50.0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-interior-start-inset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: false, + field_group: "*", + field_template: "", + default_value: "Length::Percent(-50.0)", + typedom_types: ["Length", "Percentage"], + converter: "ConvertLength", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "GapDataList::DefaultGapColorDataList()", + type_name: "GapDataList", + keywords: ["currentcolor"], + computed_style_custom_functions: ["setter"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertGapDecorationColorDataList", + invalidate: ["paint"], + }, + { + name: "row-rule-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "GapDataList::DefaultGapColorDataList()", + type_name: "GapDataList", + keywords: ["currentcolor"], + computed_style_custom_functions: ["setter"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertGapDecorationColorDataList", + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h"], + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + computed_style_custom_functions: ["setter"], + default_value: "GapDataList::DefaultGapStyleDataList()", + type_name: "GapDataList", + typedom_types: ["Keyword"], + converter: "ConvertGapDecorationStyleDataList", + invalidate: ["paint", "gap-decorations"], + }, + { + name: "row-rule-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h"], + keywords: [ + "none", "hidden", "inset", "groove", "outset", "ridge", "dotted", + "dashed", "solid", "double" + ], + computed_style_custom_functions: ["setter"], + default_value: "GapDataList::DefaultGapStyleDataList()", + type_name: "GapDataList", + typedom_types: ["Keyword"], + converter: "ConvertGapDecorationStyleDataList", + invalidate: ["paint", "gap-decorations"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-visibility-items", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["all", "around", "between", "none"], + default_value: "all", + type_name: "RuleVisibilityItems", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-visibility-items", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: false, + field_group: "*", + field_template: "keyword", + keywords: ["all", "around", "between", "none"], + default_value: "all", + type_name: "RuleVisibilityItems", + typedom_types: ["Keyword"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + default_value: "GapDataList::DefaultGapWidthDataList()", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h"], + type_name: "GapDataList", + computed_style_custom_functions: ["initial", "getter", "setter"], + style_builder_custom_functions: ["inherit"], + converter: "ConvertGapDecorationWidthDataList", + keywords: ["thin", "medium", "thick"], + typedom_types: ["Keyword", "Length"], + invalidate: ["paint"], + }, + { + name: "row-rule-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "external", + default_value: "GapDataList::DefaultGapWidthDataList()", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h"], + type_name: "GapDataList", + computed_style_custom_functions: ["initial", "getter", "setter"], + converter: "ConvertGapDecorationWidthDataList", + keywords: ["thin", "medium", "thick"], + typedom_types: ["Keyword", "Length"], + invalidate: ["paint"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-span", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["none", "all"], + default_value: "none", + getter: "GetColumnSpan", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + }, + { + name: "column-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + computed_style_custom_functions: ["setter"], + style_builder_template: "auto", + converter: "ConvertComputedLength", + keywords: ["auto"], + typedom_types: ["Keyword", "Length"], + invalidate: ["layout", "paint"], + }, + { + name: "column-height", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "*", + field_template: "primitive", + default_value: "0.0f", + type_name: "float", + computed_style_custom_functions: ["setter"], + style_builder_template: "auto", + converter: "ConvertComputedLength", + keywords: ["auto"], + typedom_types: ["Keyword", "Length"], + invalidate: ["layout", "paint"], + runtime_flag: "MulticolColumnWrapping", + }, + { + name: "column-wrap", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["auto", "nowrap", "wrap"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + runtime_flag: "MulticolColumnWrapping", + }, + { + name: "hyphenate-character", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/platform/wtf/text/atomic_string.h"], + default_value: "AtomicString()", + name_for_methods: "HyphenationString", + type_name: "AtomicString", + converter: "ConvertString", + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-line-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + keywords: ["auto", "loose", "normal", "strict", "after-white-space"], + surrogate_for: "line-break", + }, + { + name: "line-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + default_value: "auto", + type_name: "LineBreak", + keywords: ["auto", "loose", "normal", "strict", "anywhere", "after-white-space"], + typedom_types: ["Keyword"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "continue", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["auto", "collapse", "-webkit-legacy"], + default_value: "auto", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + runtime_flag: "CSSLineClamp", + }, + { + name: "max-lines", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0", + converter: "ConvertIntegerOrNone<0>", + type_name: "int", + keywords: ["none"], + invalidate: ["layout", "paint"], + runtime_flag: "CSSLineClamp", + }, + { + name: "block-ellipsis", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "no-ellipsis"], + default_value: "no-ellipsis", + typedom_types: ["Keyword"], + invalidate: ["layout", "paint"], + runtime_flag: "CSSLineClamp", + }, + { + name: "line-clamp", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["max-lines", "block-ellipsis", "continue"], + runtime_flag: "CSSLineClamp", + }, + { + name: "-webkit-line-clamp", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "primitive", + default_value: "0", + converter: "ConvertIntegerOrNone<0>", + keywords: ["none"], + type_name: "int", + invalidate: ["layout", "paint"], + name_for_methods: "WebkitLineClamp", + }, + { + name: "-alternative-webkit-line-clamp", + alternative_of: "-webkit-line-clamp", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["max-lines", "block-ellipsis", "continue"], + runtime_flag: "CSSLineClamp", + }, + { + name: "-webkit-mask-box-image-outset", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_template: "mask_box", + style_builder_template_args: { + modifier_type: "Outset", + }, + }, + { + name: "-webkit-mask-box-image-repeat", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "mask_box", + style_builder_template_args: { + modifier_type: "Repeat", + }, + }, + { + name: "-webkit-mask-box-image-slice", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_template: "mask_box", + style_builder_template_args: { + modifier_type: "Slice", + }, + }, + { + name: "-webkit-mask-box-image-source", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_custom_functions: ["value"], + includes_currentcolor: true, + }, + { + name: "-webkit-mask-box-image-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + style_builder_template: "mask_box", + style_builder_template_args: { + modifier_type: "Width", + }, + }, + { + name: "mask-mode", + keywords: ["alpha", "luminance", "match-source"], + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "MaskMode", + }, + }, + { + name: "mask-clip", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "Clip", + }, + }, + { + name: "-webkit-mask-clip", + alias_for: "mask-clip", + }, + { + name: "mask-composite", + keywords: ["add", "subtract", "intersect", "exclude"], + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "CompositingOperator", + }, + }, + { + name: "-webkit-mask-composite", + alias_for: "mask-composite", + }, + { + name: "mask-image", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "Image", + fill_type_getter: "GetImage" + }, + // {-webkit-}mask-image needs to be applied before {-webkit-}mask-composite, + // otherwise {-webkit-}mask-composite has no effect. + priority: 2, + includes_currentcolor: true, + }, + { + name: "-webkit-mask-image", + alias_for: "mask-image", + }, + { + name: "mask-origin", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "Origin", + }, + }, + { + name: "-webkit-mask-origin", + alias_for: "mask-origin", + }, + { + name: "-webkit-mask-position-x", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "PositionX", + }, + computable: false, + }, + { + name: "-webkit-mask-position-y", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "PositionY", + }, + computable: false, + }, + { + name: "mask-repeat", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "Repeat", + }, + }, + { + name: "-webkit-mask-repeat", + alias_for: "mask-repeat", + }, + { + name: "mask-size", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"], + interpolable: true, + style_builder_template: "mask_layer", + style_builder_template_args: { + fill_type: "Size", + }, + }, + { + name: "-webkit-mask-size", + alias_for: "mask-size", + }, + { + name: "-webkit-perspective-origin-x", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["inherit"], + interpolable: true, + converter: "ConvertLength", + computable: false, + affected_by_all: false, + // Overlaps with perspective-origin. + legacy_overlapping: true, + }, + { + name: "-webkit-perspective-origin-y", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["inherit"], + interpolable: true, + converter: "ConvertLength", + computable: false, + affected_by_all: false, + // Overlaps with perspective-origin. + legacy_overlapping: true, + }, + { + name: "-webkit-rtl-ordering", + property_methods: ["CSSValueFromComputedStyleInternal"], + independent: true, + inherited: true, + field_template: "keyword", + keywords: ["logical", "visual"], + default_value: "logical", + name_for_methods: "RtlOrdering", + setter: "SetRtlOrdering", + type_name: "EOrder", + invalidate: ["reshape"], + }, + { + name: "ruby-align", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["space-around", "start", "center", "space-between"], + default_value: "space-around", + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "ruby-overhang", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "none"], + default_value: "auto", + invalidate: ["layout", "paint"], + runtime_flag: "CSSRubyOverhang", + }, + { + name: "-webkit-ruby-position", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + type_name: "RubyPosition", + converter: "ConvertRubyPosition", + computable: false, + surrogate_for: "ruby-position", + valid_for_permission_element: true, + }, + { + name: "ruby-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["over", "under"], + default_value: "over", + type_name: "RubyPosition", + converter: "ConvertRubyPosition", + valid_for_first_line: true, + valid_for_permission_element: true, + invalidate: ["layout", "paint"], + }, + { + name: "-webkit-tap-highlight-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h", + "third_party/blink/renderer/core/layout/layout_theme.h"], + type_name: "StyleColor", + default_value: "StyleColor(LayoutTheme::TapHighlightColor())", + converter: "ConvertStyleColor", + includes_currentcolor: true, + }, + { + name: "-webkit-text-combine", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + surrogate_for: "text-combine-upright", + }, + { + name: "text-emphasis-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + keywords: ["currentcolor"], + includes_currentcolor: true, + typedom_types: ["Keyword"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_marker: true, + valid_for_highlight: true, + invalidate: ["color"], + }, + { + name: "text-emphasis-position", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_size: 3, + field_template: "primitive", + default_value: "ComputedStyleInitialValues::InitialTextEmphasisPosition()", + type_name: "TextEmphasisPosition", + converter: "ConvertTextTextEmphasisPosition", + computed_style_custom_functions: ["initial"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "text-emphasis-style", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + style_builder_custom_functions: ["initial", "inherit", "value"], + valid_for_marker: true, + }, + { + name: "-webkit-text-fill-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "color", + invalidate: ["color"], + }, + { + name: "text-grow", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/fit_text.h"], + default_value: "FitText()", + type_name: "FitText", + converter: "ConvertFitText", + invalidate: ["layout"], + runtime_flag: "CssFitWidthText", + }, + { + name: "-webkit-text-security", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["none", "disc", "circle", "square"], + default_value: "none", + invalidate: ["layout", "paint"], + }, + { + name: "text-shrink", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/fit_text.h"], + default_value: "FitText()", + type_name: "FitText", + converter: "ConvertFitText", + invalidate: ["layout"], + runtime_flag: "CssFitWidthText", + }, + { + name: "-webkit-text-stroke-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "color", + invalidate: ["color"], + includes_currentcolor: true, + }, + { + name: "-webkit-text-stroke-width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + field_group: "*", + field_template: "primitive", + default_value: "0", + type_name: "float", + converter: "ConvertTextStrokeWidth", + invalidate: ["layout", "paint"], + affected_by_zoom: true, + }, + { + name: "-webkit-transform-origin-x", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["inherit"], + interpolable: true, + converter: "ConvertLength", + computable: false, + affected_by_all: false, + // Overlaps with transform-origin. + legacy_overlapping: true, + }, + { + name: "-webkit-transform-origin-y", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["inherit"], + interpolable: true, + converter: "ConvertLength", + computable: false, + affected_by_all: false, + // Overlaps with transform-origin. + legacy_overlapping: true, + }, + { + name: "-webkit-transform-origin-z", + property_methods: ["ParseSingleValue"], + style_builder_custom_functions: ["inherit"], + interpolable: true, + converter: "ConvertComputedLength", + computable: false, + affected_by_all: false, + // Overlaps with transform-origin. + legacy_overlapping: true, + }, + { + name: "-webkit-user-drag", + property_methods: ["CSSValueFromComputedStyleInternal"], + field_group: "*", + field_template: "keyword", + keywords: ["auto", "none", "element"], + default_value: "auto", + valid_for_permission_element: true, + invalidate: ["paint"], + }, + { + name: "-webkit-user-modify", + property_methods: ["CSSValueFromComputedStyleInternal"], + computed_style_protected_functions: ["getter"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["read-only", "read-write", "read-write-plaintext-only"], + default_value: "read-only", + affected_by_all: false, + invalidate: ["paint"], + }, + { + name: "user-select", + property_methods: ["CSSValueFromComputedStyleInternal"], + computed_style_protected_functions: ["getter"], + inherited: true, + field_group: "*", + field_template: "keyword", + keywords: ["auto", "none", "text", "all", "contain"], + typedom_types: ["Keyword"], + default_value: "auto", + valid_for_permission_element: true, + invalidate: ["paint"], + }, + { + name: "white-space", + longhands: [ + "white-space-collapse", "text-wrap-mode" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "white-space-collapse", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "primitive", + field_size: 2, // Ensure this is in sync with `kWhiteSpaceCollapseBits`. + type_name: "WhiteSpaceCollapse", + keywords: ["collapse", "preserve", "preserve-breaks", "break-spaces"], + default_value: "WhiteSpaceCollapse::kCollapse", + include_paths: ["third_party/blink/renderer/core/css/white_space.h"], + typedom_types: ["Keyword"], + valid_for_cue: true, + valid_for_marker: true, + valid_for_page_context: true, + invalidate: ["reshape"], + }, + { + name: "text-wrap", + longhands: [ + "text-wrap-mode", "text-wrap-style" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "text-wrap-mode", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "keyword", + type_name: "TextWrapMode", + keywords: ["wrap", "nowrap"], + default_value: "wrap", + typedom_types: ["Keyword"], + valid_for_cue: true, + valid_for_marker: true, + valid_for_page_context: true, + invalidate: ["reshape"], + }, + { + name: "text-wrap-style", + property_methods: ["CSSValueFromComputedStyleInternal"], + inherited: true, + field_template: "keyword", + type_name: "TextWrapStyle", + keywords: ["auto", "balance", "pretty", "stable"], + default_value: "auto", + typedom_types: ["Keyword"], + valid_for_cue: true, + valid_for_marker: true, + valid_for_page_context: true, + invalidate: ["layout"], + }, + { + name: "widows", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + field_group: "*", + field_template: "primitive", + computed_style_custom_functions: ["setter"], + default_value: "2", + type_name: "short", + typedom_types: ["Number"], + invalidate: ["layout"], + }, + { + name: "width", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + layout_dependent: true, + field_group: "box", + field_template: "", + keywords: ["auto", "fit-content", "min-content", "max-content"], + default_value: "Length()", + typedom_types: ["Keyword", "Length", "Percentage"], + converter: "ConvertLengthSizing", + anchor_mode: "width", + logical_property_group: { + name: "size", + resolver: "horizontal", + }, + supports_incremental_style: true, + valid_for_position_try: true, + affected_by_zoom: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + invalidate: ["layout", "scroll-anchor"], + }, + { + name: "will-change", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["initial", "inherit", "value"], + keywords: ["auto"], + typedom_types: ["Keyword"], + valid_for_permission_element: true, + }, + { + name: "word-break", + property_methods: ["CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["value"], + inherited: true, + field_group: "*", + field_template: "keyword", + // Word Break Values. Matches WinIE and CSS3 + keywords: ["normal", "break-all", "keep-all", "break-word", "auto-phrase"], + default_value: "normal", + typedom_types: ["Keyword"], + valid_for_marker: true, + invalidate: ["layout", "paint"], + }, + { + name: "word-spacing", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + inherited: true, + converter: "ConvertSpacing", + keywords: ["normal"], + field_group: "inherited", + field_template: "", + default_value: "Length::Fixed()", + getter: "ComputedWordSpacing", + computed_style_custom_functions: ["getter"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_marker: true, + valid_for_permission_element: true, + valid_for_page_context: true, + affected_by_zoom: true, + }, + { + name: "z-index", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + interpolable: true, + field_group: "box", + field_template: "primitive", + default_value: "0", + type_name: "int", + computed_style_custom_functions: ["setter"], + style_builder_template: "auto", + converter: "ConvertInteger", + keywords: ["auto"], + typedom_types: ["Keyword", "Number"], + valid_for_permission_element: true, + valid_for_page_context: true, + invalidate: ["z-index"], + }, + // CSS logical props + { + name: "inline-size", + property_methods: ["ParseSingleValue"], + layout_dependent: true, + logical_property_group: { + name: "size", + resolver: "inline", + }, + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "block-size", + property_methods: ["ParseSingleValue"], + layout_dependent: true, + logical_property_group: { + name: "size", + resolver: "block", + }, + keywords: ["auto"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "min-inline-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "min-size", + resolver: "inline", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + valid_for_page_context: true, + }, + { + name: "min-block-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "min-size", + resolver: "block", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "max-inline-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "max-size", + resolver: "inline", + }, + keywords: ["none"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "max-block-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "max-size", + resolver: "block", + }, + keywords: ["none"], + typedom_types: ["Keyword", "Length", "Percentage"], + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "margin-inline-start", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "margin", + resolver: "inline-start", + }, + typedom_types: ["Length", "Percentage"], + keywords: ["auto"], + valid_for_first_letter: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "margin-inline-end", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "margin", + resolver: "inline-end", + }, + typedom_types: ["Length", "Percentage"], + keywords: ["auto"], + valid_for_first_letter: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_permission_icon: true, + valid_for_page_context: true, + }, + { + name: "margin-block-start", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "margin", + resolver: "block-start", + }, + typedom_types: ["Length", "Percentage"], + keywords: ["auto"], + valid_for_first_letter: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "margin-block-end", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "margin", + resolver: "block-end", + }, + typedom_types: ["Length", "Percentage"], + keywords: ["auto"], + valid_for_first_letter: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "padding-inline-start", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "padding", + resolver: "inline-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_page_context: true, + }, + { + name: "padding-inline-end", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "padding", + resolver: "inline-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_page_context: true, + }, + { + name: "padding-block-start", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "padding", + resolver: "block-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_page_context: true, + }, + { + name: "padding-block-end", + layout_dependent: true, + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "padding", + resolver: "block-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_page_context: true, + }, + { + name: "border-inline-start-width", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-width", + resolver: "inline-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-inline-start-style", + logical_property_group: { + name: "border-style", + resolver: "inline-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-inline-start-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-color", + resolver: "inline-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-inline-end-width", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-width", + resolver: "inline-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-inline-end-style", + logical_property_group: { + name: "border-style", + resolver: "inline-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-inline-end-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-color", + resolver: "inline-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-start-width", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-width", + resolver: "block-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-start-style", + logical_property_group: { + name: "border-style", + resolver: "block-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-start-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-color", + resolver: "block-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-end-width", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-width", + resolver: "block-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-end-style", + logical_property_group: { + name: "border-style", + resolver: "block-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-block-end-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-color", + resolver: "block-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "inset-inline-start", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "inset", + resolver: "inline-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + layout_dependent: true, + valid_for_permission_element: true, + }, + { + name: "inset-inline-end", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "inset", + resolver: "inline-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + layout_dependent: true, + valid_for_permission_element: true, + }, + { + name: "inset-block-start", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "inset", + resolver: "block-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + layout_dependent: true, + valid_for_permission_element: true, + }, + { + name: "inset-block-end", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "inset", + resolver: "block-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_position_try: true, + layout_dependent: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-start-start-radius", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-radius", + resolver: "start-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-start-end-radius", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-radius", + resolver: "start-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-end-start-radius", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-radius", + resolver: "end-start", + }, + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-end-end-radius", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "border-radius", + resolver: "end-end", + }, + typedom_types: ["Length", "Percentage"], + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-start-start-shape", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "corner-shape", + resolver: "start-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-start-end-shape", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "corner-shape", + resolver: "start-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-end-start-shape", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "corner-shape", + resolver: "end-start", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-end-end-shape", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "corner-shape", + resolver: "end-end", + }, + valid_for_first_letter: true, + valid_for_permission_element: true, + valid_for_page_context: true, + }, + // Non-standard direction aware properties + { + name: "-webkit-border-end-color", + alias_for: "border-inline-end-color", + }, + { + name: "-webkit-border-end-style", + alias_for: "border-inline-end-style", + }, + { + name: "-webkit-border-end-width", + alias_for: "border-inline-end-width", + }, + { + name: "-webkit-border-start-color", + alias_for: "border-inline-start-color", + }, + { + name: "-webkit-border-start-style", + alias_for: "border-inline-start-style", + }, + { + name: "-webkit-border-start-width", + alias_for: "border-inline-start-width", + }, + { + name: "-webkit-border-before-color", + alias_for: "border-block-start-color", + }, + { + name: "-webkit-border-before-style", + alias_for: "border-block-start-style", + }, + { + name: "-webkit-border-before-width", + alias_for: "border-block-start-width", + }, + { + name: "-webkit-border-after-color", + alias_for: "border-block-end-color", + }, + { + name: "-webkit-border-after-style", + alias_for: "border-block-end-style", + }, + { + name: "-webkit-border-after-width", + alias_for: "border-block-end-width", + }, + { + name: "-webkit-margin-end", + alias_for: "margin-inline-end", + }, + { + name: "-webkit-margin-start", + alias_for: "margin-inline-start", + }, + { + name: "-webkit-margin-before", + alias_for: "margin-block-start", + }, + { + name: "-webkit-margin-after", + alias_for: "margin-block-end", + }, + { + name: "-webkit-padding-end", + alias_for: "padding-inline-end", + }, + { + name: "-webkit-padding-start", + alias_for: "padding-inline-start", + }, + { + name: "-webkit-padding-before", + alias_for: "padding-block-start", + }, + { + name: "-webkit-padding-after", + alias_for: "padding-block-end", + }, + { + name: "-webkit-logical-width", + alias_for: "inline-size", + }, + { + name: "-webkit-logical-height", + alias_for: "block-size", + }, + { + name: "-webkit-min-logical-width", + alias_for: "min-inline-size", + }, + { + name: "-webkit-min-logical-height", + alias_for: "min-block-size", + }, + { + name: "-webkit-max-logical-width", + alias_for: "max-inline-size", + }, + { + name: "-webkit-max-logical-height", + alias_for: "max-block-size", + }, + { + name: "-webkit-print-color-adjust", + alias_for: "print-color-adjust", + }, + // Properties that we ignore in the StyleBuilder. + // TODO(timloh): This seems wrong, most of these shouldn't reach the + // StyleBuilder + { + name: "all", + affected_by_all: false, + style_builder_template: "empty", + computable: false, + }, + // TODO(hjkim3323@gmail.com): Remove -internal-font-size-delta. + // fontSizeDelta execCommand does not need separate CSS property. + { + name: "-internal-font-size-delta", + property_methods: ["ParseSingleValue"], + style_builder_template: "empty", + }, + { + name: "-webkit-text-decorations-in-effect", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + inherited: true, + style_builder_template: "empty", + }, + // Descriptor only names + { + name: "font-display", + is_descriptor: true, + is_property: false, + }, + { + name: "src", + is_descriptor: true, + is_property: false, + }, + { + name: "unicode-range", + is_descriptor: true, + is_property: false, + }, + { + name: "syntax", + is_descriptor: true, + is_property: false, + }, + { + name: "initial-value", + is_descriptor: true, + is_property: false, + }, + { + name: "inherits", + is_descriptor: true, + is_property: false, + }, + { + name: "ascent-override", + is_descriptor: true, + is_property: false, + }, + { + name: "descent-override", + is_descriptor: true, + is_property: false, + }, + { + name: "line-gap-override", + is_descriptor: true, + is_property: false, + }, + { + name: "system", + is_descriptor: true, + is_property: false, + }, + { + name: "negative", + is_descriptor: true, + is_property: false, + }, + { + name: "prefix", + is_descriptor: true, + is_property: false, + }, + { + name: "suffix", + is_descriptor: true, + is_property: false, + }, + { + name: "range", + is_descriptor: true, + is_property: false, + }, + { + name: "pad", + is_descriptor: true, + is_property: false, + }, + { + name: "fallback", + is_descriptor: true, + is_property: false, + }, + { + name: "symbols", + is_descriptor: true, + is_property: false, + }, + { + name: "additive-symbols", + is_descriptor: true, + is_property: false, + }, + { + name: "speak-as", + is_descriptor: true, + is_property: false, + }, + { + name: "size-adjust", + is_descriptor: true, + is_property: false, + }, + { + name: "base-palette", + is_descriptor: true, + is_property: false, + valid_for_permission_element: true, + }, + { + name: "override-colors", + is_descriptor: true, + is_property: false, + }, + { + name: "navigation", + is_descriptor: true, + is_property: false, + }, + { + name: "types", + is_descriptor: true, + is_property: false, + }, + { + name: "result", + is_descriptor: true, + is_property: false, + runtime_flag: "CSSFunctions", + }, + { + name: "position-area", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"], + style_builder_custom_functions: ["value", "inherit"], + field_group: "*", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/position_area.h"], + converter: "ConvertPositionArea", + default_value: "PositionArea()", + type_name: "PositionArea", + keywords: [ + "none", "top", "bottom", "center", "left", "right", "x-start", "x-end", + "y-start", "y-end", "start", "end", "self-start", "self-end", "all" + ], + typedom_types: ["Keyword"], + valid_for_position_try: true, + valid_for_permission_element: true, + // position-area needs to be applied before inset properties since + // anchor() functions compute relative to the containing block that is + // modified by position-area in AnchorEvaluatorImpl. + priority: 1, + invalidate: ["layout", "out-of-flow", "paint"], + }, + // Shorthands + { + name: "animation", + longhands: [ + "animation-duration", "animation-timing-function", "animation-delay", + "animation-iteration-count", "animation-direction", + "animation-fill-mode", "animation-play-state", "animation-name", + "animation-timeline", "animation-range-start", "animation-range-end" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "animation-range", + longhands: [ + "animation-range-start", "animation-range-end", + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: false, + valid_for_keyframe: false, + }, + { + name: "background", + longhands: [ + "background-image", "background-position-x", "background-position-y", + "background-size", "background-repeat", + "background-attachment", "background-origin", "background-clip", + "background-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "background-position", + longhands: ["background-position-x", "background-position-y"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + computable: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "border", + longhands: [ + "border-top-color", "border-top-style", "border-top-width", + "border-right-color", "border-right-style", "border-right-width", + "border-bottom-color", "border-bottom-style", "border-bottom-width", + "border-left-color", "border-left-style", "border-left-width", + "border-image-source", "border-image-slice", "border-image-width", + "border-image-outset", "border-image-repeat" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-block", + longhands: [ + "border-block-start-color", "border-block-start-style", "border-block-start-width", + "border-block-end-color", "border-block-end-style", "border-block-end-width" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-block-color", + longhands: ["border-block-start-color", "border-block-end-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-block-end", + longhands: [ + "border-block-end-width", "border-block-end-style", + "border-block-end-color" + ], + property_methods: ["ParseShorthand"], + logical_property_group: { + name: "border", + resolver: "block-end", + }, + valid_for_page_context: true, + }, + { + name: "border-block-start", + longhands: [ + "border-block-start-width", "border-block-start-style", + "border-block-start-color" + ], + property_methods: ["ParseShorthand"], + logical_property_group: { + name: "border", + resolver: "block-start", + }, + valid_for_page_context: true, + }, + { + name: "border-block-style", + longhands: ["border-block-start-style", "border-block-end-style"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-block-width", + longhands: ["border-block-start-width", "border-block-end-width"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-bottom", + longhands: [ + "border-bottom-width", "border-bottom-style", "border-bottom-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + logical_property_group: { + name: "border", + resolver: "bottom", + }, + valid_for_page_context: true, + }, + { + name: "border-color", + longhands: [ + "border-top-color", "border-right-color", "border-bottom-color", + "border-left-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "border-image", + longhands: [ + "border-image-source", "border-image-slice", "border-image-width", + "border-image-outset", "border-image-repeat" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-inline", + longhands: [ + "border-inline-start-color", "border-inline-start-style", "border-inline-start-width", + "border-inline-end-color", "border-inline-end-style", "border-inline-end-width" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-inline-color", + longhands: ["border-inline-start-color", "border-inline-end-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-inline-end", + longhands: [ + "border-inline-end-width", "border-inline-end-style", + "border-inline-end-color" + ], + property_methods: ["ParseShorthand"], + logical_property_group: { + name: "border", + resolver: "inline-end", + }, + valid_for_page_context: true, + }, + { + name: "border-inline-start", + longhands: [ + "border-inline-start-width", "border-inline-start-style", + "border-inline-start-color" + ], + property_methods: ["ParseShorthand"], + logical_property_group: { + name: "border", + resolver: "inline-start", + }, + valid_for_page_context: true, + }, + { + name: "border-inline-style", + longhands: ["border-inline-start-style", "border-inline-end-style"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-inline-width", + longhands: ["border-inline-start-width", "border-inline-end-width"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "border-left", + longhands: [ + "border-left-width", "border-left-style", "border-left-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + logical_property_group: { + name: "border", + resolver: "left", + }, + valid_for_page_context: true, + }, + { + name: "border-radius", + longhands: [ + "border-top-left-radius", "border-top-right-radius", + "border-bottom-right-radius", "border-bottom-left-radius" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "border-right", + longhands: [ + "border-right-width", "border-right-style", "border-right-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + logical_property_group: { + name: "border", + resolver: "right", + }, + valid_for_page_context: true, + }, + { + name: "border-spacing", + longhands: [ + "-webkit-border-horizontal-spacing", "-webkit-border-vertical-spacing" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "border-style", + longhands: [ + "border-top-style", "border-right-style", "border-bottom-style", + "border-left-style" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + keywords: ["none"], + valid_for_page_context: true, + }, + { + name: "border-top", + longhands: ["border-top-width", "border-top-style", "border-top-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + logical_property_group: { + name: "border", + resolver: "top", + }, + valid_for_page_context: true, + }, + { + name: "border-width", + longhands: [ + "border-top-width", "border-right-width", "border-bottom-width", + "border-left-width" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "contain-intrinsic-inline-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "contain-intrinsic-size", + resolver: "inline", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "contain-intrinsic-block-size", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "contain-intrinsic-size", + resolver: "block", + }, + typedom_types: ["Keyword", "Length"], + valid_for_permission_element: true, + }, + { + name: "contain-intrinsic-size", + longhands: [ + "contain-intrinsic-width", "contain-intrinsic-height" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + computable: true, + valid_for_permission_element: true, + }, + { + name: "container", + longhands: [ + "container-name", "container-type" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "corner-top-shape", + longhands: [ + "corner-top-left-shape", "corner-top-right-shape" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-right-shape", + longhands: [ + "corner-top-right-shape", "corner-bottom-right-shape" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-bottom-shape", + longhands: [ + "corner-bottom-left-shape", "corner-bottom-right-shape" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-left-shape", + longhands: [ + "corner-top-left-shape", "corner-bottom-left-shape" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-block-start-shape", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["corner-start-start-shape", "corner-start-end-shape"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-block-end-shape", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["corner-end-start-shape", "corner-end-end-shape"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-inline-start-shape", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["corner-start-start-shape", "corner-end-start-shape"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-inline-end-shape", + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + longhands: ["corner-start-end-shape", "corner-end-end-shape"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corner-shape", + longhands: [ + "corner-top-left-shape", "corner-top-right-shape", + "corner-bottom-right-shape", "corner-bottom-left-shape" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "corners", + longhands: [ + "border-top-left-radius", "corner-top-left-shape", + "border-top-right-radius", "corner-top-right-shape", + "border-bottom-right-radius", "corner-bottom-right-shape", + "border-bottom-left-radius", "corner-bottom-left-shape" + ], + keywords: ["normal"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSCornersShorthand", + valid_for_permission_element: true, + valid_for_page_context: true, + }, + { + name: "flex", + longhands: ["flex-grow", "flex-shrink", "flex-basis"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "flex-flow", + longhands: ["flex-direction", "flex-wrap"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "font", + longhands: [ + "font-style", "font-variant-ligatures", "font-variant-caps", + "font-variant-numeric", "font-variant-east-asian", "font-variant-alternates", + "font-variant-position", "font-variant-emoji", "font-weight", "font-stretch", + "font-size", "line-height", "font-family", "font-optical-sizing", + "font-size-adjust", "font-kerning", "font-feature-settings", + "font-variation-settings", "font-language-override" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + // Changes to anything related to fonts require that we call style.UpdateFont(), + // which the incremental path does not do. + supports_incremental_style: false, + }, + { + name: "font-variant", + longhands: [ + "font-variant-ligatures", "font-variant-caps", "font-variant-alternates", + "font-variant-numeric", "font-variant-east-asian", "font-variant-position", + "font-variant-emoji" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + is_descriptor: true, + computable: true, + // See comment on font. + supports_incremental_style: false, + }, + { + name: "font-synthesis", + longhands: ["font-synthesis-weight", "font-synthesis-style", "font-synthesis-small-caps"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + // See comment on font. + supports_incremental_style: false, + }, + { + name: "grid", + longhands: [ + "grid-template-rows", "grid-template-columns", "grid-template-areas", + "grid-auto-flow", "grid-auto-rows", "grid-auto-columns" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + }, + { + name: "place-content", + longhands: ["align-content", "justify-content"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "place-items", + longhands: ["align-items", "justify-items"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "place-self", + longhands: ["align-self", "justify-self"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_position_try: true, + }, + { + name: "gap", + longhands: ["row-gap", "column-gap"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "grid-area", + longhands: [ + "grid-row-start", "grid-column-start", "grid-row-end", + "grid-column-end" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "grid-column", + longhands: ["grid-column-start", "grid-column-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "grid-lanes", + longhands: [ "grid-template-areas", "grid-template-columns", "masonry-direction", "grid-lanes-fill"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + runtime_flag: "CSSMasonryLayout", + }, + { + name: "grid-lanes-flow", + longhands: ["masonry-direction", "grid-lanes-fill"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSMasonryLayout", + }, + { + name: "grid-row", + longhands: ["grid-row-start", "grid-row-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "grid-template", + longhands: [ + "grid-template-rows", "grid-template-columns", "grid-template-areas" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + }, + { + name: "inset", + longhands: ["top", "right", "bottom", "left"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_position_try: true, + layout_dependent: true, + }, + { + name: "inset-block", + longhands: ["inset-block-start", "inset-block-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_position_try: true, + layout_dependent: true, + }, + { + name: "inset-inline", + longhands: ["inset-inline-start", "inset-inline-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_position_try: true, + layout_dependent: true, + }, + { + name: "list-style", + longhands: ["list-style-position", "list-style-image", "list-style-type"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "margin", + longhands: ["margin-top", "margin-right", "margin-bottom", "margin-left"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + valid_for_position_try: true, + valid_for_permission_element: true, + valid_for_page_context: true, + valid_for_permission_icon: true, + }, + { + name: "margin-block", + longhands: ["margin-block-start", "margin-block-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + valid_for_position_try: true, + valid_for_page_context: true, + }, + { + name: "margin-inline", + longhands: ["margin-inline-start", "margin-inline-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + valid_for_position_try: true, + valid_for_page_context: true, + }, + { + name: "marker", + longhands: ["marker-start", "marker-mid", "marker-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "offset", + longhands: [ + "offset-position", "offset-path", "offset-distance", "offset-rotate", + "offset-anchor" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "outline", + longhands: ["outline-color", "outline-style", "outline-width"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "overflow", + longhands: ["overflow-x", "overflow-y"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + // See comment on overflow-x. + supports_incremental_style: false, + }, + { + name: "overscroll-behavior", + longhands: ["overscroll-behavior-x", "overscroll-behavior-y"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "padding", + longhands: [ + "padding-top", "padding-right", "padding-bottom", "padding-left" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + layout_dependent: true, + supports_incremental_style: true, + valid_for_page_context: true, + }, + { + name: "padding-block", + longhands: ["padding-block-start", "padding-block-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "padding-inline", + longhands: ["padding-inline-start", "padding-inline-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + valid_for_page_context: true, + }, + { + name: "page-break-after", + longhands: ["break-after"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "page-break-before", + longhands: ["break-before"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "page-break-inside", + longhands: ["break-inside"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "position-try", + longhands: ["position-try-order", "position-try-fallbacks"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "rule", + longhands: [ + "column-rule-width", "column-rule-style", "column-rule-color", + "row-rule-width", "row-rule-style", "row-rule-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "rule-break", + longhands: ["row-rule-break", "column-rule-break"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "rule-color", + longhands: ["column-rule-color", "row-rule-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "column-rule-inset", + longhands: ["column-rule-edge-start-inset", "column-rule-edge-end-inset", "column-rule-interior-start-inset", + "column-rule-interior-end-inset"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "row-rule-inset", + longhands: ["row-rule-edge-start-inset", "row-rule-edge-end-inset", "row-rule-interior-start-inset", + "row-rule-interior-end-inset"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "rule-inset", + longhands: ["row-rule-edge-start-inset", "row-rule-edge-end-inset", "row-rule-interior-start-inset", + "row-rule-interior-end-inset", "column-rule-edge-start-inset", "column-rule-edge-end-inset", + "column-rule-interior-start-inset", "column-rule-interior-end-inset"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "rule-width", + longhands: ["column-rule-width", "row-rule-width"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "rule-style", + longhands: ["column-rule-style", "row-rule-style"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "scroll-margin", + longhands: ["scroll-margin-top", "scroll-margin-right", "scroll-margin-bottom", "scroll-margin-left"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-margin-block", + longhands: ["scroll-margin-block-start", "scroll-margin-block-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-margin-inline", + longhands: ["scroll-margin-inline-start", "scroll-margin-inline-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-padding", + longhands: [ + "scroll-padding-top", "scroll-padding-right", "scroll-padding-bottom", + "scroll-padding-left" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-padding-block", + longhands: ["scroll-padding-block-start", "scroll-padding-block-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-padding-inline", + longhands: ["scroll-padding-inline-start", "scroll-padding-inline-end"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "scroll-timeline", + longhands: ["scroll-timeline-name", "scroll-timeline-axis"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "text-decoration", + longhands: ["text-decoration-line", "text-decoration-thickness", "text-decoration-style", "text-decoration-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + computable: true, + valid_for_page_context: true, + }, + { + name: "transition", + longhands: [ + "transition-property", "transition-duration", + "transition-timing-function", "transition-delay", + "transition-behavior", + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + // Animation properites are never incremental. + supports_incremental_style: false, + }, + { + name: "view-timeline", + longhands: ["view-timeline-name", "view-timeline-axis", "view-timeline-inset"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "-webkit-border-after", + alias_for: "border-block-end", + }, + { + name: "-webkit-border-before", + alias_for: "border-block-start", + }, + { + name: "-webkit-border-end", + alias_for: "border-inline-end", + }, + { + name: "-webkit-border-start", + alias_for: "border-inline-start", + }, + { + name: "-webkit-column-break-after", + longhands: ["break-after"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "-webkit-column-break-before", + longhands: ["break-before"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "-webkit-column-break-inside", + longhands: ["break-inside"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "column-rule", + longhands: [ + "column-rule-width", "column-rule-style", "column-rule-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "row-rule", + longhands: [ + "row-rule-width", "row-rule-style", "row-rule-color" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + runtime_flag: "CSSGapDecoration", + }, + { + name: "columns", + longhands: ["column-width", "column-count", "column-height", "column-wrap"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "mask", + longhands: [ + "mask-image", "-webkit-mask-position-x", + "-webkit-mask-position-y", "mask-size", "mask-repeat", "mask-origin", + "mask-clip", "mask-composite", "mask-mode" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "-webkit-mask", + alias_for: "mask", + }, + { + name: "-webkit-mask-box-image", + longhands: [ + "-webkit-mask-box-image-source", "-webkit-mask-box-image-slice", + "-webkit-mask-box-image-width", "-webkit-mask-box-image-outset", + "-webkit-mask-box-image-repeat" + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + computable: true, + }, + { + name: "mask-position", + longhands: ["-webkit-mask-position-x", "-webkit-mask-position-y"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + computable: true, + }, + { + name: "-webkit-mask-position", + alias_for: "mask-position", + }, + { + name: "text-emphasis", + longhands: ["text-emphasis-style", "text-emphasis-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + { + name: "timeline-trigger", + longhands: [ + "timeline-trigger-name", "timeline-trigger-source", + "timeline-trigger-range-start", "timeline-trigger-range-end", + "timeline-trigger-exit-range-start", "timeline-trigger-exit-range-end", + ], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + supports_incremental_style: false, + valid_for_keyframe: false, + runtime_flag: "TimelineTrigger", + }, + { + name: "-webkit-text-stroke", + longhands: ["-webkit-text-stroke-width", "-webkit-text-stroke-color"], + property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"], + }, + // Visited properties. + { + name: "-internal-visited-color", + visited_property_for: "color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kBlack)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + style_builder_custom_functions: ["initial", "inherit", "value"], + priority: 1, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + valid_for_highlight: true, + is_visited_highlight_colors: true, + invalidate: ["border-visual", "color", "currentcolor"], + }, + { + name: "-internal-visited-caret-color", + visited_property_for: "caret-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_auto_color.h"], + default_value: "StyleAutoColor::AutoColor()", + type_name: "StyleAutoColor", + converter: "ConvertStyleAutoColor", + computed_style_protected_functions: ["getter"], + style_builder_template: "visited_color", + style_builder_template_args: { + initial_color: "StyleAutoColor::AutoColor", + }, + invalidate: ["color"], + }, + { + name: "-internal-visited-column-rule-color", + visited_property_for: "column-rule-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/style/gap_data_list.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "GapDataList::DefaultGapColorDataList()", + type_name: "GapDataList", + converter: "ConvertGapDecorationColorDataList", + invalidate: ["paint"], + }, + { + name: "-internal-visited-background-color", + visited_property_for: "background-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(Color::kTransparent)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialBackgroundColor", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + is_visited_highlight_colors: true, + invalidate: ["background-color"], + }, + { + name: "-internal-visited-border-left-color", + visited_property_for: "border-left-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_first_letter: true, + logical_property_group: { + name: "visited-border-color", + resolver: "left", + }, + invalidate: ["border-outline-visited-color"], + }, + { + name: "-internal-visited-border-right-color", + visited_property_for: "border-right-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_first_letter: true, + logical_property_group: { + name: "visited-border-color", + resolver: "right", + }, + invalidate: ["border-outline-visited-color"], + }, + { + name: "-internal-visited-border-top-color", + visited_property_for: "border-top-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_first_letter: true, + logical_property_group: { + name: "visited-border-color", + resolver: "top", + }, + invalidate: ["border-outline-visited-color"], + }, + { + name: "-internal-visited-border-bottom-color", + visited_property_for: "border-bottom-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_first_letter: true, + logical_property_group: { + name: "visited-border-color", + resolver: "bottom", + }, + invalidate: ["border-outline-visited-color"], + }, + { + name: "-internal-visited-border-inline-start-color", + visited_property_for: "border-inline-start-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "visited-border-color", + resolver: "inline-start", + }, + valid_for_first_letter: true, + }, + { + name: "-internal-visited-border-inline-end-color", + visited_property_for: "border-inline-end-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "visited-border-color", + resolver: "inline-end", + }, + valid_for_first_letter: true, + }, + { + name: "-internal-visited-border-block-start-color", + visited_property_for: "border-block-start-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "visited-border-color", + resolver: "block-start", + }, + valid_for_first_letter: true, + }, + { + name: "-internal-visited-border-block-end-color", + visited_property_for: "border-block-end-color", + property_methods: ["ParseSingleValue"], + logical_property_group: { + name: "visited-border-color", + resolver: "block-end", + }, + valid_for_first_letter: true, + }, + { + name: "-internal-visited-fill", + visited_property_for: "fill", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "svginherited->fill", + field_template: "external", + type_name: "SVGPaint", + include_paths: ["third_party/blink/renderer/core/style/svg_paint.h"], + default_value: "SVGPaint(Color::kBlack)", + name_for_methods: "InternalVisitedFillPaint", + converter: "ConvertSVGPaint", + style_builder_template: "visited_color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialFillPaint", + }, + valid_for_highlight: true, + }, + { + name: "-internal-visited-outline-color", + visited_property_for: "outline-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_cue: true, + invalidate: ["border-outline-visited-color"], + }, + { + name: "-internal-visited-stroke", + visited_property_for: "stroke", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "svginherited->stroke", + field_template: "external", + type_name: "SVGPaint", + include_paths: ["third_party/blink/renderer/core/style/svg_paint.h"], + default_value: "SVGPaint()", + name_for_methods: "InternalVisitedStrokePaint", + converter: "ConvertSVGPaint", + style_builder_template: "visited_color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialStrokePaint", + }, + valid_for_highlight: true, + invalidate: ["paint"], + }, + { + name: "-internal-visited-text-decoration-color", + visited_property_for: "text-decoration-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + field_group: "misc->visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_highlight: true, + invalidate: ["color"], + }, + { + name: "-internal-visited-text-emphasis-color", + visited_property_for: "text-emphasis-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_highlight: true, + invalidate: ["color"], + }, + { + name: "-internal-visited-text-fill-color", + visited_property_for: "-webkit-text-fill-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_highlight: true, + invalidate: ["color"], + }, + { + name: "-internal-visited-text-stroke-color", + visited_property_for: "-webkit-text-stroke-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_visited", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "visited_color", + valid_for_highlight: true, + invalidate: ["color"], + }, + // Forced colors properties. + { + name: "-internal-forced-background-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + field_group: "misc->forced_colors", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css_value_keywords.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(CSSValueID::kCanvas)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "color", + style_builder_template_args: { + initial_color: "ComputedStyleInitialValues::InitialInternalForcedBackgroundColor", + }, + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + }, + { + name: "-internal-forced-border-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + field_group: "misc->forced_colors", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_first_letter: true, + }, + { + name: "-internal-forced-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_forced_colors", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css_value_keywords.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(CSSValueID::kCanvastext)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + style_builder_custom_functions: ["initial", "inherit", "value"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + }, + { + name: "-internal-forced-outline-color", + property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "ColorIncludingFallback"], + field_group: "misc->forced_colors", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor::CurrentColor()", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + converter: "ConvertStyleColor", + style_builder_template: "color", + valid_for_cue: true, + }, + { + name: "-internal-forced-visited-color", + visited_property_for: "-internal-forced-color", + property_methods: ["ParseSingleValue", "ColorIncludingFallback"], + inherited: true, + field_group: "inherited->inherited_forced_colors", + field_template: "external", + include_paths: ["third_party/blink/renderer/core/css_value_keywords.h", + "third_party/blink/renderer/core/css/style_color.h"], + default_value: "StyleColor(CSSValueID::kCanvastext)", + type_name: "StyleColor", + computed_style_protected_functions: ["getter"], + style_builder_custom_functions: ["initial", "inherit", "value"], + valid_for_first_letter: true, + valid_for_first_line: true, + valid_for_cue: true, + valid_for_marker: true, + }, + // Name: -internal-empty-line-height: + // Value: none | fabricated + // If the element is inline or contains visible text, this property has + // no effect. + // + // 'none' + // The box's intrinsic height is 0, and it defines no baseline. + // 'fabricated' + // The box has intrinsic height and baseline, computed from the current + // font metrics. + { + name: "-internal-empty-line-height", + property_methods: ["ParseSingleValue" ], + inherited: true, + field_group: "*", + field_template: "primitive", + type_name: "bool", + default_value: "false", + name_for_methods: "HasLineIfEmpty", + converter: "ConvertInternalEmptyLineHeight", + }, + { + name: "-internal-align-content-block", + property_methods: ["ParseSingleValue"], + inherited: false, + field_group: "*", + field_template: "primitive", + type_name: "bool", + default_value: "false", + name_for_methods: "AlignContentBlockCenter", + converter: "ConvertInternalAlignContentBlock", + invalidate: ["layout"], + }, + // Aliases; these map to the same CSSPropertyID + { + name: "-epub-caption-side", + alias_for: "caption-side", + }, + { + name: "-epub-text-combine", + alias_for: "-webkit-text-combine", + }, + { + name: "-epub-text-emphasis", + alias_for: "text-emphasis", + }, + { + name: "-epub-text-emphasis-color", + alias_for: "text-emphasis-color", + }, + { + name: "-epub-text-emphasis-style", + alias_for: "text-emphasis-style", + }, + { + name: "-epub-text-orientation", + alias_for: "-webkit-text-orientation", + }, + { + name: "-epub-text-transform", + alias_for: "text-transform", + }, + { + name: "-epub-word-break", + alias_for: "word-break", + }, + { + name: "-epub-writing-mode", + alias_for: "-webkit-writing-mode", + }, + { + name: "-webkit-align-content", + alias_for: "align-content", + }, + { + name: "-webkit-align-items", + alias_for: "align-items", + }, + { + name: "-webkit-align-self", + alias_for: "align-self", + }, + { + name: "-webkit-animation", + alias_for: "animation", + }, + { + name: "-webkit-animation-delay", + alias_for: "animation-delay", + }, + { + name: "-webkit-animation-direction", + alias_for: "animation-direction", + }, + { + name: "-webkit-animation-duration", + alias_for: "animation-duration", + }, + { + name: "-webkit-animation-fill-mode", + alias_for: "animation-fill-mode", + }, + { + name: "-webkit-animation-iteration-count", + alias_for: "animation-iteration-count", + }, + { + name: "-webkit-animation-name", + alias_for: "animation-name", + }, + { + name: "-webkit-animation-play-state", + alias_for: "animation-play-state", + }, + { + name: "-webkit-animation-timing-function", + alias_for: "animation-timing-function", + }, + { + name: "-webkit-backface-visibility", + alias_for: "backface-visibility", + }, + { + name: "-webkit-background-clip", + alias_for: "background-clip", + }, + // -webkit-background-origin accepts "content", "padding", and "border" + // values. See crbug.com/604023 + { + name: "-webkit-background-origin", + alias_for: "background-origin" + }, + // "-webkit-background-size: 10px" behaves as "background-size: 10px 10px" + { + name: "-webkit-background-size", + alias_for: "background-size", + }, + { + name: "-webkit-border-bottom-left-radius", + alias_for: "border-bottom-left-radius", + }, + { + name: "-webkit-border-bottom-right-radius", + alias_for: "border-bottom-right-radius", + }, + // "-webkit-border-radius: 1px 2px" behaves as "border-radius: 1px / 2px" + { + name: "-webkit-border-radius", + alias_for: "border-radius", + }, + { + name: "-webkit-border-top-left-radius", + alias_for: "border-top-left-radius", + }, + { + name: "-webkit-border-top-right-radius", + alias_for: "border-top-right-radius", + }, + { + name: "-webkit-box-shadow", + alias_for: "box-shadow", + }, + { + name: "-webkit-box-sizing", + alias_for: "box-sizing", + }, + { + name: "-webkit-clip-path", + alias_for: "clip-path", + }, + { + name: "-webkit-column-count", + alias_for: "column-count", + }, + { + name: "-webkit-column-gap", + alias_for: "column-gap", + }, + { + name: "-webkit-column-rule", + alias_for: "column-rule", + }, + { + name: "-webkit-column-rule-color", + alias_for: "column-rule-color", + }, + { + name: "-webkit-column-rule-style", + alias_for: "column-rule-style", + }, + { + name: "-webkit-column-rule-width", + alias_for: "column-rule-width", + }, + { + name: "-webkit-column-span", + alias_for: "column-span", + }, + { + name: "-webkit-column-width", + alias_for: "column-width", + }, + { + name: "-webkit-columns", + alias_for: "columns", + }, + { + name: "-webkit-filter", + alias_for: "filter", + }, + { + name: "-webkit-flex", + alias_for: "flex", + }, + { + name: "-webkit-flex-basis", + alias_for: "flex-basis", + }, + { + name: "-webkit-flex-direction", + alias_for: "flex-direction", + }, + { + name: "-webkit-flex-flow", + alias_for: "flex-flow", + }, + { + name: "-webkit-flex-grow", + alias_for: "flex-grow", + }, + { + name: "-webkit-flex-shrink", + alias_for: "flex-shrink", + }, + { + name: "-webkit-flex-wrap", + alias_for: "flex-wrap", + }, + { + name: "-webkit-font-feature-settings", + alias_for: "font-feature-settings", + }, + { + name: "-webkit-hyphenate-character", + alias_for: "hyphenate-character", + }, + { + name: "-webkit-justify-content", + alias_for: "justify-content", + }, + { + name: "-webkit-opacity", + alias_for: "opacity", + }, + { + name: "-webkit-order", + alias_for: "order", + }, + { + name: "-webkit-perspective", + alias_for: "perspective", + }, + { + name: "-webkit-perspective-origin", + alias_for: "perspective-origin", + }, + { + name: "-webkit-shape-image-threshold", + alias_for: "shape-image-threshold", + }, + { + name: "-webkit-shape-margin", + alias_for: "shape-margin", + }, + { + name: "-webkit-shape-outside", + alias_for: "shape-outside", + }, + { + name: "-webkit-text-emphasis", + alias_for: "text-emphasis", + }, + { + name: "-webkit-text-emphasis-color", + alias_for: "text-emphasis-color", + }, + { + name: "-webkit-text-emphasis-position", + alias_for: "text-emphasis-position", + }, + { + name: "-webkit-text-emphasis-style", + alias_for: "text-emphasis-style", + }, + { + name: "-webkit-text-size-adjust", + alias_for: "text-size-adjust", + }, + { + name: "-webkit-transform", + alias_for: "transform", + }, + { + name: "-webkit-transform-origin", + alias_for: "transform-origin", + }, + { + name: "-webkit-transform-style", + alias_for: "transform-style", + }, + { + name: "-webkit-transition", + alias_for: "transition", + }, + { + name: "-webkit-transition-delay", + alias_for: "transition-delay", + }, + { + name: "-webkit-transition-duration", + alias_for: "transition-duration", + }, + { + name: "-webkit-transition-property", + alias_for: "transition-property", + }, + { + name: "-webkit-transition-timing-function", + alias_for: "transition-timing-function", + }, + { + name: "-webkit-user-select", + alias_for: "user-select", + }, + { + name: "word-wrap", + alias_for: "overflow-wrap", + }, + { + name: "grid-column-gap", + alias_for: "column-gap", + }, + { + name: "grid-row-gap", + alias_for: "row-gap", + }, + { + name: "grid-gap", + alias_for: "gap", + }, + ], +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 67227b62fb..13fc24a354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -73,7 +82,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -153,6 +162,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "app_units" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467b60e4ee6761cd6fd4e03ea58acefc8eec0d1b1def995c1b3b783fa7be8a60" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -188,12 +230,41 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "az" version = "1.2.1" @@ -241,6 +312,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -253,6 +330,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block2" version = "0.5.1" @@ -262,6 +345,12 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.17.0" @@ -280,6 +369,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -297,7 +392,7 @@ dependencies = [ "polling", "rustix 0.38.44", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -341,7 +436,17 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", ] [[package]] @@ -366,11 +471,9 @@ dependencies = [ "figma-api", "futures", "gl", - "glutin", - "glutin-winit", "json-patch", "math2", - "raw-window-handle", + "pulldown-cmark", "reqwest", "rstar", "seahash", @@ -379,8 +482,7 @@ dependencies = [ "skia-safe", "taffy", "tokio", - "uuid", - "winit", + "usvg", ] [[package]] @@ -402,7 +504,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -483,12 +585,28 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -508,6 +626,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -548,6 +679,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -624,6 +764,48 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "csscascade" +version = "0.0.0" +dependencies = [ + "atomic_refcell", + "cssparser", + "euclid", + "html5ever", + "markup5ever", + "selectors", + "stylo", + "stylo_atoms", + "stylo_dom", + "stylo_traits", + "tendril", + "url", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "serde", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -665,6 +847,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "deranged" version = "0.4.0" @@ -675,6 +863,39 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dify" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" +dependencies = [ + "anyhow", + "colored", + "getopts", + "image", + "rayon", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -723,12 +944,33 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -738,6 +980,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -754,12 +1016,66 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "figma-api" version = "0.31.3" @@ -808,12 +1124,41 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "fonts" version = "0.2.0" @@ -875,6 +1220,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -974,6 +1329,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -981,8 +1345,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -992,9 +1358,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -1025,9 +1403,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "glutin" @@ -1113,10 +1491,37 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +name = "grida-dev" +version = "0.0.0" +dependencies = [ + "anyhow", + "cg", + "clap", + "dify", + "figma-api", + "futures", + "gl", + "glob", + "glutin", + "glutin-winit", + "image", + "indicatif", + "math2", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "skia-safe", + "tokio", + "toml 0.9.8", + "winit", +] + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -1190,6 +1595,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "html5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.3.1" @@ -1264,6 +1679,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1345,6 +1761,22 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.0.0" @@ -1358,6 +1790,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" + [[package]] name = "icu_normalizer" version = "2.0.0" @@ -1418,6 +1856,30 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e30e593cf9c3ca2f51aa312eb347cd1ba95715e91a842ec3fc9058eab2af4b" +dependencies = [ + "core_maths", + "displaydoc", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5360a2fbe97f617c4f8b944356dedb36d423f7da7f13c070995cf89e59f01220" + [[package]] name = "ident_case" version = "1.0.1" @@ -1445,6 +1907,52 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "1.9.3" @@ -1468,6 +1976,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1510,6 +2042,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1519,6 +2060,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1536,7 +2086,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -1576,7 +2126,7 @@ dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1595,12 +2145,45 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.8" @@ -1646,16 +2229,84 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_size_of_derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44db74bde26fdf427af23f1d146c211aed857c59e3be750cf2617f6b0b05c94" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + +[[package]] +name = "markup5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "math2" version = "0.0.2" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1690,6 +2341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1703,6 +2355,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "moxcms" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -1732,7 +2394,7 @@ dependencies = [ "ndk-sys", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1750,6 +2412,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -1760,12 +2428,68 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1776,6 +2500,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -1797,6 +2531,12 @@ dependencies = [ "syn", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "objc-sys" version = "0.3.5" @@ -2133,23 +2873,111 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] [[package]] -name = "pin-project" -version = "1.1.10" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "pin-project-internal", + "cfg-if", + "libc", + "redox_syscall 0.5.12", + "smallvec", + "windows-link 0.2.1", ] [[package]] -name = "pin-project-internal" -version = "1.1.10" +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ @@ -2204,6 +3032,19 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.8.0" @@ -2219,12 +3060,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ + "serde", "zerovec", ] @@ -2234,6 +3082,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.33" @@ -2262,6 +3125,68 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -2271,6 +3196,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2286,6 +3266,115 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2385,6 +3474,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -2392,6 +3483,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2399,8 +3491,15 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.17.14" @@ -2415,6 +3514,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rstar" version = "0.12.2" @@ -2471,6 +3576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2483,6 +3589,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -2503,6 +3610,24 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.20" @@ -2533,6 +3658,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -2575,6 +3706,26 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.33.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "bitflags 2.9.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", + "to_shmem", + "to_shmem_derive", +] + [[package]] name = "serde" version = "1.0.226" @@ -2630,9 +3781,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -2679,12 +3839,51 @@ dependencies = [ "syn", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "serde", + "stable_deref_trait", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skia-bindings" version = "0.89.0" @@ -2699,7 +3898,7 @@ dependencies = [ "regex", "serde_json", "tar", - "toml", + "toml 0.9.8", ] [[package]] @@ -2744,11 +3943,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "smallbitvec" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31d263dd118560e1a492922182ab6ca6dc1d03a3bf54e7699993f31a4150e3f" + [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" @@ -2756,61 +3961,230 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.1", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", + "bitflags 2.9.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stylo" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "app_units", + "arrayvec", + "atomic_refcell", + "bitflags 2.9.1", + "byteorder", + "cssparser", + "derive_more", + "encoding_rs", + "euclid", + "icu_segmenter", + "indexmap 2.11.4", + "itertools 0.14.0", + "itoa", + "lazy_static", + "log", + "malloc_size_of_derive", + "matches", + "mime", + "new_debug_unreachable", + "num-derive", + "num-integer", + "num-traits", + "num_cpus", + "parking_lot", + "precomputed-hash", + "rayon", + "rayon-core", + "rustc-hash", + "selectors", + "serde", + "servo_arc", + "smallbitvec", + "smallvec", + "static_assertions", + "string_cache", + "stylo_atoms", + "stylo_config", + "stylo_derive", + "stylo_dom", + "stylo_malloc_size_of", + "stylo_static_prefs", + "stylo_traits", + "thin-vec", + "to_shmem", + "to_shmem_derive", + "uluru", + "url", + "void", + "walkdir", + "web_atoms", +] + +[[package]] +name = "stylo_atoms" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "string_cache", + "string_cache_codegen", ] [[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +name = "stylo_config" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" + +[[package]] +name = "stylo_derive" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" dependencies = [ - "serde", + "darling", + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +name = "stylo_dom" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" dependencies = [ - "libc", - "windows-sys 0.52.0", + "bitflags 2.9.1", + "stylo_malloc_size_of", ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +name = "stylo_malloc_size_of" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "app_units", + "cssparser", + "euclid", + "selectors", + "servo_arc", + "smallbitvec", + "smallvec", + "string_cache", + "thin-vec", + "void", +] [[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +name = "stylo_static_prefs" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" [[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +name = "stylo_traits" +version = "0.9.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "app_units", + "bitflags 2.9.1", + "cssparser", + "euclid", + "malloc_size_of_derive", + "selectors", + "serde", + "servo_arc", + "stylo_atoms", + "stylo_malloc_size_of", + "thin-vec", + "to_shmem", + "to_shmem_derive", + "url", +] [[package]] name = "subtle" @@ -2818,6 +4192,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svgtypes" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9165405c8cf6bdc11d4417ef1c8c90521ebbadffe7da5c67bb5c1b90a14aeb1" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "2.0.101" @@ -2870,6 +4254,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.22", + "version-compare", +] + [[package]] name = "taffy" version = "0.9.1" @@ -2893,6 +4290,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.20.0" @@ -2906,13 +4309,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -2926,6 +4355,31 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.41" @@ -3002,6 +4456,46 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "to_shmem" +version = "0.3.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "cssparser", + "servo_arc", + "smallbitvec", + "smallvec", + "string_cache", + "thin-vec", +] + +[[package]] +name = "to_shmem_derive" +version = "0.1.0" +source = "git+https://github.com/gridaco/stylo#ae78004b3056c90b66dbba4d92edc0edbc24bc49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "tokio" version = "1.45.1" @@ -3064,14 +4558,26 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.9", + "toml_edit", +] + +[[package]] +name = "toml" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap 2.11.4", "serde_core", - "serde_spanned", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", @@ -3082,12 +4588,15 @@ name = "toml_datetime" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -3099,24 +4608,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap 2.11.4", + "serde", + "serde_spanned 0.6.9", "toml_datetime 0.6.9", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" @@ -3193,6 +4704,9 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] [[package]] name = "typenum" @@ -3200,18 +4714,75 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -3227,8 +4798,41 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.0.1" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "once_cell", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3242,13 +4846,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "uuid" -version = "1.17.0" +name = "v_frame" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" dependencies = [ - "getrandom 0.3.3", - "js-sys", + "aligned-vec", + "num-traits", "wasm-bindgen", ] @@ -3258,12 +4862,24 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "walkdir" version = "2.5.0" @@ -3498,6 +5114,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd0c322f146d0f8aad130ce6c187953889359584497dac6561204c8e17bb43d" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi-util" version = "0.1.9" @@ -3515,7 +5158,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings 0.4.2", ] @@ -3548,6 +5191,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" version = "0.4.0" @@ -3565,7 +5214,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -3574,7 +5223,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -3583,7 +5232,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -4004,6 +5653,12 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yoke" version = "0.8.0" @@ -4107,3 +5762,27 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 426d621709..aee87624bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ members = [ "crates/grida-canvas", "crates/grida-canvas-wasm", "crates/grida-canvas-fonts", - "crates/math2", -] \ No newline at end of file + "crates/grida-dev", + "crates/csscascade", + "crates/math2", +] diff --git a/crates/OPTIMIZATION_SIZE.md b/crates/OPTIMIZATION_SIZE.md new file mode 100644 index 0000000000..f8e57e9506 --- /dev/null +++ b/crates/OPTIMIZATION_SIZE.md @@ -0,0 +1,32 @@ +## Inspecting & Optimizing Bundle Size + +Keep the WebAssembly bundle as small as possible. Heavy transitive dependencies (e.g. `image`, `regex`, `reqwest`, full TLS stacks) can add multiple megabytes. + +### Tools + +**1. `cargo-bloat` — identify size-heavy crates** + +```bash +cargo bloat --release --crates +cargo bloat --release --target wasm32-unknown-unknown --crates +``` + +Shows ranked list of crate size contributors. Works for `wasm32-unknown-emscripten` — analyzes Rust code before Emscripten adds JS/glue, so attribution is accurate. + +**2. `cargo tree` — trace dependency paths** + +```bash +cargo tree -i crate_name # see why a crate is included +cargo tree -d # find duplicate dependencies +``` + +Use `-i` to see which path brought in a heavy dependency. Use `-d` to find duplicates (same crate with different versions/features) that bloat the bundle. + +### Workflow + +1. Run `cargo bloat --crates` before/after adding dependencies +2. Identify large crates in the output +3. Use `cargo tree -i crate_name` to trace the dependency path +4. Check `cargo tree -d` for duplicates and resolve version conflicts +5. Remove, replace, or feature-gate heavy dependencies +6. Use `[workspace.dependencies]` in `Cargo.toml` to unify versions diff --git a/crates/csscascade/ARCHITECTURE.md b/crates/csscascade/ARCHITECTURE.md new file mode 100644 index 0000000000..692540a509 --- /dev/null +++ b/crates/csscascade/ARCHITECTURE.md @@ -0,0 +1,740 @@ +# csscascade Architecture + +> **Note**: This document does not always reflect the current state of the implementation. The content may be incorrect, ahead of, or behind the actual implementation. + +**Last updated**: 2025-11-25 + +## Overview + +`csscascade` is a **CSS Cascade & Style Resolution Engine** designed for building browser-like rendering pipelines. Unlike `usvg` (which is a full SVG parser), `csscascade` focuses specifically on the CSS cascade and style resolution step. + +The project now embeds [**Stylo**](https://github.com/servo/stylo)—Servo’s production CSS engine—as its core. Instead of re‑implementing parsing, selectors, and cascade logic ourselves, we feed DOM + stylesheet data into Stylo (which runs happily on `wasm32-unknown-emscripten`) and then solve every problem Stylo intentionally leaves to embedders: DOM traversal, HTML/SVG attribute normalization, font parsing/selection, layout integration (e.g. via `taffy`), and renderer hand-off. + +**Key Difference from usvg:** + +- `usvg`: Parses SVG XML → Resolves CSS → Outputs simplified tree +- `csscascade`: Accepts already-parsed DOM tree → Resolves CSS → Outputs style-resolved tree + +**Primary Use Case:** DOM Tree → CSS Cascade → Style-Resolved Tree → Layout → Render + +This crate implements the hardest and most fundamental part of a rendering engine: the transformation from loosely-typed DOM nodes + CSS rules into a **fully computed, normalized, strongly-typed tree**, now powered by Stylo. + +### “Isn’t a full CSS engine overkill?” + +Stylo is far lighter than it sounds—our wasm bundles are roughly ~1.5 MB with `wasm-bindgen` and about ~2.5 MB when targeting `wasm32-unknown-emscripten`. Achieving full CSS3 compliance (selectors, specificity, media rules, shorthands, inheritance, etc.) is notoriously complex, and any serious renderer eventually needs browser-grade behavior. Stylo already delivers that accuracy, so we lean on it and concentrate on the remaining pieces (DOM adapters, fonts, layout, rendering). + +## Design Philosophy + +### Engine-Agnostic + +- DOM input is **not** tied to any specific parser +- Works with any DOM-like structure implementing the crate's DOM traits +- You bring your own parser (html5ever, roxmltree, pulldown-cmark output, etc.) + +### Format-Agnostic + +- HTML and SVG share a unified style pipeline +- Same cascade logic for both formats +- Future support for SVG is planned (HTML + SVG share >90% of style logic) + +### Separation of Concerns + +- **csscascade does NOT:** + + - Parse HTML/SVG (bring your own parser) + - Perform layout (block, inline, flex, grid) + - Paint or rasterize + - Handle JavaScript or dynamic updates + +- **csscascade DOES:** + - Parse and walk an HTML/XML tree (via DOM traits) + - Perform full CSS cascade (selector matching, specificity, inheritance) + - Produce a style-resolved static tree + - Normalize presentation attributes (SVG-ready) + - Expand CSS shorthands + +## System Architecture + +### High-Level Pipeline + +``` +Input DOM Tree (HTML / XML / SVG) + ↓ +[Your Parser] → DOM-like structure (implements csscascade DOM traits) + ↓ +[csscascade front-end] → CSS collection, DOM adapters, font/runtime setup + ↓ +[Stylo] → CSS Cascade & Style Resolution + ↓ +Style-Resolved Static Tree (fully computed styles) + ↓ +[Your Layout Engine (e.g. taffy)] → Layout-computed tree + ↓ +[Your Painter] → Rendered Output +``` + +### Module Structure (Planned) + +Taking direct inspiration from `usvg`’s clean pipeline (parse → collect CSS → resolve → emit tree), but letting **html5ever** and **Stylo** absorb most of the heavy lifting, the crate structure collapses to a comparatively small set of coordination modules: + +``` +csscascade/ +├── src/ +│ ├── lib.rs # Public API (Cascade, StyledTree, config) +│ ├── pipeline.rs # High-level orchestrator (bytes/DOM → StyledTree) +│ ├── html/ +│ │ ├── mod.rs # html5ever entrypoints (stream -> rcdom) +│ │ └── rcdom.rs # Thin wrappers exposing rcdom nodes as DomNode/Element +│ ├── dom/ +│ │ ├── mod.rs # DomNode trait + traversal/interning helpers +│ │ └── snapshot.rs # Optional adapters for pre-built DOMs (markdown, JSON, etc.) +│ ├── stylesheets/ +│ │ ├── mod.rs # Two-pass CSS collector (style/link tags, injected CSS) +│ │ └── loader.rs # Resource resolution hooks (data:, file:, remote) +│ ├── stylo_bridge/ +│ │ ├── mod.rs # Device + Stylist orchestration +│ │ ├── font_provider.rs # Stylo FontMetricsProvider + font fallback pipeline +│ │ └── media.rs # Viewport, DPR, prefers-color-scheme plumbing +│ ├── cascade/ +│ │ ├── mod.rs # usvg-style two-pass coordinator +│ │ └── presenter.rs # Presentation attribute + HTML/SVG default mapping +│ ├── tree/ +│ │ ├── mod.rs # StyledTree / StyledNode definitions +│ │ └── computed.rs # ComputedStyle wrappers, caching, diff helpers +│ ├── layout_hooks/ +│ │ └── mod.rs # Optional adapters for taffy/other layout engines +│ └── utils/ +│ ├── cfg.rs # Feature flags, logging, panic hooks +│ └── normalize.rs # Shared value/attribute normalization helpers +``` + +> **MVP status:** right now only the `tree` module exists in code. The rest of the layout above is aspirational; we’re intentionally building the first proof-of-concept entirely inside `src/tree/mod.rs` to validate APIs before fanning out. + +### Component Interactions + +``` +┌─────────────┐ +│ DOM Tree │ (from user's parser) +└──────┬──────┘ + │ + ▼ +┌─────────────────┐ +│ CSS Collection │ ← Two-Pass Pattern +│ (First Pass) │ +└──────┬──────────┘ + │ + ▼ +┌─────────────────┐ +│ Stylesheet │ (all CSS rules collected) +└──────┬──────────┘ + │ + ▼ +┌─────────────────┐ +│ csscascade host │ ← Second Pass controller +│ (per element) │ +└──────┬──────────┘ + │ + ▼ +┌─────────────────┐ +│ Stylo │ ← Selector matching, specificity, inheritance, +│ (cascade core) │ computed values, shorthand expansion +└──────┬──────────┘ + │ + ▼ +┌─────────────────┐ +│ StyledTree │ (output + font/layout metadata) +└─────────────────┘ +``` + +### Two-Pass CSS Collection (Inspired by usvg) + +csscascade follows the same two-pass pattern as usvg: + +1. **First Pass**: Collect all CSS from ` + + +
Hello Stylo
+ + +"# + .trim() + .to_string(), + None, + )) + } +} diff --git a/crates/csscascade/examples/html2html.rs b/crates/csscascade/examples/html2html.rs new file mode 100644 index 0000000000..f13bcbd34b --- /dev/null +++ b/crates/csscascade/examples/html2html.rs @@ -0,0 +1,46 @@ +// Copyright 2014-2017 The html5ever Project Developers. See the +// COPYRIGHT file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Parse and re-serialize a HTML5 document. +//! +//! This is meant to produce the exact same output (ignoring stderr) as +//! +//! java -classpath htmlparser-1.4.jar nu.validator.htmlparser.tools.HTML2HTML +//! +//! where htmlparser-1.4.jar comes from http://about.validator.nu/htmlparser/ + +use std::io::{self, Write}; + +use csscascade::rcdom::{RcDom, SerializableHandle}; +use html5ever::driver::ParseOpts; +use html5ever::tendril::TendrilSink; +use html5ever::tree_builder::TreeBuilderOpts; +use html5ever::{parse_document, serialize}; + +fn main() { + let opts = ParseOpts { + tree_builder: TreeBuilderOpts { + drop_doctype: true, + ..Default::default() + }, + ..Default::default() + }; + let stdin = io::stdin(); + let dom = parse_document(RcDom::default(), opts) + .from_utf8() + .read_from(&mut stdin.lock()) + .unwrap(); + + // The validator.nu HTML2HTML always prints a doctype at the very beginning. + io::stdout() + .write_all(b"\n") + .expect("writing DOCTYPE failed"); + let document: SerializableHandle = dom.document.clone().into(); + serialize(&mut io::stdout(), &document, Default::default()).expect("serialization failed"); +} diff --git a/crates/csscascade/examples/print_rcdom.rs b/crates/csscascade/examples/print_rcdom.rs new file mode 100644 index 0000000000..c12d97d5e5 --- /dev/null +++ b/crates/csscascade/examples/print_rcdom.rs @@ -0,0 +1,78 @@ +// Copyright 2014-2017 The html5ever Project Developers. See the +// COPYRIGHT file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate html5ever; + +use std::io; + +use csscascade::rcdom::{Handle, NodeData, RcDom}; + +use html5ever::parse_document; +use html5ever::tendril::TendrilSink; + +// This is not proper HTML serialization, of course. + +fn walk(indent: usize, handle: &Handle) { + let node = handle; + for _ in 0..indent { + print!(" "); + } + match node.data { + NodeData::Document => println!("#Document"), + + NodeData::Doctype { + ref name, + ref public_id, + ref system_id, + } => println!(""), + + NodeData::Text { ref contents } => { + println!("#text: {}", contents.borrow().escape_default()) + } + + NodeData::Comment { ref contents } => println!("", contents.escape_default()), + + NodeData::Element { + ref name, + ref attrs, + .. + } => { + assert!(name.ns == ns!(html)); + print!("<{}", name.local); + for attr in attrs.borrow().iter() { + assert!(attr.name.ns == ns!()); + print!(" {}=\"{}\"", attr.name.local, attr.value); + } + println!(">"); + } + + NodeData::ProcessingInstruction { .. } => unreachable!(), + } + + for child in node.children.borrow().iter() { + walk(indent + 4, child); + } +} + +fn main() { + let stdin = io::stdin(); + let dom = parse_document(RcDom::default(), Default::default()) + .from_utf8() + .read_from(&mut stdin.lock()) + .unwrap(); + walk(0, &dom.document); + + if !dom.errors.borrow().is_empty() { + println!("\nParse errors:"); + for err in dom.errors.borrow().iter() { + println!(" {err}"); + } + } +} diff --git a/crates/csscascade/examples/print_tree.rs b/crates/csscascade/examples/print_tree.rs new file mode 100644 index 0000000000..d05cf52d66 --- /dev/null +++ b/crates/csscascade/examples/print_tree.rs @@ -0,0 +1,22 @@ +use csscascade::tree::{Tree, WriteOptions}; +use std::env; +use std::fs; +use std::io::{self, Read}; + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + let html = if args.len() > 1 { + fs::read_to_string(&args[1])? + } else { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + buffer + }; + + let tree = Tree::from_str(&html)?; + let verbose = tree.to_string(&WriteOptions::ComputedValues { + include_root: (true), + })?; + println!("{verbose}"); + Ok(()) +} diff --git a/crates/csscascade/src/lib.rs b/crates/csscascade/src/lib.rs new file mode 100644 index 0000000000..aec1e873af --- /dev/null +++ b/crates/csscascade/src/lib.rs @@ -0,0 +1,10 @@ +//! csscascade - A modern, Rust-native CSS Cascade & Style Resolution Engine +//! +//! This crate implements the CSS cascade and style resolution for building +//! browser-like rendering pipelines. It takes an HTML (and later SVG) DOM tree +//! and produces a style-resolved static tree ready for layout and painting. + +// Public API will be added here as the crate is developed + +pub mod rcdom; +pub mod tree; diff --git a/crates/csscascade/src/main.rs b/crates/csscascade/src/main.rs new file mode 100644 index 0000000000..e7a11a969c --- /dev/null +++ b/crates/csscascade/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/csscascade/src/rcdom/mod.rs b/crates/csscascade/src/rcdom/mod.rs new file mode 100644 index 0000000000..8370b364bd --- /dev/null +++ b/crates/csscascade/src/rcdom/mod.rs @@ -0,0 +1,522 @@ +// Copyright 2014-2017 The html5ever Project Developers. See the +// COPYRIGHT file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! ======================================================================= +//! copied from https://github.com/servo/html5ever/blob/main/rcdom/lib.rs +//! ======================================================================= +//! +//! +//! A simple reference-counted DOM. +//! +//! This is sufficient as a static parse tree, but don't build a +//! web browser using it. :) +//! +//! A DOM is a [tree structure] with ordered children that can be represented in an XML-like +//! format. For example, the following graph +//! +//! ```text +//! div +//! +- "text node" +//! +- span +//! ``` +//! in HTML would be serialized as +//! +//! ```html +//!
text node
+//! ``` +//! +//! See the [document object model article on wikipedia][dom wiki] for more information. +//! +//! This implementation stores the information associated with each node once, and then hands out +//! refs to children. The nodes themselves are reference-counted to avoid copying - you can create +//! a new ref and then a node will outlive the document. Nodes own their children, but only have +//! weak references to their parents. +//! +//! [tree structure]: https://en.wikipedia.org/wiki/Tree_(data_structure) +//! [dom wiki]: https://en.wikipedia.org/wiki/Document_Object_Model + +use std::borrow::Cow; +use std::cell::{Cell, RefCell}; +use std::collections::{HashSet, VecDeque}; +use std::default::Default; +use std::fmt; +use std::io; +use std::mem; +use std::rc::{Rc, Weak}; + +use tendril::StrTendril; + +use markup5ever::Attribute; +use markup5ever::ExpandedName; +use markup5ever::QualName; +use markup5ever::interface::tree_builder; +use markup5ever::interface::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}; +use markup5ever::serialize::TraversalScope; +use markup5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode}; +use markup5ever::serialize::{Serialize, Serializer}; + + +/// Empty marker trait so other modules can depend on rcdom without pulling in all details. +pub trait RcDomLike {} + +/// The different kinds of nodes in the DOM. +#[derive(Debug)] +pub enum NodeData { + /// The `Document` itself - the root node of a HTML document. + Document, + + /// A `DOCTYPE` with name, public id, and system id. See + /// [document type declaration on wikipedia][dtd wiki]. + /// + /// [dtd wiki]: https://en.wikipedia.org/wiki/Document_type_declaration + Doctype { + name: StrTendril, + public_id: StrTendril, + system_id: StrTendril, + }, + + /// A text node. + Text { contents: RefCell }, + + /// A comment. + Comment { contents: StrTendril }, + + /// An element with attributes. + Element { + name: QualName, + attrs: RefCell>, + + /// For HTML \ elements, the [template contents]. + /// + /// [template contents]: https://html.spec.whatwg.org/multipage/#template-contents + template_contents: RefCell>, + + /// Whether the node is a [HTML integration point]. + /// + /// [HTML integration point]: https://html.spec.whatwg.org/multipage/#html-integration-point + mathml_annotation_xml_integration_point: bool, + }, + + /// A Processing instruction. + ProcessingInstruction { + target: StrTendril, + contents: StrTendril, + }, +} + +/// A DOM node. +pub struct Node { + /// Parent node. + pub parent: Cell>, + /// Child nodes of this node. + pub children: RefCell>, + /// Represents this node's data. + pub data: NodeData, +} + +impl Node { + /// Create a new node from its contents + pub fn new(data: NodeData) -> Rc { + Rc::new(Node { + data, + parent: Cell::new(None), + children: RefCell::new(Vec::new()), + }) + } +} + +impl Drop for Node { + fn drop(&mut self) { + let mut nodes = mem::take(&mut *self.children.borrow_mut()); + while let Some(node) = nodes.pop() { + let children = mem::take(&mut *node.children.borrow_mut()); + nodes.extend(children.into_iter()); + if let NodeData::Element { + ref template_contents, + .. + } = node.data + { + if let Some(template_contents) = template_contents.borrow_mut().take() { + nodes.push(template_contents); + } + } + } + } +} + +impl fmt::Debug for Node { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("Node") + .field("data", &self.data) + .field("children", &self.children) + .finish() + } +} + +/// Reference to a DOM node. +pub type Handle = Rc; + +/// Weak reference to a DOM node, used for parent pointers. +pub type WeakHandle = Weak; + +/// Append a parentless node to another nodes' children +fn append(new_parent: &Handle, child: Handle) { + let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent))); + // Invariant: child cannot have existing parent + assert!(previous_parent.is_none()); + new_parent.children.borrow_mut().push(child); +} + +/// If the node has a parent, get it and this node's position in its children +fn get_parent_and_index(target: &Handle) -> Option<(Handle, usize)> { + if let Some(weak) = target.parent.take() { + let parent = weak.upgrade().expect("dangling weak pointer"); + target.parent.set(Some(weak)); + let i = match parent + .children + .borrow() + .iter() + .enumerate() + .find(|&(_, child)| Rc::ptr_eq(child, target)) + { + Some((i, _)) => i, + None => panic!("have parent but couldn't find in parent's children!"), + }; + Some((parent, i)) + } else { + None + } +} + +fn append_to_existing_text(prev: &Handle, text: &str) -> bool { + match prev.data { + NodeData::Text { ref contents } => { + contents.borrow_mut().push_slice(text); + true + } + _ => false, + } +} + +fn remove_from_parent(target: &Handle) { + if let Some((parent, i)) = get_parent_and_index(target) { + parent.children.borrow_mut().remove(i); + target.parent.set(None); + } +} + +/// The DOM itself; the result of parsing. +pub struct RcDom { + /// The `Document` itself. + pub document: Handle, + + /// Errors that occurred during parsing. + pub errors: RefCell>>, + + /// The document's quirks mode. + pub quirks_mode: Cell, +} + +impl TreeSink for RcDom { + type Output = Self; + fn finish(self) -> Self { + self + } + + type Handle = Handle; + + type ElemName<'a> + = ExpandedName<'a> + where + Self: 'a; + + fn parse_error(&self, msg: Cow<'static, str>) { + self.errors.borrow_mut().push(msg); + } + + fn get_document(&self) -> Handle { + self.document.clone() + } + + fn get_template_contents(&self, target: &Handle) -> Handle { + if let NodeData::Element { + ref template_contents, + .. + } = target.data + { + template_contents + .borrow() + .as_ref() + .expect("not a template element!") + .clone() + } else { + panic!("not a template element!") + } + } + + fn set_quirks_mode(&self, mode: QuirksMode) { + self.quirks_mode.set(mode); + } + + fn same_node(&self, x: &Handle, y: &Handle) -> bool { + Rc::ptr_eq(x, y) + } + + fn elem_name<'a>(&self, target: &'a Handle) -> ExpandedName<'a> { + match target.data { + NodeData::Element { ref name, .. } => name.expanded(), + _ => panic!("not an element!"), + } + } + + fn create_element(&self, name: QualName, attrs: Vec, flags: ElementFlags) -> Handle { + Node::new(NodeData::Element { + name, + attrs: RefCell::new(attrs), + template_contents: RefCell::new(if flags.template { + Some(Node::new(NodeData::Document)) + } else { + None + }), + mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point, + }) + } + + fn create_comment(&self, text: StrTendril) -> Handle { + Node::new(NodeData::Comment { contents: text }) + } + + fn create_pi(&self, target: StrTendril, data: StrTendril) -> Handle { + Node::new(NodeData::ProcessingInstruction { + target, + contents: data, + }) + } + + fn append(&self, parent: &Handle, child: NodeOrText) { + // Append to an existing Text node if we have one. + if let NodeOrText::AppendText(text) = &child { + if let Some(h) = parent.children.borrow().last() { + if append_to_existing_text(h, text) { + return; + } + } + } + + append( + parent, + match child { + NodeOrText::AppendText(text) => Node::new(NodeData::Text { + contents: RefCell::new(text), + }), + NodeOrText::AppendNode(node) => node, + }, + ); + } + + fn append_before_sibling(&self, sibling: &Handle, child: NodeOrText) { + let (parent, i) = get_parent_and_index(sibling) + .expect("append_before_sibling called on node without parent"); + + let child = match (child, i) { + // No previous node. + (NodeOrText::AppendText(text), 0) => Node::new(NodeData::Text { + contents: RefCell::new(text), + }), + + // Look for a text node before the insertion point. + (NodeOrText::AppendText(text), i) => { + let children = parent.children.borrow(); + let prev = &children[i - 1]; + if append_to_existing_text(prev, &text) { + return; + } + Node::new(NodeData::Text { + contents: RefCell::new(text), + }) + } + + // The tree builder promises we won't have a text node after + // the insertion point. + + // Any other kind of node. + (NodeOrText::AppendNode(node), _) => node, + }; + + remove_from_parent(&child); + + child.parent.set(Some(Rc::downgrade(&parent))); + parent.children.borrow_mut().insert(i, child); + } + + fn append_based_on_parent_node( + &self, + element: &Self::Handle, + prev_element: &Self::Handle, + child: NodeOrText, + ) { + let parent = element.parent.take(); + let has_parent = parent.is_some(); + element.parent.set(parent); + + if has_parent { + self.append_before_sibling(element, child); + } else { + self.append(prev_element, child); + } + } + + fn append_doctype_to_document( + &self, + name: StrTendril, + public_id: StrTendril, + system_id: StrTendril, + ) { + append( + &self.document, + Node::new(NodeData::Doctype { + name, + public_id, + system_id, + }), + ); + } + + fn add_attrs_if_missing(&self, target: &Handle, attrs: Vec) { + let mut existing = if let NodeData::Element { ref attrs, .. } = target.data { + attrs.borrow_mut() + } else { + panic!("not an element") + }; + + let existing_names = existing + .iter() + .map(|e| e.name.clone()) + .collect::>(); + existing.extend( + attrs + .into_iter() + .filter(|attr| !existing_names.contains(&attr.name)), + ); + } + + fn remove_from_parent(&self, target: &Handle) { + remove_from_parent(target); + } + + fn reparent_children(&self, node: &Handle, new_parent: &Handle) { + let mut children = node.children.borrow_mut(); + let mut new_children = new_parent.children.borrow_mut(); + for child in children.iter() { + let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent))); + assert!(Rc::ptr_eq( + node, + &previous_parent.unwrap().upgrade().expect("dangling weak") + )) + } + new_children.extend(mem::take(&mut *children)); + } + + fn is_mathml_annotation_xml_integration_point(&self, target: &Handle) -> bool { + if let NodeData::Element { + mathml_annotation_xml_integration_point, + .. + } = target.data + { + mathml_annotation_xml_integration_point + } else { + panic!("not an element!") + } + } +} + +impl Default for RcDom { + fn default() -> RcDom { + RcDom { + document: Node::new(NodeData::Document), + errors: Default::default(), + quirks_mode: Cell::new(tree_builder::NoQuirks), + } + } +} + +enum SerializeOp { + Open(Handle), + Close(QualName), +} + +pub struct SerializableHandle(Handle); + +impl From for SerializableHandle { + fn from(h: Handle) -> SerializableHandle { + SerializableHandle(h) + } +} + +impl Serialize for SerializableHandle { + fn serialize(&self, serializer: &mut S, traversal_scope: TraversalScope) -> io::Result<()> + where + S: Serializer, + { + let mut ops = VecDeque::new(); + match traversal_scope { + IncludeNode => ops.push_back(SerializeOp::Open(self.0.clone())), + ChildrenOnly(_) => ops.extend( + self.0 + .children + .borrow() + .iter() + .map(|h| SerializeOp::Open(h.clone())), + ), + } + + while let Some(op) = ops.pop_front() { + match op { + SerializeOp::Open(handle) => match handle.data { + NodeData::Element { + ref name, + ref attrs, + .. + } => { + serializer.start_elem( + name.clone(), + attrs.borrow().iter().map(|at| (&at.name, &at.value[..])), + )?; + + ops.reserve(1 + handle.children.borrow().len()); + ops.push_front(SerializeOp::Close(name.clone())); + + for child in handle.children.borrow().iter().rev() { + ops.push_front(SerializeOp::Open(child.clone())); + } + } + + NodeData::Doctype { ref name, .. } => serializer.write_doctype(name)?, + + NodeData::Text { ref contents } => serializer.write_text(&contents.borrow())?, + + NodeData::Comment { ref contents } => serializer.write_comment(contents)?, + + NodeData::ProcessingInstruction { + ref target, + ref contents, + } => serializer.write_processing_instruction(target, contents)?, + + NodeData::Document => panic!("Can't serialize Document node itself"), + }, + + SerializeOp::Close(name) => { + serializer.end_elem(name)?; + } + } + } + + Ok(()) + } +} diff --git a/crates/csscascade/src/tree/mod.rs b/crates/csscascade/src/tree/mod.rs new file mode 100644 index 0000000000..03655014ef --- /dev/null +++ b/crates/csscascade/src/tree/mod.rs @@ -0,0 +1,565 @@ +//! Minimal style-resolved tree representation. +//! +//! This is a proof-of-concept module that models how csscascade will expose +//! style-resolved DOM snapshots to downstream layout / rendering engines. +//! The structures are intentionally small and focus on ergonomics rather than +//! completeness; they can evolve as more use-cases appear. + +use crate::rcdom::{Handle, Node, NodeData, RcDom, SerializableHandle}; +use euclid::{Scale, Size2D}; +use html5ever::driver::ParseOpts; +use html5ever::parse_document; +use html5ever::serialize::{SerializeOpts, TraversalScope, serialize}; +use html5ever::tendril::TendrilSink; +use html5ever::tree_builder::TreeBuilderOpts; +use markup5ever::{Attribute as HtmlAttribute, LocalName, QualName, ns}; +use std::cell::RefCell; +use std::error::Error as StdError; +use std::fmt::{self, Write as FmtWrite}; +use std::io::Cursor; +use std::mem; +use std::rc::Rc; +use std::sync::{Arc, OnceLock}; +use style::context::QuirksMode; +use style::font_metrics::FontMetrics; +use style::media_queries::MediaType; +use style::properties::style_structs::Font; +use style::properties::{self, LonghandId}; +use style::queries::values::PrefersColorScheme; +use style::servo::media_queries::{Device, FontMetricsProvider}; +use style::servo_arc::Arc as ServoArc; +use style::shared_lock::SharedRwLock; +use style::stylist::Stylist; +use style::values::computed::CSSPixelLength; +use style::values::computed::Length; +use style::values::computed::font::GenericFontFamily; +use style::values::specified::font::QueryFontMetricsFlags; +use tendril::StrTendril; + +/// Options that control how a [`Tree`] is serialized back to HTML. +#[derive(Debug, Clone)] +pub enum WriteOptions { + /// Output the DOM as parsed, without mutating styles. + Html { include_root: bool }, + /// Serialize with every computed property inlined into the `style` attribute. + ComputedValues { include_root: bool }, +} + +impl WriteOptions { + fn include_root(&self) -> bool { + match self { + WriteOptions::Html { include_root } => *include_root, + WriteOptions::ComputedValues { include_root } => *include_root, + } + } + + fn inline_styles(&self) -> bool { + matches!(self, WriteOptions::ComputedValues { .. }) + } +} + +impl Default for WriteOptions { + fn default() -> Self { + Self::Html { include_root: true } + } +} + +/// A style-resolved tree with a shared root node. +#[derive(Debug, Clone)] +pub struct Tree { + root: Arc, + #[allow(dead_code)] + runtime: Arc, +} + +impl Tree { + /// Creates a tree from the supplied root node. + fn new(root: StyledNode, runtime: Arc) -> Self { + Self { + root: Arc::new(root), + runtime, + } + } + + /// Returns a shared reference to the root node. + pub fn root(&self) -> &Arc { + &self.root + } + /// Parse HTML (or fragment) string directly into a tree. + /// + /// NOTE: The current implementation only mirrors the DOM structure with stub + /// styles; full cascade integration will replace this once the Stylo bridge is wired. + pub fn from_str(input: &str) -> Result { + let runtime = StyleRuntime::new()?; + let opts = default_parse_opts(); + let mut reader = Cursor::new(input.as_bytes()); + let dom = parse_document(RcDom::default(), opts) + .from_utf8() + .read_from(&mut reader) + .map_err(TreeError::HtmlParse)?; + Self::from_rcdom_with_runtime(&dom, runtime) + } + + /// Build a tree from an existing rcdom DOM snapshot. + /// + /// As with `from_str`, this currently mirrors the DOM into our minimal tree + /// without running the CSS cascade yet. + pub fn from_rcdom(dom: &RcDom) -> Result { + let runtime = StyleRuntime::new()?; + Self::from_rcdom_with_runtime(dom, runtime) + } + + fn from_rcdom_with_runtime(dom: &RcDom, runtime: Arc) -> Result { + let mut next_id = 0; + let document_children = dom.document.children.borrow(); + for child in document_children.iter() { + if let Some(node) = convert_handle(child, &mut next_id, &runtime) { + return Ok(Tree::new(node, runtime)); + } + } + Err(TreeError::NoRenderableNodes) + } + + /// Serialize the tree back to HTML using html5ever's serializer. + pub fn to_string(&self, options: &WriteOptions) -> Result { + let handle = styled_node_to_dom(&self.root, options); + let serializable: SerializableHandle = handle.into(); + let mut buffer = Vec::new(); + let mut serialize_opts = SerializeOpts::default(); + serialize_opts.traversal_scope = if options.include_root() { + TraversalScope::IncludeNode + } else { + TraversalScope::ChildrenOnly(None) + }; + serialize(&mut buffer, &serializable, serialize_opts).map_err(TreeError::Serialize)?; + String::from_utf8(buffer).map_err(TreeError::Utf8) + } +} + +/// Placeholder error type for tree construction failures. +#[derive(Debug)] +pub enum TreeError { + HtmlParse(std::io::Error), + NoRenderableNodes, + StyloUnavailable(String), + Serialize(std::io::Error), + Utf8(std::string::FromUtf8Error), +} + +impl fmt::Display for TreeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TreeError::HtmlParse(err) => write!(f, "HTML parse error: {err}"), + TreeError::NoRenderableNodes => write!(f, "no renderable nodes in DOM"), + TreeError::StyloUnavailable(msg) => write!(f, "stylo unavailable: {msg}"), + TreeError::Serialize(err) => write!(f, "serialize error: {err}"), + TreeError::Utf8(err) => write!(f, "utf8 error: {err}"), + } + } +} + +impl StdError for TreeError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + TreeError::HtmlParse(err) => Some(err), + TreeError::Serialize(err) => Some(err), + TreeError::Utf8(err) => Some(err), + _ => None, + } + } +} + +/// A style-resolved node. +#[derive(Debug)] +pub struct StyledNode { + pub node_id: NodeId, + pub tag: NodeKind, + pub attributes: Vec, + pub children: Vec, + runtime: Arc, + style_cache: OnceLock>, +} + +impl StyledNode { + /// Creates a new node builder for the provided tag. + fn builder(tag: impl Into, runtime: Arc) -> StyledNodeBuilder { + StyledNodeBuilder::new(tag.into(), runtime) + } + + pub fn get_style(&self) -> ServoArc { + self.style_cache + .get_or_init(|| self.runtime.compute_for(self)) + .clone() + } +} + +/// A thin, type-safe wrapper around node identifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(u64); + +impl NodeId { + pub fn new(raw: u64) -> Self { + Self(raw) + } +} + +impl Default for NodeId { + fn default() -> Self { + Self(0) + } +} + +/// Describes what kind of content a node holds. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NodeKind { + Element(String), + Text(String), +} + +impl From<&str> for NodeKind { + fn from(value: &str) -> Self { + Self::Element(value.to_string()) + } +} + +impl From for NodeKind { + fn from(value: String) -> Self { + Self::Element(value) + } +} + +/// Simple name/value pair for attributes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Attribute { + pub name: String, + pub value: String, +} + +impl Attribute { + pub fn new(name: impl Into, value: impl Into) -> Self { + Self { + name: name.into(), + value: value.into(), + } + } +} + +/// Builder for `StyledNode`. +pub(crate) struct StyledNodeBuilder { + node_id: NodeId, + tag: NodeKind, + attributes: Vec, + children: Vec, + runtime: Arc, +} + +impl StyledNodeBuilder { + fn new(tag: NodeKind, runtime: Arc) -> Self { + Self { + node_id: NodeId::default(), + tag, + attributes: Vec::new(), + children: Vec::new(), + runtime, + } + } + + pub fn node_id(mut self, id: NodeId) -> Self { + self.node_id = id; + self + } + + pub fn attribute(mut self, name: impl Into, value: impl Into) -> Self { + self.attributes.push(Attribute::new(name, value)); + self + } + + pub fn child(mut self, child: StyledNode) -> Self { + self.children.push(child); + self + } + + pub fn build(self) -> StyledNode { + StyledNode { + node_id: self.node_id, + tag: self.tag, + attributes: self.attributes, + children: self.children, + runtime: self.runtime.clone(), + style_cache: OnceLock::new(), + } + } +} + +/// Generates a tiny sample tree for demonstration / testing. +pub fn sample_tree() -> Tree { + let runtime = StyleRuntime::new().expect("stylo runtime init failed"); + let heading_text = + StyledNode::builder(NodeKind::Text("Hello, csscascade!".into()), runtime.clone()) + .node_id(NodeId::new(2)) + .build(); + + let heading = StyledNode::builder("h1", runtime.clone()) + .node_id(NodeId::new(1)) + .attribute("class", "title") + .child(heading_text) + .build(); + + let paragraph_text = StyledNode::builder( + NodeKind::Text("This tree was built without HTML parsing.".into()), + runtime.clone(), + ) + .node_id(NodeId::new(4)) + .build(); + + let paragraph = StyledNode::builder("p", runtime.clone()) + .node_id(NodeId::new(3)) + .child(paragraph_text) + .build(); + + let root = StyledNode::builder("div", runtime.clone()) + .node_id(NodeId::new(0)) + .attribute("id", "app") + .child(heading) + .child(paragraph) + .build(); + + Tree::new(root, runtime) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_tree_contains_expected_content() { + let tree = sample_tree(); + let root = tree.root(); + assert_eq!(root.tag, NodeKind::Element("div".into())); + assert_eq!(root.children.len(), 2); + assert_eq!(root.children[0].tag, NodeKind::Element("h1".into())); + assert_eq!( + root.children[1].children[0].tag, + NodeKind::Text("This tree was built without HTML parsing.".into()) + ); + } +} + +fn default_parse_opts() -> ParseOpts { + ParseOpts { + tree_builder: TreeBuilderOpts { + drop_doctype: false, + ..Default::default() + }, + ..Default::default() + } +} + +fn convert_handle( + handle: &Handle, + next_id: &mut u64, + runtime: &Arc, +) -> Option { + match &handle.data { + NodeData::Element { name, attrs, .. } => { + let tag_name = name.local.to_string(); + let kind = NodeKind::Element(tag_name); + let mut builder = + StyledNode::builder(kind, runtime.clone()).node_id(next_node_id(next_id)); + + let attrs_ref = attrs.borrow(); + for attr in attrs_ref.iter() { + builder = builder.attribute(attr.name.local.to_string(), attr.value.to_string()); + } + + let children = handle + .children + .borrow() + .iter() + .filter_map(|child| convert_handle(child, next_id, runtime)) + .collect::>(); + + for child in children { + builder = builder.child(child); + } + + Some(builder.build()) + } + NodeData::Text { contents } => { + let text = contents.borrow(); + let trimmed = text.trim(); + if trimmed.is_empty() { + None + } else { + let kind = NodeKind::Text(trimmed.to_string()); + Some( + StyledNode::builder(kind, runtime.clone()) + .node_id(next_node_id(next_id)) + .build(), + ) + } + } + _ => None, + } +} + +fn next_node_id(counter: &mut u64) -> NodeId { + let id = *counter; + *counter += 1; + NodeId::new(id) +} + +fn styled_node_to_dom(node: &StyledNode, options: &WriteOptions) -> Handle { + match &node.tag { + NodeKind::Element(name) => { + let qual = QualName::new(None, ns!(html), LocalName::from(name.as_str())); + let mut attrs_vec: Vec = node + .attributes + .iter() + .map(|attr| HtmlAttribute { + name: QualName::new(None, ns!(), LocalName::from(attr.name.as_str())), + value: StrTendril::from(attr.value.as_str()), + }) + .collect(); + + if options.inline_styles() { + if let Some(serialized) = serialize_all_properties(&node.get_style()) { + attrs_vec.retain(|attr| attr.name.local.as_ref() != "style"); + attrs_vec.push(HtmlAttribute { + name: QualName::new(None, ns!(), LocalName::from("style")), + value: StrTendril::from(serialized.as_str()), + }); + } + } + let handle = Node::new(NodeData::Element { + name: qual, + attrs: RefCell::new(attrs_vec), + template_contents: RefCell::new(None), + mathml_annotation_xml_integration_point: false, + }); + for child in &node.children { + let child_handle = styled_node_to_dom(child, options); + append_child(&handle, child_handle); + } + handle + } + NodeKind::Text(text) => Node::new(NodeData::Text { + contents: RefCell::new(StrTendril::from(text.as_str())), + }), + } +} + +fn append_child(parent: &Handle, child: Handle) { + child.parent.set(Some(Rc::downgrade(parent))); + parent.children.borrow_mut().push(child); +} + +fn serialize_all_properties(style: &style::properties::ComputedValues) -> Option { + let mut decls = String::new(); + let mut needs_separator = false; + for id in all_longhand_ids() { + let mut buf = String::new(); + if style + .computed_or_resolved_value(id, None, &mut buf) + .is_err() + { + continue; + } + let serialized = buf.trim(); + if serialized.is_empty() { + continue; + } + if needs_separator { + decls.push_str("; "); + } + if FmtWrite::write_fmt(&mut decls, format_args!("{}: {}", id.name(), serialized)).is_err() { + continue; + } + needs_separator = true; + } + if decls.is_empty() { None } else { Some(decls) } +} + +fn all_longhand_ids() -> impl Iterator { + (0..properties::property_counts::LONGHANDS as u16).map(|idx| unsafe { mem::transmute(idx) }) +} + +struct StyleRuntime { + #[allow(dead_code)] + stylist: Stylist, + #[allow(dead_code)] + shared_lock: SharedRwLock, + default_values: ServoArc, +} + +impl StyleRuntime { + fn new() -> Result, TreeError> { + let quirks_mode = QuirksMode::NoQuirks; + let media_type = MediaType::screen(); + let viewport_typed = Size2D::new(800.0f32, 600.0f32); + let dpr_typed = Scale::new(1.0f32); + let font_provider = Box::new(SimpleFontProvider); + let default_font = Font::initial_values(); + let initial_values = + style::properties::ComputedValues::initial_values_with_font_override(default_font); + let shared_lock = SharedRwLock::new(); + let color_scheme = PrefersColorScheme::Light; + + let device = Device::new( + media_type, + quirks_mode, + viewport_typed, + dpr_typed, + font_provider, + initial_values.clone(), + color_scheme, + ); + + let stylist = Stylist::new(device, quirks_mode); + + Ok(Arc::new(Self { + stylist, + shared_lock, + default_values: initial_values, + })) + } + + fn compute_for(&self, _node: &StyledNode) -> ServoArc { + // TODO: integrate with actual Stylist lookups per element. + self.default_values.clone() + } +} + +impl fmt::Debug for StyleRuntime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StyleRuntime").finish_non_exhaustive() + } +} + +#[derive(Debug)] +struct SimpleFontProvider; + +impl FontMetricsProvider for SimpleFontProvider { + fn query_font_metrics( + &self, + _vertical: bool, + _font: &Font, + base_size: CSSPixelLength, + _flags: QueryFontMetricsFlags, + ) -> FontMetrics { + let px = base_size.px(); + FontMetrics { + ascent: Length::new(px * 0.8), + x_height: Some(Length::new(px * 0.5)), + cap_height: Some(Length::new(px * 0.7)), + zero_advance_measure: Some(Length::new(px * 0.5)), + ic_width: Some(Length::new(px)), + script_percent_scale_down: None, + script_script_percent_scale_down: None, + } + } + + fn base_size_for_generic(&self, _generic: GenericFontFamily) -> Length { + Length::new(16.0) + } +} diff --git a/crates/grida-canvas-wasm/Cargo.toml b/crates/grida-canvas-wasm/Cargo.toml index acaa188f02..6325ce1fae 100644 --- a/crates/grida-canvas-wasm/Cargo.toml +++ b/crates/grida-canvas-wasm/Cargo.toml @@ -8,9 +8,9 @@ edition = "2021" [dependencies] cg = { path = "../grida-canvas", features = ["web"] } math2 = { path = "../math2" } -serde_json="1.0.140" fonts = { path = "../grida-canvas-fonts", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } +serde_json="1.0" [features] default = [] diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.d.ts b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.d.ts index e13cfa5dac..e92d0e2a69 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.d.ts +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.d.ts @@ -7,6 +7,8 @@ /// import type { fonts } from "../modules/fonts-bindings"; +import type { markdown } from "../modules/markdown-bindings"; +import type { svg } from "../modules/svg-bindings"; export = createGridaCanvas; export as namespace createGridaCanvas; @@ -19,5 +21,7 @@ declare namespace createGridaCanvas { interface GridaCanvasWasmBindings extends emscripten.emscripten_EXPORTED_RUNTIME_METHODS, canvas.CanvasModule, - fonts.FontsModule {} + fonts.FontsModule, + markdown.MarkdownModule, + svg.SVGModule {} } diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js index d8b5997e7e..cf5da327d6 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -5,7 +5,7 @@ var createGridaCanvas = (() => { async function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Gg"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["Fg"];updateMemoryViews();wasmTable=wasmExports["Hg"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glActiveTexture=_glActiveTexture;var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glAttachShader=_glAttachShader;var _glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQuery=_glBeginQuery;var _glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginQueryEXT=_glBeginQueryEXT;var _glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBeginTransformFeedback=_glBeginTransformFeedback;var _glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindAttribLocation=_glBindAttribLocation;var _glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBuffer=_glBindBuffer;var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferBase=_glBindBufferBase;var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindBufferRange=_glBindBufferRange;var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindFramebuffer=_glBindFramebuffer;var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindRenderbuffer=_glBindRenderbuffer;var _glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindSampler=_glBindSampler;var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTexture=_glBindTexture;var _glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindTransformFeedback=_glBindTransformFeedback;var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _emscripten_glBindVertexArray=_glBindVertexArray;var _glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArrayOES;var _glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendColor=_glBlendColor;var _glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquation=_glBlendEquation;var _glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendEquationSeparate=_glBlendEquationSeparate;var _glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFunc=_glBlendFunc;var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlendFuncSeparate=_glBlendFuncSeparate;var _glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBlitFramebuffer=_glBlitFramebuffer;var _glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferData=_glBufferData;var _glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glBufferSubData=_glBufferSubData;var _glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glCheckFramebufferStatus=_glCheckFramebufferStatus;var _glClear=x0=>GLctx.clear(x0);var _emscripten_glClear=_glClear;var _glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfi=_glClearBufferfi;var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferfv=_glClearBufferfv;var _glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferiv=_glClearBufferiv;var _glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearBufferuiv=_glClearBufferuiv;var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearColor=_glClearColor;var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearDepthf=_glClearDepthf;var _glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClearStencil=_glClearStencil;var _glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClientWaitSync=_glClientWaitSync;var _glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glClipControlEXT=_glClipControlEXT;var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glColorMask=_glColorMask;var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompileShader=_glCompileShader;var _glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage2D=_glCompressedTexImage2D;var _glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexImage3D=_glCompressedTexImage3D;var _glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage2D=_glCompressedTexSubImage2D;var _glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage3D=_glCompressedTexSubImage3D;var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyBufferSubData=_glCopyBufferSubData;var _glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexImage2D=_glCopyTexImage2D;var _glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=_glCopyTexSubImage2D;var _glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCopyTexSubImage3D=_glCopyTexSubImage3D;var _glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateProgram=_glCreateProgram;var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCreateShader=_glCreateShader;var _glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glCullFace=_glCullFace;var _glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteBuffers=_glDeleteBuffers;var _glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteFramebuffers=_glDeleteFramebuffers;var _glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteProgram=_glDeleteProgram;var _glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueries=_glDeleteQueries;var _glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=_glDeleteQueriesEXT;var _glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteRenderbuffers=_glDeleteRenderbuffers;var _glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteSamplers=_glDeleteSamplers;var _glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteShader=_glDeleteShader;var _glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteSync=_glDeleteSync;var _glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTextures=_glDeleteTextures;var _glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteTransformFeedbacks=_glDeleteTransformFeedbacks;var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _emscripten_glDeleteVertexArrays=_glDeleteVertexArrays;var _glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArraysOES;var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthFunc=_glDepthFunc;var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthMask=_glDepthMask;var _glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDepthRangef=_glDepthRangef;var _glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDetachShader=_glDetachShader;var _glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisable=_glDisable;var _glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDisableVertexAttribArray=_glDisableVertexAttribArray;var _glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArrays=_glDrawArrays;var _glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _emscripten_glDrawArraysInstanced=_glDrawArraysInstanced;var _glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstancedANGLE;var _glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstancedARB;var _glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=_glDrawArraysInstancedBaseInstanceWEBGL;var _glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstancedEXT;var _glDrawArraysInstancedNV=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstancedNV;var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _emscripten_glDrawBuffers=_glDrawBuffers;var _glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffersEXT;var _glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffersWEBGL;var _glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElements=_glDrawElements;var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _emscripten_glDrawElementsInstanced=_glDrawElementsInstanced;var _glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstancedANGLE;var _glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstancedARB;var _glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstancedEXT;var _glDrawElementsInstancedNV=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstancedNV;var _glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glDrawRangeElements=_glDrawRangeElements;var _glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnable=_glEnable;var _glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEnableVertexAttribArray=_glEnableVertexAttribArray;var _glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQuery=_glEndQuery;var _glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndQueryEXT=_glEndQueryEXT;var _glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glEndTransformFeedback=_glEndTransformFeedback;var _glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFenceSync=_glFenceSync;var _glFinish=()=>GLctx.finish();var _emscripten_glFinish=_glFinish;var _glFlush=()=>GLctx.flush();var _emscripten_glFlush=_glFlush;var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferRenderbuffer=_glFramebufferRenderbuffer;var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTexture2D=_glFramebufferTexture2D;var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFramebufferTextureLayer=_glFramebufferTextureLayer;var _glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glFrontFace=_glFrontFace;var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenBuffers=_glGenBuffers;var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenFramebuffers=_glGenFramebuffers;var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueries=_glGenQueries;var _glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenQueriesEXT=_glGenQueriesEXT;var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenRenderbuffers=_glGenRenderbuffers;var _glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenSamplers=_glGenSamplers;var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTextures=_glGenTextures;var _glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenTransformFeedbacks=_glGenTransformFeedbacks;var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _emscripten_glGenVertexArrays=_glGenVertexArrays;var _glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArraysOES;var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var _emscripten_glGenerateMipmap=_glGenerateMipmap;var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveAttrib=_glGetActiveAttrib;var _glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=_glGetActiveUniform;var _glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockName=_glGetActiveUniformBlockName;var _glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformBlockiv=_glGetActiveUniformBlockiv;var _glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetActiveUniformsiv=_glGetActiveUniformsiv;var _glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttachedShaders=_glGetAttachedShaders;var _glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetAttribLocation=_glGetAttribLocation;var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBooleanv=_glGetBooleanv;var _glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteri64v=_glGetBufferParameteri64v;var _glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetBufferParameteriv=_glGetBufferParameteriv;var _glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetError=_glGetError;var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFloatv=_glGetFloatv;var _glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFragDataLocation=_glGetFragDataLocation;var _glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var _emscripten_glGetFramebufferAttachmentParameteriv=_glGetFramebufferAttachmentParameteriv;var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:throw"internal emscriptenWebGLGetIndexed() error, bad type: "+type}};var _glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64i_v=_glGetInteger64i_v;var _glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetInteger64v=_glGetInteger64v;var _glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegeri_v=_glGetIntegeri_v;var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetIntegerv=_glGetIntegerv;var _glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetInternalformativ=_glGetInternalformativ;var _glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramBinary=_glGetProgramBinary;var _glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramInfoLog=_glGetProgramInfoLog;var _glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetProgramiv=_glGetProgramiv;var _glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjecti64vEXT=_glGetQueryObjecti64vEXT;var _glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _emscripten_glGetQueryObjectivEXT=_glGetQueryObjectivEXT;var _glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjectui64vEXT;var _glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _emscripten_glGetQueryObjectuiv=_glGetQueryObjectuiv;var _glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectuivEXT;var _glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryiv=_glGetQueryiv;var _glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetQueryivEXT=_glGetQueryivEXT;var _glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetRenderbufferParameteriv=_glGetRenderbufferParameteriv;var _glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameterfv=_glGetSamplerParameterfv;var _glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=_glGetSamplerParameteriv;var _glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderInfoLog=_glGetShaderInfoLog;var _glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderPrecisionFormat=_glGetShaderPrecisionFormat;var _glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderSource=_glGetShaderSource;var _glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var _emscripten_glGetShaderiv=_glGetShaderiv;var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetString=_glGetString;var _glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetStringi=_glGetStringi;var _glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetSynciv=_glGetSynciv;var _glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameterfv=_glGetTexParameterfv;var _glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=_glGetTexParameteriv;var _glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetTransformFeedbackVarying=_glGetTransformFeedbackVarying;var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformBlockIndex=_glGetUniformBlockIndex;var _glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetUniformIndices=_glGetUniformIndices;var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformfv=_glGetUniformfv;var _glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformiv=_glGetUniformiv;var _glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var _emscripten_glGetUniformuiv=_glGetUniformuiv;var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _emscripten_glGetVertexAttribIiv=_glGetVertexAttribIiv;var _glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIuiv;var _glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribPointerv=_glGetVertexAttribPointerv;var _glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribfv=_glGetVertexAttribfv;var _glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glGetVertexAttribiv=_glGetVertexAttribiv;var _glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glHint=_glHint;var _glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateFramebuffer=_glInvalidateFramebuffer;var _glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glInvalidateSubFramebuffer=_glInvalidateSubFramebuffer;var _glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsBuffer=_glIsBuffer;var _glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsEnabled=_glIsEnabled;var _glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsFramebuffer=_glIsFramebuffer;var _glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsProgram=_glIsProgram;var _glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQuery=_glIsQuery;var _glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsQueryEXT=_glIsQueryEXT;var _glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsRenderbuffer=_glIsRenderbuffer;var _glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsSampler=_glIsSampler;var _glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsShader=_glIsShader;var _glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsSync=_glIsSync;var _glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTexture=_glIsTexture;var _glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsTransformFeedback=_glIsTransformFeedback;var _glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _emscripten_glIsVertexArray=_glIsVertexArray;var _glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArrayOES;var _glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLineWidth=_glLineWidth;var _glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glLinkProgram=_glLinkProgram;var _glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=_glMultiDrawArraysInstancedBaseInstanceWEBGL;var _glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPauseTransformFeedback=_glPauseTransformFeedback;var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPixelStorei=_glPixelStorei;var _glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonModeWEBGL=_glPolygonModeWEBGL;var _glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffset=_glPolygonOffset;var _glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glPolygonOffsetClampEXT=_glPolygonOffsetClampEXT;var _glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramBinary=_glProgramBinary;var _glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=_glProgramParameteri;var _glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glQueryCounterEXT=_glQueryCounterEXT;var _glReadBuffer=x0=>GLctx.readBuffer(x0);var _emscripten_glReadBuffer=_glReadBuffer;var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReadPixels=_glReadPixels;var _glReleaseShaderCompiler=()=>{};var _emscripten_glReleaseShaderCompiler=_glReleaseShaderCompiler;var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorage=_glRenderbufferStorage;var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glRenderbufferStorageMultisample=_glRenderbufferStorageMultisample;var _glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glResumeTransformFeedback=_glResumeTransformFeedback;var _glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSampleCoverage=_glSampleCoverage;var _glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterf=_glSamplerParameterf;var _glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=_glSamplerParameterfv;var _glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=_glSamplerParameteri;var _glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=_glSamplerParameteriv;var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glScissor=_glScissor;var _glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderBinary=_glShaderBinary;var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glShaderSource=_glShaderSource;var _glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFunc=_glStencilFunc;var _glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilFuncSeparate=_glStencilFuncSeparate;var _glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMask=_glStencilMask;var _glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilMaskSeparate=_glStencilMaskSeparate;var _glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOp=_glStencilOp;var _glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glStencilOpSeparate=_glStencilOpSeparate;var _glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage2D=_glTexImage2D;var _glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexImage3D=_glTexImage3D;var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterf=_glTexParameterf;var _glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameterfv=_glTexParameterfv;var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteri=_glTexParameteri;var _glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexParameteriv=_glTexParameteriv;var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage2D=_glTexStorage2D;var _glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexStorage3D=_glTexStorage3D;var _glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage2D=_glTexSubImage2D;var _glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTexSubImage3D=_glTexSubImage3D;var _glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glTransformFeedbackVaryings=_glTransformFeedbackVaryings;var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1f=_glUniform1f;var miniTempWebGLFloatBuffers=[];var _glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1fv=_glUniform1fv;var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1i=_glUniform1i;var miniTempWebGLIntBuffers=[];var _glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1iv=_glUniform1iv;var _glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1ui=_glUniform1ui;var _glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform1uiv=_glUniform1uiv;var _glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2f=_glUniform2f;var _glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2fv=_glUniform2fv;var _glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2i=_glUniform2i;var _glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2iv=_glUniform2iv;var _glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2ui=_glUniform2ui;var _glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform2uiv=_glUniform2uiv;var _glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3f=_glUniform3f;var _glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3fv=_glUniform3fv;var _glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3i=_glUniform3i;var _glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3iv=_glUniform3iv;var _glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3ui=_glUniform3ui;var _glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform3uiv=_glUniform3uiv;var _glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4f=_glUniform4f;var _glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4fv=_glUniform4fv;var _glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4i=_glUniform4i;var _glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4iv=_glUniform4iv;var _glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4ui=_glUniform4ui;var _glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniform4uiv=_glUniform4uiv;var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformBlockBinding=_glUniformBlockBinding;var _glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2fv=_glUniformMatrix2fv;var _glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x3fv=_glUniformMatrix2x3fv;var _glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix2x4fv=_glUniformMatrix2x4fv;var _glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3fv=_glUniformMatrix3fv;var _glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x2fv=_glUniformMatrix3x2fv;var _glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix3x4fv=_glUniformMatrix3x4fv;var _glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4fv=_glUniformMatrix4fv;var _glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x2fv=_glUniformMatrix4x2fv;var _glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4x3fv=_glUniformMatrix4x3fv;var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glUseProgram=_glUseProgram;var _glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glValidateProgram=_glValidateProgram;var _glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1f=_glVertexAttrib1f;var _glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib1fv=_glVertexAttrib1fv;var _glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2f=_glVertexAttrib2f;var _glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib2fv=_glVertexAttrib2fv;var _glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3f=_glVertexAttrib3f;var _glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib3fv=_glVertexAttrib3fv;var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4f=_glVertexAttrib4f;var _glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttrib4fv=_glVertexAttrib4fv;var _glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _emscripten_glVertexAttribDivisor=_glVertexAttribDivisor;var _glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisorANGLE;var _glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisorARB;var _glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisorEXT;var _glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisorNV;var _glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4i=_glVertexAttribI4i;var _glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4iv=_glVertexAttribI4iv;var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4ui=_glVertexAttribI4ui;var _glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribI4uiv=_glVertexAttribI4uiv;var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribIPointer=_glVertexAttribIPointer;var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glVertexAttribPointer=_glVertexAttribPointer;var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glViewport=_glViewport;var _glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glWaitSync=_glWaitSync;var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"]}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var wasmImports={C:___cxa_begin_catch,J:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,Z:___cxa_find_matching_catch_4,za:___cxa_rethrow,H:___cxa_throw,ib:___cxa_uncaught_exceptions,d:___resumeException,Ba:___syscall_fcntl64,wb:___syscall_fstat64,sb:___syscall_getcwd,yb:___syscall_ioctl,tb:___syscall_lstat64,ub:___syscall_newfstatat,Aa:___syscall_openat,vb:___syscall_stat64,Bb:__abort_js,kb:__emscripten_throw_longjmp,pb:__gmtime_js,nb:__mmap_js,ob:__munmap_js,Cb:__tzset_js,Ab:_clock_time_get,zb:_emscripten_date_now,ca:_emscripten_get_now,Gf:_emscripten_glActiveTexture,Hf:_emscripten_glAttachShader,je:_emscripten_glBeginQuery,de:_emscripten_glBeginQueryEXT,Ic:_emscripten_glBeginTransformFeedback,If:_emscripten_glBindAttribLocation,Jf:_emscripten_glBindBuffer,Fc:_emscripten_glBindBufferBase,Gc:_emscripten_glBindBufferRange,He:_emscripten_glBindFramebuffer,Ie:_emscripten_glBindRenderbuffer,pe:_emscripten_glBindSampler,Kf:_emscripten_glBindTexture,Wb:_emscripten_glBindTransformFeedback,bf:_emscripten_glBindVertexArray,ef:_emscripten_glBindVertexArrayOES,Lf:_emscripten_glBlendColor,Mf:_emscripten_glBlendEquation,Nd:_emscripten_glBlendEquationSeparate,Nf:_emscripten_glBlendFunc,Md:_emscripten_glBlendFuncSeparate,Be:_emscripten_glBlitFramebuffer,Of:_emscripten_glBufferData,Pf:_emscripten_glBufferSubData,Je:_emscripten_glCheckFramebufferStatus,Qf:_emscripten_glClear,jc:_emscripten_glClearBufferfi,kc:_emscripten_glClearBufferfv,mc:_emscripten_glClearBufferiv,lc:_emscripten_glClearBufferuiv,Rf:_emscripten_glClearColor,Ld:_emscripten_glClearDepthf,Sf:_emscripten_glClearStencil,ye:_emscripten_glClientWaitSync,cd:_emscripten_glClipControlEXT,Tf:_emscripten_glColorMask,Uf:_emscripten_glCompileShader,Vf:_emscripten_glCompressedTexImage2D,Vc:_emscripten_glCompressedTexImage3D,Wf:_emscripten_glCompressedTexSubImage2D,Uc:_emscripten_glCompressedTexSubImage3D,Ae:_emscripten_glCopyBufferSubData,Kd:_emscripten_glCopyTexImage2D,Xf:_emscripten_glCopyTexSubImage2D,Wc:_emscripten_glCopyTexSubImage3D,Yf:_emscripten_glCreateProgram,Zf:_emscripten_glCreateShader,_f:_emscripten_glCullFace,$f:_emscripten_glDeleteBuffers,Ke:_emscripten_glDeleteFramebuffers,ag:_emscripten_glDeleteProgram,ke:_emscripten_glDeleteQueries,ee:_emscripten_glDeleteQueriesEXT,Le:_emscripten_glDeleteRenderbuffers,qe:_emscripten_glDeleteSamplers,bg:_emscripten_glDeleteShader,ze:_emscripten_glDeleteSync,cg:_emscripten_glDeleteTextures,Vb:_emscripten_glDeleteTransformFeedbacks,cf:_emscripten_glDeleteVertexArrays,ff:_emscripten_glDeleteVertexArraysOES,Jd:_emscripten_glDepthFunc,dg:_emscripten_glDepthMask,Id:_emscripten_glDepthRangef,Hd:_emscripten_glDetachShader,eg:_emscripten_glDisable,fg:_emscripten_glDisableVertexAttribArray,gg:_emscripten_glDrawArrays,$e:_emscripten_glDrawArraysInstanced,Qd:_emscripten_glDrawArraysInstancedANGLE,Ib:_emscripten_glDrawArraysInstancedARB,Ye:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,$c:_emscripten_glDrawArraysInstancedEXT,Jb:_emscripten_glDrawArraysInstancedNV,We:_emscripten_glDrawBuffers,Zc:_emscripten_glDrawBuffersEXT,Rd:_emscripten_glDrawBuffersWEBGL,hg:_emscripten_glDrawElements,af:_emscripten_glDrawElementsInstanced,Pd:_emscripten_glDrawElementsInstancedANGLE,Fb:_emscripten_glDrawElementsInstancedARB,Ze:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Hb:_emscripten_glDrawElementsInstancedEXT,_c:_emscripten_glDrawElementsInstancedNV,Qe:_emscripten_glDrawRangeElements,ig:_emscripten_glEnable,jg:_emscripten_glEnableVertexAttribArray,le:_emscripten_glEndQuery,fe:_emscripten_glEndQueryEXT,Hc:_emscripten_glEndTransformFeedback,ve:_emscripten_glFenceSync,kg:_emscripten_glFinish,lg:_emscripten_glFlush,Me:_emscripten_glFramebufferRenderbuffer,Ne:_emscripten_glFramebufferTexture2D,Lc:_emscripten_glFramebufferTextureLayer,mg:_emscripten_glFrontFace,ng:_emscripten_glGenBuffers,Oe:_emscripten_glGenFramebuffers,me:_emscripten_glGenQueries,ge:_emscripten_glGenQueriesEXT,Pe:_emscripten_glGenRenderbuffers,re:_emscripten_glGenSamplers,og:_emscripten_glGenTextures,Ub:_emscripten_glGenTransformFeedbacks,_e:_emscripten_glGenVertexArrays,gf:_emscripten_glGenVertexArraysOES,De:_emscripten_glGenerateMipmap,Gd:_emscripten_glGetActiveAttrib,Fd:_emscripten_glGetActiveUniform,ec:_emscripten_glGetActiveUniformBlockName,fc:_emscripten_glGetActiveUniformBlockiv,hc:_emscripten_glGetActiveUniformsiv,Ed:_emscripten_glGetAttachedShaders,Dd:_emscripten_glGetAttribLocation,Cd:_emscripten_glGetBooleanv,$b:_emscripten_glGetBufferParameteri64v,pg:_emscripten_glGetBufferParameteriv,qg:_emscripten_glGetError,rg:_emscripten_glGetFloatv,vc:_emscripten_glGetFragDataLocation,Ee:_emscripten_glGetFramebufferAttachmentParameteriv,ac:_emscripten_glGetInteger64i_v,cc:_emscripten_glGetInteger64v,Jc:_emscripten_glGetIntegeri_v,sg:_emscripten_glGetIntegerv,Mb:_emscripten_glGetInternalformativ,Qb:_emscripten_glGetProgramBinary,tg:_emscripten_glGetProgramInfoLog,ug:_emscripten_glGetProgramiv,ae:_emscripten_glGetQueryObjecti64vEXT,Td:_emscripten_glGetQueryObjectivEXT,be:_emscripten_glGetQueryObjectui64vEXT,ne:_emscripten_glGetQueryObjectuiv,he:_emscripten_glGetQueryObjectuivEXT,oe:_emscripten_glGetQueryiv,ie:_emscripten_glGetQueryivEXT,Fe:_emscripten_glGetRenderbufferParameteriv,Xb:_emscripten_glGetSamplerParameterfv,Yb:_emscripten_glGetSamplerParameteriv,vg:_emscripten_glGetShaderInfoLog,Zd:_emscripten_glGetShaderPrecisionFormat,Bd:_emscripten_glGetShaderSource,wg:_emscripten_glGetShaderiv,xg:_emscripten_glGetString,df:_emscripten_glGetStringi,bc:_emscripten_glGetSynciv,Ad:_emscripten_glGetTexParameterfv,zd:_emscripten_glGetTexParameteriv,Dc:_emscripten_glGetTransformFeedbackVarying,gc:_emscripten_glGetUniformBlockIndex,ic:_emscripten_glGetUniformIndices,yg:_emscripten_glGetUniformLocation,yd:_emscripten_glGetUniformfv,xd:_emscripten_glGetUniformiv,wc:_emscripten_glGetUniformuiv,Cc:_emscripten_glGetVertexAttribIiv,Bc:_emscripten_glGetVertexAttribIuiv,ud:_emscripten_glGetVertexAttribPointerv,wd:_emscripten_glGetVertexAttribfv,vd:_emscripten_glGetVertexAttribiv,td:_emscripten_glHint,_d:_emscripten_glInvalidateFramebuffer,$d:_emscripten_glInvalidateSubFramebuffer,sd:_emscripten_glIsBuffer,rd:_emscripten_glIsEnabled,qd:_emscripten_glIsFramebuffer,pd:_emscripten_glIsProgram,Tc:_emscripten_glIsQuery,Ud:_emscripten_glIsQueryEXT,od:_emscripten_glIsRenderbuffer,_b:_emscripten_glIsSampler,nd:_emscripten_glIsShader,we:_emscripten_glIsSync,zg:_emscripten_glIsTexture,Tb:_emscripten_glIsTransformFeedback,Kc:_emscripten_glIsVertexArray,Sd:_emscripten_glIsVertexArrayOES,Ag:_emscripten_glLineWidth,Bg:_emscripten_glLinkProgram,Ue:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Ve:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Sb:_emscripten_glPauseTransformFeedback,Cg:_emscripten_glPixelStorei,bd:_emscripten_glPolygonModeWEBGL,md:_emscripten_glPolygonOffset,dd:_emscripten_glPolygonOffsetClampEXT,Pb:_emscripten_glProgramBinary,Ob:_emscripten_glProgramParameteri,ce:_emscripten_glQueryCounterEXT,Xe:_emscripten_glReadBuffer,Dg:_emscripten_glReadPixels,ld:_emscripten_glReleaseShaderCompiler,Ge:_emscripten_glRenderbufferStorage,Ce:_emscripten_glRenderbufferStorageMultisample,Rb:_emscripten_glResumeTransformFeedback,kd:_emscripten_glSampleCoverage,se:_emscripten_glSamplerParameterf,Zb:_emscripten_glSamplerParameterfv,te:_emscripten_glSamplerParameteri,ue:_emscripten_glSamplerParameteriv,Eg:_emscripten_glScissor,jd:_emscripten_glShaderBinary,Ca:_emscripten_glShaderSource,Da:_emscripten_glStencilFunc,Ea:_emscripten_glStencilFuncSeparate,Fa:_emscripten_glStencilMask,Ga:_emscripten_glStencilMaskSeparate,Ha:_emscripten_glStencilOp,Ia:_emscripten_glStencilOpSeparate,Ja:_emscripten_glTexImage2D,Yc:_emscripten_glTexImage3D,Ka:_emscripten_glTexParameterf,La:_emscripten_glTexParameterfv,Ma:_emscripten_glTexParameteri,Na:_emscripten_glTexParameteriv,Re:_emscripten_glTexStorage2D,Nb:_emscripten_glTexStorage3D,Oa:_emscripten_glTexSubImage2D,Xc:_emscripten_glTexSubImage3D,Ec:_emscripten_glTransformFeedbackVaryings,Pa:_emscripten_glUniform1f,Qa:_emscripten_glUniform1fv,Cf:_emscripten_glUniform1i,Df:_emscripten_glUniform1iv,uc:_emscripten_glUniform1ui,qc:_emscripten_glUniform1uiv,Ef:_emscripten_glUniform2f,Ff:_emscripten_glUniform2fv,Bf:_emscripten_glUniform2i,Af:_emscripten_glUniform2iv,tc:_emscripten_glUniform2ui,pc:_emscripten_glUniform2uiv,zf:_emscripten_glUniform3f,yf:_emscripten_glUniform3fv,xf:_emscripten_glUniform3i,wf:_emscripten_glUniform3iv,sc:_emscripten_glUniform3ui,oc:_emscripten_glUniform3uiv,vf:_emscripten_glUniform4f,uf:_emscripten_glUniform4fv,hf:_emscripten_glUniform4i,jf:_emscripten_glUniform4iv,rc:_emscripten_glUniform4ui,nc:_emscripten_glUniform4uiv,dc:_emscripten_glUniformBlockBinding,kf:_emscripten_glUniformMatrix2fv,Sc:_emscripten_glUniformMatrix2x3fv,Qc:_emscripten_glUniformMatrix2x4fv,lf:_emscripten_glUniformMatrix3fv,Rc:_emscripten_glUniformMatrix3x2fv,Oc:_emscripten_glUniformMatrix3x4fv,mf:_emscripten_glUniformMatrix4fv,Pc:_emscripten_glUniformMatrix4x2fv,Mc:_emscripten_glUniformMatrix4x3fv,nf:_emscripten_glUseProgram,id:_emscripten_glValidateProgram,of:_emscripten_glVertexAttrib1f,hd:_emscripten_glVertexAttrib1fv,gd:_emscripten_glVertexAttrib2f,pf:_emscripten_glVertexAttrib2fv,fd:_emscripten_glVertexAttrib3f,qf:_emscripten_glVertexAttrib3fv,ed:_emscripten_glVertexAttrib4f,rf:_emscripten_glVertexAttrib4fv,Se:_emscripten_glVertexAttribDivisor,Od:_emscripten_glVertexAttribDivisorANGLE,Kb:_emscripten_glVertexAttribDivisorARB,ad:_emscripten_glVertexAttribDivisorEXT,Lb:_emscripten_glVertexAttribDivisorNV,Ac:_emscripten_glVertexAttribI4i,yc:_emscripten_glVertexAttribI4iv,zc:_emscripten_glVertexAttribI4ui,xc:_emscripten_glVertexAttribI4uiv,Te:_emscripten_glVertexAttribIPointer,sf:_emscripten_glVertexAttribPointer,tf:_emscripten_glViewport,xe:_emscripten_glWaitSync,Wa:_emscripten_request_animation_frame_loop,lb:_emscripten_resize_heap,Db:_environ_get,Eb:_environ_sizes_get,Ta:_exit,da:_fd_close,mb:_fd_pread,xb:_fd_read,qb:_fd_seek,V:_fd_write,Ra:_glGetIntegerv,ia:_glGetString,Sa:_glGetStringi,Xd:invoke_dd,Wd:invoke_ddd,Yd:invoke_dddd,xa:invoke_diii,Vd:invoke_fff,x:invoke_ffif,q:invoke_ffifif,X:invoke_ffifiii,L:invoke_fi,ea:invoke_fif,ya:invoke_fiii,fb:invoke_fiiiif,na:invoke_fiiiii,p:invoke_i,Va:invoke_if,gb:invoke_iffiiiiiiii,g:invoke_ii,E:invoke_iif,va:invoke_iiffi,h:invoke_iii,qa:invoke_iiif,f:invoke_iiii,ra:invoke_iiiif,l:invoke_iiiii,hb:invoke_iiiiid,_:invoke_iiiiii,y:invoke_iiiiiii,t:invoke_iiiiiiii,ja:invoke_iiiiiiiiii,ka:invoke_iiiiiiiiiiifiii,T:invoke_iiiiiiiiiiii,jb:invoke_j,_a:invoke_ji,m:invoke_jii,U:invoke_jiiii,Gb:invoke_jiijj,n:invoke_v,ma:invoke_vfffffiii,b:invoke_vi,ha:invoke_vid,O:invoke_vif,N:invoke_viff,G:invoke_vifff,P:invoke_viffff,z:invoke_vifffff,I:invoke_viffffffffffffffffffff,db:invoke_viffi,c:invoke_vii,A:invoke_viif,B:invoke_viiff,Ya:invoke_viiffiii,fa:invoke_viifif,s:invoke_viififif,w:invoke_viifiiifi,e:invoke_viii,D:invoke_viiif,wa:invoke_viiiffi,W:invoke_viiiffiffii,M:invoke_viiifi,Q:invoke_viiififiiiiiiiiiiii,j:invoke_viiii,ua:invoke_viiiif,R:invoke_viiiiff,ta:invoke_viiiiffi,F:invoke_viiiifi,i:invoke_viiiii,pa:invoke_viiiiiff,bb:invoke_viiiiiffiiifffi,cb:invoke_viiiiiffiiifii,sa:invoke_viiiiifi,k:invoke_viiiiii,u:invoke_viiiiiii,aa:invoke_viiiiiiii,Ua:invoke_viiiiiiiii,K:invoke_viiiiiiiiii,Xa:invoke_viiiiiiiiiii,S:invoke_viiiiiiiiiiiiiii,ab:invoke_viiiijjiiiiff,oa:invoke_viiij,eb:invoke_viiijj,ba:invoke_viij,r:invoke_viiji,v:invoke_viijiii,la:invoke_viijiiiif,$a:invoke_vijff,$:invoke_viji,Za:invoke_vijii,Nc:invoke_vijiiii,Y:invoke_vijjjj,ga:_llvm_eh_typeid_for,rb:_random_get};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["Gg"];var _init=Module["_init"]=wasmExports["Ig"];var _tick=Module["_tick"]=wasmExports["Jg"];var _resize_surface=Module["_resize_surface"]=wasmExports["Kg"];var _redraw=Module["_redraw"]=wasmExports["Lg"];var _load_scene_json=Module["_load_scene_json"]=wasmExports["Mg"];var _apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Ng"];var _pointer_move=Module["_pointer_move"]=wasmExports["Og"];var _command=Module["_command"]=wasmExports["Pg"];var _set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Qg"];var _add_image=Module["_add_image"]=wasmExports["Rg"];var _get_image_bytes=Module["_get_image_bytes"]=wasmExports["Sg"];var _get_image_size=Module["_get_image_size"]=wasmExports["Tg"];var _add_font=Module["_add_font"]=wasmExports["Ug"];var _has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["Vg"];var _list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["Wg"];var _list_available_fonts=Module["_list_available_fonts"]=wasmExports["Xg"];var _set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Yg"];var _get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["Zg"];var _get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["_g"];var _get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["$g"];var _get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["ah"];var _get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["bh"];var _export_node_as=Module["_export_node_as"]=wasmExports["ch"];var _to_vector_network=Module["_to_vector_network"]=wasmExports["dh"];var _set_debug=Module["_set_debug"]=wasmExports["eh"];var _toggle_debug=Module["_toggle_debug"]=wasmExports["fh"];var _set_verbose=Module["_set_verbose"]=wasmExports["gh"];var _devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["hh"];var _devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["ih"];var _runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["jh"];var _devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["kh"];var _devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["lh"];var _devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["mh"];var _highlight_strokes=Module["_highlight_strokes"]=wasmExports["nh"];var _load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["oh"];var _load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["ph"];var _main=Module["_main"]=wasmExports["qh"];var _grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["rh"];var _grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["sh"];var _grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["th"];var _allocate=Module["_allocate"]=wasmExports["uh"];var _deallocate=Module["_deallocate"]=wasmExports["vh"];var _malloc=wasmExports["wh"];var _emscripten_builtin_memalign=wasmExports["xh"];var _setThrew=wasmExports["yh"];var __emscripten_tempret_set=wasmExports["zh"];var __emscripten_stack_restore=wasmExports["Ah"];var __emscripten_stack_alloc=wasmExports["Bh"];var _emscripten_stack_get_current=wasmExports["Ch"];var ___cxa_decrement_exception_refcount=wasmExports["Dh"];var ___cxa_increment_exception_refcount=wasmExports["Eh"];var ___cxa_can_catch=wasmExports["Fh"];var ___cxa_get_exception_ptr=wasmExports["Gh"];function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffifif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viffffffffffffffffffff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iffiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vfffffiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_if(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ddd(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fff(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;args.forEach(arg=>{HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4});HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["ah"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["$g"];updateMemoryViews();wasmTable=wasmExports["bh"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glActiveTexture=_glActiveTexture;var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glAttachShader=_glAttachShader;var _glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQuery=_glBeginQuery;var _glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginQueryEXT=_glBeginQueryEXT;var _glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBeginTransformFeedback=_glBeginTransformFeedback;var _glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindAttribLocation=_glBindAttribLocation;var _glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBuffer=_glBindBuffer;var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferBase=_glBindBufferBase;var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindBufferRange=_glBindBufferRange;var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindFramebuffer=_glBindFramebuffer;var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindRenderbuffer=_glBindRenderbuffer;var _glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindSampler=_glBindSampler;var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTexture=_glBindTexture;var _glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindTransformFeedback=_glBindTransformFeedback;var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _emscripten_glBindVertexArray=_glBindVertexArray;var _glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArrayOES;var _glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendColor=_glBlendColor;var _glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquation=_glBlendEquation;var _glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendEquationSeparate=_glBlendEquationSeparate;var _glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFunc=_glBlendFunc;var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlendFuncSeparate=_glBlendFuncSeparate;var _glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBlitFramebuffer=_glBlitFramebuffer;var _glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferData=_glBufferData;var _glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glBufferSubData=_glBufferSubData;var _glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glCheckFramebufferStatus=_glCheckFramebufferStatus;var _glClear=x0=>GLctx.clear(x0);var _emscripten_glClear=_glClear;var _glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfi=_glClearBufferfi;var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferfv=_glClearBufferfv;var _glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferiv=_glClearBufferiv;var _glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearBufferuiv=_glClearBufferuiv;var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearColor=_glClearColor;var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearDepthf=_glClearDepthf;var _glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClearStencil=_glClearStencil;var _glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClientWaitSync=_glClientWaitSync;var _glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glClipControlEXT=_glClipControlEXT;var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glColorMask=_glColorMask;var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompileShader=_glCompileShader;var _glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage2D=_glCompressedTexImage2D;var _glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexImage3D=_glCompressedTexImage3D;var _glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage2D=_glCompressedTexSubImage2D;var _glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage3D=_glCompressedTexSubImage3D;var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyBufferSubData=_glCopyBufferSubData;var _glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexImage2D=_glCopyTexImage2D;var _glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=_glCopyTexSubImage2D;var _glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCopyTexSubImage3D=_glCopyTexSubImage3D;var _glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateProgram=_glCreateProgram;var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCreateShader=_glCreateShader;var _glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glCullFace=_glCullFace;var _glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteBuffers=_glDeleteBuffers;var _glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteFramebuffers=_glDeleteFramebuffers;var _glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteProgram=_glDeleteProgram;var _glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueries=_glDeleteQueries;var _glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=_glDeleteQueriesEXT;var _glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteRenderbuffers=_glDeleteRenderbuffers;var _glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteSamplers=_glDeleteSamplers;var _glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteShader=_glDeleteShader;var _glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteSync=_glDeleteSync;var _glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTextures=_glDeleteTextures;var _glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteTransformFeedbacks=_glDeleteTransformFeedbacks;var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _emscripten_glDeleteVertexArrays=_glDeleteVertexArrays;var _glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArraysOES;var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthFunc=_glDepthFunc;var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthMask=_glDepthMask;var _glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDepthRangef=_glDepthRangef;var _glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDetachShader=_glDetachShader;var _glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisable=_glDisable;var _glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDisableVertexAttribArray=_glDisableVertexAttribArray;var _glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArrays=_glDrawArrays;var _glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _emscripten_glDrawArraysInstanced=_glDrawArraysInstanced;var _glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstancedANGLE;var _glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstancedARB;var _glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=_glDrawArraysInstancedBaseInstanceWEBGL;var _glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstancedEXT;var _glDrawArraysInstancedNV=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstancedNV;var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _emscripten_glDrawBuffers=_glDrawBuffers;var _glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffersEXT;var _glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffersWEBGL;var _glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElements=_glDrawElements;var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _emscripten_glDrawElementsInstanced=_glDrawElementsInstanced;var _glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstancedANGLE;var _glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstancedARB;var _glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstancedEXT;var _glDrawElementsInstancedNV=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstancedNV;var _glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glDrawRangeElements=_glDrawRangeElements;var _glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnable=_glEnable;var _glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEnableVertexAttribArray=_glEnableVertexAttribArray;var _glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQuery=_glEndQuery;var _glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndQueryEXT=_glEndQueryEXT;var _glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glEndTransformFeedback=_glEndTransformFeedback;var _glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFenceSync=_glFenceSync;var _glFinish=()=>GLctx.finish();var _emscripten_glFinish=_glFinish;var _glFlush=()=>GLctx.flush();var _emscripten_glFlush=_glFlush;var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferRenderbuffer=_glFramebufferRenderbuffer;var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTexture2D=_glFramebufferTexture2D;var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFramebufferTextureLayer=_glFramebufferTextureLayer;var _glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glFrontFace=_glFrontFace;var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenBuffers=_glGenBuffers;var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenFramebuffers=_glGenFramebuffers;var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueries=_glGenQueries;var _glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenQueriesEXT=_glGenQueriesEXT;var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenRenderbuffers=_glGenRenderbuffers;var _glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenSamplers=_glGenSamplers;var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTextures=_glGenTextures;var _glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenTransformFeedbacks=_glGenTransformFeedbacks;var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _emscripten_glGenVertexArrays=_glGenVertexArrays;var _glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArraysOES;var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var _emscripten_glGenerateMipmap=_glGenerateMipmap;var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveAttrib=_glGetActiveAttrib;var _glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=_glGetActiveUniform;var _glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockName=_glGetActiveUniformBlockName;var _glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformBlockiv=_glGetActiveUniformBlockiv;var _glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetActiveUniformsiv=_glGetActiveUniformsiv;var _glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttachedShaders=_glGetAttachedShaders;var _glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetAttribLocation=_glGetAttribLocation;var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBooleanv=_glGetBooleanv;var _glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteri64v=_glGetBufferParameteri64v;var _glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetBufferParameteriv=_glGetBufferParameteriv;var _glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetError=_glGetError;var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFloatv=_glGetFloatv;var _glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFragDataLocation=_glGetFragDataLocation;var _glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var _emscripten_glGetFramebufferAttachmentParameteriv=_glGetFramebufferAttachmentParameteriv;var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:throw"internal emscriptenWebGLGetIndexed() error, bad type: "+type}};var _glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64i_v=_glGetInteger64i_v;var _glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetInteger64v=_glGetInteger64v;var _glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegeri_v=_glGetIntegeri_v;var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetIntegerv=_glGetIntegerv;var _glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetInternalformativ=_glGetInternalformativ;var _glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramBinary=_glGetProgramBinary;var _glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramInfoLog=_glGetProgramInfoLog;var _glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetProgramiv=_glGetProgramiv;var _glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjecti64vEXT=_glGetQueryObjecti64vEXT;var _glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _emscripten_glGetQueryObjectivEXT=_glGetQueryObjectivEXT;var _glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjectui64vEXT;var _glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _emscripten_glGetQueryObjectuiv=_glGetQueryObjectuiv;var _glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectuivEXT;var _glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryiv=_glGetQueryiv;var _glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetQueryivEXT=_glGetQueryivEXT;var _glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetRenderbufferParameteriv=_glGetRenderbufferParameteriv;var _glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameterfv=_glGetSamplerParameterfv;var _glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=_glGetSamplerParameteriv;var _glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderInfoLog=_glGetShaderInfoLog;var _glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderPrecisionFormat=_glGetShaderPrecisionFormat;var _glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderSource=_glGetShaderSource;var _glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var _emscripten_glGetShaderiv=_glGetShaderiv;var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetString=_glGetString;var _glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetStringi=_glGetStringi;var _glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetSynciv=_glGetSynciv;var _glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameterfv=_glGetTexParameterfv;var _glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=_glGetTexParameteriv;var _glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetTransformFeedbackVarying=_glGetTransformFeedbackVarying;var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformBlockIndex=_glGetUniformBlockIndex;var _glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetUniformIndices=_glGetUniformIndices;var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformfv=_glGetUniformfv;var _glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformiv=_glGetUniformiv;var _glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var _emscripten_glGetUniformuiv=_glGetUniformuiv;var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _emscripten_glGetVertexAttribIiv=_glGetVertexAttribIiv;var _glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIuiv;var _glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribPointerv=_glGetVertexAttribPointerv;var _glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribfv=_glGetVertexAttribfv;var _glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glGetVertexAttribiv=_glGetVertexAttribiv;var _glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glHint=_glHint;var _glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateFramebuffer=_glInvalidateFramebuffer;var _glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glInvalidateSubFramebuffer=_glInvalidateSubFramebuffer;var _glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsBuffer=_glIsBuffer;var _glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsEnabled=_glIsEnabled;var _glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsFramebuffer=_glIsFramebuffer;var _glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsProgram=_glIsProgram;var _glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQuery=_glIsQuery;var _glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsQueryEXT=_glIsQueryEXT;var _glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsRenderbuffer=_glIsRenderbuffer;var _glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsSampler=_glIsSampler;var _glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsShader=_glIsShader;var _glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsSync=_glIsSync;var _glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTexture=_glIsTexture;var _glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsTransformFeedback=_glIsTransformFeedback;var _glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _emscripten_glIsVertexArray=_glIsVertexArray;var _glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArrayOES;var _glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLineWidth=_glLineWidth;var _glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glLinkProgram=_glLinkProgram;var _glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=_glMultiDrawArraysInstancedBaseInstanceWEBGL;var _glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPauseTransformFeedback=_glPauseTransformFeedback;var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPixelStorei=_glPixelStorei;var _glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonModeWEBGL=_glPolygonModeWEBGL;var _glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffset=_glPolygonOffset;var _glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glPolygonOffsetClampEXT=_glPolygonOffsetClampEXT;var _glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramBinary=_glProgramBinary;var _glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=_glProgramParameteri;var _glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glQueryCounterEXT=_glQueryCounterEXT;var _glReadBuffer=x0=>GLctx.readBuffer(x0);var _emscripten_glReadBuffer=_glReadBuffer;var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReadPixels=_glReadPixels;var _glReleaseShaderCompiler=()=>{};var _emscripten_glReleaseShaderCompiler=_glReleaseShaderCompiler;var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorage=_glRenderbufferStorage;var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glRenderbufferStorageMultisample=_glRenderbufferStorageMultisample;var _glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glResumeTransformFeedback=_glResumeTransformFeedback;var _glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSampleCoverage=_glSampleCoverage;var _glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterf=_glSamplerParameterf;var _glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=_glSamplerParameterfv;var _glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=_glSamplerParameteri;var _glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=_glSamplerParameteriv;var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glScissor=_glScissor;var _glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderBinary=_glShaderBinary;var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glShaderSource=_glShaderSource;var _glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFunc=_glStencilFunc;var _glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilFuncSeparate=_glStencilFuncSeparate;var _glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMask=_glStencilMask;var _glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilMaskSeparate=_glStencilMaskSeparate;var _glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOp=_glStencilOp;var _glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glStencilOpSeparate=_glStencilOpSeparate;var _glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage2D=_glTexImage2D;var _glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexImage3D=_glTexImage3D;var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterf=_glTexParameterf;var _glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameterfv=_glTexParameterfv;var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteri=_glTexParameteri;var _glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexParameteriv=_glTexParameteriv;var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage2D=_glTexStorage2D;var _glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexStorage3D=_glTexStorage3D;var _glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage2D=_glTexSubImage2D;var _glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTexSubImage3D=_glTexSubImage3D;var _glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glTransformFeedbackVaryings=_glTransformFeedbackVaryings;var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1f=_glUniform1f;var miniTempWebGLFloatBuffers=[];var _glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1fv=_glUniform1fv;var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1i=_glUniform1i;var miniTempWebGLIntBuffers=[];var _glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1iv=_glUniform1iv;var _glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1ui=_glUniform1ui;var _glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform1uiv=_glUniform1uiv;var _glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2f=_glUniform2f;var _glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2fv=_glUniform2fv;var _glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2i=_glUniform2i;var _glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2iv=_glUniform2iv;var _glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2ui=_glUniform2ui;var _glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform2uiv=_glUniform2uiv;var _glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3f=_glUniform3f;var _glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3fv=_glUniform3fv;var _glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3i=_glUniform3i;var _glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3iv=_glUniform3iv;var _glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3ui=_glUniform3ui;var _glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform3uiv=_glUniform3uiv;var _glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4f=_glUniform4f;var _glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4fv=_glUniform4fv;var _glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4i=_glUniform4i;var _glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4iv=_glUniform4iv;var _glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4ui=_glUniform4ui;var _glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniform4uiv=_glUniform4uiv;var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformBlockBinding=_glUniformBlockBinding;var _glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2fv=_glUniformMatrix2fv;var _glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x3fv=_glUniformMatrix2x3fv;var _glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix2x4fv=_glUniformMatrix2x4fv;var _glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3fv=_glUniformMatrix3fv;var _glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x2fv=_glUniformMatrix3x2fv;var _glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix3x4fv=_glUniformMatrix3x4fv;var _glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4fv=_glUniformMatrix4fv;var _glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x2fv=_glUniformMatrix4x2fv;var _glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4x3fv=_glUniformMatrix4x3fv;var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glUseProgram=_glUseProgram;var _glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glValidateProgram=_glValidateProgram;var _glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1f=_glVertexAttrib1f;var _glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib1fv=_glVertexAttrib1fv;var _glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2f=_glVertexAttrib2f;var _glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib2fv=_glVertexAttrib2fv;var _glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3f=_glVertexAttrib3f;var _glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib3fv=_glVertexAttrib3fv;var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4f=_glVertexAttrib4f;var _glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttrib4fv=_glVertexAttrib4fv;var _glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _emscripten_glVertexAttribDivisor=_glVertexAttribDivisor;var _glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisorANGLE;var _glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisorARB;var _glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisorEXT;var _glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisorNV;var _glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4i=_glVertexAttribI4i;var _glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4iv=_glVertexAttribI4iv;var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4ui=_glVertexAttribI4ui;var _glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribI4uiv=_glVertexAttribI4uiv;var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribIPointer=_glVertexAttribIPointer;var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glVertexAttribPointer=_glVertexAttribPointer;var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glViewport=_glViewport;var _glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glWaitSync=_glWaitSync;var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"]}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var wasmImports={H:___cxa_begin_catch,S:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,ja:___cxa_find_matching_catch_4,Fa:___cxa_rethrow,N:___cxa_throw,lb:___cxa_uncaught_exceptions,d:___resumeException,Ia:___syscall_fcntl64,Cb:___syscall_fstat64,yb:___syscall_getcwd,rb:___syscall_getdents64,Db:___syscall_ioctl,zb:___syscall_lstat64,Ab:___syscall_newfstatat,Ha:___syscall_openat,qb:___syscall_readlinkat,Bb:___syscall_stat64,Gb:__abort_js,nb:__emscripten_throw_longjmp,vb:__gmtime_js,tb:__mmap_js,ub:__munmap_js,Hb:__tzset_js,Fb:_clock_time_get,Eb:_emscripten_date_now,pb:_emscripten_get_heap_max,ra:_emscripten_get_now,Kf:_emscripten_glActiveTexture,Lf:_emscripten_glAttachShader,ne:_emscripten_glBeginQuery,he:_emscripten_glBeginQueryEXT,Nc:_emscripten_glBeginTransformFeedback,Mf:_emscripten_glBindAttribLocation,Nf:_emscripten_glBindBuffer,Kc:_emscripten_glBindBufferBase,Lc:_emscripten_glBindBufferRange,Le:_emscripten_glBindFramebuffer,Me:_emscripten_glBindRenderbuffer,te:_emscripten_glBindSampler,Of:_emscripten_glBindTexture,_b:_emscripten_glBindTransformFeedback,ff:_emscripten_glBindVertexArray,jf:_emscripten_glBindVertexArrayOES,Pf:_emscripten_glBlendColor,Qf:_emscripten_glBlendEquation,Rd:_emscripten_glBlendEquationSeparate,Rf:_emscripten_glBlendFunc,Qd:_emscripten_glBlendFuncSeparate,Fe:_emscripten_glBlitFramebuffer,Sf:_emscripten_glBufferData,Tf:_emscripten_glBufferSubData,Ne:_emscripten_glCheckFramebufferStatus,Uf:_emscripten_glClear,nc:_emscripten_glClearBufferfi,oc:_emscripten_glClearBufferfv,qc:_emscripten_glClearBufferiv,pc:_emscripten_glClearBufferuiv,Vf:_emscripten_glClearColor,Pd:_emscripten_glClearDepthf,Wf:_emscripten_glClearStencil,Ce:_emscripten_glClientWaitSync,gd:_emscripten_glClipControlEXT,Xf:_emscripten_glColorMask,Yf:_emscripten_glCompileShader,Zf:_emscripten_glCompressedTexImage2D,Zc:_emscripten_glCompressedTexImage3D,_f:_emscripten_glCompressedTexSubImage2D,Yc:_emscripten_glCompressedTexSubImage3D,Ee:_emscripten_glCopyBufferSubData,Od:_emscripten_glCopyTexImage2D,$f:_emscripten_glCopyTexSubImage2D,_c:_emscripten_glCopyTexSubImage3D,ag:_emscripten_glCreateProgram,bg:_emscripten_glCreateShader,cg:_emscripten_glCullFace,dg:_emscripten_glDeleteBuffers,Oe:_emscripten_glDeleteFramebuffers,eg:_emscripten_glDeleteProgram,oe:_emscripten_glDeleteQueries,ie:_emscripten_glDeleteQueriesEXT,Pe:_emscripten_glDeleteRenderbuffers,ue:_emscripten_glDeleteSamplers,fg:_emscripten_glDeleteShader,De:_emscripten_glDeleteSync,gg:_emscripten_glDeleteTextures,Zb:_emscripten_glDeleteTransformFeedbacks,gf:_emscripten_glDeleteVertexArrays,kf:_emscripten_glDeleteVertexArraysOES,Nd:_emscripten_glDepthFunc,hg:_emscripten_glDepthMask,Md:_emscripten_glDepthRangef,Ld:_emscripten_glDetachShader,ig:_emscripten_glDisable,jg:_emscripten_glDisableVertexAttribArray,kg:_emscripten_glDrawArrays,df:_emscripten_glDrawArraysInstanced,Ud:_emscripten_glDrawArraysInstancedANGLE,Mb:_emscripten_glDrawArraysInstancedARB,af:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,dd:_emscripten_glDrawArraysInstancedEXT,Nb:_emscripten_glDrawArraysInstancedNV,_e:_emscripten_glDrawBuffers,bd:_emscripten_glDrawBuffersEXT,Vd:_emscripten_glDrawBuffersWEBGL,lg:_emscripten_glDrawElements,ef:_emscripten_glDrawElementsInstanced,Td:_emscripten_glDrawElementsInstancedANGLE,Kb:_emscripten_glDrawElementsInstancedARB,bf:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Lb:_emscripten_glDrawElementsInstancedEXT,cd:_emscripten_glDrawElementsInstancedNV,Ue:_emscripten_glDrawRangeElements,mg:_emscripten_glEnable,ng:_emscripten_glEnableVertexAttribArray,pe:_emscripten_glEndQuery,je:_emscripten_glEndQueryEXT,Mc:_emscripten_glEndTransformFeedback,ze:_emscripten_glFenceSync,og:_emscripten_glFinish,pg:_emscripten_glFlush,Qe:_emscripten_glFramebufferRenderbuffer,Re:_emscripten_glFramebufferTexture2D,Qc:_emscripten_glFramebufferTextureLayer,qg:_emscripten_glFrontFace,rg:_emscripten_glGenBuffers,Se:_emscripten_glGenFramebuffers,qe:_emscripten_glGenQueries,ke:_emscripten_glGenQueriesEXT,Te:_emscripten_glGenRenderbuffers,ve:_emscripten_glGenSamplers,sg:_emscripten_glGenTextures,Yb:_emscripten_glGenTransformFeedbacks,cf:_emscripten_glGenVertexArrays,lf:_emscripten_glGenVertexArraysOES,He:_emscripten_glGenerateMipmap,Kd:_emscripten_glGetActiveAttrib,Jd:_emscripten_glGetActiveUniform,ic:_emscripten_glGetActiveUniformBlockName,jc:_emscripten_glGetActiveUniformBlockiv,lc:_emscripten_glGetActiveUniformsiv,Id:_emscripten_glGetAttachedShaders,Hd:_emscripten_glGetAttribLocation,Gd:_emscripten_glGetBooleanv,dc:_emscripten_glGetBufferParameteri64v,tg:_emscripten_glGetBufferParameteriv,ug:_emscripten_glGetError,vg:_emscripten_glGetFloatv,zc:_emscripten_glGetFragDataLocation,Ie:_emscripten_glGetFramebufferAttachmentParameteriv,ec:_emscripten_glGetInteger64i_v,gc:_emscripten_glGetInteger64v,Oc:_emscripten_glGetIntegeri_v,wg:_emscripten_glGetIntegerv,Qb:_emscripten_glGetInternalformativ,Ub:_emscripten_glGetProgramBinary,xg:_emscripten_glGetProgramInfoLog,yg:_emscripten_glGetProgramiv,ee:_emscripten_glGetQueryObjecti64vEXT,Xd:_emscripten_glGetQueryObjectivEXT,fe:_emscripten_glGetQueryObjectui64vEXT,re:_emscripten_glGetQueryObjectuiv,le:_emscripten_glGetQueryObjectuivEXT,se:_emscripten_glGetQueryiv,me:_emscripten_glGetQueryivEXT,Je:_emscripten_glGetRenderbufferParameteriv,$b:_emscripten_glGetSamplerParameterfv,ac:_emscripten_glGetSamplerParameteriv,zg:_emscripten_glGetShaderInfoLog,be:_emscripten_glGetShaderPrecisionFormat,Fd:_emscripten_glGetShaderSource,Ag:_emscripten_glGetShaderiv,Bg:_emscripten_glGetString,hf:_emscripten_glGetStringi,fc:_emscripten_glGetSynciv,Ed:_emscripten_glGetTexParameterfv,Dd:_emscripten_glGetTexParameteriv,Ic:_emscripten_glGetTransformFeedbackVarying,kc:_emscripten_glGetUniformBlockIndex,mc:_emscripten_glGetUniformIndices,Cg:_emscripten_glGetUniformLocation,Cd:_emscripten_glGetUniformfv,Bd:_emscripten_glGetUniformiv,Ac:_emscripten_glGetUniformuiv,Hc:_emscripten_glGetVertexAttribIiv,Fc:_emscripten_glGetVertexAttribIuiv,yd:_emscripten_glGetVertexAttribPointerv,Ad:_emscripten_glGetVertexAttribfv,zd:_emscripten_glGetVertexAttribiv,xd:_emscripten_glHint,ce:_emscripten_glInvalidateFramebuffer,de:_emscripten_glInvalidateSubFramebuffer,wd:_emscripten_glIsBuffer,vd:_emscripten_glIsEnabled,ud:_emscripten_glIsFramebuffer,td:_emscripten_glIsProgram,Xc:_emscripten_glIsQuery,Yd:_emscripten_glIsQueryEXT,sd:_emscripten_glIsRenderbuffer,cc:_emscripten_glIsSampler,rd:_emscripten_glIsShader,Ae:_emscripten_glIsSync,Dg:_emscripten_glIsTexture,Xb:_emscripten_glIsTransformFeedback,Pc:_emscripten_glIsVertexArray,Wd:_emscripten_glIsVertexArrayOES,Eg:_emscripten_glLineWidth,Fg:_emscripten_glLinkProgram,Ye:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Ze:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Wb:_emscripten_glPauseTransformFeedback,Gg:_emscripten_glPixelStorei,fd:_emscripten_glPolygonModeWEBGL,qd:_emscripten_glPolygonOffset,hd:_emscripten_glPolygonOffsetClampEXT,Tb:_emscripten_glProgramBinary,Sb:_emscripten_glProgramParameteri,ge:_emscripten_glQueryCounterEXT,$e:_emscripten_glReadBuffer,Hg:_emscripten_glReadPixels,pd:_emscripten_glReleaseShaderCompiler,Ke:_emscripten_glRenderbufferStorage,Ge:_emscripten_glRenderbufferStorageMultisample,Vb:_emscripten_glResumeTransformFeedback,od:_emscripten_glSampleCoverage,we:_emscripten_glSamplerParameterf,bc:_emscripten_glSamplerParameterfv,xe:_emscripten_glSamplerParameteri,ye:_emscripten_glSamplerParameteriv,Ig:_emscripten_glScissor,nd:_emscripten_glShaderBinary,Jg:_emscripten_glShaderSource,Kg:_emscripten_glStencilFunc,Lg:_emscripten_glStencilFuncSeparate,Mg:_emscripten_glStencilMask,Ng:_emscripten_glStencilMaskSeparate,Og:_emscripten_glStencilOp,Pg:_emscripten_glStencilOpSeparate,Qg:_emscripten_glTexImage2D,ad:_emscripten_glTexImage3D,Rg:_emscripten_glTexParameterf,Sg:_emscripten_glTexParameterfv,Tg:_emscripten_glTexParameteri,Ug:_emscripten_glTexParameteriv,Ve:_emscripten_glTexStorage2D,Rb:_emscripten_glTexStorage3D,Vg:_emscripten_glTexSubImage2D,$c:_emscripten_glTexSubImage3D,Jc:_emscripten_glTransformFeedbackVaryings,Wg:_emscripten_glUniform1f,Xg:_emscripten_glUniform1fv,Gf:_emscripten_glUniform1i,Hf:_emscripten_glUniform1iv,yc:_emscripten_glUniform1ui,uc:_emscripten_glUniform1uiv,If:_emscripten_glUniform2f,Jf:_emscripten_glUniform2fv,Ff:_emscripten_glUniform2i,Ef:_emscripten_glUniform2iv,xc:_emscripten_glUniform2ui,tc:_emscripten_glUniform2uiv,Df:_emscripten_glUniform3f,Cf:_emscripten_glUniform3fv,Bf:_emscripten_glUniform3i,Af:_emscripten_glUniform3iv,wc:_emscripten_glUniform3ui,sc:_emscripten_glUniform3uiv,zf:_emscripten_glUniform4f,yf:_emscripten_glUniform4fv,mf:_emscripten_glUniform4i,nf:_emscripten_glUniform4iv,vc:_emscripten_glUniform4ui,rc:_emscripten_glUniform4uiv,hc:_emscripten_glUniformBlockBinding,of:_emscripten_glUniformMatrix2fv,Wc:_emscripten_glUniformMatrix2x3fv,Uc:_emscripten_glUniformMatrix2x4fv,pf:_emscripten_glUniformMatrix3fv,Vc:_emscripten_glUniformMatrix3x2fv,Sc:_emscripten_glUniformMatrix3x4fv,qf:_emscripten_glUniformMatrix4fv,Tc:_emscripten_glUniformMatrix4x2fv,Rc:_emscripten_glUniformMatrix4x3fv,rf:_emscripten_glUseProgram,md:_emscripten_glValidateProgram,sf:_emscripten_glVertexAttrib1f,ld:_emscripten_glVertexAttrib1fv,kd:_emscripten_glVertexAttrib2f,tf:_emscripten_glVertexAttrib2fv,jd:_emscripten_glVertexAttrib3f,uf:_emscripten_glVertexAttrib3fv,id:_emscripten_glVertexAttrib4f,vf:_emscripten_glVertexAttrib4fv,We:_emscripten_glVertexAttribDivisor,Sd:_emscripten_glVertexAttribDivisorANGLE,Ob:_emscripten_glVertexAttribDivisorARB,ed:_emscripten_glVertexAttribDivisorEXT,Pb:_emscripten_glVertexAttribDivisorNV,Ec:_emscripten_glVertexAttribI4i,Cc:_emscripten_glVertexAttribI4iv,Dc:_emscripten_glVertexAttribI4ui,Bc:_emscripten_glVertexAttribI4uiv,Xe:_emscripten_glVertexAttribIPointer,wf:_emscripten_glVertexAttribPointer,xf:_emscripten_glViewport,Be:_emscripten_glWaitSync,Ya:_emscripten_request_animation_frame_loop,ob:_emscripten_resize_heap,Ib:_environ_get,Jb:_environ_sizes_get,_g:_exit,fa:_fd_close,sb:_fd_pread,Ga:_fd_read,wb:_fd_seek,ea:_fd_write,Yg:_glGetIntegerv,Oa:_glGetString,Zg:_glGetStringi,$d:invoke_dd,_d:invoke_ddd,ae:invoke_dddd,la:invoke_did,Ua:invoke_didddiddd,Da:invoke_diii,u:invoke_fdiiiii,Zd:invoke_fff,D:invoke_ffif,v:invoke_ffifif,$:invoke_ffifiii,V:invoke_fi,pa:invoke_fif,Sa:invoke_fii,Ea:invoke_fiii,U:invoke_fiiif,y:invoke_fiiiidi,gb:invoke_fiiiif,Na:invoke_fiiiii,p:invoke_i,Gc:invoke_if,hb:invoke_iffiiiiiiii,h:invoke_ii,L:invoke_iif,za:invoke_iiffi,g:invoke_iii,Ja:invoke_iiif,f:invoke_iiii,Ka:invoke_iiiif,l:invoke_iiiii,kb:invoke_iiiiid,R:invoke_iiiiii,Pa:invoke_iiiiiiffiii,A:invoke_iiiiiii,r:invoke_iiiiiiii,va:invoke_iiiiiiiii,ua:invoke_iiiiiiiiii,wa:invoke_iiiiiiiiiiifiii,ba:invoke_iiiiiiiiiiii,Ta:invoke_iij,_a:invoke_iiji,mb:invoke_j,ab:invoke_ji,m:invoke_jii,ca:invoke_jiiii,fb:invoke_jiijj,n:invoke_v,Ma:invoke_vfffffiii,b:invoke_vi,T:invoke_vid,X:invoke_vif,x:invoke_viff,J:invoke_vifff,q:invoke_viffff,F:invoke_vifffff,Wa:invoke_viffffff,Q:invoke_viffffffffffffffffffff,P:invoke_vifffiiff,oa:invoke_viffi,ib:invoke_viffii,ka:invoke_vifiiiiiii,c:invoke_vii,Ra:invoke_viidii,E:invoke_viif,s:invoke_viiff,ma:invoke_viifff,Va:invoke_viiffffff,da:invoke_viiffi,Za:invoke_viiffiii,qa:invoke_viifif,G:invoke_viififif,B:invoke_viifiiifi,e:invoke_viii,Qa:invoke_viiidididii,I:invoke_viiif,Y:invoke_viiiffi,_:invoke_viiiffiffii,K:invoke_viiifi,ia:invoke_viiififiiiiiiiiiiii,j:invoke_viiii,sa:invoke_viiiif,Z:invoke_viiiiff,Ba:invoke_viiiiffi,M:invoke_viiiifi,i:invoke_viiiii,ya:invoke_viiiiiff,db:invoke_viiiiiffiiifffi,eb:invoke_viiiiiffiiifii,Aa:invoke_viiiiifi,k:invoke_viiiiii,t:invoke_viiiiiii,C:invoke_viiiiiiii,O:invoke_viiiiiiiii,W:invoke_viiiiiiiiii,na:invoke_viiiiiiiiiii,aa:invoke_viiiiiiiiiiiiiii,Xa:invoke_viiiijjiiiiff,Ca:invoke_viiij,cb:invoke_viiijj,ga:invoke_viij,w:invoke_viiji,z:invoke_viijiii,La:invoke_viijiiiif,bb:invoke_vijff,xa:invoke_viji,$a:invoke_vijii,jb:invoke_vijiiii,ha:invoke_vijjjj,ta:_llvm_eh_typeid_for,xb:_random_get};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["ah"];var _init=Module["_init"]=wasmExports["ch"];var _tick=Module["_tick"]=wasmExports["dh"];var _resize_surface=Module["_resize_surface"]=wasmExports["eh"];var _redraw=Module["_redraw"]=wasmExports["fh"];var _load_scene_json=Module["_load_scene_json"]=wasmExports["gh"];var _apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["hh"];var _pointer_move=Module["_pointer_move"]=wasmExports["ih"];var _command=Module["_command"]=wasmExports["jh"];var _set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["kh"];var _add_image=Module["_add_image"]=wasmExports["lh"];var _get_image_bytes=Module["_get_image_bytes"]=wasmExports["mh"];var _get_image_size=Module["_get_image_size"]=wasmExports["nh"];var _add_font=Module["_add_font"]=wasmExports["oh"];var _has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["ph"];var _list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["qh"];var _list_available_fonts=Module["_list_available_fonts"]=wasmExports["rh"];var _set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["sh"];var _get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["th"];var _get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["uh"];var _get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["vh"];var _get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["wh"];var _get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["xh"];var _export_node_as=Module["_export_node_as"]=wasmExports["yh"];var _to_vector_network=Module["_to_vector_network"]=wasmExports["zh"];var _set_debug=Module["_set_debug"]=wasmExports["Ah"];var _toggle_debug=Module["_toggle_debug"]=wasmExports["Bh"];var _set_verbose=Module["_set_verbose"]=wasmExports["Ch"];var _devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Dh"];var _devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["Eh"];var _runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["Fh"];var _devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Gh"];var _devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["Hh"];var _devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Ih"];var _highlight_strokes=Module["_highlight_strokes"]=wasmExports["Jh"];var _load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["Kh"];var _load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["Lh"];var _grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["Mh"];var _grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["Nh"];var _grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["Oh"];var _allocate=Module["_allocate"]=wasmExports["Ph"];var _deallocate=Module["_deallocate"]=wasmExports["Qh"];var _grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["Rh"];var _grida_svg_pack=Module["_grida_svg_pack"]=wasmExports["Sh"];var _main=Module["_main"]=wasmExports["Th"];var _malloc=wasmExports["Uh"];var _emscripten_builtin_memalign=wasmExports["Vh"];var _setThrew=wasmExports["Wh"];var __emscripten_tempret_set=wasmExports["Xh"];var __emscripten_stack_restore=wasmExports["Yh"];var __emscripten_stack_alloc=wasmExports["Zh"];var _emscripten_stack_get_current=wasmExports["_h"];var ___cxa_decrement_exception_refcount=wasmExports["$h"];var ___cxa_increment_exception_refcount=wasmExports["ai"];var ___cxa_can_catch=wasmExports["bi"];var ___cxa_get_exception_ptr=wasmExports["ci"];function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffifif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vfffffiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_if(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ffifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iffiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffffffffffffffffff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiiiffiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffiiff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffffff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_did(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_didddiddd(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ddd(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fff(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;args.forEach(arg=>{HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4});HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; return moduleRtn; diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index e7e59a7f4f..b1f385a64b 100755 --- a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm +++ b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ba87f076bc05245c2a3c305d60730f3cf0689b8fb87120fdbd17be83b74d84e -size 11165265 +oid sha256:da1b4866193daae74f29651effd3905fc0ac1931193b55b0a94f442a456b87cf +size 12952199 diff --git a/crates/grida-canvas-wasm/lib/c-api.d.ts b/crates/grida-canvas-wasm/lib/c-api.d.ts new file mode 100644 index 0000000000..46b9be069a --- /dev/null +++ b/crates/grida-canvas-wasm/lib/c-api.d.ts @@ -0,0 +1,17 @@ +declare type CAPIMethodResultOk = { + success: true; + data: T; +}; + +declare type CAPIMethodResultError = { + success: false; + error: { + message: string; + }; +}; + +declare type CAPIMethodResult = + | CAPIMethodResultOk + | CAPIMethodResultError; + +declare type CPtr = number; diff --git a/crates/grida-canvas-wasm/lib/index.ts b/crates/grida-canvas-wasm/lib/index.ts index b762d8c8a3..6ba7ba2563 100644 --- a/crates/grida-canvas-wasm/lib/index.ts +++ b/crates/grida-canvas-wasm/lib/index.ts @@ -1,8 +1,8 @@ import createGridaCanvas from "./bin/grida-canvas-wasm"; import { version as _version } from "../package.json"; import { Scene } from "./modules/canvas"; - -export { type Scene }; +import { svgtypes } from "./modules/svg-bindings"; +export { type Scene, type svgtypes }; export const version = _version; export interface GridaCanvasModuleInitOptions { @@ -30,7 +30,8 @@ export namespace types { [number, number, number], [number, number, number], ]; - export type Rectangle = { + + export type Rect = { x: number; y: number; width: number; diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index c0a5fccb26..143a227014 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -1,5 +1,7 @@ import type { types } from "../"; import { FontsAPI } from "./fonts"; +import { MarkdownAPI } from "./markdown"; +import { SVGAPI } from "./svg"; import { memory } from "./memory"; const ApplicationCommandID = { @@ -29,11 +31,15 @@ export class Scene { private module: createGridaCanvas.GridaCanvasWasmBindings; public readonly fontskit: FontsAPI; + public readonly markdownkit: MarkdownAPI; + public readonly svgkit: SVGAPI; constructor(module: createGridaCanvas.GridaCanvasWasmBindings, ptr: number) { this.module = module; this.appptr = ptr; this.fontskit = new FontsAPI(module); + this.markdownkit = new MarkdownAPI(module); + this.svgkit = new SVGAPI(module); } /** @@ -256,7 +262,7 @@ export class Scene { return JSON.parse(str); } - getNodeIdsFromEnvelope(envelope: types.Rectangle): string[] { + getNodeIdsFromEnvelope(envelope: types.Rect): string[] { const ptr = this.module._get_node_ids_from_envelope( this.appptr, envelope.x, @@ -273,7 +279,7 @@ export class Scene { return JSON.parse(str); } - getNodeAbsoluteBoundingBox(id: string): types.Rectangle | null { + getNodeAbsoluteBoundingBox(id: string): types.Rect | null { const [ptr, len] = this._alloc_string(id); const outptr = this.module._get_node_absolute_bounding_box( this.appptr, diff --git a/crates/grida-canvas-wasm/lib/modules/markdown-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/markdown-bindings.d.ts new file mode 100644 index 0000000000..dd7683e1ad --- /dev/null +++ b/crates/grida-canvas-wasm/lib/modules/markdown-bindings.d.ts @@ -0,0 +1,26 @@ +export namespace markdown { + export type MarkdownToHtmlResponse = CAPIMethodResult<{ + /** Converted HTML string */ + html: string; + }>; + + // ==================================================================================================== + // #region: WASM Function Declarations + // ==================================================================================================== + + export interface MarkdownModule { + // ==================================================================================================== + // #region: High-Level Markdown APIs + // ==================================================================================================== + + /** + * Converts markdown text to HTML. + * Parses markdown content and converts it to HTML using the pulldown-cmark library. + * + * @param markdown - Pointer to input markdown string (null-terminated C string) + * @returns Pointer to JSON string containing {@link MarkdownToHtmlResponse} + */ + _grida_markdown_to_html(markdown: CPtr): CPtr; + } +} + diff --git a/crates/grida-canvas-wasm/lib/modules/markdown.ts b/crates/grida-canvas-wasm/lib/modules/markdown.ts new file mode 100644 index 0000000000..fea3bab572 --- /dev/null +++ b/crates/grida-canvas-wasm/lib/modules/markdown.ts @@ -0,0 +1,84 @@ +// ==================================================================================================== +// #region: High-Level JavaScript Wrapper Functions +// ==================================================================================================== + +import type { markdown } from "./markdown-bindings"; + +export class MarkdownAPI { + private module: createGridaCanvas.GridaCanvasWasmBindings; + + constructor(module: any) { + this.module = module; + } + + /** + * Allocates memory for a string and returns pointer and length. + * @param txt - String to allocate + * @returns [pointer, length] tuple + */ + private _alloc_string(txt: string): [number, number] { + const len = this.module.lengthBytesUTF8(txt) + 1; + const ptr = this.module._allocate(len); + this.module.stringToUTF8(txt, ptr, len); + return [ptr, len]; + } + + /** + * Frees memory allocated for a string. + * @param ptr - Pointer to free + * @param len - Length of allocated memory + */ + private _free_string(ptr: number, len: number) { + this.module._deallocate(ptr, len); + } + + /** + * Converts a WASM-allocated string to JavaScript string and frees the WASM memory. + * @param ptr - Pointer to WASM string + * @returns JavaScript string + */ + private _string_from_wasm(ptr: number): string { + const str = this.module.UTF8ToString(ptr); + const len = this.module.lengthBytesUTF8(str) + 1; + this._free_string(ptr, len); + return str; + } + + /** + * Converts markdown text to HTML with JavaScript-friendly interface. + * Parses markdown content and converts it to HTML using the pulldown-cmark library. + * + * @param markdown - Input markdown string + * @returns MarkdownToHtmlResponse containing the converted HTML or error information + */ + toHtml(markdown: string): markdown.MarkdownToHtmlResponse { + let markdownPtr: number | null = null; + let markdownLen: number | null = null; + try { + // Allocate markdown string + [markdownPtr, markdownLen] = this._alloc_string(markdown); + + // Call WASM function + const resultPtr = this.module._grida_markdown_to_html(markdownPtr); + + // Get result + const resultJson = this._string_from_wasm(resultPtr); + const result = JSON.parse(resultJson) as markdown.MarkdownToHtmlResponse; + + return result; + } catch (error) { + return { + success: false, + error: { + message: error instanceof Error ? error.message : String(error), + }, + }; + } finally { + // Always clean up allocated memory + if (markdownPtr !== null && markdownLen !== null) { + this._free_string(markdownPtr, markdownLen); + } + } + } +} + diff --git a/crates/grida-canvas-wasm/lib/modules/memory.ts b/crates/grida-canvas-wasm/lib/modules/memory.ts index f55ad52ad3..112e07ef5c 100644 --- a/crates/grida-canvas-wasm/lib/modules/memory.ts +++ b/crates/grida-canvas-wasm/lib/modules/memory.ts @@ -1,7 +1,7 @@ import type { types } from ".."; export namespace memory { - export function rect_from_vec4(vec4: Float32Array): types.Rectangle { + export function rect_from_vec4(vec4: Float32Array): types.Rect { return { x: vec4[0], y: vec4[1], diff --git a/crates/grida-canvas-wasm/lib/modules/svg-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/svg-bindings.d.ts new file mode 100644 index 0000000000..3f0f9d762f --- /dev/null +++ b/crates/grida-canvas-wasm/lib/modules/svg-bindings.d.ts @@ -0,0 +1,185 @@ +export namespace svgtypes { + // ==================================================================================================== + // #region: Core Type Definitions + // ==================================================================================================== + + /** + * rust/serde Option equivalant + */ + type TOption = T | null; + type RGBA8888 = [r: number, g: number, b: number, a: number]; + type Transform2D = [[number, number, number], [number, number, number]]; + type StrokeCap = "butt" | "round" | "square"; + type StrokeJoin = "miter" | "round" | "bevel"; + type FillRule = "nonzero" | "evenodd"; + type BlendMode = + | "normal" + | "multiply" + | "screen" + | "overlay" + | "darken" + | "lighten" + | "color-dodge" + | "color-burn" + | "hard-light" + | "soft-light" + | "difference" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + + export enum SVGTextAnchor { + start = "start", + middle = "middle", + end = "end", + } + + export enum SVGSpreadMethod { + pad = "pad", + reflect = "reflect", + repeat = "repeat", + } + + export interface SVGSolidPaint { + kind: "solid"; + color: RGBA8888; + } + + export interface SVGGradientStop { + color: RGBA8888; + offset: number; + // opacity: number; + } + + export interface SVGLinearGradientPaint { + kind: "linear-gradient"; + id: string; + x1: number; + y1: number; + x2: number; + y2: number; + transform: Transform2D; + stops: Array; + spread_method: SVGSpreadMethod; + } + + export interface SVGRadialGradientPaint { + kind: "radial-gradient"; + id: string; + cx: number; + cy: number; + r: number; + fx: number; + fy: number; + transform: Transform2D; + stops: Array; + spread_method: SVGSpreadMethod; + } + + export type SVGPaint = + | SVGSolidPaint + | SVGLinearGradientPaint + | SVGRadialGradientPaint; + + export interface SVGFillAttributes { + paint: SVGPaint; + fill_opacity: number; + fill_rule: FillRule; + } + + export interface SVGStrokeAttributes { + paint: SVGPaint; + stroke_width: TOption; + stroke_linecap: StrokeCap; + stroke_linejoin: StrokeJoin; + stroke_miterlimit: number; + stroke_dasharray: TOption>; + stroke_opacity: number; + } + + export namespace ir { + export interface IRSVGInitialContainerNode { + width: number; + height: number; + children: Array; + } + + export type IRSVGChildNode = + | IRSVGGroupNode + | IRSVGPathNode + | IRSVGTextNode + | IRSVGImageNode; + + export interface IRSVGGroupNode { + kind: "group"; + transform: Transform2D; + opacity: number; + blend_mode: BlendMode; + children: Array; + } + + export interface IRSVGPathNode { + kind: "path"; + transform: Transform2D; + fill: TOption; + stroke: TOption; + d: string; + } + + export interface IRSVGTextNode { + kind: "text"; + transform: Transform2D; + text_content: string; + fill: TOption; + stroke: TOption; + spans: Array; + } + + export interface IRSVGTextSpanNode { + transform: Transform2D; + text: string; + fill: TOption; + stroke: TOption; + font_size: TOption; + anchor: SVGTextAnchor; + } + + export interface IRSVGImageNode { + kind: "image"; + } + } +} + +export namespace svg { + export type SVGOptimizeResponse = CAPIMethodResult<{ + /** Optimized SVG string with CSS styles resolved and inlined */ + svg_optimized: string; + }>; + + export type SVGPackResponse = CAPIMethodResult<{ + svg: svgtypes.ir.IRSVGInitialContainerNode; + }>; + + // ==================================================================================================== + // #region: WASM Function Declarations + // ==================================================================================================== + + export interface SVGModule { + // ==================================================================================================== + // #region: High-Level SVG APIs + // ==================================================================================================== + + /** + * Optimizes and resolves an SVG, producing a flat, self-contained SVG output. + * Resolves CSS styles from ` +/// +/// +/// "#; +/// +/// let output = svg_optimize(input)?; +/// // Output: SVG with styles resolved and inlined as attributes +/// // - + + + +

Hello, HTML!

+ + + \ No newline at end of file diff --git a/fixtures/test-html/L0/layout-block.html b/fixtures/test-html/L0/layout-block.html new file mode 100644 index 0000000000..98b3a0eece --- /dev/null +++ b/fixtures/test-html/L0/layout-block.html @@ -0,0 +1,66 @@ + + + + + Layout – Block Flow + + + +
+
+

Block Flow Layout

+

Each region stacks vertically and spans the available inline size.

+
+
+
+

Article A

+

Demonstrates default block behavior within a block-level parent.

+
+
+

Article B

+

Blocks consume full width, pushing the following sibling downward.

+
+
+ +
+ Footer sticks to the end of the flow. +
+
+ + + diff --git a/fixtures/test-html/L0/layout-flex.html b/fixtures/test-html/L0/layout-flex.html new file mode 100644 index 0000000000..baf53d42e5 --- /dev/null +++ b/fixtures/test-html/L0/layout-flex.html @@ -0,0 +1,80 @@ + + + + + Layout – Flexbox + + + +
+

Row Flex

+
+
Box A
+
Box B
+
Box C
+
Box D
+
+
+
+

Row aligned

+
+
Start
+
Center
+
End
+
+
+
+

Column Flex

+
+
Col 1
+
Col 2
+
Col 3
+
+
+ + + diff --git a/fixtures/test-html/L0/painting-bg-gradient.html b/fixtures/test-html/L0/painting-bg-gradient.html new file mode 100644 index 0000000000..e9560ff5a8 --- /dev/null +++ b/fixtures/test-html/L0/painting-bg-gradient.html @@ -0,0 +1,41 @@ + + + + + Painting – Background Gradient + + + +
+

Layered background

+

+ Shows radial and linear gradients stacked with translucency to validate painting order and + blend behavior. +

+
+ + + diff --git a/fixtures/test-html/L0/painting-borders.html b/fixtures/test-html/L0/painting-borders.html new file mode 100644 index 0000000000..a9e492b5a6 --- /dev/null +++ b/fixtures/test-html/L0/painting-borders.html @@ -0,0 +1,56 @@ + + + + + Painting – Borders + + + +
+
Solid border
+
Dashed border
+
Gradient border
+
Shadowed border
+
+ + + diff --git a/fixtures/test-html/L0/painting-fg-gradient.html b/fixtures/test-html/L0/painting-fg-gradient.html new file mode 100644 index 0000000000..c1b3334d0d --- /dev/null +++ b/fixtures/test-html/L0/painting-fg-gradient.html @@ -0,0 +1,43 @@ + + + + + Painting – Foreground Gradient + + + +

Gradient Text

+

+ Text rendering demo with layered gradients. The foreground uses + background clipping to produce a smooth transition across glyphs. +

+ + + diff --git a/fixtures/test-html/L0/structure-div-nested.html b/fixtures/test-html/L0/structure-div-nested.html new file mode 100644 index 0000000000..fc520fce14 --- /dev/null +++ b/fixtures/test-html/L0/structure-div-nested.html @@ -0,0 +1,80 @@ + + + + + Structure – Div Nested + + + +
+
+

Nested Regions

+

Demonstrates stacked containers with nested divisions.

+
+
+

Section A

+
+

Group A.1

+
+

Group A.1.a content block.

+
+

Leaf block with inline text only.

+
+
+
+
+
+

Section B

+
+
+

Group B.1 contains two sibling leaves.

+
+

Leaf 1

+
+
+

Leaf 2

+
+
+
+
+
+ + + diff --git a/fixtures/test-html/L0/structure-table.html b/fixtures/test-html/L0/structure-table.html new file mode 100644 index 0000000000..5f58cfc949 --- /dev/null +++ b/fixtures/test-html/L0/structure-table.html @@ -0,0 +1,102 @@ + + + + + Structure – Lists and Table + + + +
+
+

Lists

+
    +
  1. Step one
  2. +
  3. Step two
  4. +
  5. Step three
  6. +
+
    +
  • Unordered item A
  • +
  • Unordered item B
  • +
  • Unordered item C
  • +
+
+
Term
+
Definition of the term with a short sentence.
+
+
+
+

Table

+ + + + + + + + + + + + + + + + + + + + + + + + + +
LayerRoleStatus
HeaderBrandingActive
BodyContentActive
FooterNavigationMuted
+
+
+ + + diff --git a/fixtures/test-html/L0/text-article.html b/fixtures/test-html/L0/text-article.html new file mode 100644 index 0000000000..83325e492c --- /dev/null +++ b/fixtures/test-html/L0/text-article.html @@ -0,0 +1,75 @@ + + + + + Text – Article + + + +
+
+

Patterns · Case Study

+

Design tokens for expressive interfaces

+

+ · 4 min read +

+
+

+ Interfaces communicate intent through hierarchy, rhythm, and motion. A good token system + keeps those variables predictable without flattening the personality of each surface. +

+

+ Start with a concise scale, reference design primitives, and capture rationale alongside + every change so the audit trail stays human. +

+
+
Figure 1. Affordance layers across surfaces.
+
+

+ When teams remix layers, focus on the relationships: background vs. card, card vs. control. + Document the pairings so experimentation remains predictable. +

+
token.surface.card = color.neutral.900
+token.surface.raised = mix(token.surface.card, color.primary.500, 12%)
+
Written by the Grida design systems crew.
+
+ + + diff --git a/fixtures/test-html/L0/text-inline-formatting.html b/fixtures/test-html/L0/text-inline-formatting.html new file mode 100644 index 0000000000..467ac1ffd3 --- /dev/null +++ b/fixtures/test-html/L0/text-inline-formatting.html @@ -0,0 +1,51 @@ + + + + + Text – Inline Formatting + + + +
+

Strong and bold emphasis.

+

Italic and alternate italic words.

+

+ Superscript H2O and subscript CO2. +

+

+ Inline code(), strikethrough, and underline. +

+

+ Keyboard combo + K with highlight. +

+

+ CSS abbreviation plus spacing. +

+
+ + + diff --git a/fixtures/test-html/L0/text-typography.html b/fixtures/test-html/L0/text-typography.html new file mode 100644 index 0000000000..2d95f5f4f8 --- /dev/null +++ b/fixtures/test-html/L0/text-typography.html @@ -0,0 +1,80 @@ + + + + + Text – Typography + + + +
+

Heading One

+

+ Intro paragraph with a strong word, an emphasized word, and + a link to grida.co. +

+

Heading Two

+

+ Additional text with inline code() and a HTML + abbreviation. +

+

Heading Three

+
Typography quote sample highlighted with a border.
+

Heading Four

+

+ Highlighted text combined with underline for contrast. +

+
Heading Five
+

+ Small print for captions or legal disclaimers. +

+
Heading Six
+
+ + + diff --git a/fixtures/test-svg/L0/filters-feColorMatrix.svg b/fixtures/test-svg/L0/filters-feColorMatrix.svg new file mode 100644 index 0000000000..35d1e9cf78 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feColorMatrix.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + Reference + + + + + + + Identity matrix + + + + + + + rgbToGreen + + + + + + + saturate + + + + + + + hueRotate + + + + + + + luminanceToAlpha + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feComponentTransfer.svg b/fixtures/test-svg/L0/filters-feComponentTransfer.svg new file mode 100644 index 0000000000..f6462bffd6 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feComponentTransfer.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + Identity + + Table lookup + + Discrete table lookup + + Linear function + + Gamma function + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feComposite.svg b/fixtures/test-svg/L0/filters-feComposite.svg new file mode 100644 index 0000000000..7c78529412 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feComposite.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feConvolveMatrix.svg b/fixtures/test-svg/L0/filters-feConvolveMatrix.svg new file mode 100644 index 0000000000..f3a0b60119 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feConvolveMatrix.svg @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feDiffuseLighting.svg b/fixtures/test-svg/L0/filters-feDiffuseLighting.svg new file mode 100644 index 0000000000..5bef8f49b8 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feDiffuseLighting.svg @@ -0,0 +1,69 @@ + + + No Light + + + + fePointLight + + + + + + + + + + + + feDistantLight + + + + + + + + + + + + feSpotLight + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feDisplacementMap.svg b/fixtures/test-svg/L0/filters-feDisplacementMap.svg new file mode 100644 index 0000000000..ab8895d148 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feDisplacementMap.svg @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feDropShadow.svg b/fixtures/test-svg/L0/filters-feDropShadow.svg new file mode 100644 index 0000000000..19ecc49772 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feDropShadow.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feFlood.svg b/fixtures/test-svg/L0/filters-feFlood.svg new file mode 100644 index 0000000000..bc69d3b5b6 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feFlood.svg @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feGaussianBlur.svg b/fixtures/test-svg/L0/filters-feGaussianBlur.svg new file mode 100644 index 0000000000..f5fe51430e --- /dev/null +++ b/fixtures/test-svg/L0/filters-feGaussianBlur.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feMorphology.svg b/fixtures/test-svg/L0/filters-feMorphology.svg new file mode 100644 index 0000000000..0507d6e69d --- /dev/null +++ b/fixtures/test-svg/L0/filters-feMorphology.svg @@ -0,0 +1,25 @@ + + + + + + + + + Normal text + Thinned text + Fattened text + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feSpecularLighting.svg b/fixtures/test-svg/L0/filters-feSpecularLighting.svg new file mode 100644 index 0000000000..c60e0b3cb4 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feSpecularLighting.svg @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/filters-feTurbulence.svg b/fixtures/test-svg/L0/filters-feTurbulence.svg new file mode 100644 index 0000000000..3eadfbf5d6 --- /dev/null +++ b/fixtures/test-svg/L0/filters-feTurbulence.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FractalNoise Low + + + FractalNoise High + + + FractalNoise 8 Octaves + + + + Turbulence Low + + + Turbulence High + + + Stitched Tiles + + + + Anisotropic (0.1, 0.3) + \ No newline at end of file diff --git a/fixtures/test-svg/L0/groups.svg b/fixtures/test-svg/L0/groups.svg new file mode 100644 index 0000000000..bc368b9ec8 --- /dev/null +++ b/fixtures/test-svg/L0/groups.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/paint-linear-gradient-01.svg b/fixtures/test-svg/L0/paint-linear-gradient-01.svg new file mode 100644 index 0000000000..b21e9907ca --- /dev/null +++ b/fixtures/test-svg/L0/paint-linear-gradient-01.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/paint-radial-gradient-01.svg b/fixtures/test-svg/L0/paint-radial-gradient-01.svg new file mode 100644 index 0000000000..7830b2f431 --- /dev/null +++ b/fixtures/test-svg/L0/paint-radial-gradient-01.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/paints.svg b/fixtures/test-svg/L0/paints.svg new file mode 100644 index 0000000000..4ebc278499 --- /dev/null +++ b/fixtures/test-svg/L0/paints.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/shapes.svg b/fixtures/test-svg/L0/shapes.svg new file mode 100644 index 0000000000..0cdd422b55 --- /dev/null +++ b/fixtures/test-svg/L0/shapes.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/strokes.svg b/fixtures/test-svg/L0/strokes.svg new file mode 100644 index 0000000000..4e4c4351ef --- /dev/null +++ b/fixtures/test-svg/L0/strokes.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/text-generic-fonts.svg b/fixtures/test-svg/L0/text-generic-fonts.svg new file mode 100644 index 0000000000..9b5ffb2410 --- /dev/null +++ b/fixtures/test-svg/L0/text-generic-fonts.svg @@ -0,0 +1,30 @@ + + + + + + Sans-serif heading + + + Sans-serif semi-bold body copy line demonstrating default font fallback. + + + + + + Serif heading + + + Serif italic subline with generic family only. + + + + + + Monospace sample 0123456789 + + + Cursive playful text + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/texts.svg b/fixtures/test-svg/L0/texts.svg new file mode 100644 index 0000000000..910d1d3383 --- /dev/null +++ b/fixtures/test-svg/L0/texts.svg @@ -0,0 +1,46 @@ + + + + + + Sample Heading + + + Simple subtitle line + + + italic serif + + + monospace 123456 + + + + + Multi-line example: second line via tspan + third + line tinted + + + + + + left + + + center + + + right + + + + + Spans: cyan-highlight + + magenta-highlight + + + \ No newline at end of file diff --git a/fixtures/test-svg/L0/transforms.svg b/fixtures/test-svg/L0/transforms.svg new file mode 100644 index 0000000000..c3b0897797 --- /dev/null +++ b/fixtures/test-svg/L0/transforms.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/grida-canvas-io-svg/index.ts b/packages/grida-canvas-io-svg/index.ts index 68dea8cdac..949990dbf4 100644 --- a/packages/grida-canvas-io-svg/index.ts +++ b/packages/grida-canvas-io-svg/index.ts @@ -1,2 +1,3 @@ -import { iosvg } from "./lib"; +import { iosvg, svgtypes } from "./lib"; export default iosvg; +export type { svgtypes }; diff --git a/packages/grida-canvas-io-svg/lib.ts b/packages/grida-canvas-io-svg/lib.ts index 59b102f20b..aaa625fa3b 100644 --- a/packages/grida-canvas-io-svg/lib.ts +++ b/packages/grida-canvas-io-svg/lib.ts @@ -1,677 +1,292 @@ -import { parse, type INode } from "svgson"; -import parseStyle, { type Declaration } from "inline-style-parser"; -// @ts-ignore -import * as svgo from "svgo/dist/svgo.browser.js"; -import type { Config, Output } from "svgo"; import type grida from "@grida/schema"; import type cg from "@grida/cg"; import cmath from "@grida/cmath"; import vn from "@grida/vn"; -import colors from "color-name"; +import type { svgtypes } from "@grida/canvas-wasm"; -/** - * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute - */ -type SVGElementAttributes = { - // [A] - "accent-height": unknown; - accumulate: unknown; - additive: unknown; - "alignment-baseline": unknown; - alphabetic: unknown; - amplitude: unknown; - "arabic-form": unknown; - ascent: unknown; - attributeName: unknown; - attributeType: unknown; - azimuth: unknown; - // [B] - baseFrequency: unknown; - "baseline-shift": unknown; - baseProfile: unknown; - bbox: unknown; - begin: unknown; - bias: unknown; - by: unknown; - // [C] - calcMode: unknown; - "cap-height": unknown; - class: unknown; - clip: unknown; - clipPathUnits: unknown; - "clip-path": unknown; - "clip-rule": unknown; - color: unknown; - "color-interpolation": unknown; - "color-interpolation-filters": unknown; - "color-rendering": unknown; - crossorigin: unknown; - cursor: unknown; - cx: unknown; - cy: unknown; - // [D] - d: string; - // data-*: - decelerate: unknown; - decoding: unknown; - descent: unknown; - diffuseConstant: unknown; - direction: unknown; - display: unknown; - divisor: unknown; - "dominant-baseline": unknown; - dur: unknown; - dx: unknown; - dy: unknown; - // [E] - edgeMode: unknown; - elevation: unknown; - end: unknown; - exponent: unknown; - // [F] - fill: unknown; - "fill-opacity": unknown; - "fill-rule": unknown; - filter: unknown; - filterUnits: unknown; - "flood-color": unknown; - "flood-opacity": unknown; - "font-family": unknown; - "font-size": unknown; - "font-size-adjust": unknown; - "font-stretch": unknown; - "font-style": unknown; - "font-variant": unknown; - "font-weight": unknown; - format: unknown; - from: unknown; - fr: unknown; - fx: unknown; - fy: unknown; - // [G] - g1: unknown; - g2: unknown; - "glyph-name": unknown; - "glyph-orientation-horizontal": unknown; - "glyph-orientation-vertical": unknown; - glyphRef: unknown; - gradientTransform: unknown; - gradientUnits: unknown; - // [H] - hanging: unknown; - height: string; - href: unknown; - hreflang: unknown; - "horiz-adv-x": unknown; - "horiz-origin-x": unknown; - "horiz-origin-y": unknown; - // [I] - id: unknown; - ideographic: unknown; - "image-rendering": unknown; - in: unknown; - in2: unknown; - intercept: unknown; - // [K] - k: unknown; - k1: unknown; - k2: unknown; - k3: unknown; - k4: unknown; - kernelMatrix: unknown; - kernelUnitLength: unknown; - keyPoints: unknown; - keySplines: unknown; - keyTimes: unknown; - // [L] - lang: unknown; - lengthAdjust: unknown; - "letter-spacing": unknown; - "lighting-color": unknown; - limitingConeAngle: unknown; - local: unknown; - // [M] - "marker-end": unknown; - "marker-mid": unknown; - "marker-start": unknown; - markerHeight: unknown; - markerUnits: unknown; - markerWidth: unknown; - mask: unknown; - maskContentUnits: unknown; - maskUnits: unknown; - mathematical: unknown; - max: unknown; - media: unknown; - method: unknown; - min: unknown; - mode: unknown; - // [N] - name: unknown; - numOctaves: unknown; - // [O] - offset: unknown; - opacity: unknown; - operator: unknown; - order: unknown; - orient: unknown; - orientation: unknown; - origin: unknown; - overflow: unknown; - "overline-position": unknown; - "overline-thickness": unknown; - // [P] - "panose-1": unknown; - "paint-order": unknown; - path: unknown; - pathLength: unknown; - patternContentUnits: unknown; - patternTransform: unknown; - patternUnits: unknown; - ping: unknown; - "pointer-events": unknown; - points: unknown; - pointsAtX: unknown; - pointsAtY: unknown; - pointsAtZ: unknown; - preserveAlpha: unknown; - preserveAspectRatio: unknown; - primitiveUnits: unknown; - // [R] - r: unknown; - radius: unknown; - referrerPolicy: unknown; - refX: unknown; - refY: unknown; - rel: unknown; - "rendering-intent": unknown; - repeatCount: unknown; - repeatDur: unknown; - requiredExtensions: unknown; - requiredFeatures: unknown; - restart: unknown; - result: unknown; - rotate: unknown; - rx: unknown; - ry: unknown; - // [S] - scale: unknown; - seed: unknown; - "shape-rendering": unknown; - side: unknown; - slope: unknown; - spacing: unknown; - specularConstant: unknown; - specularExponent: unknown; - speed: unknown; - spreadMethod: unknown; - startOffset: unknown; - stdDeviation: unknown; - stemh: unknown; - stemv: unknown; - stitchTiles: unknown; - "stop-color": unknown; - "stop-opacity": unknown; - "strikethrough-position": unknown; - "strikethrough-thickness": unknown; - string: unknown; - stroke: unknown; - "stroke-dasharray": unknown; - "stroke-dashoffset": unknown; - "stroke-linecap": unknown; - "stroke-linejoin": unknown; - "stroke-miterlimit": unknown; - "stroke-opacity": unknown; - "stroke-width": unknown; - style: unknown; - surfaceScale: unknown; - systemLanguage: unknown; - // [T] - tabindex: unknown; - tableValues: unknown; - target: unknown; - targetX: unknown; - targetY: unknown; - "text-anchor": unknown; - "text-decoration": unknown; - "text-rendering": unknown; - textLength: unknown; - to: unknown; - transform: unknown; - "transform-origin": unknown; - type: unknown; - // [U] - u1: unknown; - u2: unknown; - "underline-position": unknown; - "underline-thickness": unknown; - unicode: unknown; - "unicode-bidi": unknown; - "unicode-range": unknown; - "units-per-em": unknown; - // [V] - "v-alphabetic": unknown; - "v-hanging": unknown; - "v-ideographic": unknown; - "v-mathematical": unknown; - values: unknown; - "vector-effect": unknown; - version: unknown; - "vert-adv-y": unknown; - "vert-origin-x": unknown; - "vert-origin-y": unknown; - viewBox: string; - visibility: unknown; - // [W] - width: string; - widths: unknown; - "word-spacing": unknown; - "writing-mode": unknown; - // [X] - x: unknown; - "x-height": unknown; - x1: unknown; - x2: unknown; - xChannelSelector: unknown; - "xlink:actuate": unknown; - "xlink:arcrole": unknown; - "xlink:hrefDeprecated": unknown; - "xlink:role": unknown; - "xlink:show": unknown; - "xlink:title": unknown; - "xlink:type": unknown; - "xml:lang": unknown; - "xml:space": unknown; - // [Y] - y: unknown; - y1: unknown; - y2: unknown; - yChannelSelector: unknown; - // [Z] - z: unknown; - zoomAndPan: unknown; -}; +// re-export svgtypes +export type { svgtypes }; interface SVGFactoryUserContext { name: string; - currentColor: cmath.color.RGBA8888; -} - -interface SVGFactoryContext extends SVGFactoryUserContext { - depth: number; + currentColor?: cmath.colorformats.RGBA8888; } export namespace iosvg { export namespace map { - export function opacity( - opacity: string | null | undefined - ): number | undefined { - if (opacity === undefined || opacity === null) { - return undefined; - } - - const parsed = parseFloat(opacity); - if (isNaN(parsed)) { - return undefined; - } - - return parsed; - } - - export function stroke( - stroke: string | undefined, - context: SVGFactoryContext - ): cg.Paint | undefined { - return paint(stroke, context); - } - - export function fill( - fill: string | undefined, - context: SVGFactoryContext - ): cg.Paint | undefined { - return paint(fill, context, "currentColor"); + /** + * Extract translation (left, top) from Transform2D matrix + * Transform2D format: [[m00, m01, m02], [m10, m11, m12]] + * Translation is in m02 (x) and m12 (y) + */ + export function extractTranslation(transform: svgtypes.Transform2D): { + left: number; + top: number; + } { + return { + left: transform[0][2] ?? 0, + top: transform[1][2] ?? 0, + }; } + /** + * Convert SVG Paint to Grida Paint with optional opacity + */ export function paint( - paint: string | undefined, - context: SVGFactoryContext, - /** - * fallback value when the paint is undefined - */ - fallback: "none" | "currentColor" = "none" + paint: svgtypes.SVGPaint | null | undefined, + opacity: number = 1.0 ): cg.Paint | undefined { - paint = paint ?? fallback; - switch (paint) { - case "none": - return undefined; - - case "currentColor": - return { type: "solid", color: context.currentColor, active: true }; - default: - const namedcolor = ( - colors as Record - )[paint]; - if (namedcolor) { - return { - type: "solid", - color: { - r: namedcolor[0], - g: namedcolor[1], - b: namedcolor[2], - a: 255, - }, - active: true, - }; - } - - if (paint.startsWith("#")) { - return { - type: "solid", - color: cmath.color.hex_to_rgba8888(paint), - active: true, - }; - } + if (!paint) { + return undefined; } - return undefined; - } - } - - export namespace v0 { - function mergeAttributes( - parent: Partial, - current: Partial - ): Partial { - return Object.assign({}, parent, current); - } - - function mapnode( - node: INode, - context: SVGFactoryContext, - inheritedAttributes: Partial = {} - ): SVGIOCompatibleNodePrototype[] | null { - const { name, attributes: _attributes, children } = node; - - const nodename = context.depth === 0 ? context.name : name; - - // Merge attributes - const attributes = mergeAttributes( - inheritedAttributes, - _attributes - ) as Partial; - - switch (name) { - // [svg] => container - case "svg": { - const { width, height, viewBox } = attributes; - - const viewbox = viewBox?.split(" ").map(parseFloat); - const [vx, vy, vw, vh] = viewbox ?? [0, 0, 0, 0]; - - return [ - { - type: "container", - name: nodename, - position: "absolute", - left: 0, - top: 0, - width: width ? parseFloat(width as string) : vw, - height: height ? parseFloat(height as string) : vh, - children: children - .map((child) => - mapnode(child, { ...context, depth: context.depth + 1 }) - ) - .flat() - .filter(Boolean) as grida.program.nodes.NodePrototype[], - } satisfies grida.program.nodes.ContainerNodePrototype, - ]; + switch (paint.kind) { + case "solid": { + // paint.color is RGBA8888 chunk [r, g, b, a] (all 0-255) from WASM + const [r, g, b, a] = paint.color; + // Convert to RGBA8888 object format, apply opacity to alpha, then convert to RGB888A32F + const rgba8888: cmath.colorformats.RGBA8888 = { + r, + g, + b, + a: a * opacity, // Apply opacity to alpha (still 0-255 range) + }; + const rgb888a32f = + cmath.colorformats.RGBA8888.intoRGB888F32A(rgba8888); + return { + type: "solid", + color: rgb888a32f, + active: true, + }; } - // [g] => ... - case "g": { - return children - .flatMap((child) => - mapnode( - child, - { ...context, depth: context.depth + 1 }, - attributes - ) - ) - .filter(Boolean) as SVGIOCompatibleNodePrototype[]; + case "linear-gradient": { + return { + type: "linear_gradient", + transform: paint.transform, + stops: paint.stops.map((stop) => { + // stop.color is RGBA8888 chunk [r, g, b, a] (all 0-255) from WASM + const [r, g, b, a] = stop.color; + // Convert to RGBA8888 object format, then convert to RGB888A32F + const rgba8888: cmath.colorformats.RGBA8888 = { r, g, b, a }; + const rgb888a32f = + cmath.colorformats.RGBA8888.intoRGB888F32A(rgba8888); + return { + offset: stop.offset, + color: rgb888a32f, + }; + }), + blendMode: "normal", + opacity: opacity, + active: true, + }; } - // [path] => path with vector network - case "path": { - const { d } = attributes; - - const { style: _style, fill: _fill, stroke, opacity } = attributes; - const style = _style ? parseStyle(_style as string) : []; - const fillstyle: Declaration | undefined = style.find( - (d) => d.type === "declaration" && d.property === "fill" - ) as Declaration | undefined; - fillstyle?.value; - - const fill = _fill ?? fillstyle?.value; - - const fillRule = - (attributes["fill-rule"] as cg.FillRule) ?? "nonzero"; - - const vectorNetwork = vn.fromSVGPathData(d!); - const bbox = vn.getBBox(vectorNetwork); - return [ - { - type: "vector", - name: nodename, - vectorNetwork: vectorNetwork, - opacity: map.opacity(opacity as string), - fill: map.fill(fill as string, context), - stroke: map.stroke(stroke as string, context), - width: bbox.width, - height: bbox.height, - left: 0, - top: 0, - fillRule: fillRule, - } satisfies grida.program.nodes.PathNodePrototype, - ]; + case "radial-gradient": { + return { + type: "radial_gradient", + transform: paint.transform, + stops: paint.stops.map((stop) => { + // stop.color is RGBA8888 chunk [r, g, b, a] (all 0-255) from WASM + const [r, g, b, a] = stop.color; + // Convert to RGBA8888 object format, then convert to RGB888A32F + const rgba8888: cmath.colorformats.RGBA8888 = { r, g, b, a }; + const rgb888a32f = + cmath.colorformats.RGBA8888.intoRGB888F32A(rgba8888); + return { + offset: stop.offset, + color: rgb888a32f, + }; + }), + blendMode: "normal", + opacity: opacity, + active: true, + }; } - // [polyline, polygon] => path with vector network - case "polyline": - case "polygon": { - const { style, fill, stroke, opacity, points } = attributes; - break; - } - - // [rect] => rectangle - case "rect": - const { - x, - y, - width, - height, - rx, - ry, // not supported - pathLength, // not supported - } = attributes; - - const { style: _style, fill: _fill, stroke, opacity } = attributes; - const style = _style ? parseStyle(_style as string) : []; - const fillstyle: Declaration | undefined = style.find( - (d) => d.type === "declaration" && d.property === "fill" - ) as Declaration | undefined; - - const fill = _fill ?? fillstyle?.value; - - return [ - { - type: "rectangle", - name: nodename, - left: parseFloat(x as string), - top: parseFloat(y as string), - width: parseFloat(width as string), - height: parseFloat(height as string), - cornerRadius: parseFloat(rx as string), - opacity: map.opacity(opacity as string), - fill: map.fill(fill as string, context), - } satisfies grida.program.nodes.RectangleNodePrototype, - ]; - break; - - // [circle, ellipse] => path via optimize - case "circle": - case "ellipse": { - const { cx, cy, r, rx: _rx, ry: _ry } = attributes; - - const rx = parseFloat((_rx ?? r) as string) ?? 0; - const ry = parseFloat((_ry ?? rx ?? r) as string) ?? 0; - - const { style: _style, fill: _fill, stroke, opacity } = attributes; - const style = _style ? parseStyle(_style as string) : []; - const fillstyle: Declaration | undefined = style.find( - (d) => d.type === "declaration" && d.property === "fill" - ) as Declaration | undefined; - - const fill = _fill ?? fillstyle?.value; - - return [ - { - type: "ellipse", - name: nodename, - left: parseFloat(cx as string) - rx, - top: parseFloat(cy as string) - ry, - width: rx * 2, - height: ry * 2, - opacity: map.opacity(opacity as string), - fill: map.fill(fill as string, context), - } satisfies grida.program.nodes.EllipseNodePrototype, - ]; - - break; - } - - // [line] => line - case "line": { - const { style, fill, stroke, opacity, x1, y1, x2, y2 } = attributes; - - // return [ - // { - // type: 'line', - // } satisfies grida.program.nodes.NodePrototype, - // ] - break; - } - - // [image] => image - case "image": - break; - - // [text, tspan] => text - case "text": - case "tspan": - - // - case "defs": - // - case "filter": - // - case "feBlend": - case "feColorMatrix": - case "feComponentTransfer": - case "feComposite": - case "feConvolveMatrix": - case "feDiffuseLighting": - case "feDisplacementMap": - case "feDistantLight": - case "feDropShadow": - case "feFlood": - case "feFuncA": - case "feFuncB": - case "feFuncG": - case "feFuncR": - case "feGaussianBlur": - case "feImage": - case "feMerge": - case "feMergeNode": - case "feMorphology": - case "feOffset": - case "fePointLight": - case "feSpecularLighting": - case "feSpotLight": - case "feTile": - case "feTurbulence": - // - case "linearGradient": - case "radialGradient": - case "pattern": - case "stop": - case "symbol": - case "use": - case "clipPath": - case "mask": - // ignored, non supported - case "desc": - case "animate": - case "animateMotion": - case "animateTransform": - case "foreignObject": - case "script": - // non-rendering - case "title": default: - return null; + return undefined; + } + } + + /** + * Convert SVG Fill Attributes to Grida Paint + */ + export function fill(fill: svgtypes.SVGFillAttributes | null | undefined): { + paint: cg.Paint | undefined; + fillRule: cg.FillRule; + opacity: number; + } { + if (!fill) { + return { paint: undefined, fillRule: "nonzero", opacity: 1.0 }; } - return null; + return { + paint: map.paint(fill.paint, fill.fill_opacity), + fillRule: fill.fill_rule, + opacity: fill.fill_opacity, + }; } - export function optimize(svgstr: string): Output { - // Optimize the SVG string - const config: Config = { - js2svg: { - indent: 2, - pretty: true, - }, - plugins: [ - { - name: "preset-default", - params: { - overrides: { - removeViewBox: false, // Keep viewBox for scalability - removeComments: { - preservePatterns: false, - }, - convertShapeToPath: { - convertArcs: true, - }, - convertColors: { - shorthex: false, - names2hex: true, - }, - }, - }, - }, - "convertStyleToAttrs", - ], + /** + * Convert SVG Stroke Attributes to Grida Paint + */ + export function stroke( + stroke: svgtypes.SVGStrokeAttributes | null | undefined + ): { + paint: cg.Paint | undefined; + opacity: number; + strokeWidth: number; + strokeCap: cg.StrokeCap; + strokeJoin: cg.StrokeJoin; + strokeMiterLimit: number; + strokeDashArray?: number[]; + } { + if (!stroke) { + return { + paint: undefined, + opacity: 1.0, + strokeWidth: 0, + strokeCap: "butt", + strokeJoin: "miter", + strokeMiterLimit: 4, + }; + } + + return { + paint: map.paint(stroke.paint, stroke.stroke_opacity), + opacity: stroke.stroke_opacity, + strokeWidth: stroke.stroke_width ?? 0, + strokeCap: stroke.stroke_linecap, + strokeJoin: stroke.stroke_linejoin, + strokeMiterLimit: stroke.stroke_miterlimit, + strokeDashArray: stroke.stroke_dasharray ?? undefined, }; - const result = svgo.optimize(svgstr, config); - return result; } + } + + type SVGIOCompatibleNodePrototype = + | grida.program.nodes.ContainerNodePrototype + | grida.program.nodes.GroupNodePrototype + | grida.program.nodes.PathNodePrototype + | grida.program.nodes.RectangleNodePrototype + | grida.program.nodes.EllipseNodePrototype; + + /** + * Convert IRSVGChildNode to Grida Node Prototype + */ + function convertChildNode( + node: svgtypes.ir.IRSVGChildNode, + name: string + ): SVGIOCompatibleNodePrototype | null { + switch (node.kind) { + case "group": { + const { transform, opacity, children } = node; + const position = map.extractTranslation(transform); + + const convertedChildren = children + .map((child, index) => + convertChildNode(child, `${name}_child_${index}`) + ) + .filter(Boolean) as grida.program.nodes.NodePrototype[]; + + return { + type: "group", + name: name, + position: "absolute", + left: position.left, + top: position.top, + opacity: opacity, + children: convertedChildren, + } satisfies grida.program.nodes.GroupNodePrototype; + } - type SVGIOCompatibleNodePrototype = - | grida.program.nodes.ContainerNodePrototype - | grida.program.nodes.PathNodePrototype - | grida.program.nodes.RectangleNodePrototype - | grida.program.nodes.EllipseNodePrototype; + case "path": { + const { transform, fill: fillAttr, stroke: strokeAttr, d } = node; + const position = map.extractTranslation(transform); + + const vectorNetwork = vn.fromSVGPathData(d); + const bbox = vn.getBBox(vectorNetwork); + const { + paint: fill, + fillRule, + opacity: fillOpacity, + } = map.fill(fillAttr); + const { + paint: stroke, + opacity: _strokeOpacity, + strokeWidth, + strokeCap, + strokeJoin, + strokeMiterLimit, + strokeDashArray, + } = map.stroke(strokeAttr); + + // Use fill opacity as the primary node opacity (stroke opacity is applied to stroke paint) + return { + type: "vector", + name: name, + vectorNetwork: vectorNetwork, + fill: fill, + stroke: stroke, + strokeWidth: strokeWidth, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + strokeMiterLimit: strokeMiterLimit, + strokeDashArray: strokeDashArray, + width: bbox.width, + height: bbox.height, + left: position.left, + top: position.top, + fillRule: fillRule, + opacity: fillOpacity, + } satisfies grida.program.nodes.PathNodePrototype; + } - export async function convert( - svgstr: string, - context: SVGFactoryUserContext - ): Promise { - const node = await parse(svgstr); + case "text": { + // Text nodes are not yet fully supported, so we'll skip for now + // TODO: Implement text node conversion when text support is added + return null; + } - // console.log("iosvg.convert(node)", node); + case "image": { + // Image nodes are not yet fully supported, so we'll skip for now + // TODO: Implement image node conversion when image support is added + return null; + } - return mapnode(node, { ...context, depth: 0 })?.[0] ?? null; + default: + return null; } } + + /** + * Convert WASM-resolved SVG tree (IRSVGInitialContainerNode) to Grida Node Prototype + * + * @param svg - The WASM-resolved SVG tree structure + * @param context - Optional context for naming and currentColor + * @returns The root container node prototype, or null if conversion fails + */ + export function convert( + svg: svgtypes.ir.IRSVGInitialContainerNode, + context?: Partial + ): SVGIOCompatibleNodePrototype | null { + const { width, height, children } = svg; + const name = context?.name ?? "svg"; + + const convertedChildren = children + .map((child, index) => convertChildNode(child, `${name}_child_${index}`)) + .filter(Boolean) as grida.program.nodes.NodePrototype[]; + + return { + type: "container", + name: name, + position: "absolute", + left: 0, + top: 0, + width: width, + height: height, + children: convertedChildren, + } satisfies grida.program.nodes.ContainerNodePrototype; + } } diff --git a/packages/grida-canvas-io-svg/package.json b/packages/grida-canvas-io-svg/package.json index 5688b9bf7d..bb18ccf88a 100644 --- a/packages/grida-canvas-io-svg/package.json +++ b/packages/grida-canvas-io-svg/package.json @@ -4,15 +4,11 @@ "private": true, "dependencies": { "@grida/cmath": "workspace:*", - "@grida/vn": "workspace:*", - "color-name": "^2.0.0", - "inline-style-parser": "^0.2.4", - "svgo": "^3.3.2", - "svgson": "^5.3.1" + "@grida/vn": "workspace:*" }, "devDependencies": { + "@grida/canvas-wasm": "workspace:*", "@grida/cg": "workspace:*", - "@grida/schema": "workspace:*", - "@types/color-name": "^2.0.0" + "@grida/schema": "workspace:*" } } diff --git a/packages/grida-cmath/index.ts b/packages/grida-cmath/index.ts index ad6629e9c0..e6cf7cc24a 100644 --- a/packages/grida-cmath/index.ts +++ b/packages/grida-cmath/index.ts @@ -5728,6 +5728,12 @@ namespace cmath { } } + /** + * @deprecated the color implementation is fundamentally flawed (where it uses 0-1 for alpha, even its named as u8) + * this legacy module claims to use RGBA8888, but its actually RGB888A32F + * + * use {@link colorformats} instead + */ export namespace color { /** * the RGBA structure itself. the rgb value may differ as it could both represent 0-1 or 0-255 by the context. @@ -5877,6 +5883,87 @@ namespace cmath { } } + export namespace colorformats { + export type RGBA32F = { + /** + * 0.0-1.0 + */ + r: number; + /** + * 0.0-1.0 + */ + g: number; + /** + * 0.0-1.0 + */ + b: number; + /** + * 0.0-1.0 + */ + a: number; + }; + + export type RGBA8888 = { + /** + * 0-255 + */ + r: number; + /** + * 0-255 + */ + g: number; + /** + * 0-255 + */ + b: number; + /** + * 0-255 + */ + a: number; + }; + + export type RGB888A32F = { + /** + * 0-255 + */ + r: number; + /** + * 0-255 + */ + g: number; + /** + * 0-255 + */ + b: number; + /** + * 0.0-1.0 + */ + a: number; + }; + + export namespace RGBA32F { + export function intoRGB888F32A(color: RGBA32F): RGB888A32F { + return { + r: Math.round(color.r * 255), + g: Math.round(color.g * 255), + b: Math.round(color.b * 255), + a: color.a, + }; + } + } + + export namespace RGBA8888 { + export function intoRGB888F32A(color: RGBA8888): RGB888A32F { + return { + r: color.r, + g: color.g, + b: color.b, + a: color.a / 255, + }; + } + } + } + /** * Rasterization utilities for drawing lines between points (e.g., "connect the dots") * in integer pixel coordinates, returning the set of covered pixels. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c77e358613..2b4963c7b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,7 +76,7 @@ importers: version: 1.3.5(react@19.0.0) tailwind-merge: specifier: ^3.2.0 - version: 3.3.0 + version: 3.3.1 three: specifier: ^0.170.0 version: 0.170.0 @@ -304,12 +304,21 @@ importers: editor: dependencies: + '@ai-sdk/anthropic': + specifier: 3.0.0-beta.50 + version: 3.0.0-beta.50(zod@4.1.12) '@ai-sdk/openai': - specifier: ^1.3.20 - version: 1.3.22(zod@3.25.42) + specifier: 3.0.0-beta.55 + version: 3.0.0-beta.55(zod@4.1.12) + '@ai-sdk/react': + specifier: 3.0.0-beta.94 + version: 3.0.0-beta.94(react@19.0.0)(zod@4.1.12) '@ai-sdk/replicate': - specifier: ^0.2.7 - version: 0.2.8(zod@3.25.42) + specifier: 2.0.0-beta.31 + version: 2.0.0-beta.31(zod@4.1.12) + '@ai-sdk/rsc': + specifier: 2.0.0-beta.94 + version: 2.0.0-beta.94(react@19.0.0)(zod@4.1.12) '@app/database': specifier: workspace:* version: link:../database @@ -404,8 +413,8 @@ importers: specifier: ^1.0.1 version: 1.0.1(@headless-tree/core@1.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@hookform/resolvers': - specifier: ^4.1.3 - version: 4.1.3(react-hook-form@7.56.4(react@19.0.0)) + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.56.4(react@19.0.0)) '@mdx-js/loader': specifier: ^3.0.1 version: 3.1.0(acorn@8.14.1)(webpack@5.98.0(esbuild@0.25.4)) @@ -437,25 +446,25 @@ importers: specifier: ^1.1.6 version: 1.1.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-avatar': - specifier: ^1.1.9 + specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-checkbox': specifier: ^1.3.1 version: 1.3.2(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-collapsible': - specifier: ^1.1.10 + specifier: ^1.1.11 version: 1.1.11(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-context-menu': specifier: ^2.2.14 version: 2.2.15(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': - specifier: ^1.1.13 + specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dropdown-menu': - specifier: ^2.1.14 + specifier: ^2.1.15 version: 2.1.15(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-hover-card': - specifier: ^1.1.13 + specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-icons': specifier: ^1.3.2 @@ -476,16 +485,16 @@ importers: specifier: ^1.1.8 version: 1.1.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-progress': - specifier: ^1.1.6 + specifier: ^1.1.7 version: 1.1.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-radio-group': specifier: ^1.3.6 version: 1.3.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-scroll-area': - specifier: ^1.2.8 + specifier: ^1.2.9 version: 1.2.9(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-select': - specifier: ^2.2.4 + specifier: ^2.2.5 version: 2.2.5(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-separator': specifier: ^1.1.7 @@ -512,8 +521,11 @@ importers: specifier: ^1.1.9 version: 1.1.10(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-tooltip': - specifier: ^1.2.6 + specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': + specifier: ^1.2.2 + version: 1.2.2(@types/react@19.1.3)(react@19.0.0) '@react-email/components': specifier: ^0.0.38 version: 0.0.38(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -609,7 +621,7 @@ importers: version: 1.6.0 '@vercel/sdk': specifier: ^1.5.0 - version: 1.7.7(zod@3.25.42) + version: 1.7.7(zod@4.1.12) '@vercel/speed-insights': specifier: ^1.0.12 version: 1.2.0(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.19)(vue@3.5.13(typescript@5.8.3)) @@ -626,11 +638,11 @@ importers: specifier: ^3.11.0 version: 3.12.0(@react-spring/web@9.7.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@xyflow/react': - specifier: ^12.1.0 + specifier: ^12.6.4 version: 12.6.4(@types/react@19.1.3)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ai: - specifier: 4.3.9 - version: 4.3.9(react@19.0.0)(zod@3.25.42) + specifier: 6.0.0-beta.94 + version: 6.0.0-beta.94(zod@4.1.12) ajv: specifier: ^8.13.0 version: 8.17.1 @@ -761,13 +773,13 @@ importers: specifier: ^0.4.4 version: 0.4.5 motion: - specifier: ^12.11.0 + specifier: ^12.15.0 version: 12.15.0(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ms: specifier: ^2.1.3 version: 2.1.3 nanoid: - specifier: ^3.1.23 + specifier: ^3.3.11 version: 3.3.11 negotiator: specifier: ^1.0.0 @@ -780,7 +792,7 @@ importers: version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) openai: specifier: ^4.96.0 - version: 4.104.0(encoding@0.1.13)(ws@8.18.2)(zod@3.25.42) + version: 4.104.0(encoding@0.1.13)(ws@8.18.2)(zod@4.1.12) p-queue: specifier: ^7.2.0 version: 7.4.1 @@ -868,12 +880,18 @@ importers: semver: specifier: ^7.7.1 version: 7.7.2 + shiki: + specifier: ^3.14.0 + version: 3.14.0 signature_pad: specifier: ^4.2.0 version: 4.2.0 sonner: specifier: ^2.0.3 version: 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + streamdown: + specifier: ^1.4.0 + version: 1.4.0(@types/react@19.1.3)(react@19.0.0) stylis: specifier: ^4.3.2 version: 4.3.6 @@ -885,16 +903,22 @@ importers: version: 2.3.3(react@19.0.0) tailwind-merge: specifier: ^3.2.0 - version: 3.3.0 + version: 3.3.1 timezones-list: specifier: ^3.0.3 version: 3.1.0 + tokenlens: + specifier: ^1.3.1 + version: 1.3.1 ua-parser-js: specifier: ^1.0.38 version: 1.0.40 use-file-picker: specifier: ^2.1.1 version: 2.1.4(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react@19.0.0) + use-stick-to-bottom: + specifier: ^1.1.1 + version: 1.1.1(react@19.0.0) use-sync-external-store: specifier: ^1.5.0 version: 1.5.0(react@19.0.0) @@ -923,8 +947,8 @@ importers: specifier: ^13.6.27 version: 13.6.27 zod: - specifier: ^3.24.4 - version: 3.25.42 + specifier: ^4.1.8 + version: 4.1.12 zustand: specifier: ^5.0.3 version: 5.0.5(@types/react@19.1.3)(immer@9.0.21)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)) @@ -980,6 +1004,9 @@ importers: '@types/geojson': specifier: ^7946.0.14 version: 7946.0.16 + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 '@types/mapbox-gl': specifier: ^3.1.0 version: 3.4.1 @@ -1034,6 +1061,9 @@ importers: eslint-config-next: specifier: 15.3.2 version: 15.3.2(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + hast: + specifier: ^1.0.0 + version: 1.0.0 import-in-the-middle: specifier: ^1.13.2 version: 1.14.0 @@ -1116,28 +1146,16 @@ importers: '@grida/vn': specifier: workspace:* version: link:../grida-canvas-vn - color-name: - specifier: ^2.0.0 - version: 2.0.0 - inline-style-parser: - specifier: ^0.2.4 - version: 0.2.4 - svgo: - specifier: ^3.3.2 - version: 3.3.2 - svgson: - specifier: ^5.3.1 - version: 5.3.1 devDependencies: + '@grida/canvas-wasm': + specifier: workspace:* + version: link:../../crates/grida-canvas-wasm '@grida/cg': specifier: workspace:* version: link:../grida-canvas-cg '@grida/schema': specifier: workspace:* version: link:../grida-canvas-schema - '@types/color-name': - specifier: ^2.0.0 - version: 2.0.0 packages/grida-canvas-pixelgrid: dependencies: @@ -1305,49 +1323,65 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/openai@1.3.22': - resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} + '@ai-sdk/anthropic@3.0.0-beta.50': + resolution: {integrity: sha512-PKTRpzWhGD449Cm27KL1EVg0vUcgT32Tu7WvxtEXyl+JoD4YVXs353QWNx1tcFqML1TAmuCbVUFq9KvoNYIFLg==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@2.2.7': - resolution: {integrity: sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==} + '@ai-sdk/gateway@2.0.0-beta.50': + resolution: {integrity: sha512-1SkmLI6y2/2YoWtMD6T9QaZZtyyTYbL9aFq6DSLJGklM5uBjeZzYj0L5eL0YCxYdL1PWj/uSsUNPpfuYBol5ew==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@2.2.8': - resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + '@ai-sdk/openai@3.0.0-beta.55': + resolution: {integrity: sha512-KWtqms0KecDjA0eME4yCQRTjh3bjUmIz63Nod7/f17vo4bvhjiyRqARsVnG/0ohA23y8/fFNwKyFi40IVHFREQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.0-beta.31': + resolution: {integrity: sha512-IfE4sAYyO4+tUAoJGyvxeEaa0FMhygrvj2OVdDFuMpGOfFBQHETlP+X7bwnJY8M0oYBp9BEB+YF9D5sCx5oc0w==} + engines: {node: '>=18'} + peerDependencies: + '@valibot/to-json-schema': ^1.3.0 + arktype: ^2.1.22 + effect: ^3.18.4 + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true - '@ai-sdk/provider@1.1.3': - resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + '@ai-sdk/provider@3.0.0-beta.15': + resolution: {integrity: sha512-vHAlJe2yRQvEsB/Ldy799HUjTSDzTWk6w3PIJFAOuhuJ3c2PvOcHrcY3IULPIx6bOnB2OgG3BE6zySey5pnyBQ==} engines: {node: '>=18'} - '@ai-sdk/react@1.2.9': - resolution: {integrity: sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==} + '@ai-sdk/react@3.0.0-beta.94': + resolution: {integrity: sha512-oOc72j/LM2YVkPfN7cOnSGgBSxUcCu12JKnxBilHG0DVpRwYj/WxpoQlR59/R3D1obXFLUwfV/afu7sZI9Kvhw==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - '@ai-sdk/replicate@0.2.8': - resolution: {integrity: sha512-l9t4+RzbAn8osstkbWs6l++Nava+4LO4dsaddnE0GQM5E0BEIgMTJ14hoyfE02Ep0rJZ0M2HlXGqv5heW47P8A==} + '@ai-sdk/replicate@2.0.0-beta.31': + resolution: {integrity: sha512-rqhYrfsLruf0v+VKXIjFaeie8Gom9QED2FkC/J0eeqG5pqmYN7SARWkFPXssYh/+0UlRJ1Wqowp0/PbwOt+MmQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/ui-utils@1.2.8': - resolution: {integrity: sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==} + '@ai-sdk/rsc@2.0.0-beta.94': + resolution: {integrity: sha512-DI3wA6Y6DnV/oCnqeiI8UhMvxkZaOCuUxNMXgVzHABn6YHT2YrqtRWlfLAEzbeAaxWoEXPIssJK7zhqXbxcFfQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + zod: + optional: true '@algolia/autocomplete-core@1.17.9': resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} @@ -1432,6 +1466,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@9.3.0': + resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -2147,6 +2187,9 @@ packages: react: ^18.0 || ^19.0 || >= 19.0.0-rc react-dom: ^18.0 || ^19.0 || >= 19.0.0-rc + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} @@ -2202,6 +2245,21 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -3049,10 +3107,10 @@ packages: react: '*' react-dom: '*' - '@hookform/resolvers@4.1.3': - resolution: {integrity: sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==} + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: - react-hook-form: ^7.0.0 + react-hook-form: ^7.55.0 '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -3074,6 +3132,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.0.2': + resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==} + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3476,6 +3540,9 @@ packages: '@mediapipe/tasks-vision@0.10.17': resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@monaco-editor/loader@1.5.0': resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} @@ -4987,6 +5054,24 @@ packages: peerDependencies: webpack: '>=4.40.0' + '@shikijs/core@3.14.0': + resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==} + + '@shikijs/engine-javascript@3.14.0': + resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==} + + '@shikijs/engine-oniguruma@3.14.0': + resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==} + + '@shikijs/langs@3.14.0': + resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==} + + '@shikijs/themes@3.14.0': + resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==} + + '@shikijs/types@3.14.0': + resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==} + '@shikijs/types@3.2.1': resolution: {integrity: sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==} @@ -5035,6 +5120,9 @@ packages: '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -5503,6 +5591,18 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tokenlens/core@1.3.0': + resolution: {integrity: sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ==} + + '@tokenlens/fetch@1.3.0': + resolution: {integrity: sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ==} + + '@tokenlens/helpers@1.3.1': + resolution: {integrity: sha512-t6yL8N6ES8337E6eVSeH4hCKnPdWkZRFpupy9w5E66Q9IeqQ9IO7XQ6gh12JKjvWiRHuyyJ8MBP5I549Cr41EQ==} + + '@tokenlens/models@1.3.0': + resolution: {integrity: sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw==} + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -5611,6 +5711,15 @@ packages: '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + '@types/d3-cloud@1.2.5': resolution: {integrity: sha512-vEIER9DsEBUOdpRiwCh3n1qE+cV6h4e1LhxhY2sLt+m8LPNAIkOOhTlqk0JDiBwD+ZPM8ynFAOU3AuPuVYBFBA==} @@ -5620,15 +5729,30 @@ packages: '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + '@types/d3-delaunay@6.0.1': resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==} + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + '@types/d3-drag@3.0.7': resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + '@types/d3-format@3.0.1': resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==} @@ -5650,12 +5774,21 @@ packages: '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + '@types/d3-random@2.2.3': resolution: {integrity: sha512-Ghs4R3CcgJ3o6svszRzIH4b8PPYex/COo+rhhZjDAs+bVducXwjmVSi27WcDOaLLCBV2t3tfVH9bYXAL76IvQA==} '@types/d3-sankey@0.12.4': resolution: {integrity: sha512-YTicQNwioitIlvuvlfW2GfO6sKxpohzg2cSQttlXAPjFwoBuN+XpGLhUN3kLutG/dI3GCLC+DUorqiJt7Naetw==} + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + '@types/d3-scale@4.0.2': resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==} @@ -5695,6 +5828,9 @@ packages: '@types/d3@3.5.53': resolution: {integrity: sha512-8yKQA9cAS6+wGsJpBysmnhlaaxlN42Qizqkw+h2nILSlS+MAG2z4JdO6p+PJrJ+ACvimkmLJL281h157e52psQ==} + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -5957,6 +6093,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ua-parser-js@0.7.39': resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} @@ -6134,6 +6273,10 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true + '@vercel/oidc@3.0.3': + resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} + engines: {node: '>= 20'} + '@vercel/sdk@1.7.7': resolution: {integrity: sha512-1qtW7eMUza7WPxuYFzZvR3ajj4v+QBqXa2e9whK8hlOYpCrvBdezYq2oseBtIO0numkIY2974RTHyKmLZQ1pSg==} hasBin: true @@ -6503,15 +6646,11 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@4.3.9: - resolution: {integrity: sha512-P2RpV65sWIPdUlA4f1pcJ11pB0N1YmqPVLEmC4j8WuBwKY0L3q9vGhYPh0Iv+spKHKyn0wUbMfas+7Z6nTfS0g==} + ai@6.0.0-beta.94: + resolution: {integrity: sha512-A8cpwbTtagP3UvIcAEh16oaS4Rl36J7mci3H4nl5+22oo0WwL1CN7GbYJnvsrbsxhNZierUS1dNG2hYzqRKhOA==} engines: {node: '>=18'} peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - react: - optional: true + zod: ^3.25.76 || ^4.1.8 ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -7033,6 +7172,14 @@ packages: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} engines: {node: '>= 6'} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -7219,6 +7366,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -7296,6 +7446,12 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig@6.0.0: resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} engines: {node: '>=8'} @@ -7490,6 +7646,20 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} @@ -7501,6 +7671,18 @@ packages: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + d3-cloud@1.2.7: resolution: {integrity: sha512-8TrgcgwRIpoZYQp7s3fGB7tATWfhckRb8KcVd1bOgqkNdkJRDGWfdSf4HkHHzZxSczwQJdSxvfPudwir5IAJ3w==} @@ -7508,6 +7690,10 @@ packages: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + d3-delaunay@6.0.2: resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==} engines: {node: '>=12'} @@ -7527,10 +7713,23 @@ packages: resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} engines: {node: '>=12'} + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} @@ -7550,6 +7749,10 @@ packages: d3-hierarchy@1.1.9: resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + d3-interpolate-path@2.2.1: resolution: {integrity: sha512-6qLLh/KJVzls0XtMsMpcxhqMhgVEN7VIbR/6YGZe2qlS8KDgyyVB20XcmGnDyB051HcefQXM/Tppa9vcANEA4Q==} @@ -7570,9 +7773,21 @@ packages: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + d3-random@2.2.2: resolution: {integrity: sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==} + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + d3-sankey@0.12.3: resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} @@ -7627,6 +7842,13 @@ packages: resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} engines: {node: '>=12'} + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -7658,6 +7880,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -7731,10 +7956,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deep-rename-keys@0.2.1: - resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} - engines: {node: '>=0.10.0'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -7880,6 +8101,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -8275,9 +8499,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@2.0.3: - resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -8288,6 +8509,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -8738,6 +8963,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -8785,6 +9014,9 @@ packages: resolution: {integrity: sha512-LQhmMl1dRQQjMXPzJc7MpZ/CqPOWWuAvVEoVJM9n/s7vHypj+c3Pd5rLQCkAsOgAoAYKbNCsYFE++LF7MvSfCQ==} engines: {node: '>=4', npm: '>=3', yarn: '>=1.3.0'} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} @@ -8842,9 +9074,6 @@ packages: hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} - hast-util-from-parse5@8.0.2: - resolution: {integrity: sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A==} - hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} @@ -8890,6 +9119,10 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hast@1.0.0: + resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==} + deprecated: Renamed to rehype + hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} @@ -8951,6 +9184,9 @@ packages: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -9211,9 +9447,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-bun-module@1.3.0: resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==} @@ -9732,9 +9965,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kind-of@3.2.2: - resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} - engines: {node: '>=0.10.0'} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} @@ -9748,6 +9980,13 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -9762,6 +10001,12 @@ packages: launch-editor@2.9.1: resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -9892,6 +10137,10 @@ packages: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -9969,6 +10218,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -10023,6 +10277,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@16.4.1: + resolution: {integrity: sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==} + engines: {node: '>= 20'} + hasBin: true + marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} engines: {node: '>= 12'} @@ -10154,6 +10413,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + mermaid@11.12.1: + resolution: {integrity: sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g==} + meshline@3.3.1: resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} peerDependencies: @@ -10684,6 +10946,12 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -10795,6 +11063,9 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-manager-detector@1.5.0: + resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + page-flip@2.0.7: resolution: {integrity: sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA==} @@ -10840,6 +11111,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10948,6 +11222,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-up@3.1.0: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} @@ -10965,6 +11242,12 @@ packages: point-in-polygon-hao@1.2.4: resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -11624,6 +11907,9 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -11805,6 +12091,12 @@ packages: maplibre-gl: optional: true + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': 19.1.3 + react: '>=18' + react-medium-image-zoom@5.2.14: resolution: {integrity: sha512-nfTVYcAUnBzXQpPDcZL+cG/e6UceYUIG+zDcnemL7jtAqbJjVVkA85RgneGtJeni12dTyiRPZVM6Szkmwd/o8w==} peerDependencies: @@ -12052,6 +12344,15 @@ packages: regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -12085,6 +12386,9 @@ packages: rehype-format@5.0.1: resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==} + rehype-harden@1.1.5: + resolution: {integrity: sha512-JrtBj5BVd/5vf3H3/blyJatXJbzQfRT9pJBmjafbTaPouQCAKxHwRyCc7dle9BXQKxv4z1OzZylz/tNamoiG3A==} + rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} @@ -12141,10 +12445,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - rename-keys@1.2.0: - resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} - engines: {node: '>= 0.8.0'} - renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} @@ -12249,6 +12549,9 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} @@ -12317,9 +12620,6 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -12441,6 +12741,9 @@ packages: engines: {node: '>=4'} hasBin: true + shiki@3.14.0: + resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} @@ -12652,6 +12955,11 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + streamdown@1.4.0: + resolution: {integrity: sha512-ylhDSQ4HpK5/nAH9v7OgIIdGJxlJB2HoYrYkJNGrO8lMpnWuKUcrz/A8xAMwA6eILA27469vIavcOTjmxctrKg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -12813,9 +13121,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - svgson@5.3.1: - resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swr@2.3.3: resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} peerDependencies: @@ -12827,8 +13132,8 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tailwind-merge@3.3.0: - resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} tailwindcss@4.1.8: resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} @@ -12961,6 +13266,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -12992,6 +13301,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tokenlens@1.3.1: + resolution: {integrity: sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -13039,6 +13351,10 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + ts-easing@0.2.0: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} @@ -13424,6 +13740,11 @@ packages: '@types/react': optional: true + use-stick-to-bottom@1.1.1: + resolution: {integrity: sha512-JkDp0b0tSmv7HQOOpL1hT7t7QaoUBXkq045WWWOFDTlLGRzgIIyW7vyzOIJzY7L2XVIG7j1yUxeDj2LHm9Vwng==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -13449,6 +13770,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -13497,6 +13822,26 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} @@ -13755,16 +14100,10 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true - xml-lexer@0.2.2: - resolution: {integrity: sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==} - xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xml-reader@2.4.3: - resolution: {integrity: sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -13854,16 +14193,11 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} - peerDependencies: - zod: ^3.24.1 - zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} - zod@3.25.42: - resolution: {integrity: sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -13923,52 +14257,84 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/openai@1.3.22(zod@3.25.42)': + '@ai-sdk/anthropic@3.0.0-beta.50(zod@4.1.12)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.42) - zod: 3.25.42 + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect - '@ai-sdk/provider-utils@2.2.7(zod@3.25.42)': + '@ai-sdk/gateway@2.0.0-beta.50(zod@4.1.12)': dependencies: - '@ai-sdk/provider': 1.1.3 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 - zod: 3.25.42 + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + '@vercel/oidc': 3.0.3 + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect - '@ai-sdk/provider-utils@2.2.8(zod@3.25.42)': + '@ai-sdk/openai@3.0.0-beta.55(zod@4.1.12)': dependencies: - '@ai-sdk/provider': 1.1.3 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 - zod: 3.25.42 + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect + + '@ai-sdk/provider-utils@4.0.0-beta.31(zod@4.1.12)': + dependencies: + '@ai-sdk/provider': 3.0.0-beta.15 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 4.1.12 - '@ai-sdk/provider@1.1.3': + '@ai-sdk/provider@3.0.0-beta.15': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@1.2.9(react@19.0.0)(zod@3.25.42)': + '@ai-sdk/react@3.0.0-beta.94(react@19.0.0)(zod@4.1.12)': dependencies: - '@ai-sdk/provider-utils': 2.2.7(zod@3.25.42) - '@ai-sdk/ui-utils': 1.2.8(zod@3.25.42) + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + ai: 6.0.0-beta.94(zod@4.1.12) react: 19.0.0 swr: 2.3.3(react@19.0.0) throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.42 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect + - zod - '@ai-sdk/replicate@0.2.8(zod@3.25.42)': + '@ai-sdk/replicate@2.0.0-beta.31(zod@4.1.12)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.42) - zod: 3.25.42 + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect - '@ai-sdk/ui-utils@1.2.8(zod@3.25.42)': + '@ai-sdk/rsc@2.0.0-beta.94(react@19.0.0)(zod@4.1.12)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.7(zod@3.25.42) - zod: 3.25.42 - zod-to-json-schema: 3.24.5(zod@3.25.42) + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) + ai: 6.0.0-beta.94(zod@4.1.12) + jsondiffpatch: 0.6.0 + react: 19.0.0 + optionalDependencies: + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)(search-insights@2.17.3)': dependencies: @@ -14084,6 +14450,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.5.0 + tinyexec: 1.0.2 + + '@antfu/utils@9.3.0': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -15178,6 +15551,8 @@ snapshots: - sugar-high - supports-color + '@braintree/sanitize-url@7.1.1': {} + '@changesets/apply-release-plan@7.0.12': dependencies: '@changesets/config': 3.1.1 @@ -15320,6 +15695,23 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -16732,7 +17124,7 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@hookform/resolvers@4.1.3(react-hook-form@7.56.4(react@19.0.0))': + '@hookform/resolvers@5.2.2(react-hook-form@7.56.4(react@19.0.0))': dependencies: '@standard-schema/utils': 0.3.0 react-hook-form: 7.56.4(react@19.0.0) @@ -16750,6 +17142,21 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.0.2': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 9.3.0 + '@iconify/types': 2.0.0 + debug: 4.4.1 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.2 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -17252,6 +17659,10 @@ snapshots: '@mediapipe/tasks-vision@0.10.17': {} + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + '@monaco-editor/loader@1.5.0': dependencies: state-local: 1.0.7 @@ -18884,6 +19295,37 @@ snapshots: - encoding - supports-color + '@shikijs/core@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/themes@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/types@3.14.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/types@3.2.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -18933,6 +19375,8 @@ snapshots: '@speed-highlight/core@1.2.7': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} '@stepperize/react@3.1.1(react@19.0.0)': @@ -19443,6 +19887,21 @@ snapshots: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/pm': 2.12.0 + '@tokenlens/core@1.3.0': {} + + '@tokenlens/fetch@1.3.0': + dependencies: + '@tokenlens/core': 1.3.0 + + '@tokenlens/helpers@1.3.1': + dependencies: + '@tokenlens/core': 1.3.0 + '@tokenlens/fetch': 1.3.0 + + '@tokenlens/models@1.3.0': + dependencies: + '@tokenlens/core': 1.3.0 + '@tootallnate/once@2.0.0': {} '@tosspayments/brandpay-types@0.2.6(typescript@5.8.3)': @@ -19567,6 +20026,16 @@ snapshots: '@types/d3-array@3.2.1': {} + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + '@types/d3-cloud@1.2.5': dependencies: '@types/d3': 3.5.53 @@ -19575,14 +20044,29 @@ snapshots: '@types/d3-color@3.1.3': {} + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.16 + '@types/d3-delaunay@6.0.1': {} + '@types/d3-dispatch@3.0.7': {} + '@types/d3-drag@3.0.7': dependencies: '@types/d3-selection': 3.0.11 + '@types/d3-dsv@3.0.7': {} + '@types/d3-ease@3.0.2': {} + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + '@types/d3-format@3.0.1': {} '@types/d3-geo@3.1.0': @@ -19603,12 +20087,18 @@ snapshots: '@types/d3-path@3.1.1': {} + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + '@types/d3-random@2.2.3': {} '@types/d3-sankey@0.12.4': dependencies: '@types/d3-shape': 1.3.12 + '@types/d3-scale-chromatic@3.1.0': {} + '@types/d3-scale@4.0.2': dependencies: '@types/d3-time': 3.0.4 @@ -19648,6 +20138,39 @@ snapshots: '@types/d3@3.5.53': {} + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.1 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.1 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 1.1.11 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 2.2.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 2.1.0 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -19939,6 +20462,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/ua-parser-js@0.7.39': {} '@types/unist@2.0.11': {} @@ -20103,9 +20629,11 @@ snapshots: '@vercel/functions@1.6.0': {} - '@vercel/sdk@1.7.7(zod@3.25.42)': + '@vercel/oidc@3.0.3': {} + + '@vercel/sdk@1.7.7(zod@4.1.12)': dependencies: - zod: 3.25.42 + zod: 4.1.12 '@vercel/speed-insights@1.2.0(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.19)(vue@3.5.13(typescript@5.8.3))': optionalDependencies: @@ -20716,17 +21244,17 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@4.3.9(react@19.0.0)(zod@3.25.42): + ai@6.0.0-beta.94(zod@4.1.12): dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.7(zod@3.25.42) - '@ai-sdk/react': 1.2.9(react@19.0.0)(zod@3.25.42) - '@ai-sdk/ui-utils': 1.2.8(zod@3.25.42) + '@ai-sdk/gateway': 2.0.0-beta.50(zod@4.1.12) + '@ai-sdk/provider': 3.0.0-beta.15 + '@ai-sdk/provider-utils': 4.0.0-beta.31(zod@4.1.12) '@opentelemetry/api': 1.9.0 - jsondiffpatch: 0.6.0 - zod: 3.25.42 - optionalDependencies: - react: 19.0.0 + zod: 4.1.12 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: @@ -21422,6 +21950,20 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -21610,6 +22152,8 @@ snapshots: confbox@0.1.8: {} + confbox@0.2.2: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -21688,6 +22232,14 @@ snapshots: core-util-is@1.0.3: {} + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cosmiconfig@6.0.0: dependencies: '@types/parse-json': 4.0.2 @@ -21916,6 +22468,18 @@ snapshots: csstype@3.1.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + d3-array@2.12.1: dependencies: internmap: 1.0.1 @@ -21928,12 +22492,30 @@ snapshots: dependencies: internmap: 2.0.3 + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + d3-cloud@1.2.7: dependencies: d3-dispatch: 1.0.6 d3-color@3.1.0: {} + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-delaunay@6.0.2: dependencies: delaunator: 5.0.1 @@ -21951,8 +22533,24 @@ snapshots: d3-dispatch: 3.0.1 d3-selection: 3.0.0 + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + d3-ease@3.0.1: {} + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + d3-format@3.1.0: {} d3-geo-voronoi@2.1.0: @@ -21972,6 +22570,8 @@ snapshots: d3-hierarchy@1.1.9: {} + d3-hierarchy@3.1.2: {} + d3-interpolate-path@2.2.1: {} d3-interpolate@3.0.1: @@ -21986,8 +22586,14 @@ snapshots: d3-path@3.1.0: {} + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + d3-random@2.2.2: {} + d3-random@3.0.1: {} + d3-sankey@0.12.3: dependencies: d3-array: 2.12.1 @@ -22054,6 +22660,44 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + damerau-levenshtein@1.0.8: {} data-bind-mapper@1.0.3: @@ -22090,6 +22734,8 @@ snapshots: date-fns@3.6.0: {} + dayjs@1.11.19: {} + debounce@1.2.1: {} debug@2.6.9: @@ -22154,11 +22800,6 @@ snapshots: deep-is@0.1.4: {} - deep-rename-keys@0.2.1: - dependencies: - kind-of: 3.2.2 - rename-keys: 1.2.0 - deepmerge@4.3.1: {} default-gateway@6.0.3: @@ -22299,6 +22940,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -22860,14 +23505,14 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@2.0.3: {} - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} events@3.3.0: {} + eventsource-parser@3.0.6: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -23374,6 +24019,8 @@ snapshots: globals@14.0.0: {} + globals@15.15.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -23435,6 +24082,8 @@ snapshots: h3-js@4.1.0: {} + hachure-fill@0.5.2: {} + handle-thing@2.0.1: {} handlebars@4.7.8: @@ -23510,17 +24159,6 @@ snapshots: vfile: 6.0.3 vfile-message: 4.0.2 - hast-util-from-parse5@8.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - devlop: 1.1.0 - hastscript: 9.0.1 - property-information: 6.5.0 - vfile: 6.0.3 - vfile-location: 5.0.3 - web-namespaces: 2.0.1 - hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -23569,11 +24207,11 @@ snapshots: '@types/hast': 3.0.4 '@types/unist': 3.0.3 '@ungap/structured-clone': 1.3.0 - hast-util-from-parse5: 8.0.2 + hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - parse5: 7.2.1 + parse5: 7.3.0 unist-util-position: 5.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 @@ -23673,6 +24311,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast@1.0.0: {} + hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -23753,6 +24393,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} html-webpack-plugin@5.6.3(webpack@5.98.0(esbuild@0.25.4)): @@ -24007,8 +24649,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-buffer@1.1.6: {} - is-bun-module@1.3.0: dependencies: semver: 7.7.2 @@ -24721,9 +25361,7 @@ snapshots: dependencies: json-buffer: 3.0.1 - kind-of@3.2.2: - dependencies: - is-buffer: 1.1.6 + khroma@2.1.0: {} kind-of@6.0.3: {} @@ -24731,6 +25369,16 @@ snapshots: kleur@4.1.5: {} + kolorist@1.8.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -24746,6 +25394,10 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.2 + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + leac@0.6.0: {} leven@3.1.0: {} @@ -24844,6 +25496,12 @@ snapshots: loader-utils@3.3.1: {} + local-pkg@1.1.2: + dependencies: + mlly: 1.7.4 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-character@3.0.0: optional: true @@ -24912,6 +25570,10 @@ snapshots: dependencies: react: 19.0.0 + lucide-react@0.542.0(react@19.0.0): + dependencies: + react: 19.0.0 + lz-string@1.5.0: {} maath@0.10.8(@types/three@0.170.0)(three@0.170.0): @@ -24995,6 +25657,8 @@ snapshots: markdown-table@3.0.4: {} + marked@16.4.1: {} + marked@4.3.0: {} marked@7.0.4: {} @@ -25257,6 +25921,31 @@ snapshots: merge2@1.4.1: {} + mermaid@11.12.1: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 3.0.2 + '@mermaid-js/parser': 0.6.3 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.0 + katex: 0.16.25 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 16.4.1 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + meshline@3.3.1(three@0.170.0): dependencies: three: 0.170.0 @@ -25988,13 +26677,21 @@ snapshots: dependencies: mimic-fn: 2.1.0 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 - openai@4.104.0(encoding@0.1.13)(ws@8.18.2)(zod@3.25.42): + openai@4.104.0(encoding@0.1.13)(ws@8.18.2)(zod@4.1.12): dependencies: '@types/node': 18.19.109 '@types/node-fetch': 2.6.12 @@ -26005,7 +26702,7 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) optionalDependencies: ws: 8.18.2 - zod: 3.25.42 + zod: 4.1.12 transitivePeerDependencies: - encoding @@ -26101,6 +26798,8 @@ snapshots: dependencies: quansync: 0.2.10 + package-manager-detector@1.5.0: {} + page-flip@2.0.7: {} pako@2.1.0: {} @@ -26160,6 +26859,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-data-parser@0.1.0: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -26251,6 +26952,12 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + pkg-up@3.1.0: dependencies: find-up: 3.0.0 @@ -26267,6 +26974,13 @@ snapshots: dependencies: robust-predicates: 3.0.2 + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} postcss-attribute-case-insensitive@7.0.1(postcss@8.5.4): @@ -26989,6 +27703,8 @@ snapshots: quansync@0.2.10: {} + quansync@0.2.11: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -27171,6 +27887,24 @@ snapshots: optionalDependencies: mapbox-gl: 3.12.0 + react-markdown@10.1.0(@types/react@19.1.3)(react@19.0.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.1.3 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.2 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-medium-image-zoom@5.2.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -27502,6 +28236,16 @@ snapshots: dependencies: '@babel/runtime': 7.27.4 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -27548,6 +28292,8 @@ snapshots: '@types/hast': 3.0.4 hast-util-format: 1.1.0 + rehype-harden@1.1.5: {} + rehype-katex@7.0.1: dependencies: '@types/hast': 3.0.4 @@ -27683,8 +28429,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - rename-keys@1.2.0: {} - renderkid@3.0.0: dependencies: css-select: 4.3.0 @@ -27801,6 +28545,13 @@ snapshots: rope-sequence@1.3.4: {} + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + rtl-css-js@1.16.1: dependencies: '@babel/runtime': 7.27.4 @@ -27879,8 +28630,6 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 - secure-json-parse@2.7.0: {} - selderee@0.11.0: dependencies: parseley: 0.12.1 @@ -28089,6 +28838,17 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 + shiki@3.14.0: + dependencies: + '@shikijs/core': 3.14.0 + '@shikijs/engine-javascript': 3.14.0 + '@shikijs/engine-oniguruma': 3.14.0 + '@shikijs/langs': 3.14.0 + '@shikijs/themes': 3.14.0 + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + shimmer@1.2.1: {} side-channel-list@1.0.0: @@ -28325,6 +29085,26 @@ snapshots: stoppable@1.1.0: {} + streamdown@1.4.0(@types/react@19.1.3)(react@19.0.0): + dependencies: + clsx: 2.1.1 + katex: 0.16.25 + lucide-react: 0.542.0(react@19.0.0) + marked: 16.4.1 + mermaid: 11.12.1 + react: 19.0.0 + react-markdown: 10.1.0(@types/react@19.1.3)(react@19.0.0) + rehype-harden: 1.1.5 + rehype-katex: 7.0.1 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + shiki: 3.14.0 + tailwind-merge: 3.3.1 + transitivePeerDependencies: + - '@types/react' + - supports-color + streamsearch@1.1.0: {} string-length@4.0.2: @@ -28529,11 +29309,6 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - svgson@5.3.1: - dependencies: - deep-rename-keys: 0.2.1 - xml-reader: 2.4.3 - swr@2.3.3(react@19.0.0): dependencies: dequal: 2.0.3 @@ -28544,7 +29319,7 @@ snapshots: tabbable@6.2.0: {} - tailwind-merge@3.3.0: {} + tailwind-merge@3.3.1: {} tailwindcss@4.1.8: {} @@ -28704,6 +29479,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -28731,6 +29508,13 @@ snapshots: toidentifier@1.0.1: {} + tokenlens@1.3.1: + dependencies: + '@tokenlens/core': 1.3.0 + '@tokenlens/fetch': 1.3.0 + '@tokenlens/helpers': 1.3.1 + '@tokenlens/models': 1.3.0 + totalist@3.0.1: {} tough-cookie@4.1.4: @@ -28774,6 +29558,8 @@ snapshots: trough@2.2.0: {} + ts-dedent@2.2.0: {} + ts-easing@0.2.0: {} ts-interface-checker@0.1.13: {} @@ -29204,6 +29990,10 @@ snapshots: optionalDependencies: '@types/react': 19.1.3 + use-stick-to-bottom@1.1.1(react@19.0.0): + dependencies: + react: 19.0.0 + use-sync-external-store@1.5.0(react@19.0.0): dependencies: react: 19.0.0 @@ -29221,6 +30011,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -29283,6 +30075,23 @@ snapshots: void-elements@3.1.0: {} + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 @@ -29619,17 +30428,8 @@ snapshots: dependencies: sax: 1.4.1 - xml-lexer@0.2.2: - dependencies: - eventemitter3: 2.0.3 - xml-name-validator@4.0.0: {} - xml-reader@2.4.3: - dependencies: - eventemitter3: 2.0.3 - xml-lexer: 0.2.2 - xmlchars@2.2.0: {} xtend@4.0.2: {} @@ -29717,13 +30517,9 @@ snapshots: cookie: 1.0.2 youch-core: 0.3.3 - zod-to-json-schema@3.24.5(zod@3.25.42): - dependencies: - zod: 3.25.42 - zod@3.22.3: {} - zod@3.25.42: {} + zod@4.1.12: {} zustand@4.5.7(@types/react@19.1.3)(immer@9.0.21)(react@19.0.0): dependencies: diff --git a/third_party/usvg/ARCHITECTURE.md b/third_party/usvg/ARCHITECTURE.md new file mode 100644 index 0000000000..cf5f2bb1d0 --- /dev/null +++ b/third_party/usvg/ARCHITECTURE.md @@ -0,0 +1,675 @@ +# usvg Architecture: Parsing and Style Resolution + +> **Note**: This document does not always reflect the current state of the implementation. The content may be incorrect, ahead of, or behind the actual implementation. + +**Last updated**: 2025-11-25 + +## Overview + +`usvg` (micro SVG) is an SVG parser that transforms complex SVG files with mixed styles (CSS, inline attributes, style attributes) into a simplified, strongly-typed tree structure. This document explains how it handles the complexity of SVG parsing, particularly focusing on CSS and style resolution. + +## System Architecture + +### High-Level Pipeline + +The parsing pipeline follows these stages: + +``` +SVG XML String + ↓ +[roxmltree] → XML Document (DOM-like structure) + ↓ +[svgtree::parse] → Intermediate SVG Tree (with CSS resolution) + ↓ +[converter] → Final Tree (simplified, strongly-typed) +``` + +### Module Structure + +``` +usvg/ +├── src/ +│ ├── lib.rs # Public API entry point +│ ├── parser/ # Parsing and CSS resolution +│ │ ├── mod.rs # Parser module, error types +│ │ ├── svgtree/ # Intermediate tree representation +│ │ │ ├── mod.rs # Document, Node, Attribute structures +│ │ │ ├── parse.rs # XML → svgtree conversion (two-pass CSS) +│ │ │ ├── names.rs # EId, AId enums (element/attribute IDs) +│ │ │ └── text.rs # Text node handling +│ │ ├── style.rs # Style resolution (fill, stroke, paint) +│ │ ├── converter.rs # svgtree → tree::Tree conversion +│ │ ├── shapes.rs # Shape element parsing (rect, circle, etc.) +│ │ ├── text.rs # Text element parsing +│ │ ├── use_node.rs # element resolution +│ │ ├── paint_server.rs # Gradient, pattern resolution +│ │ ├── filter.rs # Filter effects +│ │ ├── mask.rs # Mask elements +│ │ ├── clippath.rs # Clip path elements +│ │ ├── marker.rs # Marker elements +│ │ ├── image.rs # Image elements +│ │ ├── switch.rs # element +│ │ ├── units.rs # Unit conversion +│ │ └── options.rs # Parser options +│ ├── tree/ # Final tree structure +│ │ ├── mod.rs # Tree, Node, Group, Path, Image, Text +│ │ ├── geom.rs # Geometry types (Rect, Transform, etc.) +│ │ ├── filter.rs # Filter types +│ │ └── text.rs # Text-specific types +│ ├── text/ # Text rendering (optional feature) +│ │ ├── mod.rs +│ │ ├── layout.rs +│ │ ├── flatten.rs +│ │ └── colr.rs +│ └── writer.rs # SVG output (optional) +``` + +### Key Components + +1. **XML Parsing Layer** (`roxmltree`) + + - Parses raw SVG XML into a DOM-like structure + - Handles namespaces, attributes, and element hierarchy + - Provides zero-copy string storage via `StringStorage` + +2. **Intermediate SVG Tree** (`svgtree::Document`) + + - Custom tree structure optimized for SVG + - Stores attributes as strongly-typed `AId` (Attribute ID) enums + - Resolves CSS and style attributes into presentation attributes + - Handles attribute inheritance + - **Key design**: Attributes stored separately from nodes (memory efficient) + +3. **Style Resolution Layer** (`parser::style.rs`, `parser::svgtree::parse.rs`) + + - Two-pass CSS collection + - Selector matching via `simplecss` + - Precedence handling (`!important`, specificity) + - Inheritance resolution + +4. **Conversion Layer** (`parser::converter.rs`) + + - Converts `svgtree::Document` to `tree::Tree` + - Resolves all references (`url(#id)`) + - Converts shapes to paths + - Computes paint objects (Color, Gradient, Pattern) + - Calculates bounding boxes + +5. **Final Tree** (`tree::Tree`) + - Simplified, render-ready structure + - All references resolved + - All styles computed and applied + - Only contains renderable elements (Group, Path, Image, Text) + +## CSS and Style Resolution + +### The Challenge + +SVG supports styles in multiple ways: + +- **Presentation attributes**: `fill="red"`, `stroke-width="2"` +- **Style attributes**: `style="fill:red; stroke-width:2"` +- **CSS stylesheets**: `` +- **External stylesheets**: `` +- **Inheritance**: Some attributes inherit from parent elements + +### Full Document CSS Scan + +**Yes, usvg scans the entire document first to extract all CSS before parsing any elements.** + +The process works like this: + +```rust +fn parse<'input>( + xml: &roxmltree::Document<'input>, + injected_stylesheet: Option<&'input str>, +) -> Result, Error> { + // ... setup ... + + // STEP 1: Scan entire document for ALL + + + + +``` + +**Processing steps:** + +1. **Full Document CSS Scan** (happens first): + + ```rust + resolve_css(xml, None) + ``` + + - Scans entire document with `xml.descendants()` + - Finds ` + +``` + +will become + +```xml + +``` + +The produced SVG tree never has `style` elements and `class` attributes. + +## `inherit` will be resolved + +SVG allows setting some attribute values to `inherit`, +in which case the actual value should be taken from a parent element. + +Not only it applies only to some attributes. +But some attributes also allow `inherit` only from the direct parent. + +`rosvgtree` handles this for us. + +## Recursive links removal + +SVG supports referencing other elements via IRI and FuncIRI value types. +IRI is `xlink:href="#id"` and FuncIRI is `url(#id)`. + +As in any link-based system this could lead to recursive references, +which when handled incorrectly can crash your app. + +We're trying to detect all common cases, but it's +not 100% guarantee that there will be no recursive links left, but we're pretty close. + +This includes simple cases like + +```xml + +``` + +and more complex one like + +```xml + + + + + + +``` + +## Remember all elements with an ID + +As mentioned above, SVG supports references. And it can reference any element in the document.
+Instead of checking each element in the tree each time, which would be pretty slow, +we have an ID<->Node HashMap to quickly retrieve a requested element. + +## Links are groups + +The `` element in SVG is just a `` with a URL.
+Since we really support only the static SVG subset, we can replace `
` with ``. + +## `tref` resolving + +[`tref`](https://www.w3.org/TR/SVG11/text.html#TRefElement) is a pretty weird SVG element. +It's basically a way to reference text nodes. + +We resolve them automatically and replace them with `tspan`. + +```xml + + Text + + +``` + +will become + +```xml +Text +``` + +## `use` will be resolved + +This is probably the only breaking change to the SVG structure. + +The way the `use` works, is that it creates a shadow tree of nodes +that it's referencing. This is a great way to save space, +but it makes style properties resolving way harder. + +This is because when you want to get a parent element from inside the `use`, +the tree should return `use`'s parent and not the referenced element parent. + +To illustrate: + +```xml + + + + + + + +``` + +If you simply call `node.parent().attribute("fill")` it will return `red`, not `green`. +Because the current node is `rect1`. + +As you can imagine, this is pretty hard to handle using a typical DOM model. +So instead we're simply coping referenced elements inside +the `use` so it can be treated as a regular group. + +```xml + + +``` + +will become + +```xml + + + + +``` + +
+ +The main limitation of this approach, excluding the fact we're creating way more elements +that we had initially, is that copied elements must not have an `id` attribute, +otherwise we would end up with multiple duplicates. diff --git a/third_party/usvg/docs/spec.adoc b/third_party/usvg/docs/spec.adoc new file mode 100644 index 0000000000..797a86099f --- /dev/null +++ b/third_party/usvg/docs/spec.adoc @@ -0,0 +1,771 @@ += Micro SVG Document Structure +:toc: + +== Intro + +SVG Micro represents a strip down SVG Full 1.1 subset. + +Here is the main differences between SVG Full and SVG Micro. + +- No XML DTD. +- No CSS. +- `use`, `marker` and nested `svg` will be resolved. +- Simplified path notation. Only absolute MoveTo, LineTo, CurveTo + and ClosePath segments are allowed. +- No inheritable attributes. +- No `xlink:href`, except the `image` element. +- No recursive references. +- Only valid elements and attributes. +- No unused elements. +- No redundant groups. +- No units. +- No `objectBoundingBox` units. +- No `viewBox` and `preserveAspectRatio` attributes. +- No `style` attribute, except for `mix-blend-mode` and `isolation` +- Default attributes are implicit. + +You can use `usvg` command to convert a random SVG into a SVG Micro almost losslessly. + +== Elements + +[[svg-element]] + +=== The `svg` element + +The `svg` element is the root element of the document. +It's defined only once and can't be nested, unlike by the SVG spec. + +*Children:* + +* <> +* <> +* <> +* <> + +*Attributes:* + +* `width` = < >> + + The width of the rectangular region into which the referenced document is placed. +* `height` = < >> + + The height of the rectangular region into which the referenced document is placed. + +[[defs-element]] + +=== The `defs` element + +Always present. Always the first `svg` child. Can be empty. + +*Children:* + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + +*Attributes:* + +* none + +[[linearGradient-element]] + +=== The `linearGradient` element + +Doesn't have a `xlink:href` attribute because all attributes and `stop` +children will be resolved. + +*Children:* + +* At least two <> + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `x1` = < >> +* `y1` = < >> +* `x2` = < >> +* `y2` = < >> +* `gradientUnits` = `userSpaceOnUse`? +* `spreadMethod` = `reflect | repeat`? +* `gradientTransform` = < >>? + +[[radialGradient-element]] + +=== The `radialGradient` element + +Doesn't have a `xlink:href` attribute because all attributes and `stop` +children will be resolved. + +*Children:* + +* At least two <> + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `cx` = < >> +* `cy` = < >> +* `fx` = < >> + + Guarantee to be the circle defined by `cx`, `cy` and `r`. +* `fy` = < >> + + Guarantee to be inside the circle defined by `cx`, `cy` and `r`. +* `r` = < >> +* `gradientUnits` = `userSpaceOnUse` +* `spreadMethod` = `reflect | repeat`? +* `gradientTransform` = < >>? + +[[stop-element]] + +=== The `stop` element + +Gradient's `stop` children will always have unique, ordered `offset` values +in the 0..1 range. + +*Children:* + +* none + +*Attributes:* + +* `offset` = < >> +* `stop-color` = < >> +* `stop-opacity` = < >>? + + Default: 1 + +[[pattern-element]] + +=== The `pattern` element + +Doesn't have a `xlink:href` attribute because all attributes and children will be resolved. + +*Children:* + +* `g` +* `path` +* `image` + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `x` = < >> +* `y` = < >> +* `width` = < >> +* `height` = < >> +* `patternUnits` = `userSpaceOnUse` +* `patternTransform` = < >>? + +[[clipPath-element]] + +=== The `clipPath` element + +*Children:* + +* `path` + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `clip-path` = < >>? + + An optional reference to a supplemental `clipPath`. + + Default: none +* `transform` = < >>? + +[[mask-element]] + +=== The `mask` element + +*Children:* + +* `g` +* `path` +* `image` + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `mask` = < >>? + + An optional reference to a supplemental `mask`. + + Default: none +* `x` = < >> +* `y` = < >> +* `width` = < >> +* `height` = < >> +* `mask-type` = `alpha`? + + Default: luminance +* `maskUnits` = `userSpaceOnUse` + +[[filter-element]] + +=== The `filter` element + +Doesn't have a `xlink:href` attribute because all attributes and children will be resolved. + +*Children:* + +* <> + +*Attributes:* + +* `id` = < >> + + The element ID. Always set. Guarantee to be unique. +* `x` = < >> +* `y` = < >> +* `width` = < >> +* `height` = < >> +* `filterUnits` = `userSpaceOnUse` + +[[g-element]] + +=== The `g` element + +The group element indicates that a new canvas should be created. +All group's children elements will be rendered on it and then merged into +the parent canvas. + +Since it's pretty expensive, especially memory wise, _usvg_ +will remove as many groups as possible. +And all the remaining one will indicate that a new canvas must be created. + +A group can have no children when it has a `filter` attribute. + +A group will have at least one of the attributes present. + +*Children:* + +* <> +* <> +* <> + +*Attributes:* + +* `id` = < >>? + + An optional, but never empty, element ID. +* `opacity` = < >>? +* `clip-path` = < >>? + + Cannot be set to `none`. +* `mask` = < >>? + + Cannot be set to `none`. +* `filter` = < >>+ + + Cannot be set to `none`. +* `transform` = < >>? +* `style` = < >>? + + This is the only place where the `style` attribute is used. + For reasons unknown, `mix-blend-mode` and `isolation` properties must not be set as attributes, + only as part of the `style` attribute. + + The set attribute will look like `mix-blend-mode:screen;isolation:isolate`. + Both properties are always set. + + The attribute is not present only in case of `mix-blend-mode:norma;isolation:auto` + +[[path-element]] + +=== The `path` element + +*Children:* + +* none + +*Attributes:* + +* `id` = < >>? + + An optional, but never empty, element ID. +* `d` = < >> + +* `fill` = `none` | < >> | < >> + + If set to `none` than all fill-* attributes will not be set too. + + Default: black +* `fill-opacity` = < >>? + + Default: 1 +* `fill-rule` = `evenodd`? + + Default: nonzero +* `stroke` = `none` | < >> | < >> + + If set to `none` than all stroke-* attributes will not be set too. + + Default: none +* `stroke-width` = < >>? + + Default: 1 +* `stroke-linecap` = `round | square`? + + Default: butt +* `stroke-linejoin` = `round | bevel`? + + Default: miter +* `stroke-miterlimit` = < >>? + + Guarantee to be > 1. + + Default: 4 +* `stroke-dasharray` = ``? + + Guarantee to have even amount of numbers. + + Default: none +* `stroke-dashoffset` = < >>? +* `stroke-opacity` = < >>? + + Default: 1 +* `paint-order` = `normal | stroke`? + + Default: `normal` + + Only `stroke` will be written. +* `clip-rule` = `evenodd`? + + Will be set only inside the <>, instead of `fill-rule`. +* `clip-path` = < >>? + + Available only inside the <>. +* `shape-rendering` = `optimizeSpeed | crispEdges`? + + Default: geometricPrecision +* `visibility` = `hidden`? + + Default: visible +* `transform` = < >>? + + Can only be set on paths inside of `clipPath`. + +[[image-element]] + +=== The `image` element + +*Children:* + +* none + +*Attributes:* + +* `id` = < >>? + + An optional, but never empty, element ID. +* `xlink:href` = < >> + + The IRI contains a base64 encoded image. +* `width` = < >> +* `height` = < >> +* `image-rendering` = `optimizeSpeed`? + + Default: optimizeQuality +* `visibility` = `hidden`? + + Default: visible + +== Filter primitives + +=== Filter primitive attributes + +The attributes below are the same for all filter primitives. + +* `color-interpolation-filters` = `sRGB`? + + Default: linearRGB +* `x` = < >>? +* `y` = < >>? +* `width` = < >>? +* `height` = < >>? +* `result` = < >> + +The `x`, `y`, `width` and `height` attributes can be omitted. +SVG has a pretty complex +https://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveSubRegion[rules of resolving them] +and I don't fully understand them yet. +Neither do others, because they are pretty poorly implemented. + +=== Filter primitive `feBlend` + +*Attributes:* + +* `in` = < >> +* `in2` = < >> +* `mode` = `normal | multiply | screen | overlay | darken | lighten | color-dodge |color-burn | +hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity` +* <> + +=== Filter primitive `feColorMatrix` + +*Attributes:* + +* `in` = < >> +* `type` = `matrix | saturate | hueRotate | luminanceToAlpha` +* `values` = ``? + +** For `type=matrix`, contains 20 numbers. +** For `type=saturate`, contains a single number in a 0..1 range. +** For `type=hueRotate`, contains a single number. +** Not present for `type=luminanceToAlpha`. +* <> + +=== Filter primitive `feComponentTransfer` + +*Children:* + +* `feFuncR` +* `feFuncG` +* `feFuncB` +* `feFuncA` + +The all four will always be present. + +*Attributes:* + +* `in` = < >> +* <> + +*`feFunc(R|G|B|A)` attributes:* + +* `type` = `identity | table | discrete | linear | gamma` +* `tableValues` = ``? + + Present only when `type=table | discrete`. Can be empty. +* `slope` = < >>? + + Present only when `type=linear`. +* `intercept` = < >>? + + Present only when `type=linear`. +* `amplitude` = < >>? + + Present only when `type=gamma`. +* `exponent` = < >>? + + Present only when `type=gamma`. +* `offset` = < >>? + + Present only when `type=gamma`. + +=== Filter primitive `feComposite` + +*Attributes:* + +* `in` = < >> +* `in2` = < >> +* `operator` = `over | in | out | atop | xor | arithmetic` +* `k1` = < >>? + + Present only when `operator=arithmetic`. +* `k2` = < >>? + + Present only when `operator=arithmetic`. +* `k3` = < >>? + + Present only when `operator=arithmetic`. +* `k4` = < >>? + + Present only when `operator=arithmetic`. +* <> + +=== Filter primitive `feConvolveMatrix` + +*Attributes:* + +* `in` = < >> +* `order` = < >> " " < >> + + Both numbers are never 0. +* `kernelMatrix` = `` +* `divisor` = < >> + + Never 0. +* `bias` = < >> +* `targetX` = < >> + + Always smaller than the number of columns in the matrix. +* `targetY` = < >> + + Always smaller than the number of rows in the matrix. +* `edgeMode` = `none | duplicate | wrap` +* `preserveAlpha` = `true | false` +* <> + +=== Filter primitive `feDiffuseLighting` + +*Children:* + +Only one of: + +* `feDistantLight` +* `fePointLight` +* `feSpotLight` + +*Attributes:* + +* `in` = < >> +* `surfaceScale` = < >> +* `diffuseConstant` = < >> +* `lighting-color` = < >> +* <> + +`feDistantLight` *attributes:* + +* `azimuth` = < >> +* `elevation` = < >> + +`fePointLight` *attributes:* + +* `x` = < >> +* `y` = < >> +* `z` = < >> + +`feSpotLight` *attributes:* + +* `x` = < >> +* `y` = < >> +* `z` = < >> +* `pointsAtX` = < >> +* `pointsAtY` = < >> +* `pointsAtZ` = < >> +* `specularExponent` = < >> +* `limitingConeAngle` = < >>? + +=== Filter primitive `feDisplacementMap` + +*Attributes:* + +* `in` = < >> +* `in2` = < >> +* `scale` = < >> +* `xChannelSelector` = `R | G | B | A` +* `yChannelSelector` = `R | G | B | A` +* <> + +=== Filter primitive `feDropShadow` + +*Attributes:* + +* `in` = < >> +* `stdDeviation` = < >> " " < >> +* `dx` = < >> +* `dy` = < >> +* `flood-color` = < >> +* `flood-opacity` = < >> +* <> + +=== Filter primitive `feFlood` + +*Attributes:* + +* `flood-color` = < >> +* `flood-opacity` = < >> +* <> + +=== Filter primitive `feGaussianBlur` + +*Attributes:* + +* `in` = < >> +* `stdDeviation` = < >> " " < >> +* <> + +=== Filter primitive `feImage` + +*Attributes:* + +* `xlink:href` = < >> + + The IRI contains a link to an element (like `use`). + base64 encoded is not allowed and will be represented as a link to an `image`. +* <> + +=== Filter primitive `feMerge` + +*Children:* + +* `feMergeNode` + +*Attributes:* + +* <> + +*`feMergeNode` attributes:* + +* `in` = < >> + +=== Filter primitive `feMorphology` + +*Attributes:* + +* `in` = < >> +* `operator` = `erode | dilate` +* `radius` = < >> " " < >> +* <> + +=== Filter primitive `feOffset` + +*Attributes:* + +* `in` = < >> +* `dx` = < >> +* `dy` = < >> +* <> + +=== Filter primitive `feSpecularLighting` + +*Children:* + +Only one of: + +* `feDistantLight` +* `fePointLight` +* `feSpotLight` + +*Attributes:* + +* `in` = < >> +* `surfaceScale` = < >> +* `specularConstant` = < >> +* `specularExponent` = < >> + + Number in a 1..128 range. +* `lighting-color` = < >> +* <> + +`feDistantLight` *attributes:* + +* `azimuth` = < >> +* `elevation` = < >> + +`fePointLight` *attributes:* + +* `x` = < >> +* `y` = < >> +* `z` = < >> + +`feSpotLight` *attributes:* + +* `x` = < >> +* `y` = < >> +* `z` = < >> +* `pointsAtX` = < >> +* `pointsAtY` = < >> +* `pointsAtZ` = < >> +* `specularExponent` = < >> +* `limitingConeAngle` = < >>? + +=== Filter primitive `feTile` + +*Attributes:* + +* `in` = < >> +* <> + +=== Filter primitive `feTurbulence` + +*Attributes:* + +* `baseFrequency` = < >> " " < >> +* `numOctaves` = < >> +* `seed` = < >> +* `stitchTiles` = `stitch | noStitch` +* `type` = `fractalNoise | turbulence` +* <> + +== Data types + +If an attribute has the `?` symbol after the type that's mean that +that this attribute is optional. + +[[string-type]] + +** - A Unicode (UTF-8) string. + + +[[number-type]] + +** - A real number. + +`number ::= [-]? [0-9]+ "." [0-9]+` + + +[[positive-number-type]] + +** - A positive real <>. + +`positive-number ::= [0-9]+ "." [0-9]+` + + +[[integer-type]] + +** - An integer. + +`integer ::= [-]? [0-9]+` + + +[[positive-integer-type]] + +** - A positive integer. + +`positive-integer ::= [0-9]+` + + +[[opacity-type]] + +** - A real <> in a 0..1 range. + +`opacity ::= positive-number` + + +[[offset-type]] + +** - A real <> in a 0..1 range. + +`offset ::= positive-number` + + +[[color-type]] + +** - A hex-encoded RGB color. +``` +color ::= "#" hexdigit hexdigit hexdigit hexdigit hexdigit hexdigit +hexdigit ::= [0-9a-f] +``` + + +[[iri-type]] + +** - An Internationalized Resource Identifier. +Always a valid, local reference. + +`IRI ::= string` + + +[[func-iri-type]] + +** - Functional notation for an <>. +Always a valid, local reference. + +`FuncIRI ::= url( )` + + +[[filter-input-type]] + +** - A filter source. A reference to a _result_ guarantee to be valid. + +``` +filter-input ::= SourceGraphic | SourceAlpha | +``` + +We do not support `FillPaint`, `StrokePaint`, `BackgroundImage` and `BackgroundAlpha`. + + +[[transform-type]] + +** - A transformation matrix. +Always a `matrix` and not `translate`, `scale`, etc. +Numbers are space-separated. + +`transform ::= matrix( " " " " " " " " " " )` + + +[[path-data-type]] + +** - A path data. + +* Contains only absolute MoveTo, LineTo, CurveTo and ClosePath segments. +* All segments are explicit. +* The first segment is guarantee to be MoveTo. +* Segments, commands and coordinates are separated only by space. +* Path and all subpaths are guarantee to have at least two segments. + +Grammar: + +``` +svg-path: + moveto-drawto-command-groups +moveto-drawto-command-groups: + moveto-drawto-command-group + | moveto-drawto-command-group " " moveto-drawto-command-groups +moveto-drawto-command-group: + moveto " " drawto-commands +drawto-commands: + drawto-command + | drawto-command " " drawto-commands +drawto-command: + closepath + | lineto + | curveto +moveto: + "M " coordinate-pair +lineto: + "L " coordinate-pair +curveto: + "C " coordinate-pair " " coordinate-pair " " coordinate-pair +closepath: + "Z" +coordinate-pair: + coordinate " " coordinate +coordinate: + sign? digit-sequence "." digit-sequence +sign: + "-" +digit-sequence: + digit + | digit digit-sequence +digit: + "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +``` + +Basically, a path looks like this: `M 10.5 20 L 30 40`. +Commands and numbers are separated by a space. +Numbers with an exponent are not allowed. +Trimmed numbers like `-.5` are not allowed. diff --git a/third_party/usvg/src/lib.rs b/third_party/usvg/src/lib.rs new file mode 100644 index 0000000000..d5e4ef7184 --- /dev/null +++ b/third_party/usvg/src/lib.rs @@ -0,0 +1,73 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/*! +`usvg` (micro SVG) is an [SVG] parser that tries to solve most of SVG complexity. + +> **Note:** This is a fork maintained for the Grida project. See README.md for details. + +SVG is notoriously hard to parse. `usvg` presents a layer between an XML library and +a potential SVG rendering library. It will parse an input SVG into a strongly-typed tree structure +were all the elements, attributes, references and other SVG features are already resolved +and presented in the simplest way possible. +So a caller doesn't have to worry about most of the issues related to SVG parsing +and can focus just on the rendering part. + +## Features + +- All supported attributes are resolved. + No need to worry about inheritable, implicit and default attributes +- CSS will be applied +- Only simple paths + - Basic shapes (like `rect` and `circle`) will be converted into paths + - Paths contain only absolute *MoveTo*, *LineTo*, *QuadTo*, *CurveTo* and *ClosePath* segments. + ArcTo, implicit and relative segments will be converted +- `use` will be resolved and replaced with the reference content +- Nested `svg` will be resolved +- Invalid, malformed elements will be removed +- Relative length units (mm, em, etc.) will be converted into pixels/points +- External images will be loaded +- Internal, base64 images will be decoded +- All references (like `#elem` and `url(#elem)`) will be resolved +- `switch` will be resolved +- Text elements, which are probably the hardest part of SVG, will be completely resolved. + This includes all the attributes resolving, whitespaces preprocessing (`xml:space`), + text chunks and spans resolving +- Markers will be converted into regular elements. No need to place them manually +- All filters are supported. Including filter functions, like `filter="contrast(50%)"` +- Recursive elements will be detected and removed +- `objectBoundingBox` will be replaced with `userSpaceOnUse` + +## Limitations + +- Unsupported SVG features will be ignored +- CSS support is minimal +- Only [static](http://www.w3.org/TR/SVG11/feature#SVG-static) SVG features, + e.g. no `a`, `view`, `cursor`, `script`, no events and no animations + +[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics +*/ + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] + +mod parser; +#[cfg(feature = "text")] +mod text; +mod tree; +mod writer; + +pub use parser::*; +#[cfg(feature = "text")] +pub use text::*; +pub use tree::*; + +pub use roxmltree; + +#[cfg(feature = "text")] +pub use fontdb; + +pub use writer::WriteOptions; +pub use xmlwriter::Indent; diff --git a/third_party/usvg/src/main.rs b/third_party/usvg/src/main.rs new file mode 100644 index 0000000000..84ae50250e --- /dev/null +++ b/third_party/usvg/src/main.rs @@ -0,0 +1,514 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::fs::File; +use std::io::{self, Read, Write}; +use std::path::PathBuf; +use std::process; +use std::sync::Arc; + +use pico_args::Arguments; + +const HELP: &str = "\ +usvg (micro SVG) is an SVG simplification tool. + +USAGE: + usvg [OPTIONS] # from file to file + usvg [OPTIONS] -c # from file to stdout + usvg [OPTIONS] - # from stdin to file + usvg [OPTIONS] - -c # from stdin to stdout + +OPTIONS: + -h, --help Prints help information + -V, --version Prints version information + -c Prints the output SVG to the stdout + + --dpi DPI Sets the resolution + [default: 96] [possible values: 10..4000 (inclusive)] + --stylesheet PATH Inject a stylesheet that should be used when resolving + CSS attributes. + --languages LANG Sets a comma-separated list of languages that + will be used during the 'systemLanguage' + attribute resolving + Examples: 'en-US', 'en-US, ru-RU', 'en, ru' + [default: en] + --shape-rendering HINT Selects the default shape rendering method + [default: geometricPrecision] + [possible values: optimizeSpeed, crispEdges, + geometricPrecision] + --text-rendering HINT Selects the default text rendering method + [default: optimizeLegibility] + [possible values: optimizeSpeed, optimizeLegibility, + geometricPrecision] + --image-rendering HINT Selects the default image rendering method + [default: optimizeQuality] + [possible values: optimizeQuality, optimizeSpeed, smooth, high-quality, crisp-edges, pixelated] + --resources-dir DIR Sets a directory that will be used during + relative paths resolving. + Expected to be the same as the directory that + contains the SVG file, but can be set to any. + [default: input file directory + or none when reading from stdin] + + --font-family FAMILY Sets the default font family that will be + used when no 'font-family' is present + [default: Times New Roman] + --font-size SIZE Sets the default font size that will be + used when no 'font-size' is present + [default: 12] [possible values: 1..192 (inclusive)] + --serif-family FAMILY Sets the 'serif' font family. + Will be used when no 'font-family' is present + [default: Times New Roman] + --sans-serif-family FAMILY Sets the 'sans-serif' font family + [default: Arial] + --cursive-family FAMILY Sets the 'cursive' font family + [default: Comic Sans MS] + --fantasy-family FAMILY Sets the 'fantasy' font family + [default: Impact] + --monospace-family FAMILY Sets the 'monospace' font family + [default: Courier New] + --use-font-file PATH Load a specified font file into the fonts database. + Will be used during text to path conversion. + This option can be set multiple times + --use-fonts-dir PATH Loads all fonts from the specified directory + into the fonts database. + Will be used during text to path conversion. + This option can be set multiple times + --skip-system-fonts Disables system fonts loading. + You should add some fonts manually using + --use-font-file and/or --use-fonts-dir + Otherwise, text elements will not be processes + --list-fonts Lists successfully loaded font faces. + Useful for debugging + --default-width LENGTH Sets the default width of the SVG viewport. Like + the '--default-height' option, this option + controls what size relative units in the document + will use as a base if there is no viewBox and + document width or height are relative. + [values: 1..4294967295 (inclusive)] [default: 100] + --default-height LENGTH Sets the default height of the SVG viewport. + Refer to the explanation of the '--default-width' + option. [values: 1..4294967295 (inclusive)] [default: 100] + + --preserve-text Do not convert text into paths. + --id-prefix Adds a prefix to each ID attribute + --indent INDENT Sets the XML nodes indent + [values: none, 0, 1, 2, 3, 4, tabs] [default: 4] + --attrs-indent INDENT Sets the XML attributes indent + [values: none, 0, 1, 2, 3, 4, tabs] [default: none] + --coordinates-precision NUM Set the coordinates numeric precision. + Smaller precision can lead to a malformed output in some cases + [values: 2..8 (inclusive)] [default: 8] + --transforms-precision NUM Set the transform values numeric precision. + Smaller precision can lead to a malformed output in some cases + [values: 2..8 (inclusive)] [default: 8] + --quiet Disables warnings + +ARGS: + Input file + Output file +"; + +#[derive(Debug)] +struct Args { + dpi: u32, + languages: Vec, + shape_rendering: usvg::ShapeRendering, + text_rendering: usvg::TextRendering, + image_rendering: usvg::ImageRendering, + resources_dir: Option, + + font_family: Option, + font_size: u32, + serif_family: Option, + sans_serif_family: Option, + cursive_family: Option, + fantasy_family: Option, + monospace_family: Option, + font_files: Vec, + font_dirs: Vec, + skip_system_fonts: bool, + preserve_text: bool, + list_fonts: bool, + default_width: u32, + default_height: u32, + + id_prefix: Option, + indent: xmlwriter::Indent, + attrs_indent: xmlwriter::Indent, + coordinates_precision: Option, + transforms_precision: Option, + style_sheet: Option, + + quiet: bool, + + input: String, + output: String, +} + +fn collect_args() -> Result { + let mut input = Arguments::from_env(); + + if input.contains(["-h", "--help"]) { + print!("{}", HELP); + process::exit(0); + } + + if input.contains(["-V", "--version"]) { + println!("{}", env!("CARGO_PKG_VERSION")); + process::exit(0); + } + + Ok(Args { + dpi: input.opt_value_from_fn("--dpi", parse_dpi)?.unwrap_or(96), + languages: input + .opt_value_from_fn("--languages", parse_languages)? + .unwrap_or(vec!["en".to_string()]), // TODO: use system language + shape_rendering: input + .opt_value_from_str("--shape-rendering")? + .unwrap_or_default(), + text_rendering: input + .opt_value_from_str("--text-rendering")? + .unwrap_or_default(), + image_rendering: input + .opt_value_from_str("--image-rendering")? + .unwrap_or_default(), + resources_dir: input + .opt_value_from_str("--resources-dir") + .unwrap_or_default(), + + font_family: input.opt_value_from_str("--font-family")?, + font_size: input + .opt_value_from_fn("--font-size", parse_font_size)? + .unwrap_or(12), + serif_family: input.opt_value_from_str("--serif-family")?, + sans_serif_family: input.opt_value_from_str("--sans-serif-family")?, + cursive_family: input.opt_value_from_str("--cursive-family")?, + fantasy_family: input.opt_value_from_str("--fantasy-family")?, + monospace_family: input.opt_value_from_str("--monospace-family")?, + font_files: input.values_from_str("--use-font-file")?, + font_dirs: input.values_from_str("--use-fonts-dir")?, + skip_system_fonts: input.contains("--skip-system-fonts"), + preserve_text: input.contains("--preserve-text"), + list_fonts: input.contains("--list-fonts"), + default_width: input + .opt_value_from_fn("--default-width", parse_length)? + .unwrap_or(100), + default_height: input + .opt_value_from_fn("--default-height", parse_length)? + .unwrap_or(100), + + id_prefix: input.opt_value_from_str("--id-prefix")?, + indent: input + .opt_value_from_fn("--indent", parse_indent)? + .unwrap_or(xmlwriter::Indent::Spaces(4)), + attrs_indent: input + .opt_value_from_fn("--attrs-indent", parse_indent)? + .unwrap_or(xmlwriter::Indent::None), + coordinates_precision: input + .opt_value_from_fn("--coordinates-precision", parse_precision)?, + transforms_precision: input.opt_value_from_fn("--transforms-precision", parse_precision)?, + style_sheet: input.opt_value_from_str("--stylesheet").unwrap_or_default(), + + quiet: input.contains("--quiet"), + + input: input.free_from_str()?, + output: input.free_from_str()?, + }) +} + +fn parse_dpi(s: &str) -> Result { + let n: u32 = s.parse().map_err(|_| "invalid number")?; + + if n >= 10 && n <= 4000 { + Ok(n) + } else { + Err("DPI out of bounds".to_string()) + } +} + +fn parse_font_size(s: &str) -> Result { + let n: u32 = s.parse().map_err(|_| "invalid number")?; + + if n > 0 && n <= 192 { + Ok(n) + } else { + Err("font size out of bounds".to_string()) + } +} + +fn parse_languages(s: &str) -> Result, String> { + let mut langs = Vec::new(); + for lang in s.split(',') { + langs.push(lang.trim().to_string()); + } + + if langs.is_empty() { + return Err("languages list cannot be empty".to_string()); + } + + Ok(langs) +} + +fn parse_indent(s: &str) -> Result { + let indent = match s { + "none" => xmlwriter::Indent::None, + "0" => xmlwriter::Indent::Spaces(0), + "1" => xmlwriter::Indent::Spaces(1), + "2" => xmlwriter::Indent::Spaces(2), + "3" => xmlwriter::Indent::Spaces(3), + "4" => xmlwriter::Indent::Spaces(4), + "tabs" => xmlwriter::Indent::Tabs, + _ => return Err("invalid INDENT value".to_string()), + }; + + Ok(indent) +} + +fn parse_length(s: &str) -> Result { + let n: u32 = s.parse().map_err(|_| "invalid length")?; + + if n > 0 { + Ok(n) + } else { + Err("LENGTH cannot be zero".to_string()) + } +} + +fn parse_precision(s: &str) -> Result { + let n: u8 = s.parse().map_err(|_| "invalid precision NUM value")?; + + if (2..=8).contains(&n) { + Ok(n) + } else { + Err("precision NUM cannot be smaller than 2 or larger than 8".to_string()) + } +} + +#[derive(Clone, PartialEq, Debug)] +enum InputFrom<'a> { + Stdin, + File(&'a str), +} + +#[derive(Clone, PartialEq, Debug)] +enum OutputTo<'a> { + Stdout, + File(&'a str), +} + +fn main() { + let args = match collect_args() { + Ok(v) => v, + Err(e) => { + eprintln!("Error: {}.", e); + process::exit(1); + } + }; + + if !args.quiet { + if let Ok(()) = log::set_logger(&LOGGER) { + log::set_max_level(log::LevelFilter::Warn); + } + } + + if let Err(e) = process(args) { + eprintln!("Error: {}.", e); + process::exit(1); + } +} + +fn process(args: Args) -> Result<(), String> { + let (in_svg, out_svg) = { + let in_svg = args.input.as_str(); + let out_svg = args.output.as_str(); + + let svg_from = if in_svg == "-" { + InputFrom::Stdin + } else if in_svg == "-c" { + return Err("-c should be set after input".to_string()); + } else { + InputFrom::File(in_svg) + }; + + let svg_to = if out_svg == "-c" { + OutputTo::Stdout + } else { + OutputTo::File(out_svg) + }; + + (svg_from, svg_to) + }; + + let mut fontdb = usvg::fontdb::Database::new(); + if !args.skip_system_fonts { + // TODO: only when needed + fontdb.load_system_fonts(); + } + + for path in &args.font_files { + if let Err(e) = fontdb.load_font_file(path) { + log::warn!("Failed to load '{}' cause {}.", path.display(), e); + } + } + + for path in &args.font_dirs { + fontdb.load_fonts_dir(path); + } + + let take_or = |mut family: Option, fallback: &str| { + family.take().unwrap_or_else(|| fallback.to_string()) + }; + + fontdb.set_serif_family(take_or(args.serif_family, "Times New Roman")); + fontdb.set_sans_serif_family(take_or(args.sans_serif_family, "Arial")); + fontdb.set_cursive_family(take_or(args.cursive_family, "Comic Sans MS")); + fontdb.set_fantasy_family(take_or(args.fantasy_family, "Impact")); + fontdb.set_monospace_family(take_or(args.monospace_family, "Courier New")); + + if args.list_fonts { + for face in fontdb.faces() { + if let usvg::fontdb::Source::File(ref path) = &face.source { + let families: Vec<_> = face + .families + .iter() + .map(|f| format!("{} ({}, {})", f.0, f.1.primary_language(), f.1.region())) + .collect(); + + println!( + "{}: '{}', {}, {:?}, {:?}, {:?}", + path.display(), + families.join("', '"), + face.index, + face.style, + face.weight.0, + face.stretch + ); + } + } + } + + let resources_dir = match args.resources_dir { + Some(v) => Some(v), + None => { + match in_svg { + InputFrom::Stdin => None, + InputFrom::File(ref f) => { + // Get input file absolute directory. + std::fs::canonicalize(f) + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + } + } + } + }; + + let style_sheet = match args.style_sheet.as_ref() { + Some(p) => Some( + std::fs::read(&p) + .ok() + .and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string())) + .ok_or("failed to read stylesheet".to_string())?, + ), + None => None, + }; + + let re_opt = usvg::Options { + resources_dir, + dpi: args.dpi as f32, + font_family: args + .font_family + .as_deref() + .unwrap_or("Times New Roman") + .to_string(), + font_size: args.font_size as f32, + languages: args.languages, + shape_rendering: args.shape_rendering, + text_rendering: args.text_rendering, + image_rendering: args.image_rendering, + default_size: usvg::Size::from_wh(args.default_width as f32, args.default_height as f32) + .unwrap(), + image_href_resolver: usvg::ImageHrefResolver::default(), + font_resolver: usvg::FontResolver::default(), + fontdb: Arc::new(fontdb), + style_sheet, + }; + + let input_svg = match in_svg { + InputFrom::Stdin => load_stdin(), + InputFrom::File(ref path) => std::fs::read(path).map_err(|e| e.to_string()), + }?; + + let tree = usvg::Tree::from_data(&input_svg, &re_opt).map_err(|e| format!("{}", e))?; + + let xml_opt = usvg::WriteOptions { + id_prefix: args.id_prefix, + preserve_text: args.preserve_text, + coordinates_precision: args.coordinates_precision.unwrap_or(8), + transforms_precision: args.transforms_precision.unwrap_or(8), + use_single_quote: false, + indent: args.indent, + attributes_indent: args.attrs_indent, + }; + + let s = tree.to_string(&xml_opt); + match out_svg { + OutputTo::Stdout => { + io::stdout() + .write_all(s.as_bytes()) + .map_err(|_| "failed to write to the stdout".to_string())?; + } + OutputTo::File(path) => { + let mut f = + File::create(path).map_err(|_| "failed to create the output file".to_string())?; + f.write_all(s.as_bytes()) + .map_err(|_| "failed to write to the output file".to_string())?; + } + } + + Ok(()) +} + +fn load_stdin() -> Result, String> { + let mut buf = Vec::new(); + let stdin = io::stdin(); + let mut handle = stdin.lock(); + + handle + .read_to_end(&mut buf) + .map_err(|_| "failed to read from stdin".to_string())?; + + Ok(buf) +} + +/// A simple stderr logger. +static LOGGER: SimpleLogger = SimpleLogger; +struct SimpleLogger; +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::LevelFilter::Warn + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let target = if record.target().len() > 0 { + record.target() + } else { + record.module_path().unwrap_or_default() + }; + + let line = record.line().unwrap_or(0); + let args = record.args(); + + match record.level() { + log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args), + log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args), + log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args), + log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args), + log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args), + } + } + } + + fn flush(&self) {} +} diff --git a/third_party/usvg/src/parser/clippath.rs b/third_party/usvg/src/parser/clippath.rs new file mode 100644 index 0000000000..03f6b91f78 --- /dev/null +++ b/third_party/usvg/src/parser/clippath.rs @@ -0,0 +1,125 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::str::FromStr; +use std::sync::Arc; + +use super::converter; +use super::svgtree::{AId, EId, SvgNode}; +use crate::{ClipPath, Group, NonEmptyString, NonZeroRect, Transform, Units}; + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + object_bbox: Option, + cache: &mut converter::Cache, +) -> Option> { + // A `clip-path` attribute must reference a `clipPath` element. + if node.tag_name() != Some(EId::ClipPath) { + return None; + } + + // The whole clip path should be ignored when a transform is invalid. + let mut transform = resolve_clip_path_transform(node, state)?; + + let units = node + .attribute(AId::ClipPathUnits) + .unwrap_or(Units::UserSpaceOnUse); + + // Check if this element was already converted. + // + // Only `userSpaceOnUse` clipPaths can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse; + if cacheable { + if let Some(clip) = cache.clip_paths.get(node.element_id()) { + return Some(clip.clone()); + } + } + + if units == Units::ObjectBoundingBox { + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!("Clipping of zero-sized shapes is not allowed."); + return None; + } + }; + + let ts = Transform::from_bbox(object_bbox); + transform = transform.pre_concat(ts); + } + + // Resolve linked clip path. + let mut clip_path = None; + if let Some(link) = node.attribute::(AId::ClipPath) { + clip_path = convert(link, state, object_bbox, cache); + + // Linked `clipPath` must be valid. + if clip_path.is_none() { + return None; + } + } + + let mut id = NonEmptyString::new(node.element_id().to_string())?; + // Generate ID only when we're parsing `objectBoundingBox` clip for the second time. + if !cacheable && cache.clip_paths.contains_key(id.get()) { + id = cache.gen_clip_path_id(); + } + let id_copy = id.get().to_string(); + + let mut clip = ClipPath { + id, + transform, + clip_path, + root: Group::empty(), + }; + + let mut clip_state = state.clone(); + clip_state.parent_clip_path = Some(node); + converter::convert_clip_path_elements(node, &clip_state, cache, &mut clip.root); + + if clip.root.has_children() { + clip.root.calculate_bounding_boxes(); + let clip = Arc::new(clip); + cache.clip_paths.insert(id_copy, clip.clone()); + Some(clip) + } else { + // A clip path without children is invalid. + None + } +} + +fn resolve_clip_path_transform(node: SvgNode, state: &converter::State) -> Option { + // Do not use Node::attribute::, because it will always + // return a valid transform. + + let value: &str = match node.attribute(AId::Transform) { + Some(v) => v, + None => return Some(Transform::default()), + }; + + let ts = match svgtypes::Transform::from_str(value) { + Ok(v) => v, + Err(_) => { + log::warn!("Failed to parse {} value: '{}'.", AId::Transform, value); + return None; + } + }; + + let ts = Transform::from_row( + ts.a as f32, + ts.b as f32, + ts.c as f32, + ts.d as f32, + ts.e as f32, + ts.f as f32, + ); + + if ts.is_valid() { + Some(node.resolve_transform(AId::Transform, state)) + } else { + None + } +} diff --git a/third_party/usvg/src/parser/converter.rs b/third_party/usvg/src/parser/converter.rs new file mode 100644 index 0000000000..7f5758ca95 --- /dev/null +++ b/third_party/usvg/src/parser/converter.rs @@ -0,0 +1,1060 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; +use std::sync::Arc; + +#[cfg(feature = "text")] +use fontdb::Database; +#[cfg(feature = "text")] +use fontdb::ID; +#[cfg(feature = "text")] +use rustybuzz::ttf_parser::GlyphId; +use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; +use tiny_skia_path::PathBuilder; + +use super::svgtree::{self, AId, EId, FromValue, SvgNode}; +use super::units::{self, convert_length}; +use super::{marker, Error, Options}; +#[cfg(feature = "text")] +use crate::flatten::BitmapImage; +use crate::parser::paint_server::process_paint; +#[cfg(feature = "text")] +use crate::text::flatten::DatabaseExt; +use crate::*; + +#[derive(Clone)] +pub struct State<'a> { + pub(crate) parent_clip_path: Option>, + pub(crate) parent_markers: Vec>, + /// Stores the resolved fill and stroke of a use node + /// or a path element (for markers) + pub(crate) context_element: Option<(Option, Option)>, + pub(crate) fe_image_link: bool, + /// A viewBox of the parent SVG element. + pub(crate) view_box: NonZeroRect, + /// A size of the parent `use` element. + /// Used only during nested `svg` size resolving. + /// Width and height can be set independently. + pub(crate) use_size: (Option, Option), + pub(crate) opt: &'a Options<'a>, +} + +#[derive(Clone)] +pub struct Cache { + /// This fontdb is initialized from [`Options::fontdb`] and then populated + /// over the course of conversion. + #[cfg(feature = "text")] + pub fontdb: Arc, + + #[cfg(feature = "text")] + cache_outline: HashMap<(ID, GlyphId), Option>, + #[cfg(feature = "text")] + cache_colr: HashMap<(ID, GlyphId), Option>, + #[cfg(feature = "text")] + cache_svg: HashMap<(ID, GlyphId), Option>, + #[cfg(feature = "text")] + cache_raster: HashMap<(ID, GlyphId), Option>, + + pub clip_paths: HashMap>, + pub masks: HashMap>, + pub filters: HashMap>, + pub paint: HashMap, + + // used for ID generation + all_ids: HashSet, + linear_gradient_index: usize, + radial_gradient_index: usize, + pattern_index: usize, + clip_path_index: usize, + mask_index: usize, + filter_index: usize, + image_index: usize, +} + +macro_rules! font_lookup { + ($method_name:ident, $cache_map:ident, $font_variant:ident, $return_type:ty) => { + #[cfg(feature = "text")] + pub(crate) fn $method_name(&mut self, font: ID, glyph: GlyphId) -> Option<$return_type> { + let key = (font, glyph); + match self.$cache_map.get(&key) { + Some(cache_hit) => cache_hit.clone(), + None => { + let lookup = self.fontdb.$font_variant(font, glyph); + self.$cache_map.insert(key, lookup.clone()); + lookup + } + } + } + }; +} + +impl Cache { + pub(crate) fn new(#[cfg(feature = "text")] fontdb: Arc) -> Self { + Self { + #[cfg(feature = "text")] + fontdb, + + #[cfg(feature = "text")] + cache_outline: HashMap::new(), + #[cfg(feature = "text")] + cache_colr: HashMap::new(), + #[cfg(feature = "text")] + cache_svg: HashMap::new(), + #[cfg(feature = "text")] + cache_raster: HashMap::new(), + + clip_paths: HashMap::new(), + masks: HashMap::new(), + filters: HashMap::new(), + paint: HashMap::new(), + + all_ids: HashSet::new(), + linear_gradient_index: 0, + radial_gradient_index: 0, + pattern_index: 0, + clip_path_index: 0, + mask_index: 0, + filter_index: 0, + image_index: 0, + } + } + + // TODO: macros? + pub(crate) fn gen_linear_gradient_id(&mut self) -> NonEmptyString { + loop { + self.linear_gradient_index += 1; + let new_id = format!("linearGradient{}", self.linear_gradient_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_radial_gradient_id(&mut self) -> NonEmptyString { + loop { + self.radial_gradient_index += 1; + let new_id = format!("radialGradient{}", self.radial_gradient_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_pattern_id(&mut self) -> NonEmptyString { + loop { + self.pattern_index += 1; + let new_id = format!("pattern{}", self.pattern_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_clip_path_id(&mut self) -> NonEmptyString { + loop { + self.clip_path_index += 1; + let new_id = format!("clipPath{}", self.clip_path_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_mask_id(&mut self) -> NonEmptyString { + loop { + self.mask_index += 1; + let new_id = format!("mask{}", self.mask_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_filter_id(&mut self) -> NonEmptyString { + loop { + self.filter_index += 1; + let new_id = format!("filter{}", self.filter_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_image_id(&mut self) -> NonEmptyString { + loop { + self.image_index += 1; + let new_id = format!("image{}", self.image_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + font_lookup!(fontdb_outline, cache_outline, outline, tiny_skia_path::Path); + font_lookup!(fontdb_colr, cache_colr, colr, Tree); + font_lookup!(fontdb_svg, cache_svg, svg, Node); + font_lookup!(fontdb_raster, cache_raster, raster, BitmapImage); +} + +// TODO: is there a simpler way? +fn string_hash(s: &str) -> u64 { + let mut h = std::collections::hash_map::DefaultHasher::new(); + s.hash(&mut h); + h.finish() +} + +impl<'a, 'input: 'a> SvgNode<'a, 'input> { + pub(crate) fn convert_length( + &self, + aid: AId, + object_units: Units, + state: &State, + def: Length, + ) -> f32 { + units::convert_length( + self.attribute(aid).unwrap_or(def), + *self, + aid, + object_units, + state, + ) + } + + pub fn convert_user_length(&self, aid: AId, state: &State, def: Length) -> f32 { + self.convert_length(aid, Units::UserSpaceOnUse, state, def) + } + + pub fn parse_viewbox(&self) -> Option { + let vb: svgtypes::ViewBox = self.attribute(AId::ViewBox)?; + NonZeroRect::from_xywh(vb.x as f32, vb.y as f32, vb.w as f32, vb.h as f32) + } + + pub fn resolve_length(&self, aid: AId, state: &State, def: f32) -> f32 { + debug_assert!( + !matches!(aid, AId::BaselineShift | AId::FontSize), + "{} cannot be resolved via this function", + aid + ); + + if let Some(n) = self.ancestors().find(|n| n.has_attribute(aid)) { + if let Some(length) = n.attribute(aid) { + return units::convert_user_length(length, n, aid, state); + } + } + + def + } + + pub fn resolve_valid_length( + &self, + aid: AId, + state: &State, + def: f32, + ) -> Option { + let n = self.resolve_length(aid, state, def); + NonZeroPositiveF32::new(n) + } + + pub(crate) fn try_convert_length( + &self, + aid: AId, + object_units: Units, + state: &State, + ) -> Option { + Some(units::convert_length( + self.attribute(aid)?, + *self, + aid, + object_units, + state, + )) + } + + pub fn has_valid_transform(&self, aid: AId) -> bool { + // Do not use Node::attribute::, because it will always + // return a valid transform. + + let attr = match self.attribute(aid) { + Some(attr) => attr, + None => return true, + }; + + let ts = match svgtypes::Transform::from_str(attr) { + Ok(v) => v, + Err(_) => return true, + }; + + let ts = Transform::from_row( + ts.a as f32, + ts.b as f32, + ts.c as f32, + ts.d as f32, + ts.e as f32, + ts.f as f32, + ); + ts.is_valid() + } + + pub fn is_visible_element(&self, opt: &crate::Options) -> bool { + self.attribute(AId::Display) != Some("none") + && self.has_valid_transform(AId::Transform) + && super::switch::is_condition_passed(*self, opt) + } +} + +pub trait SvgColorExt { + fn split_alpha(self) -> (Color, Opacity); +} + +impl SvgColorExt for svgtypes::Color { + fn split_alpha(self) -> (Color, Opacity) { + ( + Color::new_rgb(self.red, self.green, self.blue), + Opacity::new_u8(self.alpha), + ) + } +} + +/// Converts an input `Document` into a `Tree`. +/// +/// # Errors +/// +/// - If `Document` doesn't have an SVG node - returns an empty tree. +/// - If `Document` doesn't have a valid size - returns `Error::InvalidSize`. +pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result { + let svg = svg_doc.root_element(); + let (size, restore_viewbox) = resolve_svg_size(&svg, opt); + let size = size?; + let view_box = ViewBox { + rect: svg + .parse_viewbox() + .unwrap_or_else(|| size.to_non_zero_rect(0.0, 0.0)), + aspect: svg.attribute(AId::PreserveAspectRatio).unwrap_or_default(), + }; + + let background_color = svg + .attribute::<&str>(AId::BackgroundColor) + .and_then(|s| svgtypes::Paint::from_str(s).ok()) + .and_then(|paint| match paint { + svgtypes::Paint::Color(c) => Some(c), + _ => None, + }); + + let mut tree = Tree { + size, + root: Group::empty(), + linear_gradients: Vec::new(), + radial_gradients: Vec::new(), + patterns: Vec::new(), + clip_paths: Vec::new(), + masks: Vec::new(), + filters: Vec::new(), + #[cfg(feature = "text")] + fontdb: opt.fontdb.clone(), + }; + + if !svg.is_visible_element(opt) { + return Ok(tree); + } + + let state = State { + parent_clip_path: None, + context_element: None, + parent_markers: Vec::new(), + fe_image_link: false, + view_box: view_box.rect, + use_size: (None, None), + opt, + }; + + let mut cache = Cache::new( + #[cfg(feature = "text")] + opt.fontdb.clone(), + ); + + for node in svg_doc.descendants() { + if let Some(tag) = node.tag_name() { + if matches!( + tag, + EId::ClipPath + | EId::Filter + | EId::LinearGradient + | EId::Mask + | EId::Pattern + | EId::RadialGradient + | EId::Image + ) { + if !node.element_id().is_empty() { + cache.all_ids.insert(string_hash(node.element_id())); + } + } + } + } + + let root_ts = view_box.to_transform(tree.size()); + if root_ts.is_identity() && background_color.is_none() { + convert_children(svg_doc.root(), &state, &mut cache, &mut tree.root); + } else { + let mut g = Group::empty(); + + if let Some(background_color) = background_color { + if let Some(path) = background_path(background_color, view_box.rect.to_rect()) { + g.children.push(Node::Path(Box::new(path))); + } + } + + g.transform = root_ts; + g.abs_transform = root_ts; + convert_children(svg_doc.root(), &state, &mut cache, &mut g); + g.calculate_bounding_boxes(); + tree.root.children.push(Node::Group(Box::new(g))); + } + + // Clear cache to make sure that all `Arc` objects have a single strong reference. + cache.clip_paths.clear(); + cache.masks.clear(); + cache.filters.clear(); + cache.paint.clear(); + + super::paint_server::update_paint_servers( + &mut tree.root, + Transform::default(), + None, + None, + &mut cache, + ); + tree.collect_paint_servers(); + tree.root.collect_clip_paths(&mut tree.clip_paths); + tree.root.collect_masks(&mut tree.masks); + tree.root.collect_filters(&mut tree.filters); + tree.root.calculate_bounding_boxes(); + + // The fontdb might have been mutated and we want to apply these changes to + // the tree's fontdb. + #[cfg(feature = "text")] + { + tree.fontdb = cache.fontdb; + } + + if restore_viewbox { + calculate_svg_bbox(&mut tree); + } + + Ok(tree) +} + +fn background_path(background_color: svgtypes::Color, area: Rect) -> Option { + let path = PathBuilder::from_rect(area); + + let fill = Fill { + paint: Paint::Color(Color::new_rgb( + background_color.red, + background_color.green, + background_color.blue, + )), + opacity: NormalizedF32::new(background_color.alpha as f32 / 255.0)?, + ..Default::default() + }; + + let mut path = Path::new_simple(Arc::new(path))?; + path.fill = Some(fill); + + Some(path) +} + +fn resolve_svg_size(svg: &SvgNode, opt: &Options) -> (Result, bool) { + let mut state = State { + parent_clip_path: None, + context_element: None, + parent_markers: Vec::new(), + fe_image_link: false, + view_box: NonZeroRect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), + use_size: (None, None), + opt, + }; + + let def = Length::new(100.0, Unit::Percent); + let mut width: Length = svg.attribute(AId::Width).unwrap_or(def); + let mut height: Length = svg.attribute(AId::Height).unwrap_or(def); + + let view_box = svg.parse_viewbox(); + + let restore_viewbox = + if (width.unit == Unit::Percent || height.unit == Unit::Percent) && view_box.is_none() { + // Apply the percentages to the fallback size. + if width.unit == Unit::Percent { + width = Length::new( + (width.number / 100.0) * state.opt.default_size.width() as f64, + Unit::None, + ); + } + + if height.unit == Unit::Percent { + height = Length::new( + (height.number / 100.0) * state.opt.default_size.height() as f64, + Unit::None, + ); + } + + true + } else { + false + }; + + let size = if let Some(vbox) = view_box { + state.view_box = vbox; + + let w = if width.unit == Unit::Percent { + vbox.width() * (width.number as f32 / 100.0) + } else { + svg.convert_user_length(AId::Width, &state, def) + }; + + let h = if height.unit == Unit::Percent { + vbox.height() * (height.number as f32 / 100.0) + } else { + svg.convert_user_length(AId::Height, &state, def) + }; + + Size::from_wh(w, h) + } else { + Size::from_wh( + svg.convert_user_length(AId::Width, &state, def), + svg.convert_user_length(AId::Height, &state, def), + ) + }; + + (size.ok_or(Error::InvalidSize), restore_viewbox) +} + +/// Calculates SVG's size and viewBox in case there were not set. +/// +/// Simply iterates over all nodes and calculates a bounding box. +fn calculate_svg_bbox(tree: &mut Tree) { + let bbox = tree.root.abs_bounding_box(); + if let Some(size) = Size::from_wh(bbox.right(), bbox.bottom()) { + tree.size = size; + } +} + +#[inline(never)] +pub(crate) fn convert_children( + parent_node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + for node in parent_node.children() { + convert_element(node, state, cache, parent); + } +} + +#[inline(never)] +pub(crate) fn convert_element(node: SvgNode, state: &State, cache: &mut Cache, parent: &mut Group) { + let tag_name = match node.tag_name() { + Some(v) => v, + None => return, + }; + + if !tag_name.is_graphic() && !matches!(tag_name, EId::G | EId::Switch | EId::Svg) { + return; + } + + if !node.is_visible_element(state.opt) { + return; + } + + if tag_name == EId::Use { + super::use_node::convert(node, state, cache, parent); + return; + } + + if tag_name == EId::Switch { + super::switch::convert(node, state, cache, parent); + return; + } + + if let Some(g) = convert_group(node, state, false, cache, parent, &|cache, g| { + convert_element_impl(tag_name, node, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); + } +} + +#[inline(never)] +fn convert_element_impl( + tag_name: EId, + node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + match tag_name { + EId::Rect + | EId::Circle + | EId::Ellipse + | EId::Line + | EId::Polyline + | EId::Polygon + | EId::Path => { + if let Some(path) = super::shapes::convert(node, state) { + convert_path(node, path, state, cache, parent); + } + } + EId::Image => { + super::image::convert(node, state, cache, parent); + } + EId::Text => { + #[cfg(feature = "text")] + { + super::text::convert(node, state, cache, parent); + } + } + EId::Svg => { + if node.parent_element().is_some() { + super::use_node::convert_svg(node, state, cache, parent); + } else { + // Skip root `svg`. + convert_children(node, state, cache, parent); + } + } + EId::G => { + convert_children(node, state, cache, parent); + } + _ => {} + } +} + +// `clipPath` can have only shape and `text` children. +// +// `line` doesn't impact rendering because stroke is always disabled +// for `clipPath` children. +#[inline(never)] +pub(crate) fn convert_clip_path_elements( + clip_node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + for node in clip_node.children() { + let tag_name = match node.tag_name() { + Some(v) => v, + None => continue, + }; + + if !tag_name.is_graphic() { + continue; + } + + if !node.is_visible_element(state.opt) { + continue; + } + + if tag_name == EId::Use { + super::use_node::convert(node, state, cache, parent); + continue; + } + + if let Some(g) = convert_group(node, state, false, cache, parent, &|cache, g| { + convert_clip_path_elements_impl(tag_name, node, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); + } + } +} + +#[inline(never)] +fn convert_clip_path_elements_impl( + tag_name: EId, + node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + match tag_name { + EId::Rect | EId::Circle | EId::Ellipse | EId::Polyline | EId::Polygon | EId::Path => { + if let Some(path) = super::shapes::convert(node, state) { + convert_path(node, path, state, cache, parent); + } + } + EId::Text => { + #[cfg(feature = "text")] + { + super::text::convert(node, state, cache, parent); + } + } + _ => { + log::warn!("'{}' is no a valid 'clip-path' child.", tag_name); + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum Isolation { + Auto, + Isolate, +} + +impl Default for Isolation { + fn default() -> Self { + Self::Auto + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Isolation { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "auto" => Some(Isolation::Auto), + "isolate" => Some(Isolation::Isolate), + _ => None, + } + } +} + +// TODO: explain +pub(crate) fn convert_group( + node: SvgNode, + state: &State, + force: bool, + cache: &mut Cache, + parent: &mut Group, + collect_children: &dyn Fn(&mut Cache, &mut Group), +) -> Option { + // A `clipPath` child cannot have an opacity. + let opacity = if state.parent_clip_path.is_none() { + node.attribute::(AId::Opacity) + .unwrap_or(Opacity::ONE) + } else { + Opacity::ONE + }; + + let transform = node.resolve_transform(AId::Transform, state); + let blend_mode: BlendMode = node.attribute(AId::MixBlendMode).unwrap_or_default(); + let isolation: Isolation = node.attribute(AId::Isolation).unwrap_or_default(); + let isolate = isolation == Isolation::Isolate; + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let is_g_or_use = matches!(node.tag_name(), Some(EId::G) | Some(EId::Use)); + let id = if is_g_or_use && state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + + let abs_transform = parent.abs_transform.pre_concat(transform); + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + let mut g = Group { + id, + transform, + abs_transform, + opacity, + blend_mode, + isolate, + clip_path: None, + mask: None, + filters: Vec::new(), + is_context_element: false, + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + abs_layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + children: Vec::new(), + }; + collect_children(cache, &mut g); + + // We need to know group's bounding box before converting + // clipPaths, masks and filters. + let object_bbox = g.calculate_object_bbox(); + + // `mask` and `filter` cannot be set on `clipPath` children. + // But `clip-path` can. + + let mut clip_path = None; + if let Some(link) = node.attribute::(AId::ClipPath) { + clip_path = super::clippath::convert(link, state, object_bbox, cache); + if clip_path.is_none() { + return None; + } + } + + let mut mask = None; + if state.parent_clip_path.is_none() { + if let Some(link) = node.attribute::(AId::Mask) { + mask = super::mask::convert(link, state, object_bbox, cache); + if mask.is_none() { + return None; + } + } + } + + let filters = { + let mut filters = Vec::new(); + if state.parent_clip_path.is_none() { + if node.attribute(AId::Filter) == Some("none") { + // Do nothing. + } else if node.has_attribute(AId::Filter) { + if let Ok(f) = super::filter::convert(node, state, object_bbox, cache) { + filters = f; + } else { + // A filter that not a link or a filter with a link to a non existing element. + // + // Unlike `clip-path` and `mask`, when a `filter` link is invalid + // then the whole element should be ignored. + // + // This is kinda an undefined behaviour. + // In most cases, Chrome, Firefox and rsvg will ignore such elements, + // but in some cases Chrome allows it. Not sure why. + // Inkscape (0.92) simply ignores such attributes, rendering element as is. + // Batik (1.12) crashes. + // + // Test file: e-filter-051.svg + return None; + } + } + } + + filters + }; + + let required = opacity.get().approx_ne_ulps(&1.0, 4) + || clip_path.is_some() + || mask.is_some() + || !filters.is_empty() + || !transform.is_identity() + || blend_mode != BlendMode::Normal + || isolate + || is_g_or_use + || force; + + if !required { + parent.children.append(&mut g.children); + return None; + } + + g.clip_path = clip_path; + g.mask = mask; + g.filters = filters; + + // Must be called after we set Group::filters + g.calculate_bounding_boxes(); + + Some(g) +} + +fn convert_path( + node: SvgNode, + tiny_skia_path: Arc, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + debug_assert!(tiny_skia_path.len() >= 2); + if tiny_skia_path.len() < 2 { + return; + } + + let has_bbox = tiny_skia_path.bounds().width() > 0.0 && tiny_skia_path.bounds().height() > 0.0; + let mut fill = super::style::resolve_fill(node, has_bbox, state, cache); + let mut stroke = super::style::resolve_stroke(node, has_bbox, state, cache); + let visibility: Visibility = node.find_attribute(AId::Visibility).unwrap_or_default(); + let mut visible = visibility == Visibility::Visible; + let rendering_mode: ShapeRendering = node + .find_attribute(AId::ShapeRendering) + .unwrap_or(state.opt.shape_rendering); + + // TODO: handle `markers` before `stroke` + let raw_paint_order: svgtypes::PaintOrder = + node.find_attribute(AId::PaintOrder).unwrap_or_default(); + let paint_order = svg_paint_order_to_usvg(raw_paint_order); + let path_transform = parent.abs_transform; + + // If a path doesn't have a fill or a stroke then it's invisible. + // By setting `visibility` to `hidden` we are disabling rendering of this path. + if fill.is_none() && stroke.is_none() { + visible = false; + } + + if let Some(fill) = fill.as_mut() { + if let Some(ContextElement::PathNode(context_transform, context_bbox)) = + fill.context_element + { + process_paint( + &mut fill.paint, + true, + context_transform, + context_bbox.map(|r| r.to_rect()), + path_transform, + tiny_skia_path.bounds(), + cache, + ); + fill.context_element = None; + } + } + + if let Some(stroke) = stroke.as_mut() { + if let Some(ContextElement::PathNode(context_transform, context_bbox)) = + stroke.context_element + { + process_paint( + &mut stroke.paint, + true, + context_transform, + context_bbox.map(|r| r.to_rect()), + path_transform, + tiny_skia_path.bounds(), + cache, + ); + stroke.context_element = None; + } + } + + let mut marker = None; + if marker::is_valid(node) && visibility == Visibility::Visible { + let mut marker_group = Group { + abs_transform: parent.abs_transform, + ..Group::empty() + }; + + let mut marker_state = state.clone(); + + let bbox = tiny_skia_path + .compute_tight_bounds() + .and_then(|r| r.to_non_zero_rect()); + + let fill = fill.clone().map(|mut f| { + f.context_element = Some(ContextElement::PathNode(path_transform, bbox)); + f + }); + + let stroke = stroke.clone().map(|mut s| { + s.context_element = Some(ContextElement::PathNode(path_transform, bbox)); + s + }); + + marker_state.context_element = Some((fill, stroke)); + + marker::convert( + node, + &tiny_skia_path, + &marker_state, + cache, + &mut marker_group, + ); + marker_group.calculate_bounding_boxes(); + marker = Some(marker_group); + } + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let id = if state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + + let path = Path::new( + id, + visible, + fill, + stroke, + paint_order, + rendering_mode, + tiny_skia_path, + path_transform, + ); + + let path = match path { + Some(v) => v, + None => return, + }; + + match (raw_paint_order.order, marker) { + ([PaintOrderKind::Markers, _, _], Some(markers_node)) => { + parent.children.push(Node::Group(Box::new(markers_node))); + parent.children.push(Node::Path(Box::new(path.clone()))); + } + ([first, PaintOrderKind::Markers, last], Some(markers_node)) => { + append_single_paint_path(first, &path, parent); + parent.children.push(Node::Group(Box::new(markers_node))); + append_single_paint_path(last, &path, parent); + } + ([_, _, PaintOrderKind::Markers], Some(markers_node)) => { + parent.children.push(Node::Path(Box::new(path.clone()))); + parent.children.push(Node::Group(Box::new(markers_node))); + } + _ => parent.children.push(Node::Path(Box::new(path.clone()))), + } +} + +fn append_single_paint_path(paint_order_kind: PaintOrderKind, path: &Path, parent: &mut Group) { + match paint_order_kind { + PaintOrderKind::Fill => { + if path.fill.is_some() { + let mut fill_path = path.clone(); + fill_path.stroke = None; + fill_path.id = String::new(); + parent.children.push(Node::Path(Box::new(fill_path))); + } + } + PaintOrderKind::Stroke => { + if path.stroke.is_some() { + let mut stroke_path = path.clone(); + stroke_path.fill = None; + stroke_path.id = String::new(); + parent.children.push(Node::Path(Box::new(stroke_path))); + } + } + _ => {} + } +} + +pub fn svg_paint_order_to_usvg(order: svgtypes::PaintOrder) -> PaintOrder { + match (order.order[0], order.order[1]) { + (svgtypes::PaintOrderKind::Stroke, _) => PaintOrder::StrokeAndFill, + (svgtypes::PaintOrderKind::Markers, svgtypes::PaintOrderKind::Stroke) => { + PaintOrder::StrokeAndFill + } + _ => PaintOrder::FillAndStroke, + } +} + +impl SvgNode<'_, '_> { + pub(crate) fn resolve_transform(&self, transform_aid: AId, state: &State) -> Transform { + let mut transform: Transform = self.attribute(transform_aid).unwrap_or_default(); + let transform_origin: Option = self.attribute(AId::TransformOrigin); + + if let Some(transform_origin) = transform_origin { + let dx = convert_length( + transform_origin.x_offset, + *self, + AId::Width, + Units::UserSpaceOnUse, + state, + ); + let dy = convert_length( + transform_origin.y_offset, + *self, + AId::Height, + Units::UserSpaceOnUse, + state, + ); + transform = Transform::default() + .pre_translate(dx, dy) + .pre_concat(transform) + .pre_translate(-dx, -dy); + } + + transform + } +} diff --git a/third_party/usvg/src/parser/filter.rs b/third_party/usvg/src/parser/filter.rs new file mode 100644 index 0000000000..6cf48aa680 --- /dev/null +++ b/third_party/usvg/src/parser/filter.rs @@ -0,0 +1,1279 @@ +// Copyright 2022 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A collection of SVG filters. + +use std::collections::HashSet; +use std::str::FromStr; +use std::sync::Arc; + +use strict_num::PositiveF32; +use svgtypes::{AspectRatio, Length, LengthUnit as Unit}; + +use crate::{ + filter::{self, *}, + ApproxZeroUlps, Color, Group, Node, NonEmptyString, NonZeroF32, NonZeroRect, Opacity, Size, + Units, +}; + +use super::converter::{self, SvgColorExt}; +use super::paint_server::{convert_units, resolve_number}; +use super::svgtree::{AId, EId, FromValue, SvgNode}; +use super::OptionLog; + +impl<'a, 'input: 'a> FromValue<'a, 'input> for filter::ColorInterpolation { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "sRGB" => Some(filter::ColorInterpolation::SRGB), + "linearRGB" => Some(filter::ColorInterpolation::LinearRGB), + _ => None, + } + } +} + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + object_bbox: Option, + cache: &mut converter::Cache, +) -> Result>, ()> { + let value = match node.attribute::<&str>(AId::Filter) { + Some(v) => v, + None => return Ok(Vec::new()), + }; + + let mut has_invalid_urls = false; + let mut filters = Vec::new(); + + let create_base_filter_func = + |kind, filters: &mut Vec>, cache: &mut converter::Cache| { + // Filter functions, unlike `filter` elements, do not have a filter region. + // We're currently do not support an unlimited region, so we simply use a fairly large one. + // This if far from ideal, but good for now. + // TODO: Should be fixed eventually. + let mut rect = match kind { + Kind::DropShadow(_) | Kind::GaussianBlur(_) => { + NonZeroRect::from_xywh(-0.5, -0.5, 2.0, 2.0).unwrap() + } + _ => NonZeroRect::from_xywh(-0.1, -0.1, 1.2, 1.2).unwrap(), + }; + + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!( + "Filter '{}' has an invalid region. Skipped.", + node.element_id() + ); + return; + } + }; + + rect = rect.bbox_transform(object_bbox); + + filters.push(Arc::new(Filter { + id: cache.gen_filter_id(), + rect, + primitives: vec![Primitive { + rect, + // Unlike `filter` elements, filter functions use sRGB colors by default. + color_interpolation: ColorInterpolation::SRGB, + result: "result".to_string(), + kind, + }], + })); + }; + + for func in svgtypes::FilterValueListParser::from(value) { + let func = match func { + Ok(v) => v, + Err(e) => { + // Skip the whole attribute list on error. + log::warn!("Failed to parse a filter value cause {}. Skipping.", e); + return Ok(Vec::new()); + } + }; + + match func { + svgtypes::FilterValue::Blur(std_dev) => create_base_filter_func( + convert_blur_function(node, std_dev, state), + &mut filters, + cache, + ), + svgtypes::FilterValue::DropShadow { + color, + dx, + dy, + std_dev, + } => create_base_filter_func( + convert_drop_shadow_function(node, color, dx, dy, std_dev, state), + &mut filters, + cache, + ), + svgtypes::FilterValue::Brightness(amount) => { + create_base_filter_func(convert_brightness_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Contrast(amount) => { + create_base_filter_func(convert_contrast_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Grayscale(amount) => { + create_base_filter_func(convert_grayscale_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::HueRotate(angle) => { + create_base_filter_func(convert_hue_rotate_function(angle), &mut filters, cache); + } + svgtypes::FilterValue::Invert(amount) => { + create_base_filter_func(convert_invert_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Opacity(amount) => { + create_base_filter_func(convert_opacity_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Sepia(amount) => { + create_base_filter_func(convert_sepia_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Saturate(amount) => { + create_base_filter_func(convert_saturate_function(amount), &mut filters, cache); + } + svgtypes::FilterValue::Url(url) => { + if let Some(link) = node.document().element_by_id(url) { + if let Ok(res) = convert_url(link, state, object_bbox, cache) { + if let Some(f) = res { + filters.push(f); + } + } else { + has_invalid_urls = true; + } + } else { + has_invalid_urls = true; + } + } + } + } + + // If a `filter` attribute had urls pointing to a missing elements + // and there are no valid filters at all - this is an error. + // + // Note that an invalid url is not an error in general. + if filters.is_empty() && has_invalid_urls { + return Err(()); + } + + Ok(filters) +} + +fn convert_url( + node: SvgNode, + state: &converter::State, + object_bbox: Option, + cache: &mut converter::Cache, +) -> Result>, ()> { + let units = convert_units(node, AId::FilterUnits, Units::ObjectBoundingBox); + let primitive_units = convert_units(node, AId::PrimitiveUnits, Units::UserSpaceOnUse); + + // Check if this element was already converted. + // + // Only `userSpaceOnUse` clipPaths can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse && primitive_units == Units::UserSpaceOnUse; + if cacheable { + if let Some(filter) = cache.filters.get(node.element_id()) { + return Ok(Some(filter.clone())); + } + } + + let rect = NonZeroRect::from_xywh( + resolve_number( + node, + AId::X, + units, + state, + Length::new(-10.0, Unit::Percent), + ), + resolve_number( + node, + AId::Y, + units, + state, + Length::new(-10.0, Unit::Percent), + ), + resolve_number( + node, + AId::Width, + units, + state, + Length::new(120.0, Unit::Percent), + ), + resolve_number( + node, + AId::Height, + units, + state, + Length::new(120.0, Unit::Percent), + ), + ); + + let mut rect = rect + .log_none(|| { + log::warn!( + "Filter '{}' has an invalid region. Skipped.", + node.element_id() + ); + }) + .ok_or(())?; + + if units == Units::ObjectBoundingBox { + if let Some(object_bbox) = object_bbox { + rect = rect.bbox_transform(object_bbox); + } else { + log::warn!("Filters on zero-sized shapes are not allowed."); + return Err(()); + } + } + + let node_with_primitives = match find_filter_with_primitives(node) { + Some(v) => v, + None => return Err(()), + }; + let primitives = collect_children( + &node_with_primitives, + primitive_units, + state, + object_bbox, + rect, + cache, + ); + if primitives.is_empty() { + return Err(()); + } + + let mut id = NonEmptyString::new(node.element_id().to_string()).ok_or(())?; + // Generate ID only when we're parsing `objectBoundingBox` filter for the second time. + if !cacheable && cache.filters.contains_key(id.get()) { + id = cache.gen_filter_id(); + } + let id_copy = id.get().to_string(); + + let filter = Arc::new(Filter { + id, + rect, + primitives, + }); + + cache.filters.insert(id_copy, filter.clone()); + + Ok(Some(filter)) +} + +fn find_filter_with_primitives<'a>(node: SvgNode<'a, 'a>) -> Option> { + for link in node.href_iter() { + if link.tag_name() != Some(EId::Filter) { + log::warn!( + "Filter '{}' cannot reference '{}' via 'xlink:href'.", + node.element_id(), + link.tag_name().unwrap() + ); + return None; + } + + if link.has_children() { + return Some(link); + } + } + + None +} + +struct FilterResults { + names: HashSet, + idx: usize, +} + +fn collect_children( + filter: &SvgNode, + units: Units, + state: &converter::State, + object_bbox: Option, + filter_region: NonZeroRect, + cache: &mut converter::Cache, +) -> Vec { + let mut primitives = Vec::new(); + + let mut results = FilterResults { + names: HashSet::new(), + idx: 1, + }; + + let scale = if units == Units::ObjectBoundingBox { + if let Some(object_bbox) = object_bbox { + object_bbox.size() + } else { + // No need to warn. Already checked. + return Vec::new(); + } + } else { + Size::from_wh(1.0, 1.0).unwrap() + }; + + for child in filter.children() { + let tag_name = match child.tag_name() { + Some(v) => v, + None => continue, + }; + + let filter_subregion = match resolve_primitive_region( + child, + tag_name, + units, + state, + object_bbox, + filter_region, + ) { + Some(v) => v, + None => break, + }; + + let kind = + match tag_name { + EId::FeDropShadow => convert_drop_shadow(child, scale, &primitives), + EId::FeGaussianBlur => convert_gaussian_blur(child, scale, &primitives), + EId::FeOffset => convert_offset(child, scale, &primitives), + EId::FeBlend => convert_blend(child, &primitives), + EId::FeFlood => convert_flood(child), + EId::FeComposite => convert_composite(child, &primitives), + EId::FeMerge => convert_merge(child, &primitives), + EId::FeTile => convert_tile(child, &primitives), + EId::FeImage => convert_image(child, filter_subregion, state, cache), + EId::FeComponentTransfer => convert_component_transfer(child, &primitives), + EId::FeColorMatrix => convert_color_matrix(child, &primitives), + EId::FeConvolveMatrix => convert_convolve_matrix(child, &primitives) + .unwrap_or_else(create_dummy_primitive), + EId::FeMorphology => convert_morphology(child, scale, &primitives), + EId::FeDisplacementMap => convert_displacement_map(child, scale, &primitives), + EId::FeTurbulence => convert_turbulence(child), + EId::FeDiffuseLighting => convert_diffuse_lighting(child, &primitives) + .unwrap_or_else(create_dummy_primitive), + EId::FeSpecularLighting => convert_specular_lighting(child, &primitives) + .unwrap_or_else(create_dummy_primitive), + tag_name => { + log::warn!("'{}' is not a valid filter primitive. Skipped.", tag_name); + continue; + } + }; + + let color_interpolation = child + .find_attribute(AId::ColorInterpolationFilters) + .unwrap_or_default(); + + primitives.push(Primitive { + rect: filter_subregion, + color_interpolation, + result: gen_result(child, &mut results), + kind, + }); + } + + // TODO: remove primitives which results are not used + + primitives +} + +// TODO: rewrite/simplify/explain/whatever +fn resolve_primitive_region( + fe: SvgNode, + kind: EId, + units: Units, + state: &converter::State, + bbox: Option, + filter_region: NonZeroRect, +) -> Option { + let x = fe.try_convert_length(AId::X, units, state); + let y = fe.try_convert_length(AId::Y, units, state); + let width = fe.try_convert_length(AId::Width, units, state); + let height = fe.try_convert_length(AId::Height, units, state); + + let region = match kind { + EId::FeFlood | EId::FeImage => { + // `feImage` uses the object bbox. + if units == Units::ObjectBoundingBox { + let bbox = bbox?; + + // TODO: wrong + // let ts_bbox = tiny_skia::Rect::new(ts.e, ts.f, ts.a, ts.d).unwrap(); + + let r = NonZeroRect::from_xywh( + x.unwrap_or(0.0), + y.unwrap_or(0.0), + width.unwrap_or(1.0), + height.unwrap_or(1.0), + )?; + + return Some(r.bbox_transform(bbox)); + } else { + filter_region + } + } + _ => filter_region, + }; + + // TODO: Wrong! Does not account rotate and skew. + if units == Units::ObjectBoundingBox { + let subregion_bbox = NonZeroRect::from_xywh( + x.unwrap_or(0.0), + y.unwrap_or(0.0), + width.unwrap_or(1.0), + height.unwrap_or(1.0), + )?; + + Some(region.bbox_transform(subregion_bbox)) + } else { + NonZeroRect::from_xywh( + x.unwrap_or(region.x()), + y.unwrap_or(region.y()), + width.unwrap_or(region.width()), + height.unwrap_or(region.height()), + ) + } +} + +// A malformed filter primitive usually should produce a transparent image. +// But since `FilterKind` structs are designed to always be valid, +// we are using `FeFlood` as fallback. +#[inline(never)] +pub(crate) fn create_dummy_primitive() -> Kind { + Kind::Flood(Flood { + color: Color::black(), + opacity: Opacity::ZERO, + }) +} + +#[inline(never)] +fn resolve_input(node: SvgNode, aid: AId, primitives: &[Primitive]) -> Input { + match node.attribute(aid) { + Some(s) => { + let input = parse_in(s); + + // If `in` references an unknown `result` than fallback + // to previous result or `SourceGraphic`. + if let Input::Reference(ref name) = input { + if !primitives.iter().any(|p| p.result == *name) { + return if let Some(prev) = primitives.last() { + Input::Reference(prev.result.clone()) + } else { + Input::SourceGraphic + }; + } + } + + input + } + None => { + if let Some(prev) = primitives.last() { + // If `in` is not set and this is not the first primitive + // than the input is a result of the previous primitive. + Input::Reference(prev.result.clone()) + } else { + // If `in` is not set and this is the first primitive + // than the input is `SourceGraphic`. + Input::SourceGraphic + } + } + } +} + +fn parse_in(s: &str) -> Input { + match s { + "SourceGraphic" => Input::SourceGraphic, + "SourceAlpha" => Input::SourceAlpha, + "BackgroundImage" | "BackgroundAlpha" | "FillPaint" | "StrokePaint" => { + log::warn!("{} filter input isn't supported and not planed.", s); + Input::SourceGraphic + } + _ => Input::Reference(s.to_string()), + } +} + +fn gen_result(node: SvgNode, results: &mut FilterResults) -> String { + match node.attribute::<&str>(AId::Result) { + Some(s) => { + // Remember predefined result. + results.names.insert(s.to_string()); + results.idx += 1; + + s.to_string() + } + None => { + // Generate an unique name for `result`. + loop { + let name = format!("result{}", results.idx); + results.idx += 1; + + if !results.names.contains(&name) { + return name; + } + } + } + } +} + +fn convert_blend(fe: SvgNode, primitives: &[Primitive]) -> Kind { + let mode = fe.attribute(AId::Mode).unwrap_or_default(); + let input1 = resolve_input(fe, AId::In, primitives); + let input2 = resolve_input(fe, AId::In2, primitives); + Kind::Blend(Blend { + mode, + input1, + input2, + }) +} + +fn convert_color_matrix(fe: SvgNode, primitives: &[Primitive]) -> Kind { + let kind = convert_color_matrix_kind(fe).unwrap_or_default(); + Kind::ColorMatrix(ColorMatrix { + input: resolve_input(fe, AId::In, primitives), + kind, + }) +} + +fn convert_color_matrix_kind(fe: SvgNode) -> Option { + match fe.attribute(AId::Type) { + Some("saturate") => { + if let Some(list) = fe.attribute::>(AId::Values) { + if !list.is_empty() { + let n = crate::f32_bound(0.0, list[0], 1.0); + return Some(ColorMatrixKind::Saturate(PositiveF32::new(n).unwrap())); + } else { + return Some(ColorMatrixKind::Saturate(PositiveF32::new(1.0).unwrap())); + } + } + } + Some("hueRotate") => { + if let Some(list) = fe.attribute::>(AId::Values) { + if !list.is_empty() { + return Some(ColorMatrixKind::HueRotate(list[0])); + } else { + return Some(ColorMatrixKind::HueRotate(0.0)); + } + } + } + Some("luminanceToAlpha") => { + return Some(ColorMatrixKind::LuminanceToAlpha); + } + _ => { + // Fallback to `matrix`. + if let Some(list) = fe.attribute::>(AId::Values) { + if list.len() == 20 { + return Some(ColorMatrixKind::Matrix(list)); + } + } + } + } + + None +} + +fn convert_component_transfer(fe: SvgNode, primitives: &[Primitive]) -> Kind { + let mut kind = ComponentTransfer { + input: resolve_input(fe, AId::In, primitives), + func_r: TransferFunction::Identity, + func_g: TransferFunction::Identity, + func_b: TransferFunction::Identity, + func_a: TransferFunction::Identity, + }; + + for child in fe.children().filter(|n| n.is_element()) { + if let Some(func) = convert_transfer_function(child) { + match child.tag_name().unwrap() { + EId::FeFuncR => kind.func_r = func, + EId::FeFuncG => kind.func_g = func, + EId::FeFuncB => kind.func_b = func, + EId::FeFuncA => kind.func_a = func, + _ => {} + } + } + } + + Kind::ComponentTransfer(kind) +} + +fn convert_transfer_function(node: SvgNode) -> Option { + match node.attribute(AId::Type)? { + "identity" => Some(TransferFunction::Identity), + "table" => match node.attribute::>(AId::TableValues) { + Some(values) => Some(TransferFunction::Table(values)), + None => Some(TransferFunction::Table(Vec::new())), + }, + "discrete" => match node.attribute::>(AId::TableValues) { + Some(values) => Some(TransferFunction::Discrete(values)), + None => Some(TransferFunction::Discrete(Vec::new())), + }, + "linear" => Some(TransferFunction::Linear { + slope: node.attribute(AId::Slope).unwrap_or(1.0), + intercept: node.attribute(AId::Intercept).unwrap_or(0.0), + }), + "gamma" => Some(TransferFunction::Gamma { + amplitude: node.attribute(AId::Amplitude).unwrap_or(1.0), + exponent: node.attribute(AId::Exponent).unwrap_or(1.0), + offset: node.attribute(AId::Offset).unwrap_or(0.0), + }), + _ => None, + } +} + +fn convert_composite(fe: SvgNode, primitives: &[Primitive]) -> Kind { + let operator = match fe.attribute(AId::Operator).unwrap_or("over") { + "in" => CompositeOperator::In, + "out" => CompositeOperator::Out, + "atop" => CompositeOperator::Atop, + "xor" => CompositeOperator::Xor, + "arithmetic" => CompositeOperator::Arithmetic { + k1: fe.attribute(AId::K1).unwrap_or(0.0), + k2: fe.attribute(AId::K2).unwrap_or(0.0), + k3: fe.attribute(AId::K3).unwrap_or(0.0), + k4: fe.attribute(AId::K4).unwrap_or(0.0), + }, + _ => CompositeOperator::Over, + }; + + let input1 = resolve_input(fe, AId::In, primitives); + let input2 = resolve_input(fe, AId::In2, primitives); + + Kind::Composite(Composite { + operator, + input1, + input2, + }) +} + +fn convert_convolve_matrix(fe: SvgNode, primitives: &[Primitive]) -> Option { + fn parse_target(target: Option, order: u32) -> Option { + let default_target = (order as f32 / 2.0).floor() as u32; + let target = target.unwrap_or(default_target as f32) as i32; + if target < 0 || target >= order as i32 { + None + } else { + Some(target as u32) + } + } + + let mut order_x = 3; + let mut order_y = 3; + if let Some(value) = fe.attribute::<&str>(AId::Order) { + let mut s = svgtypes::NumberListParser::from(value); + let x = s.next().and_then(|a| a.ok()).map(|n| n as i32).unwrap_or(3); + let y = s.next().and_then(|a| a.ok()).map(|n| n as i32).unwrap_or(x); + if x > 0 && y > 0 { + order_x = x as u32; + order_y = y as u32; + } + } + + let mut matrix = Vec::new(); + if let Some(list) = fe.attribute::>(AId::KernelMatrix) { + if list.len() == (order_x * order_y) as usize { + matrix = list; + } + } + + let mut kernel_sum: f32 = matrix.iter().sum(); + // Round up to prevent float precision issues. + kernel_sum = (kernel_sum * 1_000_000.0).round() / 1_000_000.0; + if kernel_sum.approx_zero_ulps(4) { + kernel_sum = 1.0; + } + + let divisor = fe.attribute(AId::Divisor).unwrap_or(kernel_sum); + if divisor.approx_zero_ulps(4) { + return None; + } + + let bias = fe.attribute(AId::Bias).unwrap_or(0.0); + + let target_x = parse_target(fe.attribute(AId::TargetX), order_x)?; + let target_y = parse_target(fe.attribute(AId::TargetY), order_y)?; + + let kernel_matrix = ConvolveMatrixData::new(target_x, target_y, order_x, order_y, matrix)?; + + let edge_mode = match fe.attribute(AId::EdgeMode).unwrap_or("duplicate") { + "none" => EdgeMode::None, + "wrap" => EdgeMode::Wrap, + _ => EdgeMode::Duplicate, + }; + + let preserve_alpha = fe.attribute(AId::PreserveAlpha).unwrap_or("false") == "true"; + + Some(Kind::ConvolveMatrix(ConvolveMatrix { + input: resolve_input(fe, AId::In, primitives), + matrix: kernel_matrix, + divisor: NonZeroF32::new(divisor).unwrap(), + bias, + edge_mode, + preserve_alpha, + })) +} + +fn convert_displacement_map(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let parse_channel = |aid| match fe.attribute(aid).unwrap_or("A") { + "R" => ColorChannel::R, + "G" => ColorChannel::G, + "B" => ColorChannel::B, + _ => ColorChannel::A, + }; + + // TODO: should probably split scale to scale_x and scale_y, + // but resvg doesn't support displacement map anyway... + let scale = (scale.width() + scale.height()) / 2.0; + + Kind::DisplacementMap(DisplacementMap { + input1: resolve_input(fe, AId::In, primitives), + input2: resolve_input(fe, AId::In2, primitives), + scale: fe.attribute(AId::Scale).unwrap_or(0.0) * scale, + x_channel_selector: parse_channel(AId::XChannelSelector), + y_channel_selector: parse_channel(AId::YChannelSelector), + }) +} + +fn convert_drop_shadow(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, scale, "2 2"); + + let (color, opacity) = fe + .attribute(AId::FloodColor) + .unwrap_or_else(svgtypes::Color::black) + .split_alpha(); + + let flood_opacity = fe + .attribute::(AId::FloodOpacity) + .unwrap_or(Opacity::ONE); + + Kind::DropShadow(DropShadow { + input: resolve_input(fe, AId::In, primitives), + dx: fe.attribute(AId::Dx).unwrap_or(2.0) * scale.width(), + dy: fe.attribute(AId::Dy).unwrap_or(2.0) * scale.height(), + std_dev_x, + std_dev_y, + color, + opacity: opacity * flood_opacity, + }) +} + +fn convert_flood(fe: SvgNode) -> Kind { + let (color, opacity) = fe + .attribute(AId::FloodColor) + .unwrap_or_else(svgtypes::Color::black) + .split_alpha(); + + let flood_opacity = fe + .attribute::(AId::FloodOpacity) + .unwrap_or(Opacity::ONE); + + Kind::Flood(Flood { + color, + opacity: opacity * flood_opacity, + }) +} + +fn convert_gaussian_blur(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, scale, "0 0"); + Kind::GaussianBlur(GaussianBlur { + input: resolve_input(fe, AId::In, primitives), + std_dev_x, + std_dev_y, + }) +} + +fn convert_std_dev_attr(fe: SvgNode, scale: Size, default: &str) -> (PositiveF32, PositiveF32) { + let text = fe.attribute(AId::StdDeviation).unwrap_or(default); + let mut parser = svgtypes::NumberListParser::from(text); + + let n1 = parser.next().and_then(|n| n.ok()); + let n2 = parser.next().and_then(|n| n.ok()); + // `stdDeviation` must have no more than two values. + // Otherwise we should fallback to `0 0`. + let n3 = parser.next().and_then(|n| n.ok()); + + let (std_dev_x, std_dev_y) = match (n1, n2, n3) { + (Some(n1), Some(n2), None) => (n1, n2), + (Some(n1), None, None) => (n1, n1), + _ => (0.0, 0.0), + }; + + let std_dev_x = (std_dev_x as f32) * scale.width(); + let std_dev_y = (std_dev_y as f32) * scale.height(); + + let std_dev_x = PositiveF32::new(std_dev_x).unwrap_or(PositiveF32::ZERO); + let std_dev_y = PositiveF32::new(std_dev_y).unwrap_or(PositiveF32::ZERO); + + (std_dev_x, std_dev_y) +} + +fn convert_image( + fe: SvgNode, + filter_subregion: NonZeroRect, + state: &converter::State, + cache: &mut converter::Cache, +) -> Kind { + match convert_image_inner(fe, filter_subregion, state, cache) { + Some(kind) => kind, + None => create_dummy_primitive(), + } +} + +fn convert_image_inner( + fe: SvgNode, + filter_subregion: NonZeroRect, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + let rendering_mode = fe + .find_attribute(AId::ImageRendering) + .unwrap_or(state.opt.image_rendering); + + if let Some(node) = fe.try_attribute::(AId::Href) { + let mut state = state.clone(); + state.fe_image_link = true; + let mut root = Group::empty(); + super::converter::convert_element(node, &state, cache, &mut root); + return if root.has_children() { + root.calculate_bounding_boxes(); + // Transfer node id from group's child to the group itself if needed. + if let Some(Node::Group(ref mut g)) = root.children.first_mut() { + if let Some(child2) = g.children.first_mut() { + g.id = child2.id().to_string(); + match child2 { + Node::Group(ref mut g2) => g2.id.clear(), + Node::Path(ref mut path) => path.id.clear(), + Node::Image(ref mut image) => image.id.clear(), + Node::Text(ref mut text) => text.id.clear(), + } + } + } + + Some(Kind::Image(Image { root })) + } else { + None + }; + } + + let href = fe.try_attribute(AId::Href).log_none(|| { + log::warn!("The 'feImage' element lacks the 'xlink:href' attribute. Skipped."); + })?; + let img_data = super::image::get_href_data(href, state)?; + let actual_size = img_data.actual_size()?; + + let aspect: AspectRatio = fe.attribute(AId::PreserveAspectRatio).unwrap_or_default(); + + let mut root = Group::empty(); + super::image::convert_inner( + img_data, + cache.gen_image_id().take(), + true, + rendering_mode, + aspect, + actual_size, + filter_subregion.translate_to(0.0, 0.0)?, + cache, + &mut root, + ); + root.calculate_bounding_boxes(); + + Some(Kind::Image(Image { root })) +} + +fn convert_diffuse_lighting(fe: SvgNode, primitives: &[Primitive]) -> Option { + let light_source = convert_light_source(fe)?; + Some(Kind::DiffuseLighting(DiffuseLighting { + input: resolve_input(fe, AId::In, primitives), + surface_scale: fe.attribute(AId::SurfaceScale).unwrap_or(1.0), + diffuse_constant: fe.attribute(AId::DiffuseConstant).unwrap_or(1.0), + lighting_color: convert_lighting_color(fe), + light_source, + })) +} + +fn convert_specular_lighting(fe: SvgNode, primitives: &[Primitive]) -> Option { + let light_source = convert_light_source(fe)?; + + let specular_exponent = fe.attribute(AId::SpecularExponent).unwrap_or(1.0); + if !(1.0..=128.0).contains(&specular_exponent) { + // When exponent is out of range, the whole filter primitive should be ignored. + return None; + } + + let specular_exponent = crate::f32_bound(1.0, specular_exponent, 128.0); + + Some(Kind::SpecularLighting(SpecularLighting { + input: resolve_input(fe, AId::In, primitives), + surface_scale: fe.attribute(AId::SurfaceScale).unwrap_or(1.0), + specular_constant: fe.attribute(AId::SpecularConstant).unwrap_or(1.0), + specular_exponent, + lighting_color: convert_lighting_color(fe), + light_source, + })) +} + +#[inline(never)] +fn convert_lighting_color(node: SvgNode) -> Color { + // Color's alpha doesn't affect lighting-color. Simply skip it. + match node.attribute(AId::LightingColor) { + Some("currentColor") => { + node.find_attribute(AId::Color) + // Yes, a missing `currentColor` resolves to black and not white. + .unwrap_or(svgtypes::Color::black()) + .split_alpha() + .0 + } + Some(value) => { + if let Ok(c) = svgtypes::Color::from_str(value) { + c.split_alpha().0 + } else { + log::warn!("Failed to parse lighting-color value: '{}'.", value); + Color::white() + } + } + _ => Color::white(), + } +} + +#[inline(never)] +fn convert_light_source(parent: SvgNode) -> Option { + let child = parent.children().find(|n| { + matches!( + n.tag_name(), + Some(EId::FeDistantLight) | Some(EId::FePointLight) | Some(EId::FeSpotLight) + ) + })?; + + match child.tag_name() { + Some(EId::FeDistantLight) => Some(LightSource::DistantLight(DistantLight { + azimuth: child.attribute(AId::Azimuth).unwrap_or(0.0), + elevation: child.attribute(AId::Elevation).unwrap_or(0.0), + })), + Some(EId::FePointLight) => Some(LightSource::PointLight(PointLight { + x: child.attribute(AId::X).unwrap_or(0.0), + y: child.attribute(AId::Y).unwrap_or(0.0), + z: child.attribute(AId::Z).unwrap_or(0.0), + })), + Some(EId::FeSpotLight) => { + let specular_exponent = child.attribute(AId::SpecularExponent).unwrap_or(1.0); + let specular_exponent = PositiveF32::new(specular_exponent) + .unwrap_or_else(|| PositiveF32::new(1.0).unwrap()); + + Some(LightSource::SpotLight(SpotLight { + x: child.attribute(AId::X).unwrap_or(0.0), + y: child.attribute(AId::Y).unwrap_or(0.0), + z: child.attribute(AId::Z).unwrap_or(0.0), + points_at_x: child.attribute(AId::PointsAtX).unwrap_or(0.0), + points_at_y: child.attribute(AId::PointsAtY).unwrap_or(0.0), + points_at_z: child.attribute(AId::PointsAtZ).unwrap_or(0.0), + specular_exponent, + limiting_cone_angle: child.attribute(AId::LimitingConeAngle), + })) + } + _ => None, + } +} + +fn convert_merge(fe: SvgNode, primitives: &[Primitive]) -> Kind { + let mut inputs = Vec::new(); + for child in fe.children() { + inputs.push(resolve_input(child, AId::In, primitives)); + } + + Kind::Merge(Merge { inputs }) +} + +fn convert_morphology(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let operator = match fe.attribute(AId::Operator).unwrap_or("erode") { + "dilate" => MorphologyOperator::Dilate, + _ => MorphologyOperator::Erode, + }; + + let mut radius_x = PositiveF32::new(scale.width()).unwrap(); + let mut radius_y = PositiveF32::new(scale.height()).unwrap(); + if let Some(list) = fe.attribute::>(AId::Radius) { + let mut rx = 0.0; + let mut ry = 0.0; + if list.len() == 2 { + rx = list[0]; + ry = list[1]; + } else if list.len() == 1 { + rx = list[0]; + ry = list[0]; // The same as `rx`. + } + + if rx.approx_zero_ulps(4) && ry.approx_zero_ulps(4) { + rx = 1.0; + ry = 1.0; + } + + // If only one of the values is zero, reset it to 1.0 + // This is not specified in the spec, but this is how Chrome and Safari work. + if rx.approx_zero_ulps(4) && !ry.approx_zero_ulps(4) { + rx = 1.0; + } + if !rx.approx_zero_ulps(4) && ry.approx_zero_ulps(4) { + ry = 1.0; + } + + // Both values must be positive. + if rx.is_sign_positive() && ry.is_sign_positive() { + radius_x = PositiveF32::new(rx * scale.width()).unwrap(); + radius_y = PositiveF32::new(ry * scale.height()).unwrap(); + } + } + + Kind::Morphology(Morphology { + input: resolve_input(fe, AId::In, primitives), + operator, + radius_x, + radius_y, + }) +} + +fn convert_offset(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + Kind::Offset(Offset { + input: resolve_input(fe, AId::In, primitives), + dx: fe.attribute(AId::Dx).unwrap_or(0.0) * scale.width(), + dy: fe.attribute(AId::Dy).unwrap_or(0.0) * scale.height(), + }) +} + +fn convert_tile(fe: SvgNode, primitives: &[Primitive]) -> Kind { + Kind::Tile(Tile { + input: resolve_input(fe, AId::In, primitives), + }) +} + +fn convert_turbulence(fe: SvgNode) -> Kind { + let mut base_frequency_x = PositiveF32::ZERO; + let mut base_frequency_y = PositiveF32::ZERO; + if let Some(list) = fe.attribute::>(AId::BaseFrequency) { + let mut x = 0.0; + let mut y = 0.0; + if list.len() == 2 { + x = list[0]; + y = list[1]; + } else if list.len() == 1 { + x = list[0]; + y = list[0]; // The same as `x`. + } + + if x.is_sign_positive() && y.is_sign_positive() { + base_frequency_x = PositiveF32::new(x).unwrap(); + base_frequency_y = PositiveF32::new(y).unwrap(); + } + } + + let mut num_octaves = fe.attribute(AId::NumOctaves).unwrap_or(1.0); + if num_octaves.is_sign_negative() { + num_octaves = 0.0; + } + + let kind = match fe.attribute(AId::Type).unwrap_or("turbulence") { + "fractalNoise" => TurbulenceKind::FractalNoise, + _ => TurbulenceKind::Turbulence, + }; + + Kind::Turbulence(Turbulence { + base_frequency_x, + base_frequency_y, + num_octaves: num_octaves.round() as u32, + seed: fe.attribute::(AId::Seed).unwrap_or(0.0).trunc() as i32, + stitch_tiles: fe.attribute(AId::StitchTiles) == Some("stitch"), + kind, + }) +} + +#[inline(never)] +fn convert_grayscale_function(amount: f64) -> Kind { + let amount = amount.min(1.0) as f32; + Kind::ColorMatrix(ColorMatrix { + input: Input::SourceGraphic, + kind: ColorMatrixKind::Matrix(vec![ + (0.2126 + 0.7874 * (1.0 - amount)), + (0.7152 - 0.7152 * (1.0 - amount)), + (0.0722 - 0.0722 * (1.0 - amount)), + 0.0, + 0.0, + (0.2126 - 0.2126 * (1.0 - amount)), + (0.7152 + 0.2848 * (1.0 - amount)), + (0.0722 - 0.0722 * (1.0 - amount)), + 0.0, + 0.0, + (0.2126 - 0.2126 * (1.0 - amount)), + (0.7152 - 0.7152 * (1.0 - amount)), + (0.0722 + 0.9278 * (1.0 - amount)), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]), + }) +} + +#[inline(never)] +fn convert_sepia_function(amount: f64) -> Kind { + let amount = amount.min(1.0) as f32; + Kind::ColorMatrix(ColorMatrix { + input: Input::SourceGraphic, + kind: ColorMatrixKind::Matrix(vec![ + (0.393 + 0.607 * (1.0 - amount)), + (0.769 - 0.769 * (1.0 - amount)), + (0.189 - 0.189 * (1.0 - amount)), + 0.0, + 0.0, + (0.349 - 0.349 * (1.0 - amount)), + (0.686 + 0.314 * (1.0 - amount)), + (0.168 - 0.168 * (1.0 - amount)), + 0.0, + 0.0, + (0.272 - 0.272 * (1.0 - amount)), + (0.534 - 0.534 * (1.0 - amount)), + (0.131 + 0.869 * (1.0 - amount)), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ]), + }) +} + +#[inline(never)] +fn convert_saturate_function(amount: f64) -> Kind { + let amount = PositiveF32::new(amount as f32).unwrap_or(PositiveF32::ZERO); + Kind::ColorMatrix(ColorMatrix { + input: Input::SourceGraphic, + kind: ColorMatrixKind::Saturate(amount), + }) +} + +#[inline(never)] +fn convert_hue_rotate_function(amount: svgtypes::Angle) -> Kind { + Kind::ColorMatrix(ColorMatrix { + input: Input::SourceGraphic, + kind: ColorMatrixKind::HueRotate(amount.to_degrees() as f32), + }) +} + +#[inline(never)] +fn convert_invert_function(amount: f64) -> Kind { + let amount = amount.min(1.0) as f32; + Kind::ComponentTransfer(ComponentTransfer { + input: Input::SourceGraphic, + func_r: TransferFunction::Table(vec![amount, 1.0 - amount]), + func_g: TransferFunction::Table(vec![amount, 1.0 - amount]), + func_b: TransferFunction::Table(vec![amount, 1.0 - amount]), + func_a: TransferFunction::Identity, + }) +} + +#[inline(never)] +fn convert_opacity_function(amount: f64) -> Kind { + let amount = amount.min(1.0) as f32; + Kind::ComponentTransfer(ComponentTransfer { + input: Input::SourceGraphic, + func_r: TransferFunction::Identity, + func_g: TransferFunction::Identity, + func_b: TransferFunction::Identity, + func_a: TransferFunction::Table(vec![0.0, amount]), + }) +} + +#[inline(never)] +fn convert_brightness_function(amount: f64) -> Kind { + let amount = amount as f32; + Kind::ComponentTransfer(ComponentTransfer { + input: Input::SourceGraphic, + func_r: TransferFunction::Linear { + slope: amount, + intercept: 0.0, + }, + func_g: TransferFunction::Linear { + slope: amount, + intercept: 0.0, + }, + func_b: TransferFunction::Linear { + slope: amount, + intercept: 0.0, + }, + func_a: TransferFunction::Identity, + }) +} + +#[inline(never)] +fn convert_contrast_function(amount: f64) -> Kind { + let amount = amount as f32; + Kind::ComponentTransfer(ComponentTransfer { + input: Input::SourceGraphic, + func_r: TransferFunction::Linear { + slope: amount, + intercept: -(0.5 * amount) + 0.5, + }, + func_g: TransferFunction::Linear { + slope: amount, + intercept: -(0.5 * amount) + 0.5, + }, + func_b: TransferFunction::Linear { + slope: amount, + intercept: -(0.5 * amount) + 0.5, + }, + func_a: TransferFunction::Identity, + }) +} + +#[inline(never)] +fn convert_blur_function(node: SvgNode, std_dev: Length, state: &converter::State) -> Kind { + let std_dev = PositiveF32::new(super::units::convert_user_length( + std_dev, + node, + AId::Dx, + state, + )) + .unwrap_or(PositiveF32::ZERO); + Kind::GaussianBlur(GaussianBlur { + input: Input::SourceGraphic, + std_dev_x: std_dev, + std_dev_y: std_dev, + }) +} + +#[inline(never)] +fn convert_drop_shadow_function( + node: SvgNode, + color: Option, + dx: Length, + dy: Length, + std_dev: Length, + state: &converter::State, +) -> Kind { + let std_dev = PositiveF32::new(super::units::convert_user_length( + std_dev, + node, + AId::Dx, + state, + )) + .unwrap_or(PositiveF32::ZERO); + + let (color, opacity) = color + .unwrap_or_else(|| { + node.find_attribute(AId::Color) + .unwrap_or_else(svgtypes::Color::black) + }) + .split_alpha(); + + Kind::DropShadow(DropShadow { + input: Input::SourceGraphic, + dx: super::units::convert_user_length(dx, node, AId::Dx, state), + dy: super::units::convert_user_length(dy, node, AId::Dy, state), + std_dev_x: std_dev, + std_dev_y: std_dev, + color, + opacity, + }) +} diff --git a/third_party/usvg/src/parser/image.rs b/third_party/usvg/src/parser/image.rs new file mode 100644 index 0000000000..e6034c0469 --- /dev/null +++ b/third_party/usvg/src/parser/image.rs @@ -0,0 +1,348 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use svgtypes::{AspectRatio, Length}; + +use super::svgtree::{AId, SvgNode}; +use super::{converter, OptionLog, Options}; +use crate::{ + ClipPath, Group, Image, ImageKind, ImageRendering, Node, NonZeroRect, Path, Size, Transform, + Tree, Visibility, +}; + +/// A shorthand for [ImageHrefResolver]'s data function. +pub type ImageHrefDataResolverFn<'a> = + Box>, &Options) -> Option + Send + Sync + 'a>; + +/// A shorthand for [ImageHrefResolver]'s string function. +pub type ImageHrefStringResolverFn<'a> = + Box Option + Send + Sync + 'a>; + +/// An `xlink:href` resolver for `` elements. +/// +/// This type can be useful if you want to have an alternative `xlink:href` handling +/// to the default one. For example, you can forbid access to local files (which is allowed by default) +/// or add support for resolving actual URLs (usvg doesn't do any network requests). +pub struct ImageHrefResolver<'a> { + /// Resolver function that will be used when `xlink:href` contains a + /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). + /// + /// A function would be called with mime, decoded base64 data and parsing options. + pub resolve_data: ImageHrefDataResolverFn<'a>, + + /// Resolver function that will be used to handle an arbitrary string in `xlink:href`. + pub resolve_string: ImageHrefStringResolverFn<'a>, +} + +impl Default for ImageHrefResolver<'_> { + fn default() -> Self { + ImageHrefResolver { + resolve_data: ImageHrefResolver::default_data_resolver(), + resolve_string: ImageHrefResolver::default_string_resolver(), + } + } +} + +impl ImageHrefResolver<'_> { + /// Creates a default + /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) + /// resolver closure. + /// + /// base64 encoded data is already decoded. + /// + /// The default implementation would try to load JPEG, PNG, GIF, WebP, SVG and SVGZ types. + /// Note that it will simply match the `mime` or data's magic. + /// The actual images would not be decoded. It's up to the renderer. + pub fn default_data_resolver() -> ImageHrefDataResolverFn<'static> { + Box::new( + move |mime: &str, data: Arc>, opts: &Options| match mime { + "image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)), + "image/png" => Some(ImageKind::PNG(data)), + "image/gif" => Some(ImageKind::GIF(data)), + "image/webp" => Some(ImageKind::WEBP(data)), + "image/svg+xml" => load_sub_svg(&data, opts), + "text/plain" => match get_image_data_format(&data) { + Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)), + Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)), + Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)), + Some(ImageFormat::WEBP) => Some(ImageKind::WEBP(data)), + _ => load_sub_svg(&data, opts), + }, + _ => None, + }, + ) + } + + /// Creates a default string resolver. + /// + /// The default implementation treats an input string as a file path and tries to open. + /// If a string is an URL or something else it would be ignored. + /// + /// Paths have to be absolute or relative to the input SVG file or relative to + /// [Options::resources_dir](crate::Options::resources_dir). + pub fn default_string_resolver() -> ImageHrefStringResolverFn<'static> { + Box::new(move |href: &str, opts: &Options| { + let path = opts.get_abs_path(std::path::Path::new(href)); + + if path.exists() { + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(_) => { + log::warn!("Failed to load '{}'. Skipped.", href); + return None; + } + }; + + match get_image_file_format(&path, &data) { + Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), + Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), + Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), + Some(ImageFormat::WEBP) => Some(ImageKind::WEBP(Arc::new(data))), + Some(ImageFormat::SVG) => load_sub_svg(&data, opts), + _ => { + log::warn!("'{}' is not a PNG, JPEG, GIF, WebP or SVG(Z) image.", href); + None + } + } + } else { + log::warn!("'{}' is not a path to an image.", href); + None + } + }) + } +} + +impl std::fmt::Debug for ImageHrefResolver<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ImageHrefResolver { .. }") + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum ImageFormat { + PNG, + JPEG, + GIF, + WEBP, + SVG, +} + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) -> Option<()> { + let href = node + .try_attribute(AId::Href) + .log_none(|| log::warn!("Image lacks the 'xlink:href' attribute. Skipped."))?; + + let kind = get_href_data(href, state)?; + + let visibility: Visibility = node.find_attribute(AId::Visibility).unwrap_or_default(); + let visible = visibility == Visibility::Visible; + + let rendering_mode = node + .find_attribute(AId::ImageRendering) + .unwrap_or(state.opt.image_rendering); + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let id = if state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + + let actual_size = kind.actual_size()?; + + let x = node.convert_user_length(AId::X, state, Length::zero()); + let y = node.convert_user_length(AId::Y, state, Length::zero()); + let mut width = node.convert_user_length( + AId::Width, + state, + Length::new_number(actual_size.width() as f64), + ); + let mut height = node.convert_user_length( + AId::Height, + state, + Length::new_number(actual_size.height() as f64), + ); + + match ( + node.attribute::(AId::Width), + node.attribute::(AId::Height), + ) { + (Some(_), None) => { + // Only width was defined, so we need to scale height accordingly. + height = actual_size.height() * (width / actual_size.width()); + } + (None, Some(_)) => { + // Only height was defined, so we need to scale width accordingly. + width = actual_size.width() * (height / actual_size.height()); + } + _ => {} + }; + + let aspect: AspectRatio = node.attribute(AId::PreserveAspectRatio).unwrap_or_default(); + + let rect = NonZeroRect::from_xywh(x, y, width, height); + let rect = rect.log_none(|| log::warn!("Image has an invalid size. Skipped."))?; + + convert_inner( + kind, + id, + visible, + rendering_mode, + aspect, + actual_size, + rect, + cache, + parent, + ) +} + +pub(crate) fn convert_inner( + kind: ImageKind, + id: String, + visible: bool, + rendering_mode: ImageRendering, + aspect: AspectRatio, + actual_size: Size, + rect: NonZeroRect, + cache: &mut converter::Cache, + parent: &mut Group, +) -> Option<()> { + let aligned_size = fit_view_box(actual_size, rect, aspect); + let (aligned_x, aligned_y) = crate::aligned_pos( + aspect.align, + rect.x(), + rect.y(), + rect.width() - aligned_size.width(), + rect.height() - aligned_size.height(), + ); + let view_box = aligned_size.to_non_zero_rect(aligned_x, aligned_y); + + let image_ts = Transform::from_row( + view_box.width() / actual_size.width(), + 0.0, + 0.0, + view_box.height() / actual_size.height(), + view_box.x(), + view_box.y(), + ); + + let abs_transform = parent.abs_transform.pre_concat(image_ts); + let abs_bounding_box = view_box.transform(parent.abs_transform)?; + + let mut g = Group::empty(); + g.id = id; + g.children.push(Node::Image(Box::new(Image { + id: String::new(), + visible, + size: actual_size, + rendering_mode, + kind, + abs_transform, + abs_bounding_box, + }))); + g.transform = image_ts; + g.abs_transform = abs_transform; + g.calculate_bounding_boxes(); + + if aspect.slice { + // Image slice acts like a rectangular clip. + let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( + rect.to_rect(), + ))) + .unwrap(); + path.fill = Some(crate::Fill::default()); + + let mut clip = ClipPath::empty(cache.gen_clip_path_id()); + clip.root.children.push(Node::Path(Box::new(path))); + + // Clip path should not be affected by the image viewbox transform. + // The final structure should look like: + // + // + // + // + // + + let mut g2 = Group::empty(); + std::mem::swap(&mut g.id, &mut g2.id); + g2.abs_transform = parent.abs_transform; + g2.clip_path = Some(Arc::new(clip)); + g2.children.push(Node::Group(Box::new(g))); + g2.calculate_bounding_boxes(); + + parent.children.push(Node::Group(Box::new(g2))); + } else { + parent.children.push(Node::Group(Box::new(g))); + } + + Some(()) +} + +pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option { + if let Ok(url) = data_url::DataUrl::process(href) { + let (data, _) = url.decode_to_vec().ok()?; + + let mime = format!( + "{}/{}", + url.mime_type().type_.as_str(), + url.mime_type().subtype.as_str() + ); + + (state.opt.image_href_resolver.resolve_data)(&mime, Arc::new(data), state.opt) + } else { + (state.opt.image_href_resolver.resolve_string)(href, state.opt) + } +} + +/// Checks that file has a PNG, a GIF, a JPEG or a WebP magic bytes. +/// Or an SVG(Z) extension. +fn get_image_file_format(path: &std::path::Path, data: &[u8]) -> Option { + let ext = path.extension().and_then(|e| e.to_str())?.to_lowercase(); + if ext == "svg" || ext == "svgz" { + return Some(ImageFormat::SVG); + } + + get_image_data_format(data) +} + +/// Checks that file has a PNG, a GIF, a JPEG or a WebP magic bytes. +fn get_image_data_format(data: &[u8]) -> Option { + match imagesize::image_type(data).ok()? { + imagesize::ImageType::Gif => Some(ImageFormat::GIF), + imagesize::ImageType::Jpeg => Some(ImageFormat::JPEG), + imagesize::ImageType::Png => Some(ImageFormat::PNG), + imagesize::ImageType::Webp => Some(ImageFormat::WEBP), + _ => None, + } +} + +/// Tries to load the `ImageData` content as an SVG image or emits a warning and returns `None`. +pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option { + match Tree::from_data_nested(data, opt) { + Ok(tree) => Some(ImageKind::SVG(tree)), + Err(_) => { + log::warn!("Failed to load nested SVG image."); + None + } + } +} + +/// Fits size into a viewbox. +fn fit_view_box(size: Size, rect: NonZeroRect, aspect: AspectRatio) -> Size { + let s = rect.size(); + + if aspect.align == svgtypes::Align::None { + s + } else if aspect.slice { + size.expand_to(s) + } else { + size.scale_to(s) + } +} diff --git a/third_party/usvg/src/parser/marker.rs b/third_party/usvg/src/parser/marker.rs new file mode 100644 index 0000000000..d577ae0907 --- /dev/null +++ b/third_party/usvg/src/parser/marker.rs @@ -0,0 +1,497 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use strict_num::NonZeroPositiveF32; +use svgtypes::Length; +use tiny_skia_path::Point; + +use super::converter; +use super::svgtree::{AId, EId, SvgNode}; +use crate::{ + ApproxEqUlps, ApproxZeroUlps, ClipPath, Fill, Group, Node, NonZeroRect, Path, Size, Transform, + ViewBox, +}; + +// Similar to `tiny_skia_path::PathSegment`, but without the `QuadTo`. +#[derive(Copy, Clone, Debug)] +enum Segment { + MoveTo(Point), + LineTo(Point), + CubicTo(Point, Point, Point), + Close, +} + +pub(crate) fn is_valid(node: SvgNode) -> bool { + // `marker-*` attributes cannot be set on shapes inside a `clipPath`. + if node + .ancestors() + .any(|n| n.tag_name() == Some(EId::ClipPath)) + { + return false; + } + + let start = node.find_attribute::(AId::MarkerStart); + let mid = node.find_attribute::(AId::MarkerMid); + let end = node.find_attribute::(AId::MarkerEnd); + start.is_some() || mid.is_some() || end.is_some() +} + +pub(crate) fn convert( + node: SvgNode, + path: &tiny_skia_path::Path, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) { + let list = [ + (AId::MarkerStart, MarkerKind::Start), + (AId::MarkerMid, MarkerKind::Middle), + (AId::MarkerEnd, MarkerKind::End), + ]; + + for (aid, kind) in &list { + let mut marker = None; + if let Some(link) = node.find_attribute::(*aid) { + if link.tag_name() == Some(EId::Marker) { + marker = Some(link); + } + } + + if let Some(marker) = marker { + // TODO: move to svgtree + // Check for recursive marker. + if state.parent_markers.contains(&marker) { + log::warn!("Recursive marker detected: {}", marker.element_id()); + continue; + } + + resolve(node, path, marker, *kind, state, cache, parent); + } + } +} + +#[derive(Clone, Copy)] +enum MarkerKind { + Start, + Middle, + End, +} + +enum MarkerOrientation { + Auto, + AutoStartReverse, + Angle(f32), +} + +fn resolve( + shape_node: SvgNode, + path: &tiny_skia_path::Path, + marker_node: SvgNode, + marker_kind: MarkerKind, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) -> Option<()> { + let stroke_scale = stroke_scale(shape_node, marker_node, state)?.get(); + + let r = convert_rect(marker_node, state)?; + + let view_box = marker_node.parse_viewbox().map(|vb| ViewBox { + rect: vb, + aspect: marker_node + .attribute(AId::PreserveAspectRatio) + .unwrap_or_default(), + }); + + let has_overflow = { + let overflow = marker_node.attribute(AId::Overflow); + // `overflow` is `hidden` by default. + overflow.is_none() || overflow == Some("hidden") || overflow == Some("scroll") + }; + + let clip_path = if has_overflow { + let clip_rect = if let Some(vbox) = view_box { + vbox.rect + } else { + r.size().to_non_zero_rect(0.0, 0.0) + }; + + let mut clip_path = ClipPath::empty(cache.gen_clip_path_id()); + + let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( + clip_rect.to_rect(), + )))?; + path.fill = Some(Fill::default()); + + clip_path.root.children.push(Node::Path(Box::new(path))); + + Some(Arc::new(clip_path)) + } else { + None + }; + + // TODO: avoid allocation + let mut segments: Vec = Vec::with_capacity(path.len()); + let mut prev = Point::zero(); + let mut prev_move = Point::zero(); + for seg in path.segments() { + match seg { + tiny_skia_path::PathSegment::MoveTo(p) => { + segments.push(Segment::MoveTo(p)); + prev = p; + prev_move = p; + } + tiny_skia_path::PathSegment::LineTo(p) => { + segments.push(Segment::LineTo(p)); + prev = p; + } + tiny_skia_path::PathSegment::QuadTo(p1, p) => { + let (p1, p2, p) = quad_to_curve(prev, p1, p); + segments.push(Segment::CubicTo(p1, p2, p)); + prev = p; + } + tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => { + segments.push(Segment::CubicTo(p1, p2, p)); + prev = p; + } + tiny_skia_path::PathSegment::Close => { + segments.push(Segment::Close); + prev = prev_move; + } + } + } + + let draw_marker = |p: tiny_skia_path::Point, idx: usize| { + let mut ts = Transform::from_translate(p.x, p.y); + + let angle = match convert_orientation(marker_node) { + MarkerOrientation::AutoStartReverse if idx == 0 => { + (calc_vertex_angle(&segments, idx) + 180.0) % 360.0 + } + MarkerOrientation::Auto | MarkerOrientation::AutoStartReverse => { + calc_vertex_angle(&segments, idx) + } + MarkerOrientation::Angle(angle) => angle, + }; + + if !angle.approx_zero_ulps(4) { + ts = ts.pre_rotate(angle); + } + + if let Some(vbox) = view_box { + let size = Size::from_wh(r.width() * stroke_scale, r.height() * stroke_scale).unwrap(); + let vbox_ts = vbox.to_transform(size); + let (sx, sy) = vbox_ts.get_scale(); + ts = ts.pre_scale(sx, sy); + } else { + ts = ts.pre_scale(stroke_scale, stroke_scale); + } + + ts = ts.pre_translate(-r.x(), -r.y()); + + // TODO: do not create a group when no clipPath + let mut g = Group { + transform: ts, + abs_transform: parent.abs_transform.pre_concat(ts), + clip_path: clip_path.clone(), + ..Group::empty() + }; + + let mut marker_state = state.clone(); + marker_state.parent_markers.push(marker_node); + converter::convert_children(marker_node, &marker_state, cache, &mut g); + g.calculate_bounding_boxes(); + + if g.has_children() { + parent.children.push(Node::Group(Box::new(g))); + } + }; + + draw_markers(&segments, marker_kind, draw_marker); + + Some(()) +} + +fn stroke_scale( + path_node: SvgNode, + marker_node: SvgNode, + state: &converter::State, +) -> Option { + match marker_node.attribute(AId::MarkerUnits) { + Some("userSpaceOnUse") => NonZeroPositiveF32::new(1.0), + _ => path_node.resolve_valid_length(AId::StrokeWidth, state, 1.0), + } +} + +fn draw_markers

(path: &[Segment], kind: MarkerKind, mut draw_marker: P) +where + P: FnMut(tiny_skia_path::Point, usize), +{ + match kind { + MarkerKind::Start => { + if let Some(Segment::MoveTo(p)) = path.first().cloned() { + draw_marker(p, 0); + } + } + MarkerKind::Middle => { + let total = path.len() - 1; + let mut i = 1; + while i < total { + let p = match path[i] { + Segment::MoveTo(p) => p, + Segment::LineTo(p) => p, + Segment::CubicTo(_, _, p) => p, + _ => { + i += 1; + continue; + } + }; + + draw_marker(p, i); + + i += 1; + } + } + MarkerKind::End => { + let idx = path.len() - 1; + match path.last().cloned() { + Some(Segment::LineTo(p)) => { + draw_marker(p, idx); + } + Some(Segment::CubicTo(_, _, p)) => { + draw_marker(p, idx); + } + Some(Segment::Close) => { + let p = get_subpath_start(path, idx); + draw_marker(p, idx); + } + _ => {} + } + } + } +} + +fn calc_vertex_angle(path: &[Segment], idx: usize) -> f32 { + if idx == 0 { + // First segment. + + debug_assert!(path.len() > 1); + + let seg1 = path[0]; + let seg2 = path[1]; + + match (seg1, seg2) { + (Segment::MoveTo(pm), Segment::LineTo(p)) => calc_line_angle(pm.x, pm.y, p.x, p.y), + (Segment::MoveTo(pm), Segment::CubicTo(p1, _, p)) => { + if pm.x.approx_eq_ulps(&p1.x, 4) && pm.y.approx_eq_ulps(&p1.y, 4) { + calc_line_angle(pm.x, pm.y, p.x, p.y) + } else { + calc_line_angle(pm.x, pm.y, p1.x, p1.y) + } + } + _ => 0.0, + } + } else if idx == path.len() - 1 { + // Last segment. + + let seg1 = path[idx - 1]; + let seg2 = path[idx]; + + match (seg1, seg2) { + (_, Segment::MoveTo(_)) => 0.0, // unreachable + (_, Segment::LineTo(p)) => { + let prev = get_prev_vertex(path, idx); + calc_line_angle(prev.x, prev.y, p.x, p.y) + } + (_, Segment::CubicTo(p1, p2, p)) => { + if p2.x.approx_eq_ulps(&p.x, 4) && p2.y.approx_eq_ulps(&p.y, 4) { + calc_line_angle(p1.x, p1.y, p.x, p.y) + } else { + calc_line_angle(p2.x, p2.y, p.x, p.y) + } + } + (Segment::LineTo(p), Segment::Close) => { + let next = get_subpath_start(path, idx); + calc_line_angle(p.x, p.y, next.x, next.y) + } + (Segment::CubicTo(_, p2, p), Segment::Close) => { + let prev = get_prev_vertex(path, idx); + let next = get_subpath_start(path, idx); + calc_curves_angle( + prev.x, prev.y, p2.x, p2.y, p.x, p.y, next.x, next.y, next.x, next.y, + ) + } + (_, Segment::Close) => 0.0, + } + } else { + // Middle segments. + + let seg1 = path[idx]; + let seg2 = path[idx + 1]; + + // TODO: Not sure if there is a better way. + match (seg1, seg2) { + (Segment::MoveTo(pm), Segment::LineTo(p)) => calc_line_angle(pm.x, pm.y, p.x, p.y), + (Segment::MoveTo(pm), Segment::CubicTo(p1, _, _)) => { + calc_line_angle(pm.x, pm.y, p1.x, p1.y) + } + (Segment::LineTo(p1), Segment::LineTo(p2)) => { + let prev = get_prev_vertex(path, idx); + calc_angle(prev.x, prev.y, p1.x, p1.y, p1.x, p1.y, p2.x, p2.y) + } + (Segment::CubicTo(_, c1_p2, c1_p), Segment::CubicTo(c2_p1, _, c2_p)) => { + let prev = get_prev_vertex(path, idx); + calc_curves_angle( + prev.x, prev.y, c1_p2.x, c1_p2.y, c1_p.x, c1_p.y, c2_p1.x, c2_p1.y, c2_p.x, + c2_p.y, + ) + } + (Segment::LineTo(pl), Segment::CubicTo(p1, _, p)) => { + let prev = get_prev_vertex(path, idx); + calc_curves_angle( + prev.x, prev.y, prev.x, prev.y, pl.x, pl.y, p1.x, p1.y, p.x, p.y, + ) + } + (Segment::CubicTo(_, p2, p), Segment::LineTo(pl)) => { + let prev = get_prev_vertex(path, idx); + calc_curves_angle(prev.x, prev.y, p2.x, p2.y, p.x, p.y, pl.x, pl.y, pl.x, pl.y) + } + (Segment::LineTo(p), Segment::MoveTo(_)) => { + let prev = get_prev_vertex(path, idx); + calc_line_angle(prev.x, prev.y, p.x, p.y) + } + (Segment::CubicTo(_, p2, p), Segment::MoveTo(_)) => { + if p.x.approx_eq_ulps(&p2.x, 4) && p.y.approx_eq_ulps(&p2.y, 4) { + let prev = get_prev_vertex(path, idx); + calc_line_angle(prev.x, prev.y, p.x, p.y) + } else { + calc_line_angle(p2.x, p2.y, p.x, p.y) + } + } + (Segment::LineTo(p), Segment::Close) => { + let prev = get_prev_vertex(path, idx); + let next = get_subpath_start(path, idx); + calc_angle(prev.x, prev.y, p.x, p.y, p.x, p.y, next.x, next.y) + } + (_, Segment::Close) => { + let prev = get_prev_vertex(path, idx); + let next = get_subpath_start(path, idx); + calc_line_angle(prev.x, prev.y, next.x, next.y) + } + (_, Segment::MoveTo(_)) | (Segment::Close, _) => 0.0, + } + } +} + +fn calc_line_angle(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + calc_angle(x1, y1, x2, y2, x1, y1, x2, y2) +} + +fn calc_curves_angle( + px: f32, + py: f32, // previous vertex + cx1: f32, + cy1: f32, // previous control point + x: f32, + y: f32, // current vertex + cx2: f32, + cy2: f32, // next control point + nx: f32, + ny: f32, // next vertex +) -> f32 { + if cx1.approx_eq_ulps(&x, 4) && cy1.approx_eq_ulps(&y, 4) { + calc_angle(px, py, x, y, x, y, cx2, cy2) + } else if x.approx_eq_ulps(&cx2, 4) && y.approx_eq_ulps(&cy2, 4) { + calc_angle(cx1, cy1, x, y, x, y, nx, ny) + } else { + calc_angle(cx1, cy1, x, y, x, y, cx2, cy2) + } +} + +fn calc_angle(x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, x4: f32, y4: f32) -> f32 { + use std::f32::consts::*; + + fn normalize(rad: f32) -> f32 { + let v = rad % (PI * 2.0); + if v < 0.0 { + v + PI * 2.0 + } else { + v + } + } + + fn vector_angle(vx: f32, vy: f32) -> f32 { + let rad = vy.atan2(vx); + if rad.is_nan() { + 0.0 + } else { + normalize(rad) + } + } + + let in_a = vector_angle(x2 - x1, y2 - y1); + let out_a = vector_angle(x4 - x3, y4 - y3); + let d = (out_a - in_a) * 0.5; + + let mut angle = in_a + d; + if FRAC_PI_2 < d.abs() { + angle -= PI; + } + + normalize(angle).to_degrees() +} + +fn get_subpath_start(segments: &[Segment], idx: usize) -> tiny_skia_path::Point { + let offset = segments.len() - idx; + for seg in segments.iter().rev().skip(offset) { + if let Segment::MoveTo(p) = *seg { + return p; + } + } + + tiny_skia_path::Point::zero() +} + +fn get_prev_vertex(segments: &[Segment], idx: usize) -> tiny_skia_path::Point { + match segments[idx - 1] { + Segment::MoveTo(p) => p, + Segment::LineTo(p) => p, + Segment::CubicTo(_, _, p) => p, + Segment::Close => get_subpath_start(segments, idx), + } +} + +fn convert_rect(node: SvgNode, state: &converter::State) -> Option { + NonZeroRect::from_xywh( + node.convert_user_length(AId::RefX, state, Length::zero()), + node.convert_user_length(AId::RefY, state, Length::zero()), + node.convert_user_length(AId::MarkerWidth, state, Length::new_number(3.0)), + node.convert_user_length(AId::MarkerHeight, state, Length::new_number(3.0)), + ) +} + +fn convert_orientation(node: SvgNode) -> MarkerOrientation { + match node.attribute(AId::Orient) { + Some("auto") => MarkerOrientation::Auto, + Some("auto-start-reverse") => MarkerOrientation::AutoStartReverse, + _ => match node.attribute::(AId::Orient) { + Some(angle) => MarkerOrientation::Angle(angle.to_degrees() as f32), + None => MarkerOrientation::Angle(0.0), + }, + } +} + +fn quad_to_curve(prev: Point, p1: Point, p: Point) -> (Point, Point, Point) { + #[inline] + fn calc(n1: f32, n2: f32) -> f32 { + (n1 + n2 * 2.0) / 3.0 + } + + ( + Point::from_xy(calc(prev.x, p1.x), calc(prev.y, p1.y)), + Point::from_xy(calc(p.x, p1.x), calc(p.y, p1.y)), + p, + ) +} diff --git a/third_party/usvg/src/parser/mask.rs b/third_party/usvg/src/parser/mask.rs new file mode 100644 index 0000000000..c262c68f2c --- /dev/null +++ b/third_party/usvg/src/parser/mask.rs @@ -0,0 +1,150 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use svgtypes::{Length, LengthUnit as Unit}; + +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, OptionLog}; +use crate::{Group, Mask, MaskType, Node, NonEmptyString, NonZeroRect, Transform, Units}; + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + object_bbox: Option, + cache: &mut converter::Cache, +) -> Option> { + // A `mask` attribute must reference a `mask` element. + if node.tag_name() != Some(EId::Mask) { + return None; + } + + let units = node + .attribute(AId::MaskUnits) + .unwrap_or(Units::ObjectBoundingBox); + + let content_units = node + .attribute(AId::MaskContentUnits) + .unwrap_or(Units::UserSpaceOnUse); + + // Check if this element was already converted. + // + // Only `userSpaceOnUse` masks can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse && content_units == Units::UserSpaceOnUse; + if cacheable { + if let Some(mask) = cache.masks.get(node.element_id()) { + return Some(mask.clone()); + } + } + + let rect = NonZeroRect::from_xywh( + node.convert_length(AId::X, units, state, Length::new(-10.0, Unit::Percent)), + node.convert_length(AId::Y, units, state, Length::new(-10.0, Unit::Percent)), + node.convert_length(AId::Width, units, state, Length::new(120.0, Unit::Percent)), + node.convert_length(AId::Height, units, state, Length::new(120.0, Unit::Percent)), + ); + let mut rect = + rect.log_none(|| log::warn!("Mask '{}' has an invalid size. Skipped.", node.element_id()))?; + + let mut mask_all = false; + if units == Units::ObjectBoundingBox { + if let Some(bbox) = object_bbox { + rect = rect.bbox_transform(bbox); + } else { + // When mask units are `objectBoundingBox` and bbox is zero-sized - the whole + // element should be masked. + // Technically an UB, but this is what Chrome and Firefox do. + mask_all = true; + } + } + + let mut id = NonEmptyString::new(node.element_id().to_string())?; + // Generate ID only when we're parsing `objectBoundingBox` mask for the second time. + if !cacheable && cache.masks.contains_key(id.get()) { + id = cache.gen_mask_id(); + } + let id_copy = id.get().to_string(); + + if mask_all { + let mask = Arc::new(Mask { + id, + rect, + kind: MaskType::Luminance, + mask: None, + root: Group::empty(), + }); + cache.masks.insert(id_copy, mask.clone()); + return Some(mask); + } + + // Resolve linked mask. + let mut mask = None; + if let Some(link) = node.attribute::(AId::Mask) { + mask = convert(link, state, object_bbox, cache); + + // Linked `mask` must be valid. + if mask.is_none() { + return None; + } + } + + let kind = if node.attribute(AId::MaskType) == Some("alpha") { + MaskType::Alpha + } else { + MaskType::Luminance + }; + + let mut mask = Mask { + id, + rect, + kind, + mask, + root: Group::empty(), + }; + + // To emulate content `objectBoundingBox` units we have to put + // mask children into a group with a transform. + let mut subroot = None; + if content_units == Units::ObjectBoundingBox { + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!("Masking of zero-sized shapes is not allowed."); + return None; + } + }; + + let mut g = Group::empty(); + g.transform = Transform::from_bbox(object_bbox); + // Make sure to set `abs_transform`, because it must propagate to all children. + g.abs_transform = g.transform; + + subroot = Some(g); + } + + { + // Prefer `subroot` to `mask.root`. + let real_root = subroot.as_mut().unwrap_or(&mut mask.root); + converter::convert_children(node, state, cache, real_root); + + // A mask without children at this point is invalid. + // Only masks with zero bbox and `objectBoundingBox` can be empty. + if !real_root.has_children() { + return None; + } + } + + if let Some(mut subroot) = subroot { + subroot.calculate_bounding_boxes(); + mask.root.children.push(Node::Group(Box::new(subroot))); + } + + mask.root.calculate_bounding_boxes(); + + let mask = Arc::new(mask); + cache.masks.insert(id_copy, mask.clone()); + Some(mask) +} diff --git a/third_party/usvg/src/parser/mod.rs b/third_party/usvg/src/parser/mod.rs new file mode 100644 index 0000000000..b3fbccdd64 --- /dev/null +++ b/third_party/usvg/src/parser/mod.rs @@ -0,0 +1,190 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod clippath; +mod converter; +mod filter; +mod image; +mod marker; +mod mask; +mod options; +mod paint_server; +mod shapes; +mod style; +mod svgtree; +mod switch; +mod units; +mod use_node; + +#[cfg(feature = "text")] +mod text; +#[cfg(feature = "text")] +pub(crate) use converter::Cache; +pub use image::{ImageHrefDataResolverFn, ImageHrefResolver, ImageHrefStringResolverFn}; +pub use options::Options; +pub(crate) use svgtree::{AId, EId}; + +/// List of all errors. +#[derive(Debug)] +pub enum Error { + /// Only UTF-8 content are supported. + NotAnUtf8Str, + + /// Compressed SVG must use the GZip algorithm. + MalformedGZip, + + /// We do not allow SVG with more than 1_000_000 elements for security reasons. + ElementsLimitReached, + + /// SVG doesn't have a valid size. + /// + /// Occurs when width and/or height are <= 0. + /// + /// Also occurs if width, height and viewBox are not set. + InvalidSize, + + /// Failed to parse an SVG data. + ParsingFailed(roxmltree::Error), +} + +impl From for Error { + fn from(e: roxmltree::Error) -> Self { + Error::ParsingFailed(e) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Error::NotAnUtf8Str => { + write!(f, "provided data has not an UTF-8 encoding") + } + Error::MalformedGZip => { + write!(f, "provided data has a malformed GZip content") + } + Error::ElementsLimitReached => { + write!(f, "the maximum number of SVG elements has been reached") + } + Error::InvalidSize => { + write!(f, "SVG has an invalid size") + } + Error::ParsingFailed(ref e) => { + write!(f, "SVG data parsing failed cause {}", e) + } + } + } +} + +impl std::error::Error for Error {} + +pub(crate) trait OptionLog { + fn log_none(self, f: F) -> Self; +} + +impl OptionLog for Option { + #[inline] + fn log_none(self, f: F) -> Self { + self.or_else(|| { + f(); + None + }) + } +} + +impl crate::Tree { + /// Parses `Tree` from an SVG data. + /// + /// Can contain an SVG string or a gzip compressed data. + pub fn from_data(data: &[u8], opt: &Options) -> Result { + if data.starts_with(&[0x1f, 0x8b]) { + let data = decompress_svgz(data)?; + let text = std::str::from_utf8(&data).map_err(|_| Error::NotAnUtf8Str)?; + Self::from_str(text, opt) + } else { + let text = std::str::from_utf8(data).map_err(|_| Error::NotAnUtf8Str)?; + Self::from_str(text, opt) + } + } + + /// Similar to the `from_data` method, except that it ignores all `image` elements linking to + /// external files, as required by the SVG specification when SVG files are loaded + /// for `` tags. + pub fn from_data_nested(data: &[u8], opt: &Options) -> Result { + let nested_opt = Options { + resources_dir: None, + dpi: opt.dpi, + font_size: opt.font_size, + languages: opt.languages.clone(), + shape_rendering: opt.shape_rendering, + text_rendering: opt.text_rendering, + image_rendering: opt.image_rendering, + default_size: opt.default_size, + image_href_resolver: ImageHrefResolver { + resolve_data: Box::new(|a, b, c| (opt.image_href_resolver.resolve_data)(a, b, c)), + // External images should be ignored. + resolve_string: Box::new(|_, _| None), + }, + // In the referenced SVG, we start with the unmodified user-provided + // fontdb, not the one from the cache. + #[cfg(feature = "text")] + fontdb: opt.fontdb.clone(), + // Can't clone the resolver, so we create a new one that forwards to it. + #[cfg(feature = "text")] + font_resolver: crate::FontResolver { + select_font: Box::new(|font, db| (opt.font_resolver.select_font)(font, db)), + select_fallback: Box::new(|c, used_fonts, db| { + (opt.font_resolver.select_fallback)(c, used_fonts, db) + }), + }, + ..Options::default() + }; + + Self::from_data(data, &nested_opt) + } + + /// Parses `Tree` from an SVG string. + pub fn from_str(text: &str, opt: &Options) -> Result { + let xml_opt = roxmltree::ParsingOptions { + allow_dtd: true, + ..Default::default() + }; + + let doc = + roxmltree::Document::parse_with_options(text, xml_opt).map_err(Error::ParsingFailed)?; + + Self::from_xmltree(&doc, opt) + } + + /// Parses `Tree` from `roxmltree::Document`. + pub fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result { + let doc = svgtree::Document::parse_tree(doc, opt.style_sheet.as_deref())?; + self::converter::convert_doc(&doc, opt) + } +} + +/// Decompresses an SVGZ file. +pub fn decompress_svgz(data: &[u8]) -> Result, Error> { + use std::io::Read; + + let mut decoder = flate2::read::GzDecoder::new(data); + let mut decoded = Vec::with_capacity(data.len() * 2); + decoder + .read_to_end(&mut decoded) + .map_err(|_| Error::MalformedGZip)?; + Ok(decoded) +} + +#[inline] +pub(crate) fn f32_bound(min: f32, val: f32, max: f32) -> f32 { + debug_assert!(min.is_finite()); + debug_assert!(val.is_finite()); + debug_assert!(max.is_finite()); + + if val > max { + max + } else if val < min { + min + } else { + val + } +} diff --git a/third_party/usvg/src/parser/options.rs b/third_party/usvg/src/parser/options.rs new file mode 100644 index 0000000000..fcf70b1146 --- /dev/null +++ b/third_party/usvg/src/parser/options.rs @@ -0,0 +1,144 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#[cfg(feature = "text")] +use std::sync::Arc; + +#[cfg(feature = "text")] +use crate::FontResolver; +use crate::{ImageHrefResolver, ImageRendering, ShapeRendering, Size, TextRendering}; + +/// Processing options. +#[derive(Debug)] +pub struct Options<'a> { + /// Directory that will be used during relative paths resolving. + /// + /// Expected to be the same as the directory that contains the SVG file, + /// but can be set to any. + /// + /// Default: `None` + pub resources_dir: Option, + + /// Target DPI. + /// + /// Impacts units conversion. + /// + /// Default: 96.0 + pub dpi: f32, + + /// A default font family. + /// + /// Will be used when no `font-family` attribute is set in the SVG. + /// + /// Default: Times New Roman + pub font_family: String, + + /// A default font size. + /// + /// Will be used when no `font-size` attribute is set in the SVG. + /// + /// Default: 12 + pub font_size: f32, + + /// A list of languages. + /// + /// Will be used to resolve a `systemLanguage` conditional attribute. + /// + /// Format: en, en-US. + /// + /// Default: `[en]` + pub languages: Vec, + + /// Specifies the default shape rendering method. + /// + /// Will be used when an SVG element's `shape-rendering` property is set to `auto`. + /// + /// Default: GeometricPrecision + pub shape_rendering: ShapeRendering, + + /// Specifies the default text rendering method. + /// + /// Will be used when an SVG element's `text-rendering` property is set to `auto`. + /// + /// Default: OptimizeLegibility + pub text_rendering: TextRendering, + + /// Specifies the default image rendering method. + /// + /// Will be used when an SVG element's `image-rendering` property is set to `auto`. + /// + /// Default: OptimizeQuality + pub image_rendering: ImageRendering, + + /// Default viewport size to assume if there is no `viewBox` attribute and + /// the `width` or `height` attributes are relative. + /// + /// Default: `(100, 100)` + pub default_size: Size, + + /// Specifies the way `xlink:href` in `` elements should be handled. + /// + /// Default: see type's documentation for details + pub image_href_resolver: ImageHrefResolver<'a>, + + /// Specifies how fonts should be resolved and loaded. + #[cfg(feature = "text")] + pub font_resolver: FontResolver<'a>, + + /// A database of fonts usable by text. + /// + /// This is a base database. If a custom `font_resolver` is specified, + /// additional fonts can be loaded during parsing. Those will be added to a + /// copy of this database. The full database containing all fonts referenced + /// in a `Tree` becomes available as [`Tree::fontdb`](crate::Tree::fontdb) + /// after parsing. If no fonts were loaded dynamically, that database will + /// be the same as this one. + #[cfg(feature = "text")] + pub fontdb: Arc, + /// A CSS stylesheet that should be injected into the SVG. Can be used to overwrite + /// certain attributes. + pub style_sheet: Option, +} + +impl Default for Options<'_> { + fn default() -> Options<'static> { + Options { + resources_dir: None, + dpi: 96.0, + // Default font is user-agent dependent so we can use whichever we like. + font_family: "Times New Roman".to_owned(), + font_size: 12.0, + languages: vec!["en".to_string()], + shape_rendering: ShapeRendering::default(), + text_rendering: TextRendering::default(), + image_rendering: ImageRendering::default(), + default_size: Size::from_wh(100.0, 100.0).unwrap(), + image_href_resolver: ImageHrefResolver::default(), + #[cfg(feature = "text")] + font_resolver: FontResolver::default(), + #[cfg(feature = "text")] + fontdb: Arc::new(fontdb::Database::new()), + style_sheet: None, + } + } +} + +impl Options<'_> { + /// Converts a relative path into absolute relative to the SVG file itself. + /// + /// If `Options::resources_dir` is not set, returns itself. + pub fn get_abs_path(&self, rel_path: &std::path::Path) -> std::path::PathBuf { + match self.resources_dir { + Some(ref dir) => dir.join(rel_path), + None => rel_path.into(), + } + } + + /// Mutably acquires the database. + /// + /// This clones the database if it is currently shared. + #[cfg(feature = "text")] + pub fn fontdb_mut(&mut self) -> &mut fontdb::Database { + Arc::make_mut(&mut self.fontdb) + } +} diff --git a/third_party/usvg/src/parser/paint_server.rs b/third_party/usvg/src/parser/paint_server.rs new file mode 100644 index 0000000000..17af79f07f --- /dev/null +++ b/third_party/usvg/src/parser/paint_server.rs @@ -0,0 +1,1087 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::str::FromStr; +use std::sync::Arc; + +use strict_num::PositiveF32; +use svgtypes::{Length, LengthUnit as Unit}; + +use super::converter::{self, Cache, SvgColorExt}; +use super::svgtree::{AId, EId, SvgNode}; +use super::OptionLog; +use crate::*; + +pub(crate) enum ServerOrColor { + Server(Paint), + Color { color: Color, opacity: Opacity }, +} + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + // Check for existing. + if let Some(paint) = cache.paint.get(node.element_id()) { + return Some(ServerOrColor::Server(paint.clone())); + } + + // Unwrap is safe, because we already checked for is_paint_server(). + let paint = match node.tag_name().unwrap() { + EId::LinearGradient => convert_linear(node, state), + EId::RadialGradient => convert_radial(node, state), + EId::Pattern => convert_pattern(node, state, cache), + _ => unreachable!(), + }; + + if let Some(ServerOrColor::Server(ref paint)) = paint { + cache + .paint + .insert(node.element_id().to_string(), paint.clone()); + } + + paint +} + +#[inline(never)] +fn convert_linear(node: SvgNode, state: &converter::State) -> Option { + let id = NonEmptyString::new(node.element_id().to_string())?; + + let stops = convert_stops(find_gradient_with_stops(node)?); + if stops.len() < 2 { + return stops_to_color(&stops); + } + + let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); + let transform = node.resolve_transform(AId::GradientTransform, state); + + let gradient = LinearGradient { + x1: resolve_number(node, AId::X1, units, state, Length::zero()), + y1: resolve_number(node, AId::Y1, units, state, Length::zero()), + x2: resolve_number( + node, + AId::X2, + units, + state, + Length::new(100.0, Unit::Percent), + ), + y2: resolve_number(node, AId::Y2, units, state, Length::zero()), + base: BaseGradient { + id, + units, + transform, + spread_method: convert_spread_method(node), + stops, + }, + }; + + Some(ServerOrColor::Server(Paint::LinearGradient(Arc::new( + gradient, + )))) +} + +#[inline(never)] +fn convert_radial(node: SvgNode, state: &converter::State) -> Option { + let id = NonEmptyString::new(node.element_id().to_string())?; + + let stops = convert_stops(find_gradient_with_stops(node)?); + if stops.len() < 2 { + return stops_to_color(&stops); + } + + let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); + let r = resolve_number(node, AId::R, units, state, Length::new(50.0, Unit::Percent)); + + // 'A value of zero will cause the area to be painted as a single color + // using the color and opacity of the last gradient stop.' + // + // https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementRAttribute + if !r.is_valid_length() { + let stop = stops.last().unwrap(); + return Some(ServerOrColor::Color { + color: stop.color, + opacity: stop.opacity, + }); + } + + let spread_method = convert_spread_method(node); + let cx = resolve_number( + node, + AId::Cx, + units, + state, + Length::new(50.0, Unit::Percent), + ); + let cy = resolve_number( + node, + AId::Cy, + units, + state, + Length::new(50.0, Unit::Percent), + ); + let fx = resolve_number(node, AId::Fx, units, state, Length::new_number(cx as f64)); + let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy as f64)); + let transform = node.resolve_transform(AId::GradientTransform, state); + + let gradient = RadialGradient { + cx, + cy, + r: PositiveF32::new(r).unwrap(), + fx, + fy, + base: BaseGradient { + id, + units, + transform, + spread_method, + stops, + }, + }; + + Some(ServerOrColor::Server(Paint::RadialGradient(Arc::new( + gradient, + )))) +} + +#[inline(never)] +fn convert_pattern( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + let node_with_children = find_pattern_with_children(node)?; + + let id = NonEmptyString::new(node.element_id().to_string())?; + + let view_box = { + let n1 = resolve_attr(node, AId::ViewBox); + let n2 = resolve_attr(node, AId::PreserveAspectRatio); + n1.parse_viewbox().map(|vb| ViewBox { + rect: vb, + aspect: n2.attribute(AId::PreserveAspectRatio).unwrap_or_default(), + }) + }; + + let units = convert_units(node, AId::PatternUnits, Units::ObjectBoundingBox); + let content_units = convert_units(node, AId::PatternContentUnits, Units::UserSpaceOnUse); + + let transform = node.resolve_transform(AId::PatternTransform, state); + + let rect = NonZeroRect::from_xywh( + resolve_number(node, AId::X, units, state, Length::zero()), + resolve_number(node, AId::Y, units, state, Length::zero()), + resolve_number(node, AId::Width, units, state, Length::zero()), + resolve_number(node, AId::Height, units, state, Length::zero()), + ); + let rect = rect.log_none(|| { + log::warn!( + "Pattern '{}' has an invalid size. Skipped.", + node.element_id() + ); + })?; + + let mut patt = Pattern { + id, + units, + content_units, + transform, + rect, + view_box, + root: Group::empty(), + }; + + // We can apply viewbox transform only for user space coordinates. + // Otherwise we need a bounding box, which is unknown at this point. + if patt.view_box.is_some() + && patt.units == Units::UserSpaceOnUse + && patt.content_units == Units::UserSpaceOnUse + { + let mut g = Group::empty(); + g.transform = view_box.unwrap().to_transform(rect.size()); + g.abs_transform = g.transform; + + converter::convert_children(node_with_children, state, cache, &mut g); + if !g.has_children() { + return None; + } + + g.calculate_bounding_boxes(); + patt.root.children.push(Node::Group(Box::new(g))); + } else { + converter::convert_children(node_with_children, state, cache, &mut patt.root); + if !patt.root.has_children() { + return None; + } + } + + patt.root.calculate_bounding_boxes(); + + Some(ServerOrColor::Server(Paint::Pattern(Arc::new(patt)))) +} + +fn convert_spread_method(node: SvgNode) -> SpreadMethod { + let node = resolve_attr(node, AId::SpreadMethod); + node.attribute(AId::SpreadMethod).unwrap_or_default() +} + +pub(crate) fn convert_units(node: SvgNode, name: AId, def: Units) -> Units { + let node = resolve_attr(node, name); + node.attribute(name).unwrap_or(def) +} + +fn find_gradient_with_stops<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, +) -> Option> { + for link in node.href_iter() { + if !link.tag_name().unwrap().is_gradient() { + log::warn!( + "Gradient '{}' cannot reference '{}' via 'xlink:href'.", + node.element_id(), + link.tag_name().unwrap() + ); + return None; + } + + if link.children().any(|n| n.tag_name() == Some(EId::Stop)) { + return Some(link); + } + } + + None +} + +fn find_pattern_with_children<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, +) -> Option> { + for link in node.href_iter() { + if link.tag_name() != Some(EId::Pattern) { + log::warn!( + "Pattern '{}' cannot reference '{}' via 'xlink:href'.", + node.element_id(), + link.tag_name().unwrap() + ); + return None; + } + + if link.has_children() { + return Some(link); + } + } + + None +} + +fn convert_stops(grad: SvgNode) -> Vec { + let mut stops = Vec::new(); + + { + let mut prev_offset = Length::zero(); + for stop in grad.children() { + if stop.tag_name() != Some(EId::Stop) { + log::warn!("Invalid gradient child: '{:?}'.", stop.tag_name().unwrap()); + continue; + } + + // `number` can be either a number or a percentage. + let offset = stop.attribute(AId::Offset).unwrap_or(prev_offset); + let offset = match offset.unit { + Unit::None => offset.number, + Unit::Percent => offset.number / 100.0, + _ => prev_offset.number, + }; + prev_offset = Length::new_number(offset); + let offset = crate::f32_bound(0.0, offset as f32, 1.0); + + let (color, opacity) = match stop.attribute(AId::StopColor) { + Some("currentColor") => stop + .find_attribute(AId::Color) + .unwrap_or_else(svgtypes::Color::black), + Some(value) => { + if let Ok(c) = svgtypes::Color::from_str(value) { + c + } else { + log::warn!("Failed to parse stop-color value: '{}'.", value); + svgtypes::Color::black() + } + } + _ => svgtypes::Color::black(), + } + .split_alpha(); + + let stop_opacity = stop + .attribute::(AId::StopOpacity) + .unwrap_or(Opacity::ONE); + stops.push(Stop { + offset: StopOffset::new_clamped(offset), + color, + opacity: opacity * stop_opacity, + }); + } + } + + // Remove stops with equal offset. + // + // Example: + // offset="0.5" + // offset="0.7" + // offset="0.7" <-- this one should be removed + // offset="0.7" + // offset="0.9" + if stops.len() >= 3 { + let mut i = 0; + while i < stops.len() - 2 { + let offset1 = stops[i + 0].offset.get(); + let offset2 = stops[i + 1].offset.get(); + let offset3 = stops[i + 2].offset.get(); + + if offset1.approx_eq_ulps(&offset2, 4) && offset2.approx_eq_ulps(&offset3, 4) { + // Remove offset in the middle. + stops.remove(i + 1); + } else { + i += 1; + } + } + } + + // Remove zeros. + // + // From: + // offset="0.0" + // offset="0.0" + // offset="0.7" + // + // To: + // offset="0.0" + // offset="0.00000001" + // offset="0.7" + if stops.len() >= 2 { + let mut i = 0; + while i < stops.len() - 1 { + let offset1 = stops[i + 0].offset.get(); + let offset2 = stops[i + 1].offset.get(); + + if offset1.approx_eq_ulps(&0.0, 4) && offset2.approx_eq_ulps(&0.0, 4) { + stops[i + 1].offset = StopOffset::new_clamped(offset1 + f32::EPSILON); + } + + i += 1; + } + } + + // Shift equal offsets. + // + // From: + // offset="0.5" + // offset="0.7" + // offset="0.7" + // + // To: + // offset="0.5" + // offset="0.699999999" + // offset="0.7" + { + let mut i = 1; + while i < stops.len() { + let offset1 = stops[i - 1].offset.get(); + let offset2 = stops[i - 0].offset.get(); + + // Next offset must be smaller then previous. + if offset1 > offset2 || offset1.approx_eq_ulps(&offset2, 4) { + // Make previous offset a bit smaller. + let new_offset = offset1 - f32::EPSILON; + stops[i - 1].offset = StopOffset::new_clamped(new_offset); + stops[i - 0].offset = StopOffset::new_clamped(offset1); + } + + i += 1; + } + } + + stops +} + +#[inline(never)] +pub(crate) fn resolve_number( + node: SvgNode, + name: AId, + units: Units, + state: &converter::State, + def: Length, +) -> f32 { + resolve_attr(node, name).convert_length(name, units, state, def) +} + +fn resolve_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + if node.has_attribute(name) { + return node; + } + + match node.tag_name().unwrap() { + EId::LinearGradient => resolve_lg_attr(node, name), + EId::RadialGradient => resolve_rg_attr(node, name), + EId::Pattern => resolve_pattern_attr(node, name), + EId::Filter => resolve_filter_attr(node, name), + _ => node, + } +} + +fn resolve_lg_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + match (name, tag_name) { + // Coordinates can be resolved only from + // ref element with the same type. + (AId::X1, EId::LinearGradient) + | (AId::Y1, EId::LinearGradient) + | (AId::X2, EId::LinearGradient) + | (AId::Y2, EId::LinearGradient) + // Other attributes can be resolved + // from any kind of gradient. + | (AId::GradientUnits, EId::LinearGradient) + | (AId::GradientUnits, EId::RadialGradient) + | (AId::SpreadMethod, EId::LinearGradient) + | (AId::SpreadMethod, EId::RadialGradient) + | (AId::GradientTransform, EId::LinearGradient) + | (AId::GradientTransform, EId::RadialGradient) => { + if link.has_attribute(name) { + return link; + } + } + _ => break, + } + } + + node +} + +fn resolve_rg_attr<'a, 'input>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + match (name, tag_name) { + // Coordinates can be resolved only from + // ref element with the same type. + (AId::Cx, EId::RadialGradient) + | (AId::Cy, EId::RadialGradient) + | (AId::R, EId::RadialGradient) + | (AId::Fx, EId::RadialGradient) + | (AId::Fy, EId::RadialGradient) + // Other attributes can be resolved + // from any kind of gradient. + | (AId::GradientUnits, EId::LinearGradient) + | (AId::GradientUnits, EId::RadialGradient) + | (AId::SpreadMethod, EId::LinearGradient) + | (AId::SpreadMethod, EId::RadialGradient) + | (AId::GradientTransform, EId::LinearGradient) + | (AId::GradientTransform, EId::RadialGradient) => { + if link.has_attribute(name) { + return link; + } + } + _ => break, + } + } + + node +} + +fn resolve_pattern_attr<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, + name: AId, +) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + if tag_name != EId::Pattern { + break; + } + + if link.has_attribute(name) { + return link; + } + } + + node +} + +fn resolve_filter_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, aid: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + if tag_name != EId::Filter { + break; + } + + if link.has_attribute(aid) { + return link; + } + } + + node +} + +fn stops_to_color(stops: &[Stop]) -> Option { + if stops.is_empty() { + None + } else { + Some(ServerOrColor::Color { + color: stops[0].color, + opacity: stops[0].opacity, + }) + } +} + +// Update paints servers by doing the following: +// 1. Replace context fills/strokes that are linked to +// a use node with their actual values. +// 2. Convert all object units to UserSpaceOnUse +pub fn update_paint_servers( + group: &mut Group, + context_transform: Transform, + context_bbox: Option, + text_bbox: Option, + cache: &mut Cache, +) { + for child in &mut group.children { + // Set context transform and bbox if applicable if the + // current group is a use node. + let (context_transform, context_bbox) = if group.is_context_element { + (group.abs_transform, Some(group.bounding_box)) + } else { + (context_transform, context_bbox) + }; + + node_to_user_coordinates(child, context_transform, context_bbox, text_bbox, cache); + } +} + +// When parsing clipPaths, masks and filters we already know group's bounding box. +// But with gradients and patterns we don't, because we have to know text bounding box +// before we even parsed it. Which is impossible. +// Therefore our only choice is to parse gradients and patterns preserving their units +// and then replace them with `userSpaceOnUse` after the whole tree parsing is finished. +// So while gradients and patterns do still store their units, +// they are not exposed in the public API and for the caller they are always `userSpaceOnUse`. +fn node_to_user_coordinates( + node: &mut Node, + context_transform: Transform, + context_bbox: Option, + text_bbox: Option, + cache: &mut Cache, +) { + match node { + Node::Group(ref mut g) => { + // No need to check clip paths, because they cannot have paint servers. + if let Some(ref mut mask) = g.mask { + if let Some(ref mut mask) = Arc::get_mut(mask) { + update_paint_servers( + &mut mask.root, + context_transform, + context_bbox, + None, + cache, + ); + + if let Some(ref mut sub_mask) = mask.mask { + if let Some(ref mut sub_mask) = Arc::get_mut(sub_mask) { + update_paint_servers( + &mut sub_mask.root, + context_transform, + context_bbox, + None, + cache, + ); + } + } + } + } + + for filter in &mut g.filters { + if let Some(ref mut filter) = Arc::get_mut(filter) { + for primitive in &mut filter.primitives { + if let filter::Kind::Image(ref mut image) = primitive.kind { + update_paint_servers( + &mut image.root, + context_transform, + context_bbox, + None, + cache, + ); + } + } + } + } + + update_paint_servers(g, context_transform, context_bbox, text_bbox, cache); + } + Node::Path(ref mut path) => { + // Paths inside `Text::flattened` are special and must use text's bounding box + // instead of their own. + let bbox = text_bbox.unwrap_or(path.bounding_box); + + process_fill( + &mut path.fill, + path.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut path.stroke, + path.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + } + Node::Image(ref mut image) => { + if let ImageKind::SVG(ref mut tree) = image.kind { + update_paint_servers(&mut tree.root, context_transform, context_bbox, None, cache); + } + } + Node::Text(ref mut text) => { + // By the SVG spec, `tspan` doesn't have a bbox and uses the parent `text` bbox. + // Therefore we have to use text's bbox when converting tspan and flatted text + // paint servers. + let bbox = text.bounding_box; + + // We need to update three things: + // 1. The fills/strokes of the original elements in the usvg tree. + // 2. The fills/strokes of the layouted elements of the text. + // 3. The fills/strokes of the outlined text. + + // 1. + for chunk in &mut text.chunks { + for span in &mut chunk.spans { + process_fill( + &mut span.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut span.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_text_decoration(&mut span.decoration.underline, bbox, cache); + process_text_decoration(&mut span.decoration.overline, bbox, cache); + process_text_decoration(&mut span.decoration.line_through, bbox, cache); + } + } + + // 2. + #[cfg(feature = "text")] + for span in &mut text.layouted { + process_fill( + &mut span.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut span.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + + let mut process_decoration = |path: &mut Path| { + process_fill( + &mut path.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut path.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + }; + + if let Some(ref mut path) = span.overline { + process_decoration(path); + } + + if let Some(ref mut path) = span.underline { + process_decoration(path); + } + + if let Some(ref mut path) = span.line_through { + process_decoration(path); + } + } + + // 3. + update_paint_servers( + &mut text.flattened, + context_transform, + context_bbox, + Some(bbox), + cache, + ); + } + } +} + +fn process_fill( + fill: &mut Option, + path_transform: Transform, + context_transform: Transform, + context_bbox: Option, + bbox: Rect, + cache: &mut Cache, +) { + let mut ok = false; + if let Some(ref mut fill) = fill { + // Path context elements (i.e. for markers) have already been resolved, + // so we only care about use nodes. + ok = process_paint( + &mut fill.paint, + matches!(fill.context_element, Some(ContextElement::UseNode)), + context_transform, + context_bbox, + path_transform, + bbox, + cache, + ); + } + if !ok { + *fill = None; + } +} + +fn process_stroke( + stroke: &mut Option, + path_transform: Transform, + context_transform: Transform, + context_bbox: Option, + bbox: Rect, + cache: &mut Cache, +) { + let mut ok = false; + if let Some(ref mut stroke) = stroke { + // Path context elements (i.e. for markers) have already been resolved, + // so we only care about use nodes. + ok = process_paint( + &mut stroke.paint, + matches!(stroke.context_element, Some(ContextElement::UseNode)), + context_transform, + context_bbox, + path_transform, + bbox, + cache, + ); + } + if !ok { + *stroke = None; + } +} + +fn process_context_paint( + paint: &mut Paint, + context_transform: Transform, + path_transform: Transform, + cache: &mut Cache, +) -> Option<()> { + // The idea is the following: We have a certain context element that has + // a transform A, and further below in the tree we have for example a path + // whose paint has a transform C. In order to get from A to C, there is some + // transformation matrix B such that A x B = C. We now need to figure out + // a way to get from C back to A, so that the transformation of the paint + // matches the one from the context element, even if B was applied. How + // do we do that? We calculate CxB^(-1), which will overall then have + // the same effect as A. How do we calculate B^(-1)? + // --> (A^(-1)xC)^(-1) + let rev_transform = context_transform + .invert()? + .pre_concat(path_transform) + .invert()?; + + match paint { + Paint::Color(_) => {} + Paint::LinearGradient(ref lg) => { + let transform = lg.transform.post_concat(rev_transform); + *paint = Paint::LinearGradient(Arc::new(LinearGradient { + x1: lg.x1, + y1: lg.y1, + x2: lg.x2, + y2: lg.y2, + base: BaseGradient { + id: cache.gen_linear_gradient_id(), + units: lg.units, + transform, + spread_method: lg.spread_method, + stops: lg.stops.clone(), + }, + })); + } + Paint::RadialGradient(ref rg) => { + let transform = rg.transform.post_concat(rev_transform); + *paint = Paint::RadialGradient(Arc::new(RadialGradient { + cx: rg.cx, + cy: rg.cy, + r: rg.r, + fx: rg.fx, + fy: rg.fy, + base: BaseGradient { + id: cache.gen_radial_gradient_id(), + units: rg.units, + transform, + spread_method: rg.spread_method, + stops: rg.stops.clone(), + }, + })); + } + Paint::Pattern(ref pat) => { + let transform = pat.transform.post_concat(rev_transform); + *paint = Paint::Pattern(Arc::new(Pattern { + id: cache.gen_pattern_id(), + units: pat.units, + content_units: pat.content_units, + transform, + rect: pat.rect, + view_box: pat.view_box, + root: pat.root.clone(), + })); + } + } + + Some(()) +} + +pub(crate) fn process_paint( + paint: &mut Paint, + has_context: bool, + context_transform: Transform, + context_bbox: Option, + path_transform: Transform, + bbox: Rect, + cache: &mut Cache, +) -> bool { + if paint.units() == Units::ObjectBoundingBox + || paint.content_units() == Units::ObjectBoundingBox + { + let bbox = if has_context { + let Some(bbox) = context_bbox else { + return false; + }; + bbox + } else { + bbox + }; + + if paint.to_user_coordinates(bbox, cache).is_none() { + return false; + } + } + + if let Paint::Pattern(ref mut patt) = paint { + if let Some(ref mut patt) = Arc::get_mut(patt) { + update_paint_servers(&mut patt.root, Transform::default(), None, None, cache); + } + } + + if has_context { + process_context_paint(paint, context_transform, path_transform, cache); + } + + true +} + +fn process_text_decoration(style: &mut Option, bbox: Rect, cache: &mut Cache) { + if let Some(ref mut style) = style { + process_fill( + &mut style.fill, + Transform::default(), + Transform::default(), + None, + bbox, + cache, + ); + process_stroke( + &mut style.stroke, + Transform::default(), + Transform::default(), + None, + bbox, + cache, + ); + } +} + +impl Paint { + fn to_user_coordinates(&mut self, bbox: Rect, cache: &mut Cache) -> Option<()> { + let name = if matches!(self, Paint::Pattern(_)) { + "Pattern" + } else { + "Gradient" + }; + let bbox = bbox + .to_non_zero_rect() + .log_none(|| log::warn!("{} on zero-sized shapes is not allowed.", name))?; + + // `Arc::get_mut()` allow us to modify some paint servers in-place. + // This reduces the amount of cloning and preserves the original ID as well. + match self { + Paint::Color(_) => {} // unreachable + Paint::LinearGradient(ref mut lg) => { + let transform = lg.transform.post_concat(Transform::from_bbox(bbox)); + if let Some(ref mut lg) = Arc::get_mut(lg) { + lg.base.transform = transform; + lg.base.units = Units::UserSpaceOnUse; + } else { + *lg = Arc::new(LinearGradient { + x1: lg.x1, + y1: lg.y1, + x2: lg.x2, + y2: lg.y2, + base: BaseGradient { + id: cache.gen_linear_gradient_id(), + units: Units::UserSpaceOnUse, + transform, + spread_method: lg.spread_method, + stops: lg.stops.clone(), + }, + }); + } + } + Paint::RadialGradient(ref mut rg) => { + let transform = rg.transform.post_concat(Transform::from_bbox(bbox)); + if let Some(ref mut rg) = Arc::get_mut(rg) { + rg.base.transform = transform; + rg.base.units = Units::UserSpaceOnUse; + } else { + *rg = Arc::new(RadialGradient { + cx: rg.cx, + cy: rg.cy, + r: rg.r, + fx: rg.fx, + fy: rg.fy, + base: BaseGradient { + id: cache.gen_radial_gradient_id(), + units: Units::UserSpaceOnUse, + transform, + spread_method: rg.spread_method, + stops: rg.stops.clone(), + }, + }); + } + } + Paint::Pattern(ref mut patt) => { + let rect = if patt.units == Units::ObjectBoundingBox { + patt.rect.bbox_transform(bbox) + } else { + patt.rect + }; + + if let Some(ref mut patt) = Arc::get_mut(patt) { + patt.rect = rect; + patt.units = Units::UserSpaceOnUse; + + if patt.content_units == Units::ObjectBoundingBox && patt.view_box.is_none() { + // No need to shift patterns. + let transform = Transform::from_scale(bbox.width(), bbox.height()); + push_pattern_transform(&mut patt.root, transform); + } + + if let Some(view_box) = patt.view_box { + push_pattern_transform(&mut patt.root, view_box.to_transform(rect.size())); + } + + patt.content_units = Units::UserSpaceOnUse; + } else { + let mut root = if patt.content_units == Units::ObjectBoundingBox + && patt.view_box.is_none() + { + // No need to shift patterns. + let transform = Transform::from_scale(bbox.width(), bbox.height()); + + let mut g = patt.root.clone(); + push_pattern_transform(&mut g, transform); + g + } else { + patt.root.clone() + }; + + if let Some(view_box) = patt.view_box { + push_pattern_transform(&mut root, view_box.to_transform(rect.size())); + } + + *patt = Arc::new(Pattern { + id: cache.gen_pattern_id(), + units: Units::UserSpaceOnUse, + content_units: Units::UserSpaceOnUse, + transform: patt.transform, + rect, + view_box: patt.view_box, + root, + }); + } + } + } + + Some(()) + } +} + +fn push_pattern_transform(root: &mut Group, transform: Transform) { + // TODO: we should update abs_transform in all descendants as well + let mut g = std::mem::replace(root, Group::empty()); + g.transform = transform; + g.abs_transform = transform; + + root.children.push(Node::Group(Box::new(g))); + root.calculate_bounding_boxes(); +} + +impl Paint { + #[inline] + pub(crate) fn units(&self) -> Units { + match self { + Self::Color(_) => Units::UserSpaceOnUse, + Self::LinearGradient(ref lg) => lg.units, + Self::RadialGradient(ref rg) => rg.units, + Self::Pattern(ref patt) => patt.units, + } + } + + #[inline] + pub(crate) fn content_units(&self) -> Units { + match self { + Self::Pattern(ref patt) => patt.content_units, + _ => Units::UserSpaceOnUse, + } + } +} diff --git a/third_party/usvg/src/parser/shapes.rs b/third_party/usvg/src/parser/shapes.rs new file mode 100644 index 0000000000..6cfbfb3051 --- /dev/null +++ b/third_party/usvg/src/parser/shapes.rs @@ -0,0 +1,329 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use svgtypes::Length; +use tiny_skia_path::Path; + +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, units}; +use crate::{ApproxEqUlps, IsValidLength, Rect}; + +pub(crate) fn convert(node: SvgNode, state: &converter::State) -> Option> { + match node.tag_name()? { + EId::Rect => convert_rect(node, state), + EId::Circle => convert_circle(node, state), + EId::Ellipse => convert_ellipse(node, state), + EId::Line => convert_line(node, state), + EId::Polyline => convert_polyline(node), + EId::Polygon => convert_polygon(node), + EId::Path => convert_path(node), + _ => None, + } +} + +pub(crate) fn convert_path(node: SvgNode) -> Option> { + let value: &str = node.attribute(AId::D)?; + let mut builder = tiny_skia_path::PathBuilder::new(); + for segment in svgtypes::SimplifyingPathParser::from(value) { + let segment = match segment { + Ok(v) => v, + Err(_) => break, + }; + + match segment { + svgtypes::SimplePathSegment::MoveTo { x, y } => { + builder.move_to(x as f32, y as f32); + } + svgtypes::SimplePathSegment::LineTo { x, y } => { + builder.line_to(x as f32, y as f32); + } + svgtypes::SimplePathSegment::Quadratic { x1, y1, x, y } => { + builder.quad_to(x1 as f32, y1 as f32, x as f32, y as f32); + } + svgtypes::SimplePathSegment::CurveTo { + x1, + y1, + x2, + y2, + x, + y, + } => { + builder.cubic_to( + x1 as f32, y1 as f32, x2 as f32, y2 as f32, x as f32, y as f32, + ); + } + svgtypes::SimplePathSegment::ClosePath => { + builder.close(); + } + } + } + + builder.finish().map(Arc::new) +} + +fn convert_rect(node: SvgNode, state: &converter::State) -> Option> { + // 'width' and 'height' attributes must be positive and non-zero. + let width = node.convert_user_length(AId::Width, state, Length::zero()); + let height = node.convert_user_length(AId::Height, state, Length::zero()); + if !width.is_valid_length() { + log::warn!( + "Rect '{}' has an invalid 'width' value. Skipped.", + node.element_id() + ); + return None; + } + if !height.is_valid_length() { + log::warn!( + "Rect '{}' has an invalid 'height' value. Skipped.", + node.element_id() + ); + return None; + } + + let x = node.convert_user_length(AId::X, state, Length::zero()); + let y = node.convert_user_length(AId::Y, state, Length::zero()); + + let (mut rx, mut ry) = resolve_rx_ry(node, state); + + // Clamp rx/ry to the half of the width/height. + // + // Should be done only after resolving. + if rx > width / 2.0 { + rx = width / 2.0; + } + if ry > height / 2.0 { + ry = height / 2.0; + } + + // Conversion according to https://www.w3.org/TR/SVG11/shapes.html#RectElement + let path = if rx.approx_eq_ulps(&0.0, 4) { + tiny_skia_path::PathBuilder::from_rect(Rect::from_xywh(x, y, width, height)?) + } else { + let mut builder = tiny_skia_path::PathBuilder::new(); + builder.move_to(x + rx, y); + + builder.line_to(x + width - rx, y); + builder.arc_to(rx, ry, 0.0, false, true, x + width, y + ry); + + builder.line_to(x + width, y + height - ry); + builder.arc_to(rx, ry, 0.0, false, true, x + width - rx, y + height); + + builder.line_to(x + rx, y + height); + builder.arc_to(rx, ry, 0.0, false, true, x, y + height - ry); + + builder.line_to(x, y + ry); + builder.arc_to(rx, ry, 0.0, false, true, x + rx, y); + + builder.close(); + + builder.finish()? + }; + + Some(Arc::new(path)) +} + +fn resolve_rx_ry(node: SvgNode, state: &converter::State) -> (f32, f32) { + let mut rx_opt = node.attribute::(AId::Rx); + let mut ry_opt = node.attribute::(AId::Ry); + + // Remove negative values first. + if let Some(v) = rx_opt { + if v.number.is_sign_negative() { + rx_opt = None; + } + } + if let Some(v) = ry_opt { + if v.number.is_sign_negative() { + ry_opt = None; + } + } + + // Resolve. + match (rx_opt, ry_opt) { + (None, None) => (0.0, 0.0), + (Some(rx), None) => { + let rx = units::convert_user_length(rx, node, AId::Rx, state); + (rx, rx) + } + (None, Some(ry)) => { + let ry = units::convert_user_length(ry, node, AId::Ry, state); + (ry, ry) + } + (Some(rx), Some(ry)) => { + let rx = units::convert_user_length(rx, node, AId::Rx, state); + let ry = units::convert_user_length(ry, node, AId::Ry, state); + (rx, ry) + } + } +} + +fn convert_line(node: SvgNode, state: &converter::State) -> Option> { + let x1 = node.convert_user_length(AId::X1, state, Length::zero()); + let y1 = node.convert_user_length(AId::Y1, state, Length::zero()); + let x2 = node.convert_user_length(AId::X2, state, Length::zero()); + let y2 = node.convert_user_length(AId::Y2, state, Length::zero()); + + let mut builder = tiny_skia_path::PathBuilder::new(); + builder.move_to(x1, y1); + builder.line_to(x2, y2); + builder.finish().map(Arc::new) +} + +fn convert_polyline(node: SvgNode) -> Option> { + let builder = points_to_path(node, "Polyline")?; + builder.finish().map(Arc::new) +} + +fn convert_polygon(node: SvgNode) -> Option> { + let mut builder = points_to_path(node, "Polygon")?; + builder.close(); + builder.finish().map(Arc::new) +} + +fn points_to_path(node: SvgNode, eid: &str) -> Option { + use svgtypes::PointsParser; + + let mut builder = tiny_skia_path::PathBuilder::new(); + match node.attribute::<&str>(AId::Points) { + Some(text) => { + for (x, y) in PointsParser::from(text) { + if builder.is_empty() { + builder.move_to(x as f32, y as f32); + } else { + builder.line_to(x as f32, y as f32); + } + } + } + _ => { + log::warn!( + "{} '{}' has an invalid 'points' value. Skipped.", + eid, + node.element_id() + ); + return None; + } + }; + + // 'polyline' and 'polygon' elements must contain at least 2 points. + if builder.len() < 2 { + log::warn!( + "{} '{}' has less than 2 points. Skipped.", + eid, + node.element_id() + ); + return None; + } + + Some(builder) +} + +fn convert_circle(node: SvgNode, state: &converter::State) -> Option> { + let cx = node.convert_user_length(AId::Cx, state, Length::zero()); + let cy = node.convert_user_length(AId::Cy, state, Length::zero()); + let r = node.convert_user_length(AId::R, state, Length::zero()); + + if !r.is_valid_length() { + log::warn!( + "Circle '{}' has an invalid 'r' value. Skipped.", + node.element_id() + ); + return None; + } + + ellipse_to_path(cx, cy, r, r) +} + +fn convert_ellipse(node: SvgNode, state: &converter::State) -> Option> { + let cx = node.convert_user_length(AId::Cx, state, Length::zero()); + let cy = node.convert_user_length(AId::Cy, state, Length::zero()); + let (rx, ry) = resolve_rx_ry(node, state); + + if !rx.is_valid_length() { + log::warn!( + "Ellipse '{}' has an invalid 'rx' value. Skipped.", + node.element_id() + ); + return None; + } + + if !ry.is_valid_length() { + log::warn!( + "Ellipse '{}' has an invalid 'ry' value. Skipped.", + node.element_id() + ); + return None; + } + + ellipse_to_path(cx, cy, rx, ry) +} + +fn ellipse_to_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Option> { + let mut builder = tiny_skia_path::PathBuilder::new(); + builder.move_to(cx + rx, cy); + builder.arc_to(rx, ry, 0.0, false, true, cx, cy + ry); + builder.arc_to(rx, ry, 0.0, false, true, cx - rx, cy); + builder.arc_to(rx, ry, 0.0, false, true, cx, cy - ry); + builder.arc_to(rx, ry, 0.0, false, true, cx + rx, cy); + builder.close(); + builder.finish().map(Arc::new) +} + +trait PathBuilderExt { + fn arc_to( + &mut self, + rx: f32, + ry: f32, + x_axis_rotation: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ); +} + +impl PathBuilderExt for tiny_skia_path::PathBuilder { + fn arc_to( + &mut self, + rx: f32, + ry: f32, + x_axis_rotation: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ) { + let prev = match self.last_point() { + Some(v) => v, + None => return, + }; + + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(prev.x as f64, prev.y as f64), + to: kurbo::Point::new(x as f64, y as f64), + radii: kurbo::Vec2::new(rx as f64, ry as f64), + x_rotation: (x_axis_rotation as f64).to_radians(), + large_arc, + sweep, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + arc.to_cubic_beziers(0.1, |p1, p2, p| { + self.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }); + } + None => { + self.line_to(x, y); + } + } + } +} diff --git a/third_party/usvg/src/parser/style.rs b/third_party/usvg/src/parser/style.rs new file mode 100644 index 0000000000..cefdc5c135 --- /dev/null +++ b/third_party/usvg/src/parser/style.rs @@ -0,0 +1,296 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use super::converter::{self, SvgColorExt}; +use super::paint_server; +use super::svgtree::{AId, FromValue, SvgNode}; +use crate::tree::ContextElement; +use crate::{ + ApproxEqUlps, Color, Fill, FillRule, LineCap, LineJoin, Opacity, Paint, Stroke, + StrokeMiterlimit, Units, +}; + +impl<'a, 'input: 'a> FromValue<'a, 'input> for LineCap { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "butt" => Some(LineCap::Butt), + "round" => Some(LineCap::Round), + "square" => Some(LineCap::Square), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for LineJoin { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "miter" => Some(LineJoin::Miter), + "miter-clip" => Some(LineJoin::MiterClip), + "round" => Some(LineJoin::Round), + "bevel" => Some(LineJoin::Bevel), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for FillRule { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "nonzero" => Some(FillRule::NonZero), + "evenodd" => Some(FillRule::EvenOdd), + _ => None, + } + } +} + +pub(crate) fn resolve_fill( + node: SvgNode, + has_bbox: bool, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + if state.parent_clip_path.is_some() { + // A `clipPath` child can be filled only with a black color. + return Some(Fill { + paint: Paint::Color(Color::black()), + opacity: Opacity::ONE, + rule: node.find_attribute(AId::ClipRule).unwrap_or_default(), + context_element: None, + }); + } + + let mut sub_opacity = Opacity::ONE; + let (paint, context_element) = + if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Fill)) { + let value: &str = n.attribute(AId::Fill)?; + convert_paint( + node, + value, + AId::Fill, + has_bbox, + state, + &mut sub_opacity, + cache, + )? + } else { + (Paint::Color(Color::black()), None) + }; + + let fill_opacity = node + .find_attribute::(AId::FillOpacity) + .unwrap_or(Opacity::ONE); + + Some(Fill { + paint, + opacity: sub_opacity * fill_opacity, + rule: node.find_attribute(AId::FillRule).unwrap_or_default(), + context_element, + }) +} + +pub(crate) fn resolve_stroke( + node: SvgNode, + has_bbox: bool, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + if state.parent_clip_path.is_some() { + // A `clipPath` child cannot be stroked. + return None; + } + + let mut sub_opacity = Opacity::ONE; + let (paint, context_element) = + if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Stroke)) { + let value: &str = n.attribute(AId::Stroke)?; + + convert_paint( + node, + value, + AId::Stroke, + has_bbox, + state, + &mut sub_opacity, + cache, + )? + } else { + return None; + }; + + let width = node.resolve_valid_length(AId::StrokeWidth, state, 1.0)?; + + // Must be bigger than 1. + let miterlimit = node.find_attribute(AId::StrokeMiterlimit).unwrap_or(4.0); + let miterlimit = if miterlimit < 1.0 { 1.0 } else { miterlimit }; + let miterlimit = StrokeMiterlimit::new(miterlimit); + + let stroke_opacity = node + .find_attribute::(AId::StrokeOpacity) + .unwrap_or(Opacity::ONE); + + let stroke = Stroke { + paint, + dasharray: conv_dasharray(node, state), + dashoffset: node.resolve_length(AId::StrokeDashoffset, state, 0.0), + miterlimit, + opacity: sub_opacity * stroke_opacity, + width, + linecap: node.find_attribute(AId::StrokeLinecap).unwrap_or_default(), + linejoin: node.find_attribute(AId::StrokeLinejoin).unwrap_or_default(), + context_element, + }; + + Some(stroke) +} + +fn convert_paint( + node: SvgNode, + value: &str, + aid: AId, + has_bbox: bool, + state: &converter::State, + opacity: &mut Opacity, + cache: &mut converter::Cache, +) -> Option<(Paint, Option)> { + let paint = match svgtypes::Paint::from_str(value) { + Ok(v) => v, + Err(_) => { + if aid == AId::Fill { + log::warn!( + "Failed to parse fill value: '{}'. Fallback to black.", + value + ); + svgtypes::Paint::Color(svgtypes::Color::black()) + } else if aid == AId::Stroke { + log::warn!( + "Failed to parse stroke value: '{}'. Fallback to no stroke.", + value + ); + return None; + } else { + return None; + } + } + }; + + match paint { + svgtypes::Paint::None => None, + svgtypes::Paint::Inherit => None, // already resolved by svgtree + svgtypes::Paint::ContextFill => state + .context_element + .clone() + .and_then(|(f, _)| f) + .map(|f| (f.paint, f.context_element)), + svgtypes::Paint::ContextStroke => state + .context_element + .clone() + .and_then(|(_, s)| s) + .map(|s| (s.paint, s.context_element)), + svgtypes::Paint::CurrentColor => { + let svg_color: svgtypes::Color = node + .find_attribute(AId::Color) + .unwrap_or_else(svgtypes::Color::black); + let (color, alpha) = svg_color.split_alpha(); + *opacity = alpha; + Some((Paint::Color(color), None)) + } + svgtypes::Paint::Color(svg_color) => { + let (color, alpha) = svg_color.split_alpha(); + *opacity = alpha; + Some((Paint::Color(color), None)) + } + svgtypes::Paint::FuncIRI(func_iri, fallback) => { + if let Some(link) = node.document().element_by_id(func_iri) { + let tag_name = link.tag_name().unwrap(); + if tag_name.is_paint_server() { + match paint_server::convert(link, state, cache) { + Some(paint_server::ServerOrColor::Server(paint)) => { + // We can use a paint server node with ObjectBoundingBox units + // for painting only when the shape itself has a bbox. + // + // See SVG spec 7.11 for details. + + if !has_bbox && paint.units() == Units::ObjectBoundingBox { + from_fallback(node, fallback, opacity).map(|p| (p, None)) + } else { + Some((paint, None)) + } + } + Some(paint_server::ServerOrColor::Color { color, opacity: so }) => { + *opacity = so; + Some((Paint::Color(color), None)) + } + None => from_fallback(node, fallback, opacity).map(|p| (p, None)), + } + } else { + log::warn!("'{}' cannot be used to {} a shape.", tag_name, aid); + None + } + } else { + from_fallback(node, fallback, opacity).map(|p| (p, None)) + } + } + } +} + +fn from_fallback( + node: SvgNode, + fallback: Option, + opacity: &mut Opacity, +) -> Option { + match fallback? { + svgtypes::PaintFallback::None => None, + svgtypes::PaintFallback::CurrentColor => { + let svg_color: svgtypes::Color = node + .find_attribute(AId::Color) + .unwrap_or_else(svgtypes::Color::black); + let (color, alpha) = svg_color.split_alpha(); + *opacity = alpha; + Some(Paint::Color(color)) + } + svgtypes::PaintFallback::Color(svg_color) => { + let (color, alpha) = svg_color.split_alpha(); + *opacity = alpha; + Some(Paint::Color(color)) + } + } +} + +// Prepare the 'stroke-dasharray' according to: +// https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty +fn conv_dasharray(node: SvgNode, state: &converter::State) -> Option> { + let node = node + .ancestors() + .find(|n| n.has_attribute(AId::StrokeDasharray))?; + let list = super::units::convert_list(node, AId::StrokeDasharray, state)?; + + // `A negative value is an error` + if list.iter().any(|n| n.is_sign_negative()) { + return None; + } + + // `If the sum of the values is zero, then the stroke is rendered + // as if a value of none were specified.` + { + // no Iter::sum(), because of f64 + + let mut sum: f32 = 0.0; + for n in list.iter() { + sum += *n; + } + + if sum.approx_eq_ulps(&0.0, 4) { + return None; + } + } + + // `If an odd number of values is provided, then the list of values + // is repeated to yield an even number of values.` + if list.len() % 2 != 0 { + let mut tmp_list = list.clone(); + tmp_list.extend_from_slice(&list); + return Some(tmp_list); + } + + Some(list) +} diff --git a/third_party/usvg/src/parser/svgtree/mod.rs b/third_party/usvg/src/parser/svgtree/mod.rs new file mode 100644 index 0000000000..30308dcb80 --- /dev/null +++ b/third_party/usvg/src/parser/svgtree/mod.rs @@ -0,0 +1,1067 @@ +// Copyright 2021 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::collections::HashMap; +use std::num::NonZeroU32; +use std::str::FromStr; + +#[rustfmt::skip] mod names; +mod parse; +mod text; + +use tiny_skia_path::Transform; + +use crate::{ + BlendMode, ImageRendering, Opacity, ShapeRendering, SpreadMethod, TextRendering, Units, + Visibility, +}; +pub use names::{AId, EId}; + +/// An SVG tree container. +/// +/// Contains only element and text nodes. +/// Text nodes are present only inside the `text` element. +pub struct Document<'input> { + nodes: Vec, + attrs: Vec>, + links: HashMap, +} + +impl<'input> Document<'input> { + /// Returns the root node. + #[inline] + pub fn root<'a>(&'a self) -> SvgNode<'a, 'input> { + SvgNode { + id: NodeId::new(0), + d: &self.nodes[0], + doc: self, + } + } + + /// Returns the root element. + #[inline] + pub fn root_element<'a>(&'a self) -> SvgNode<'a, 'input> { + // `unwrap` is safe, because `Document` is guarantee to have at least one element. + self.root().first_element_child().unwrap() + } + + /// Returns an iterator over document's descendant nodes. + /// + /// Shorthand for `doc.root().descendants()`. + #[inline] + pub fn descendants<'a>(&'a self) -> Descendants<'a, 'input> { + self.root().descendants() + } + + /// Returns an element by ID. + /// + /// Unlike the [`Descendants`] iterator, this is just a HashMap lookup. + /// Meaning it's way faster. + #[inline] + pub fn element_by_id<'a>(&'a self, id: &str) -> Option> { + let node_id = self.links.get(id)?; + Some(self.get(*node_id)) + } + + #[inline] + fn get<'a>(&'a self, id: NodeId) -> SvgNode<'a, 'input> { + SvgNode { + id, + d: &self.nodes[id.get_usize()], + doc: self, + } + } +} + +impl std::fmt::Debug for Document<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + if !self.root().has_children() { + return write!(f, "Document []"); + } + + macro_rules! writeln_indented { + ($depth:expr, $f:expr, $fmt:expr) => { + for _ in 0..$depth { write!($f, " ")?; } + writeln!($f, $fmt)?; + }; + ($depth:expr, $f:expr, $fmt:expr, $($arg:tt)*) => { + for _ in 0..$depth { write!($f, " ")?; } + writeln!($f, $fmt, $($arg)*)?; + }; + } + + fn print_children( + parent: SvgNode, + depth: usize, + f: &mut std::fmt::Formatter, + ) -> Result<(), std::fmt::Error> { + for child in parent.children() { + if child.is_element() { + writeln_indented!(depth, f, "Element {{"); + writeln_indented!(depth, f, " tag_name: {:?}", child.tag_name()); + + if !child.attributes().is_empty() { + writeln_indented!(depth + 1, f, "attributes: ["); + for attr in child.attributes() { + writeln_indented!(depth + 2, f, "{:?}", attr); + } + writeln_indented!(depth + 1, f, "]"); + } + + if child.has_children() { + writeln_indented!(depth, f, " children: ["); + print_children(child, depth + 2, f)?; + writeln_indented!(depth, f, " ]"); + } + + writeln_indented!(depth, f, "}}"); + } else { + writeln_indented!(depth, f, "{:?}", child); + } + } + + Ok(()) + } + + writeln!(f, "Document [")?; + print_children(self.root(), 1, f)?; + writeln!(f, "]")?; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct ShortRange { + start: u32, + end: u32, +} + +impl ShortRange { + #[inline] + fn new(start: u32, end: u32) -> Self { + ShortRange { start, end } + } + + #[inline] + fn to_urange(self) -> std::ops::Range { + self.start as usize..self.end as usize + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) struct NodeId(NonZeroU32); + +impl NodeId { + #[inline] + fn new(id: u32) -> Self { + debug_assert!(id < u32::MAX); + + // We are using `NonZeroU32` to reduce overhead of `Option`. + NodeId(NonZeroU32::new(id + 1).unwrap()) + } + + #[inline] + fn get(self) -> u32 { + self.0.get() - 1 + } + + #[inline] + fn get_usize(self) -> usize { + self.get() as usize + } +} + +impl From for NodeId { + #[inline] + fn from(id: usize) -> Self { + // We already checked that `id` is limited by u32::MAX. + debug_assert!(id <= u32::MAX as usize); + NodeId::new(id as u32) + } +} + +pub(crate) enum NodeKind { + Root, + Element { + tag_name: EId, + attributes: ShortRange, + }, + Text(String), +} + +struct NodeData { + parent: Option, + next_sibling: Option, + children: Option<(NodeId, NodeId)>, + kind: NodeKind, +} + +/// An attribute. +#[derive(Clone)] +pub struct Attribute<'input> { + /// Attribute's name. + pub name: AId, + /// Attribute's value. + pub value: roxmltree::StringStorage<'input>, + /// Attribute's importance + pub important: bool, +} + +impl std::fmt::Debug for Attribute<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!( + f, + "Attribute {{ name: {:?}, value: {}, important: {} }}", + self.name, self.value, self.important + ) + } +} + +/// An SVG node. +#[derive(Clone, Copy)] +pub struct SvgNode<'a, 'input: 'a> { + id: NodeId, + doc: &'a Document<'input>, + d: &'a NodeData, +} + +impl Eq for SvgNode<'_, '_> {} + +impl PartialEq for SvgNode<'_, '_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.id == other.id && std::ptr::eq(self.doc, other.doc) && std::ptr::eq(self.d, other.d) + } +} + +impl<'a, 'input: 'a> SvgNode<'a, 'input> { + #[inline] + fn id(&self) -> NodeId { + self.id + } + + /// Checks if the current node is an element. + #[inline] + pub fn is_element(&self) -> bool { + matches!(self.d.kind, NodeKind::Element { .. }) + } + + /// Checks if the current node is a text. + #[inline] + pub fn is_text(&self) -> bool { + matches!(self.d.kind, NodeKind::Text(_)) + } + + /// Returns node's document. + #[inline] + pub fn document(&self) -> &'a Document<'input> { + self.doc + } + + /// Returns element's tag name, unless the current node is text. + #[inline] + pub fn tag_name(&self) -> Option { + match self.d.kind { + NodeKind::Element { tag_name, .. } => Some(tag_name), + _ => None, + } + } + /// Returns element's `id` attribute value. + /// + /// Returns an empty string otherwise. + #[inline] + pub fn element_id(&self) -> &'a str { + self.attribute(AId::Id).unwrap_or("") + } + + /// Returns an attribute value. + pub fn attribute>(&self, aid: AId) -> Option { + let value = self + .attributes() + .iter() + .find(|a| a.name == aid) + .map(|a| a.value.as_str())?; + match T::parse(*self, aid, value) { + Some(v) => Some(v), + None => { + // TODO: show position in XML + log::warn!("Failed to parse {} value: '{}'.", aid, value); + None + } + } + } + + /// Returns an attribute value. + /// + /// Same as `SvgNode::attribute`, but doesn't show a warning. + pub fn try_attribute>(&self, aid: AId) -> Option { + let value = self + .attributes() + .iter() + .find(|a| a.name == aid) + .map(|a| a.value.as_str())?; + T::parse(*self, aid, value) + } + + #[inline] + fn node_attribute(&self, aid: AId) -> Option> { + let value = self.attribute(aid)?; + let id = if aid == AId::Href { + svgtypes::IRI::from_str(value).ok().map(|v| v.0) + } else { + svgtypes::FuncIRI::from_str(value).ok().map(|v| v.0) + }?; + + self.document().element_by_id(id) + } + + /// Checks if an attribute is present. + #[inline] + pub fn has_attribute(&self, aid: AId) -> bool { + self.attributes().iter().any(|a| a.name == aid) + } + + /// Returns a list of all element's attributes. + #[inline] + pub fn attributes(&self) -> &'a [Attribute<'input>] { + match self.d.kind { + NodeKind::Element { ref attributes, .. } => &self.doc.attrs[attributes.to_urange()], + _ => &[], + } + } + + #[inline] + fn attribute_id(&self, aid: AId) -> Option { + match self.d.kind { + NodeKind::Element { ref attributes, .. } => { + let idx = self.attributes().iter().position(|attr| attr.name == aid)?; + Some(attributes.start as usize + idx) + } + _ => None, + } + } + + /// Finds a [`Node`] that contains the required attribute. + /// + /// For inheritable attributes walks over ancestors until a node with + /// the specified attribute is found. + /// + /// For non-inheritable attributes checks only the current node and the parent one. + /// As per SVG spec. + pub fn find_attribute>(&self, aid: AId) -> Option { + self.find_attribute_impl(aid)?.attribute(aid) + } + + fn find_attribute_impl(&self, aid: AId) -> Option> { + if aid.is_inheritable() { + for n in self.ancestors() { + if n.has_attribute(aid) { + return Some(n); + } + } + + None + } else { + if self.has_attribute(aid) { + Some(*self) + } else { + // Non-inheritable attributes can inherit a value only from a direct parent. + let n = self.parent_element()?; + if n.has_attribute(aid) { + Some(n) + } else { + None + } + } + } + } + + /// Returns node's text data. + /// + /// For text nodes returns its content. For elements returns the first child node text. + #[inline] + pub fn text(&self) -> &'a str { + match self.d.kind { + NodeKind::Element { .. } => match self.first_child() { + Some(child) if child.is_text() => match self.doc.nodes[child.id.get_usize()].kind { + NodeKind::Text(ref text) => text, + _ => "", + }, + _ => "", + }, + NodeKind::Text(ref text) => text, + _ => "", + } + } + + /// Returns a parent node. + #[inline] + pub fn parent(&self) -> Option { + self.d.parent.map(|id| self.doc.get(id)) + } + + /// Returns the parent element. + #[inline] + pub fn parent_element(&self) -> Option { + self.ancestors().skip(1).find(|n| n.is_element()) + } + + /// Returns the next sibling. + #[inline] + pub fn next_sibling(&self) -> Option { + self.d.next_sibling.map(|id| self.doc.get(id)) + } + + /// Returns the first child. + #[inline] + pub fn first_child(&self) -> Option { + self.d.children.map(|(id, _)| self.doc.get(id)) + } + + /// Returns the first child element. + #[inline] + pub fn first_element_child(&self) -> Option { + self.children().find(|n| n.is_element()) + } + + /// Returns the last child. + #[inline] + pub fn last_child(&self) -> Option { + self.d.children.map(|(_, id)| self.doc.get(id)) + } + + /// Checks if the node has child nodes. + #[inline] + pub fn has_children(&self) -> bool { + self.d.children.is_some() + } + + /// Returns an iterator over ancestor nodes starting at this node. + #[inline] + pub fn ancestors(&self) -> Ancestors<'a, 'input> { + Ancestors(Some(*self)) + } + + /// Returns an iterator over children nodes. + #[inline] + pub fn children(&self) -> Children<'a, 'input> { + Children { + front: self.first_child(), + back: self.last_child(), + } + } + + /// Returns an iterator which traverses the subtree starting at this node. + #[inline] + fn traverse(&self) -> Traverse<'a, 'input> { + Traverse { + root: *self, + edge: None, + } + } + + /// Returns an iterator over this node and its descendants. + #[inline] + pub fn descendants(&self) -> Descendants<'a, 'input> { + Descendants(self.traverse()) + } + + /// Returns an iterator over elements linked via `xlink:href`. + #[inline] + pub fn href_iter(&self) -> HrefIter<'a, 'input> { + HrefIter { + doc: self.document(), + origin: self.id(), + curr: self.id(), + is_first: true, + is_finished: false, + } + } +} + +impl std::fmt::Debug for SvgNode<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self.d.kind { + NodeKind::Root => write!(f, "Root"), + NodeKind::Element { .. } => { + write!( + f, + "Element {{ tag_name: {:?}, attributes: {:?} }}", + self.tag_name(), + self.attributes() + ) + } + NodeKind::Text(ref text) => write!(f, "Text({:?})", text), + } + } +} + +/// An iterator over ancestor nodes. +#[derive(Clone, Debug)] +pub struct Ancestors<'a, 'input: 'a>(Option>); + +impl<'a, 'input: 'a> Iterator for Ancestors<'a, 'input> { + type Item = SvgNode<'a, 'input>; + + #[inline] + fn next(&mut self) -> Option { + let node = self.0.take(); + self.0 = node.as_ref().and_then(SvgNode::parent); + node + } +} + +/// An iterator over children nodes. +#[derive(Clone, Debug)] +pub struct Children<'a, 'input: 'a> { + front: Option>, + back: Option>, +} + +impl<'a, 'input: 'a> Iterator for Children<'a, 'input> { + type Item = SvgNode<'a, 'input>; + + fn next(&mut self) -> Option { + let node = self.front.take(); + if self.front == self.back { + self.back = None; + } else { + self.front = node.as_ref().and_then(SvgNode::next_sibling); + } + node + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum Edge<'a, 'input: 'a> { + Open(SvgNode<'a, 'input>), + Close(SvgNode<'a, 'input>), +} + +#[derive(Clone, Debug)] +struct Traverse<'a, 'input: 'a> { + root: SvgNode<'a, 'input>, + edge: Option>, +} + +impl<'a, 'input: 'a> Iterator for Traverse<'a, 'input> { + type Item = Edge<'a, 'input>; + + fn next(&mut self) -> Option { + match self.edge { + Some(Edge::Open(node)) => { + self.edge = Some(match node.first_child() { + Some(first_child) => Edge::Open(first_child), + None => Edge::Close(node), + }); + } + Some(Edge::Close(node)) => { + if node == self.root { + self.edge = None; + } else if let Some(next_sibling) = node.next_sibling() { + self.edge = Some(Edge::Open(next_sibling)); + } else { + self.edge = node.parent().map(Edge::Close); + } + } + None => { + self.edge = Some(Edge::Open(self.root)); + } + } + + self.edge + } +} + +/// A descendants iterator. +#[derive(Clone, Debug)] +pub struct Descendants<'a, 'input: 'a>(Traverse<'a, 'input>); + +impl<'a, 'input: 'a> Iterator for Descendants<'a, 'input> { + type Item = SvgNode<'a, 'input>; + + #[inline] + fn next(&mut self) -> Option { + for edge in &mut self.0 { + if let Edge::Open(node) = edge { + return Some(node); + } + } + + None + } +} + +/// An iterator over `xlink:href` references. +#[derive(Clone, Debug)] +pub struct HrefIter<'a, 'input: 'a> { + doc: &'a Document<'input>, + origin: NodeId, + curr: NodeId, + is_first: bool, + is_finished: bool, +} + +impl<'a, 'input: 'a> Iterator for HrefIter<'a, 'input> { + type Item = SvgNode<'a, 'input>; + + fn next(&mut self) -> Option { + if self.is_finished { + return None; + } + + if self.is_first { + self.is_first = false; + return Some(self.doc.get(self.curr)); + } + + if let Some(link) = self.doc.get(self.curr).node_attribute(AId::Href) { + if link.id() == self.curr || link.id() == self.origin { + log::warn!( + "Element '#{}' cannot reference itself via 'xlink:href'.", + self.doc.get(self.origin).element_id() + ); + self.is_finished = true; + return None; + } + + self.curr = link.id(); + Some(self.doc.get(self.curr)) + } else { + None + } + } +} + +impl EId { + /// Checks if this is a + /// [graphics element](https://www.w3.org/TR/SVG11/intro.html#TermGraphicsElement). + pub fn is_graphic(&self) -> bool { + matches!( + self, + EId::Circle + | EId::Ellipse + | EId::Image + | EId::Line + | EId::Path + | EId::Polygon + | EId::Polyline + | EId::Rect + | EId::Text + | EId::Use + ) + } + + /// Checks if this is a + /// [gradient element](https://www.w3.org/TR/SVG11/intro.html#TermGradientElement). + pub fn is_gradient(&self) -> bool { + matches!(self, EId::LinearGradient | EId::RadialGradient) + } + + /// Checks if this is a + /// [paint server element](https://www.w3.org/TR/SVG11/intro.html#TermPaint). + pub fn is_paint_server(&self) -> bool { + matches!( + self, + EId::LinearGradient | EId::RadialGradient | EId::Pattern + ) + } +} + +impl AId { + fn is_presentation(&self) -> bool { + matches!( + self, + AId::AlignmentBaseline + | AId::BaselineShift + | AId::BackgroundColor // non-standard SVG attribute + | AId::ClipPath + | AId::ClipRule + | AId::Color + | AId::ColorInterpolation + | AId::ColorInterpolationFilters + | AId::ColorRendering + | AId::Direction + | AId::Display + | AId::DominantBaseline + | AId::Fill + | AId::FillOpacity + | AId::FillRule + | AId::Filter + | AId::FloodColor + | AId::FloodOpacity + | AId::FontFamily + | AId::FontKerning // technically not presentation + | AId::FontSize + | AId::FontSizeAdjust + | AId::FontStretch + | AId::FontStyle + | AId::FontVariant + | AId::FontWeight + | AId::GlyphOrientationHorizontal + | AId::GlyphOrientationVertical + | AId::ImageRendering + | AId::Isolation // technically not presentation + | AId::LetterSpacing + | AId::LightingColor + | AId::MarkerEnd + | AId::MarkerMid + | AId::MarkerStart + | AId::Mask + | AId::MaskType + | AId::MixBlendMode // technically not presentation + | AId::Opacity + | AId::Overflow + | AId::PaintOrder + | AId::ShapeRendering + | AId::StopColor + | AId::StopOpacity + | AId::Stroke + | AId::StrokeDasharray + | AId::StrokeDashoffset + | AId::StrokeLinecap + | AId::StrokeLinejoin + | AId::StrokeMiterlimit + | AId::StrokeOpacity + | AId::StrokeWidth + | AId::TextAnchor + | AId::TextDecoration + | AId::TextOverflow + | AId::TextRendering + | AId::Transform + | AId::TransformOrigin + | AId::UnicodeBidi + | AId::VectorEffect + | AId::Visibility + | AId::WhiteSpace + | AId::WordSpacing + | AId::WritingMode + ) + } + + /// Checks if the current attribute is inheritable. + fn is_inheritable(&self) -> bool { + if self.is_presentation() { + !is_non_inheritable(*self) + } else { + false + } + } + + fn allows_inherit_value(&self) -> bool { + matches!( + self, + AId::AlignmentBaseline + | AId::BaselineShift + | AId::ClipPath + | AId::ClipRule + | AId::Color + | AId::ColorInterpolationFilters + | AId::Direction + | AId::Display + | AId::DominantBaseline + | AId::Fill + | AId::FillOpacity + | AId::FillRule + | AId::Filter + | AId::FloodColor + | AId::FloodOpacity + | AId::FontFamily + | AId::FontKerning + | AId::FontSize + | AId::FontStretch + | AId::FontStyle + | AId::FontVariant + | AId::FontWeight + | AId::ImageRendering + | AId::Kerning + | AId::LetterSpacing + | AId::MarkerEnd + | AId::MarkerMid + | AId::MarkerStart + | AId::Mask + | AId::Opacity + | AId::Overflow + | AId::ShapeRendering + | AId::StopColor + | AId::StopOpacity + | AId::Stroke + | AId::StrokeDasharray + | AId::StrokeDashoffset + | AId::StrokeLinecap + | AId::StrokeLinejoin + | AId::StrokeMiterlimit + | AId::StrokeOpacity + | AId::StrokeWidth + | AId::TextAnchor + | AId::TextDecoration + | AId::TextRendering + | AId::Visibility + | AId::WordSpacing + | AId::WritingMode + ) + } +} + +fn is_non_inheritable(id: AId) -> bool { + matches!( + id, + AId::AlignmentBaseline + | AId::BaselineShift + | AId::ClipPath + | AId::Display + | AId::DominantBaseline + | AId::Filter + | AId::FloodColor + | AId::FloodOpacity + | AId::Mask + | AId::Opacity + | AId::Overflow + | AId::LightingColor + | AId::StopColor + | AId::StopOpacity + | AId::TextDecoration + | AId::Transform + | AId::TransformOrigin + ) +} + +// TODO: is there a way yo make it less ugly? Too many lifetimes. +/// A trait for parsing attribute values. +pub trait FromValue<'a, 'input: 'a>: Sized { + /// Parses an attribute value. + /// + /// When `None` is returned, the attribute value will be logged as a parsing failure. + fn parse(node: SvgNode<'a, 'input>, aid: AId, value: &'a str) -> Option; +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for &'a str { + fn parse(_: SvgNode<'a, 'input>, _: AId, value: &'a str) -> Option { + Some(value) + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for f32 { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + svgtypes::Number::from_str(value).ok().map(|v| v.0 as f32) + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::Length { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + svgtypes::Length::from_str(value).ok() + } +} + +// TODO: to svgtypes? +impl<'a, 'input: 'a> FromValue<'a, 'input> for Opacity { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + let length = svgtypes::Length::from_str(value).ok()?; + if length.unit == svgtypes::LengthUnit::Percent { + Some(Opacity::new_clamped(length.number as f32 / 100.0)) + } else if length.unit == svgtypes::LengthUnit::None { + Some(Opacity::new_clamped(length.number as f32)) + } else { + None + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Transform { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + let ts = match svgtypes::Transform::from_str(value) { + Ok(v) => v, + Err(_) => return None, + }; + + let ts = Transform::from_row( + ts.a as f32, + ts.b as f32, + ts.c as f32, + ts.d as f32, + ts.e as f32, + ts.f as f32, + ); + + if ts.is_valid() { + Some(ts) + } else { + Some(Transform::default()) + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::TransformOrigin { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::ViewBox { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Units { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "userSpaceOnUse" => Some(Units::UserSpaceOnUse), + "objectBoundingBox" => Some(Units::ObjectBoundingBox), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::AspectRatio { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::PaintOrder { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::Color { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::Angle { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::EnableBackground { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::Paint<'a> { + fn parse(_: SvgNode, _: AId, value: &'a str) -> Option { + Self::from_str(value).ok() + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Vec { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + let mut list = Vec::new(); + for n in svgtypes::NumberListParser::from(value) { + list.push(n.ok()? as f32); + } + + Some(list) + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Vec { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + let mut list = Vec::new(); + for n in svgtypes::LengthListParser::from(value) { + list.push(n.ok()?); + } + + Some(list) + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Visibility { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "visible" => Some(Visibility::Visible), + "hidden" => Some(Visibility::Hidden), + "collapse" => Some(Visibility::Collapse), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for SpreadMethod { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "pad" => Some(SpreadMethod::Pad), + "reflect" => Some(SpreadMethod::Reflect), + "repeat" => Some(SpreadMethod::Repeat), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for ShapeRendering { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "optimizeSpeed" => Some(ShapeRendering::OptimizeSpeed), + "crispEdges" => Some(ShapeRendering::CrispEdges), + "auto" | "geometricPrecision" => Some(ShapeRendering::GeometricPrecision), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for TextRendering { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "optimizeSpeed" => Some(TextRendering::OptimizeSpeed), + "auto" | "optimizeLegibility" => Some(TextRendering::OptimizeLegibility), + "geometricPrecision" => Some(TextRendering::GeometricPrecision), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for ImageRendering { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "auto" | "optimizeQuality" => Some(ImageRendering::OptimizeQuality), + "optimizeSpeed" => Some(ImageRendering::OptimizeSpeed), + "smooth" => Some(ImageRendering::Smooth), + "high-quality" => Some(ImageRendering::HighQuality), + "crisp-edges" => Some(ImageRendering::CrispEdges), + "pixelated" => Some(ImageRendering::Pixelated), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for BlendMode { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "normal" => Some(BlendMode::Normal), + "multiply" => Some(BlendMode::Multiply), + "screen" => Some(BlendMode::Screen), + "overlay" => Some(BlendMode::Overlay), + "darken" => Some(BlendMode::Darken), + "lighten" => Some(BlendMode::Lighten), + "color-dodge" => Some(BlendMode::ColorDodge), + "color-burn" => Some(BlendMode::ColorBurn), + "hard-light" => Some(BlendMode::HardLight), + "soft-light" => Some(BlendMode::SoftLight), + "difference" => Some(BlendMode::Difference), + "exclusion" => Some(BlendMode::Exclusion), + "hue" => Some(BlendMode::Hue), + "saturation" => Some(BlendMode::Saturation), + "color" => Some(BlendMode::Color), + "luminosity" => Some(BlendMode::Luminosity), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for SvgNode<'a, 'input> { + fn parse(node: SvgNode<'a, 'input>, aid: AId, value: &str) -> Option { + let id = if aid == AId::Href { + svgtypes::IRI::from_str(value).ok().map(|v| v.0) + } else { + svgtypes::FuncIRI::from_str(value).ok().map(|v| v.0) + }?; + + node.document().element_by_id(id) + } +} diff --git a/third_party/usvg/src/parser/svgtree/names.rs b/third_party/usvg/src/parser/svgtree/names.rs new file mode 100644 index 0000000000..1e6e2590cc --- /dev/null +++ b/third_party/usvg/src/parser/svgtree/names.rs @@ -0,0 +1,717 @@ +// Copyright 2019 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// This file is autogenerated. Do not edit it! +// See ./codegen for details. + +/// An element ID. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq)] +pub enum EId { + A, + Circle, + ClipPath, + Defs, + Ellipse, + FeBlend, + FeColorMatrix, + FeComponentTransfer, + FeComposite, + FeConvolveMatrix, + FeDiffuseLighting, + FeDisplacementMap, + FeDistantLight, + FeDropShadow, + FeFlood, + FeFuncA, + FeFuncB, + FeFuncG, + FeFuncR, + FeGaussianBlur, + FeImage, + FeMerge, + FeMergeNode, + FeMorphology, + FeOffset, + FePointLight, + FeSpecularLighting, + FeSpotLight, + FeTile, + FeTurbulence, + Filter, + G, + Image, + Line, + LinearGradient, + Marker, + Mask, + Path, + Pattern, + Polygon, + Polyline, + RadialGradient, + Rect, + Stop, + Style, + Svg, + Switch, + Symbol, + Text, + TextPath, + Tref, + Tspan, + Use +} + +static ELEMENTS: Map = Map { + key: 732231254413039614, + disps: &[ + (0, 12), + (1, 11), + (10, 26), + (2, 42), + (1, 19), + (0, 5), + (1, 13), + (8, 50), + (0, 0), + (1, 0), + (7, 45), + ], + entries: &[ + ("feFlood", EId::FeFlood), + ("radialGradient", EId::RadialGradient), + ("feImage", EId::FeImage), + ("stop", EId::Stop), + ("fePointLight", EId::FePointLight), + ("feConvolveMatrix", EId::FeConvolveMatrix), + ("feComposite", EId::FeComposite), + ("clipPath", EId::ClipPath), + ("feMerge", EId::FeMerge), + ("defs", EId::Defs), + ("mask", EId::Mask), + ("svg", EId::Svg), + ("symbol", EId::Symbol), + ("linearGradient", EId::LinearGradient), + ("feSpecularLighting", EId::FeSpecularLighting), + ("feFuncB", EId::FeFuncB), + ("filter", EId::Filter), + ("feFuncG", EId::FeFuncG), + ("circle", EId::Circle), + ("g", EId::G), + ("tref", EId::Tref), + ("feFuncA", EId::FeFuncA), + ("image", EId::Image), + ("text", EId::Text), + ("line", EId::Line), + ("pattern", EId::Pattern), + ("use", EId::Use), + ("feDropShadow", EId::FeDropShadow), + ("feSpotLight", EId::FeSpotLight), + ("marker", EId::Marker), + ("style", EId::Style), + ("switch", EId::Switch), + ("tspan", EId::Tspan), + ("feColorMatrix", EId::FeColorMatrix), + ("feOffset", EId::FeOffset), + ("path", EId::Path), + ("feGaussianBlur", EId::FeGaussianBlur), + ("feTile", EId::FeTile), + ("feTurbulence", EId::FeTurbulence), + ("feMergeNode", EId::FeMergeNode), + ("feMorphology", EId::FeMorphology), + ("a", EId::A), + ("textPath", EId::TextPath), + ("ellipse", EId::Ellipse), + ("feComponentTransfer", EId::FeComponentTransfer), + ("feDistantLight", EId::FeDistantLight), + ("polyline", EId::Polyline), + ("polygon", EId::Polygon), + ("feBlend", EId::FeBlend), + ("feDisplacementMap", EId::FeDisplacementMap), + ("feDiffuseLighting", EId::FeDiffuseLighting), + ("rect", EId::Rect), + ("feFuncR", EId::FeFuncR), + ], +}; + +impl EId { + pub(crate) fn from_str(text: &str) -> Option { + ELEMENTS.get(text).cloned() + } + + /// Returns the original string. + #[inline(never)] + pub fn to_str(self) -> &'static str { + ELEMENTS.key(&self) + } +} + +impl std::fmt::Debug for EId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +impl std::fmt::Display for EId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +/// An attribute ID. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq)] +pub enum AId { + AlignmentBaseline, + Amplitude, + Azimuth, + BackgroundColor, + BaseFrequency, + BaselineShift, + Bias, + Class, + Clip, + ClipPath, + ClipRule, + ClipPathUnits, + Color, + ColorInterpolation, + ColorInterpolationFilters, + ColorProfile, + ColorRendering, + Cx, + Cy, + D, + DiffuseConstant, + Direction, + Display, + Divisor, + DominantBaseline, + Dx, + Dy, + EdgeMode, + Elevation, + EnableBackground, + Exponent, + Fill, + FillOpacity, + FillRule, + Filter, + FilterUnits, + FloodColor, + FloodOpacity, + Font, + FontFamily, + FontFeatureSettings, + FontKerning, + FontSize, + FontSizeAdjust, + FontStretch, + FontStyle, + FontSynthesis, + FontVariant, + FontVariantCaps, + FontVariantEastAsian, + FontVariantLigatures, + FontVariantNumeric, + FontVariantPosition, + FontWeight, + Fr, + Fx, + Fy, + GlyphOrientationHorizontal, + GlyphOrientationVertical, + GradientTransform, + GradientUnits, + Height, + Href, + Id, + ImageRendering, + In, + In2, + InlineSize, + Intercept, + Isolation, + K1, + K2, + K3, + K4, + KernelMatrix, + KernelUnitLength, + Kerning, + LengthAdjust, + LetterSpacing, + LightingColor, + LimitingConeAngle, + LineHeight, + MarkerEnd, + MarkerMid, + MarkerStart, + MarkerHeight, + MarkerUnits, + MarkerWidth, + Mask, + MaskBorder, + MaskBorderMode, + MaskBorderOutset, + MaskBorderRepeat, + MaskBorderSlice, + MaskBorderSource, + MaskBorderWidth, + MaskClip, + MaskComposite, + MaskImage, + MaskMode, + MaskOrigin, + MaskPosition, + MaskSize, + MaskType, + MaskContentUnits, + MaskUnits, + MixBlendMode, + Mode, + NumOctaves, + Offset, + Opacity, + Operator, + Order, + Orient, + Overflow, + PaintOrder, + Path, + PathLength, + PatternContentUnits, + PatternTransform, + PatternUnits, + Points, + PointsAtX, + PointsAtY, + PointsAtZ, + PreserveAlpha, + PreserveAspectRatio, + PrimitiveUnits, + R, + Radius, + RefX, + RefY, + RequiredExtensions, + RequiredFeatures, + Result, + Rotate, + Rx, + Ry, + Scale, + Seed, + ShapeImageThreshold, + ShapeInside, + ShapeMargin, + ShapePadding, + ShapeRendering, + ShapeSubtract, + Side, + Slope, + Space, + SpecularConstant, + SpecularExponent, + SpreadMethod, + StartOffset, + StdDeviation, + StitchTiles, + StopColor, + StopOpacity, + Stroke, + StrokeDasharray, + StrokeDashoffset, + StrokeLinecap, + StrokeLinejoin, + StrokeMiterlimit, + StrokeOpacity, + StrokeWidth, + Style, + SurfaceScale, + SystemLanguage, + TableValues, + TargetX, + TargetY, + TextAlign, + TextAlignLast, + TextAnchor, + TextDecoration, + TextDecorationColor, + TextDecorationFill, + TextDecorationLine, + TextDecorationStroke, + TextDecorationStyle, + TextIndent, + TextOrientation, + TextOverflow, + TextRendering, + TextUnderlinePosition, + TextLength, + Transform, + TransformBox, + TransformOrigin, + Type, + UnicodeBidi, + UnicodeRange, + Values, + VectorEffect, + ViewBox, + Visibility, + WhiteSpace, + Width, + WordSpacing, + WritingMode, + X, + X1, + X2, + XChannelSelector, + Y, + Y1, + Y2, + YChannelSelector, + Z +} + +static ATTRIBUTES: Map = Map { + key: 3347381344252206323, + disps: &[ + (0, 111), + (0, 2), + (0, 45), + (0, 5), + (0, 1), + (2, 56), + (0, 5), + (2, 99), + (13, 198), + (0, 61), + (0, 52), + (1, 29), + (0, 21), + (0, 70), + (0, 164), + (2, 60), + (3, 52), + (0, 1), + (0, 86), + (0, 10), + (0, 0), + (0, 4), + (2, 175), + (6, 59), + (1, 14), + (0, 13), + (3, 175), + (1, 10), + (2, 76), + (0, 53), + (0, 24), + (123, 202), + (0, 14), + (0, 30), + (0, 62), + (0, 98), + (11, 193), + (8, 79), + (0, 17), + (22, 5), + (36, 106), + (1, 1), + ], + entries: &[ + ("mask-border-source", AId::MaskBorderSource), + ("stop-opacity", AId::StopOpacity), + ("stroke-linejoin", AId::StrokeLinejoin), + ("dominant-baseline", AId::DominantBaseline), + ("spreadMethod", AId::SpreadMethod), + ("order", AId::Order), + ("stroke", AId::Stroke), + ("stitchTiles", AId::StitchTiles), + ("height", AId::Height), + ("font-size", AId::FontSize), + ("background-color", AId::BackgroundColor), + ("tableValues", AId::TableValues), + ("x1", AId::X1), + ("y", AId::Y), + ("width", AId::Width), + ("text-indent", AId::TextIndent), + ("fill-opacity", AId::FillOpacity), + ("word-spacing", AId::WordSpacing), + ("cy", AId::Cy), + ("scale", AId::Scale), + ("x2", AId::X2), + ("lengthAdjust", AId::LengthAdjust), + ("glyph-orientation-horizontal", AId::GlyphOrientationHorizontal), + ("opacity", AId::Opacity), + ("mask-border", AId::MaskBorder), + ("font-stretch", AId::FontStretch), + ("stroke-dashoffset", AId::StrokeDashoffset), + ("fill", AId::Fill), + ("space", AId::Space), + ("baseline-shift", AId::BaselineShift), + ("text-align-last", AId::TextAlignLast), + ("font-variant-east-asian", AId::FontVariantEastAsian), + ("mask-border-mode", AId::MaskBorderMode), + ("font-variant-caps", AId::FontVariantCaps), + ("gradientUnits", AId::GradientUnits), + ("exponent", AId::Exponent), + ("text-decoration-color", AId::TextDecorationColor), + ("refX", AId::RefX), + ("enable-background", AId::EnableBackground), + ("mask-border-width", AId::MaskBorderWidth), + ("numOctaves", AId::NumOctaves), + ("kerning", AId::Kerning), + ("mix-blend-mode", AId::MixBlendMode), + ("mask-clip", AId::MaskClip), + ("mask-mode", AId::MaskMode), + ("type", AId::Type), + ("class", AId::Class), + ("font", AId::Font), + ("mask-border-repeat", AId::MaskBorderRepeat), + ("stroke-miterlimit", AId::StrokeMiterlimit), + ("text-decoration-stroke", AId::TextDecorationStroke), + ("z", AId::Z), + ("dx", AId::Dx), + ("clip-path", AId::ClipPath), + ("markerHeight", AId::MarkerHeight), + ("text-underline-position", AId::TextUnderlinePosition), + ("stdDeviation", AId::StdDeviation), + ("id", AId::Id), + ("paint-order", AId::PaintOrder), + ("elevation", AId::Elevation), + ("specularConstant", AId::SpecularConstant), + ("result", AId::Result), + ("font-size-adjust", AId::FontSizeAdjust), + ("mask-origin", AId::MaskOrigin), + ("direction", AId::Direction), + ("font-variant-numeric", AId::FontVariantNumeric), + ("startOffset", AId::StartOffset), + ("maskUnits", AId::MaskUnits), + ("font-variant", AId::FontVariant), + ("text-orientation", AId::TextOrientation), + ("amplitude", AId::Amplitude), + ("rx", AId::Rx), + ("mask-type", AId::MaskType), + ("filter", AId::Filter), + ("in", AId::In), + ("display", AId::Display), + ("seed", AId::Seed), + ("unicode-range", AId::UnicodeRange), + ("color-profile", AId::ColorProfile), + ("x", AId::X), + ("href", AId::Href), + ("font-feature-settings", AId::FontFeatureSettings), + ("fill-rule", AId::FillRule), + ("fr", AId::Fr), + ("font-variant-ligatures", AId::FontVariantLigatures), + ("text-decoration-style", AId::TextDecorationStyle), + ("radius", AId::Radius), + ("xChannelSelector", AId::XChannelSelector), + ("orient", AId::Orient), + ("isolation", AId::Isolation), + ("gradientTransform", AId::GradientTransform), + ("transform-box", AId::TransformBox), + ("pointsAtY", AId::PointsAtY), + ("text-decoration-line", AId::TextDecorationLine), + ("requiredFeatures", AId::RequiredFeatures), + ("patternContentUnits", AId::PatternContentUnits), + ("shape-padding", AId::ShapePadding), + ("text-overflow", AId::TextOverflow), + ("clipPathUnits", AId::ClipPathUnits), + ("azimuth", AId::Azimuth), + ("line-height", AId::LineHeight), + ("viewBox", AId::ViewBox), + ("preserveAspectRatio", AId::PreserveAspectRatio), + ("path", AId::Path), + ("k4", AId::K4), + ("systemLanguage", AId::SystemLanguage), + ("stroke-width", AId::StrokeWidth), + ("specularExponent", AId::SpecularExponent), + ("writing-mode", AId::WritingMode), + ("transform-origin", AId::TransformOrigin), + ("stroke-linecap", AId::StrokeLinecap), + ("points", AId::Points), + ("style", AId::Style), + ("pointsAtZ", AId::PointsAtZ), + ("targetX", AId::TargetX), + ("font-synthesis", AId::FontSynthesis), + ("maskContentUnits", AId::MaskContentUnits), + ("text-align", AId::TextAlign), + ("cx", AId::Cx), + ("alignment-baseline", AId::AlignmentBaseline), + ("font-kerning", AId::FontKerning), + ("requiredExtensions", AId::RequiredExtensions), + ("clip-rule", AId::ClipRule), + ("mask-border-outset", AId::MaskBorderOutset), + ("primitiveUnits", AId::PrimitiveUnits), + ("textLength", AId::TextLength), + ("text-decoration-fill", AId::TextDecorationFill), + ("fy", AId::Fy), + ("mask-size", AId::MaskSize), + ("k3", AId::K3), + ("marker-start", AId::MarkerStart), + ("mode", AId::Mode), + ("k1", AId::K1), + ("refY", AId::RefY), + ("y1", AId::Y1), + ("shape-rendering", AId::ShapeRendering), + ("operator", AId::Operator), + ("mask-image", AId::MaskImage), + ("marker-end", AId::MarkerEnd), + ("rotate", AId::Rotate), + ("limitingConeAngle", AId::LimitingConeAngle), + ("surfaceScale", AId::SurfaceScale), + ("intercept", AId::Intercept), + ("font-variant-position", AId::FontVariantPosition), + ("clip", AId::Clip), + ("fx", AId::Fx), + ("visibility", AId::Visibility), + ("shape-margin", AId::ShapeMargin), + ("font-style", AId::FontStyle), + ("y2", AId::Y2), + ("dy", AId::Dy), + ("yChannelSelector", AId::YChannelSelector), + ("ry", AId::Ry), + ("color-rendering", AId::ColorRendering), + ("white-space", AId::WhiteSpace), + ("patternUnits", AId::PatternUnits), + ("shape-subtract", AId::ShapeSubtract), + ("markerWidth", AId::MarkerWidth), + ("d", AId::D), + ("shape-inside", AId::ShapeInside), + ("preserveAlpha", AId::PreserveAlpha), + ("shape-image-threshold", AId::ShapeImageThreshold), + ("image-rendering", AId::ImageRendering), + ("marker-mid", AId::MarkerMid), + ("filterUnits", AId::FilterUnits), + ("bias", AId::Bias), + ("mask-border-slice", AId::MaskBorderSlice), + ("pointsAtX", AId::PointsAtX), + ("kernelMatrix", AId::KernelMatrix), + ("color-interpolation", AId::ColorInterpolation), + ("glyph-orientation-vertical", AId::GlyphOrientationVertical), + ("color", AId::Color), + ("patternTransform", AId::PatternTransform), + ("kernelUnitLength", AId::KernelUnitLength), + ("markerUnits", AId::MarkerUnits), + ("font-weight", AId::FontWeight), + ("overflow", AId::Overflow), + ("stop-color", AId::StopColor), + ("r", AId::R), + ("k2", AId::K2), + ("text-anchor", AId::TextAnchor), + ("inline-size", AId::InlineSize), + ("unicode-bidi", AId::UnicodeBidi), + ("font-family", AId::FontFamily), + ("color-interpolation-filters", AId::ColorInterpolationFilters), + ("slope", AId::Slope), + ("baseFrequency", AId::BaseFrequency), + ("transform", AId::Transform), + ("text-rendering", AId::TextRendering), + ("divisor", AId::Divisor), + ("edgeMode", AId::EdgeMode), + ("letter-spacing", AId::LetterSpacing), + ("flood-color", AId::FloodColor), + ("in2", AId::In2), + ("side", AId::Side), + ("mask-composite", AId::MaskComposite), + ("offset", AId::Offset), + ("values", AId::Values), + ("vector-effect", AId::VectorEffect), + ("mask", AId::Mask), + ("pathLength", AId::PathLength), + ("lighting-color", AId::LightingColor), + ("mask-position", AId::MaskPosition), + ("stroke-dasharray", AId::StrokeDasharray), + ("text-decoration", AId::TextDecoration), + ("stroke-opacity", AId::StrokeOpacity), + ("targetY", AId::TargetY), + ("flood-opacity", AId::FloodOpacity), + ("diffuseConstant", AId::DiffuseConstant), + ], +}; + +impl AId { + pub(crate) fn from_str(text: &str) -> Option { + ATTRIBUTES.get(text).cloned() + } + + /// Returns the original string. + #[inline(never)] + pub fn to_str(self) -> &'static str { + ATTRIBUTES.key(&self) + } +} + +impl std::fmt::Debug for AId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +impl std::fmt::Display for AId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +// A stripped down `phf` crate fork. +// +// https://github.com/sfackler/rust-phf + +struct Map { + pub key: u64, + pub disps: &'static [(u32, u32)], + pub entries: &'static [(&'static str, V)], +} + +impl Map { + fn get(&self, key: &str) -> Option<&V> { + let hash = hash(key, self.key); + let index = get_index(hash, self.disps, self.entries.len()); + let entry = &self.entries[index as usize]; + let b = entry.0; + if b == key { + Some(&entry.1) + } else { + None + } + } + + fn key(&self, value: &V) -> &'static str { + self.entries.iter().find(|kv| kv.1 == *value).unwrap().0 + } +} + +#[inline] +fn hash(x: &str, key: u64) -> u64 { + use std::hash::Hasher; + + let mut hasher = siphasher::sip::SipHasher13::new_with_keys(0, key); + hasher.write(x.as_bytes()); + hasher.finish() +} + +#[inline] +fn get_index(hash: u64, disps: &[(u32, u32)], len: usize) -> u32 { + let (g, f1, f2) = split(hash); + let (d1, d2) = disps[(g % (disps.len() as u32)) as usize]; + displace(f1, f2, d1, d2) % (len as u32) +} + +#[inline] +fn split(hash: u64) -> (u32, u32, u32) { + const BITS: u32 = 21; + const MASK: u64 = (1 << BITS) - 1; + + ((hash & MASK) as u32, + ((hash >> BITS) & MASK) as u32, + ((hash >> (2 * BITS)) & MASK) as u32) +} + +#[inline] +fn displace(f1: u32, f2: u32, d1: u32, d2: u32) -> u32 { + d2 + f1 * d1 + f2 +} diff --git a/third_party/usvg/src/parser/svgtree/parse.rs b/third_party/usvg/src/parser/svgtree/parse.rs new file mode 100644 index 0000000000..2eae321b6f --- /dev/null +++ b/third_party/usvg/src/parser/svgtree/parse.rs @@ -0,0 +1,811 @@ +// Copyright 2021 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::collections::HashMap; + +use roxmltree::Error; +use simplecss::Declaration; +use svgtypes::FontShorthand; + +use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange}; + +const SVG_NS: &str = "http://www.w3.org/2000/svg"; +const XLINK_NS: &str = "http://www.w3.org/1999/xlink"; +const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace"; + +impl<'input> Document<'input> { + /// Parses a [`Document`] from a [`roxmltree::Document`]. + pub fn parse_tree( + xml: &roxmltree::Document<'input>, + injected_stylesheet: Option<&'input str>, + ) -> Result, Error> { + parse(xml, injected_stylesheet) + } + + pub(crate) fn append(&mut self, parent_id: NodeId, kind: NodeKind) -> NodeId { + let new_child_id = NodeId::from(self.nodes.len()); + self.nodes.push(NodeData { + parent: Some(parent_id), + next_sibling: None, + children: None, + kind, + }); + + let last_child_id = self.nodes[parent_id.get_usize()].children.map(|(_, id)| id); + + if let Some(id) = last_child_id { + self.nodes[id.get_usize()].next_sibling = Some(new_child_id); + } + + self.nodes[parent_id.get_usize()].children = Some( + if let Some((first_child_id, _)) = self.nodes[parent_id.get_usize()].children { + (first_child_id, new_child_id) + } else { + (new_child_id, new_child_id) + }, + ); + + new_child_id + } + + fn append_attribute( + &mut self, + name: AId, + value: roxmltree::StringStorage<'input>, + important: bool, + ) { + self.attrs.push(Attribute { + name, + value, + important, + }); + } +} + +fn parse<'input>( + xml: &roxmltree::Document<'input>, + injected_stylesheet: Option<&'input str>, +) -> Result, Error> { + let mut doc = Document { + nodes: Vec::new(), + attrs: Vec::new(), + links: HashMap::new(), + }; + + // build a map of id -> node for resolve_href + let mut id_map = HashMap::new(); + for node in xml.descendants() { + if let Some(id) = node.attribute("id") { + if !id_map.contains_key(id) { + id_map.insert(id, node); + } + } + } + + // Add a root node. + doc.nodes.push(NodeData { + parent: None, + next_sibling: None, + children: None, + kind: NodeKind::Root, + }); + + let style_sheet = resolve_css(xml, injected_stylesheet); + + parse_xml_node_children( + xml.root(), + xml.root(), + doc.root().id, + &style_sheet, + false, + 0, + &mut doc, + &id_map, + )?; + + // Check that the root element is `svg`. + match doc.root().first_element_child() { + Some(child) => { + if child.tag_name() != Some(EId::Svg) { + return Err(roxmltree::Error::NoRootNode); + } + } + None => return Err(roxmltree::Error::NoRootNode), + } + + // Collect all elements with `id` attribute. + let mut links = HashMap::new(); + for node in doc.descendants() { + if let Some(id) = node.attribute::<&str>(AId::Id) { + links.insert(id.to_string(), node.id); + } + } + doc.links = links; + + fix_recursive_patterns(&mut doc); + fix_recursive_links(EId::ClipPath, AId::ClipPath, &mut doc); + fix_recursive_links(EId::Mask, AId::Mask, &mut doc); + fix_recursive_links(EId::Filter, AId::Filter, &mut doc); + fix_recursive_fe_image(&mut doc); + + Ok(doc) +} + +pub(crate) fn parse_tag_name(node: roxmltree::Node) -> Option { + if !node.is_element() { + return None; + } + + if !matches!(node.tag_name().namespace(), None | Some(SVG_NS)) { + return None; + } + + EId::from_str(node.tag_name().name()) +} + +fn parse_xml_node_children<'input>( + parent: roxmltree::Node<'_, 'input>, + origin: roxmltree::Node, + parent_id: NodeId, + style_sheet: &simplecss::StyleSheet, + ignore_ids: bool, + depth: u32, + doc: &mut Document<'input>, + id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>, +) -> Result<(), Error> { + for node in parent.children() { + parse_xml_node( + node, + origin, + parent_id, + style_sheet, + ignore_ids, + depth, + doc, + id_map, + )?; + } + + Ok(()) +} + +fn parse_xml_node<'input>( + node: roxmltree::Node<'_, 'input>, + origin: roxmltree::Node, + parent_id: NodeId, + style_sheet: &simplecss::StyleSheet, + ignore_ids: bool, + depth: u32, + doc: &mut Document<'input>, + id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>, +) -> Result<(), Error> { + if depth > 1024 { + return Err(Error::NodesLimitReached); + } + + let mut tag_name = match parse_tag_name(node) { + Some(id) => id, + None => return Ok(()), + }; + + if tag_name == EId::Style { + return Ok(()); + } + + // TODO: remove? + // Treat links as groups. + if tag_name == EId::A { + tag_name = EId::G; + } + + let node_id = parse_svg_element(node, parent_id, tag_name, style_sheet, ignore_ids, doc)?; + if tag_name == EId::Text { + super::text::parse_svg_text_element(node, node_id, style_sheet, doc)?; + } else if tag_name == EId::Use { + parse_svg_use_element(node, origin, node_id, style_sheet, depth + 1, doc, id_map)?; + } else { + parse_xml_node_children( + node, + origin, + node_id, + style_sheet, + ignore_ids, + depth + 1, + doc, + id_map, + )?; + } + + Ok(()) +} + +pub(crate) fn parse_svg_element<'input>( + xml_node: roxmltree::Node<'_, 'input>, + parent_id: NodeId, + tag_name: EId, + style_sheet: &simplecss::StyleSheet, + ignore_ids: bool, + doc: &mut Document<'input>, +) -> Result { + let attrs_start_idx = doc.attrs.len(); + + // Copy presentational attributes first. + for attr in xml_node.attributes() { + match attr.namespace() { + None | Some(SVG_NS) | Some(XLINK_NS) | Some(XML_NAMESPACE_NS) => {} + _ => continue, + } + + let aid = match AId::from_str(attr.name()) { + Some(v) => v, + None => continue, + }; + + // During a `use` resolving, all `id` attributes must be ignored. + // Otherwise we will get elements with duplicated id's. + if ignore_ids && aid == AId::Id { + continue; + } + + // For some reason those properties are allowed only inside a `style` attribute and CSS. + if matches!(aid, AId::MixBlendMode | AId::Isolation | AId::FontKerning) { + continue; + } else if aid == AId::ImageRendering + && matches!( + attr.value(), + "smooth" | "high-quality" | "crisp-edges" | "pixelated" + ) + { + continue; + } + + append_attribute( + parent_id, + tag_name, + aid, + attr.value_storage().clone(), + false, + doc, + ); + } + + let mut insert_attribute = |aid, value: &str, important: bool| { + // Check that attribute already exists. + let idx = doc.attrs[attrs_start_idx..] + .iter_mut() + .position(|a| a.name == aid); + + // Append an attribute as usual. + let added = append_attribute( + parent_id, + tag_name, + aid, + roxmltree::StringStorage::new_owned(value), + important, + doc, + ); + + // Check that attribute was actually added, because it could be skipped. + if added { + if let Some(idx) = idx { + let last_idx = doc.attrs.len() - 1; + let existing_idx = attrs_start_idx + idx; + + // See https://developer.mozilla.org/en-US/docs/Web/CSS/important + // When a declaration is important, the order of precedence is reversed. + // Declarations marked as important in the user-agent style sheets override + // all important declarations in the user style sheets. Similarly, all important + // declarations in the user style sheets override all important declarations in the + // author's style sheets. Finally, all important declarations take precedence over + // all animations. + // + // Which means: + // 1) Existing is not important, new is not important -> swap + // 2) Existing is important, new is not important -> don't swap + // 3) Existing is not important, new is important -> swap + // 4) Existing is important, new is important -> don't swap (since the order + // is reversed, so existing important attributes take precedence over new + // important attributes) + let has_precedence = !doc.attrs[existing_idx].important; + + if has_precedence { + doc.attrs.swap(existing_idx, last_idx); + } + + // Remove last. + doc.attrs.pop(); + } + } + }; + + let mut write_declaration = |declaration: &Declaration| { + // TODO: perform XML attribute normalization + let imp = declaration.important; + let val = declaration.value; + + if declaration.name == "marker" { + insert_attribute(AId::MarkerStart, val, imp); + insert_attribute(AId::MarkerMid, val, imp); + insert_attribute(AId::MarkerEnd, val, imp); + } else if declaration.name == "font" { + if let Ok(shorthand) = FontShorthand::from_str(val) { + // First we need to reset all values to their default. + insert_attribute(AId::FontStyle, "normal", imp); + insert_attribute(AId::FontVariant, "normal", imp); + insert_attribute(AId::FontWeight, "normal", imp); + insert_attribute(AId::FontStretch, "normal", imp); + insert_attribute(AId::LineHeight, "normal", imp); + insert_attribute(AId::FontSizeAdjust, "none", imp); + insert_attribute(AId::FontKerning, "auto", imp); + insert_attribute(AId::FontVariantCaps, "normal", imp); + insert_attribute(AId::FontVariantLigatures, "normal", imp); + insert_attribute(AId::FontVariantNumeric, "normal", imp); + insert_attribute(AId::FontVariantEastAsian, "normal", imp); + insert_attribute(AId::FontVariantPosition, "normal", imp); + + // Then, we set the properties that have been declared. + shorthand + .font_stretch + .map(|s| insert_attribute(AId::FontStretch, s, imp)); + shorthand + .font_weight + .map(|s| insert_attribute(AId::FontWeight, s, imp)); + shorthand + .font_variant + .map(|s| insert_attribute(AId::FontVariant, s, imp)); + shorthand + .font_style + .map(|s| insert_attribute(AId::FontStyle, s, imp)); + insert_attribute(AId::FontSize, shorthand.font_size, imp); + insert_attribute(AId::FontFamily, shorthand.font_family, imp); + } else { + log::warn!( + "Failed to parse {} value: '{}'", + AId::Font, + declaration.value + ); + } + } else if let Some(aid) = AId::from_str(declaration.name) { + // Parse only the presentation attributes. + if aid.is_presentation() { + insert_attribute(aid, val, imp); + } + } + }; + + // Apply CSS. + for rule in &style_sheet.rules { + if rule.selector.matches(&XmlNode(xml_node)) { + for declaration in &rule.declarations { + write_declaration(declaration); + } + } + } + + // Split a `style` attribute. + if let Some(value) = xml_node.attribute("style") { + for declaration in simplecss::DeclarationTokenizer::from(value) { + write_declaration(&declaration); + } + } + + if doc.nodes.len() > 1_000_000 { + return Err(Error::NodesLimitReached); + } + + let node_id = doc.append( + parent_id, + NodeKind::Element { + tag_name, + attributes: ShortRange::new(attrs_start_idx as u32, doc.attrs.len() as u32), + }, + ); + + Ok(node_id) +} + +fn append_attribute<'input>( + parent_id: NodeId, + tag_name: EId, + aid: AId, + value: roxmltree::StringStorage<'input>, + important: bool, + doc: &mut Document<'input>, +) -> bool { + match aid { + // The `style` attribute will be split into attributes, so we don't need it. + AId::Style | + // No need to copy a `class` attribute since CSS were already resolved. + AId::Class => return false, + _ => {} + } + + // Ignore `xlink:href` on `tspan` (which was originally `tref` or `a`), + // because we will convert `tref` into `tspan` anyway. + if tag_name == EId::Tspan && aid == AId::Href { + return false; + } + + if aid.allows_inherit_value() && &*value == "inherit" { + return resolve_inherit(parent_id, aid, doc); + } + + doc.append_attribute(aid, value, important); + true +} + +fn resolve_inherit(parent_id: NodeId, aid: AId, doc: &mut Document) -> bool { + if aid.is_inheritable() { + // Inheritable attributes can inherit a value from an any ancestor. + let node_id = doc + .get(parent_id) + .ancestors() + .find(|n| n.has_attribute(aid)) + .map(|n| n.id); + if let Some(node_id) = node_id { + if let Some(attr) = doc + .get(node_id) + .attributes() + .iter() + .find(|a| a.name == aid) + .cloned() + { + doc.attrs.push(Attribute { + name: aid, + value: attr.value, + important: attr.important, + }); + + return true; + } + } + } else { + // Non-inheritable attributes can inherit a value only from a direct parent. + if let Some(attr) = doc + .get(parent_id) + .attributes() + .iter() + .find(|a| a.name == aid) + .cloned() + { + doc.attrs.push(Attribute { + name: aid, + value: attr.value, + important: attr.important, + }); + + return true; + } + } + + // Fallback to a default value if possible. + let value = match aid { + AId::ImageRendering | AId::ShapeRendering | AId::TextRendering => "auto", + + AId::ClipPath + | AId::Filter + | AId::MarkerEnd + | AId::MarkerMid + | AId::MarkerStart + | AId::Mask + | AId::Stroke + | AId::StrokeDasharray + | AId::TextDecoration => "none", + + AId::FontStretch + | AId::FontStyle + | AId::FontVariant + | AId::FontWeight + | AId::LetterSpacing + | AId::WordSpacing => "normal", + + AId::Fill | AId::FloodColor | AId::StopColor => "black", + + AId::FillOpacity + | AId::FloodOpacity + | AId::Opacity + | AId::StopOpacity + | AId::StrokeOpacity => "1", + + AId::ClipRule | AId::FillRule => "nonzero", + + AId::BaselineShift => "baseline", + AId::ColorInterpolationFilters => "linearRGB", + AId::Direction => "ltr", + AId::Display => "inline", + AId::FontSize => "medium", + AId::Overflow => "visible", + AId::StrokeDashoffset => "0", + AId::StrokeLinecap => "butt", + AId::StrokeLinejoin => "miter", + AId::StrokeMiterlimit => "4", + AId::StrokeWidth => "1", + AId::TextAnchor => "start", + AId::Visibility => "visible", + AId::WritingMode => "lr-tb", + _ => return false, + }; + + doc.append_attribute(aid, roxmltree::StringStorage::Borrowed(value), false); + true +} + +fn resolve_href<'a, 'input: 'a>( + node: roxmltree::Node<'a, 'input>, + id_map: &HashMap<&str, roxmltree::Node<'a, 'input>>, +) -> Option> { + let link_value = node + .attribute((XLINK_NS, "href")) + .or_else(|| node.attribute("href"))?; + + let link_id = svgtypes::IRI::from_str(link_value).ok()?.0; + + id_map.get(link_id).copied() +} + +fn parse_svg_use_element<'input>( + node: roxmltree::Node<'_, 'input>, + origin: roxmltree::Node, + parent_id: NodeId, + style_sheet: &simplecss::StyleSheet, + depth: u32, + doc: &mut Document<'input>, + id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>, +) -> Result<(), Error> { + let link = match resolve_href(node, id_map) { + Some(v) => v, + None => return Ok(()), + }; + + if link == node || link == origin { + log::warn!( + "Recursive 'use' detected. '{}' will be skipped.", + node.attribute((SVG_NS, "id")).unwrap_or_default() + ); + return Ok(()); + } + + // Make sure we're linked to an SVG element. + if parse_tag_name(link).is_none() { + return Ok(()); + } + + // Check that none of the linked node's children reference current `use` node + // via other `use` node. + // + // Example: + // + // + // + // + // + // `use2` should be removed. + // + // Also, child should not reference its parent: + // + // + // + // + // `use1` should be removed. + let mut is_recursive = false; + for link_child in link + .descendants() + .skip(1) + .filter(|n| n.has_tag_name((SVG_NS, "use"))) + { + if let Some(link2) = resolve_href(link_child, id_map) { + if link2 == node || link2 == link { + is_recursive = true; + break; + } + } + } + + if is_recursive { + log::warn!( + "Recursive 'use' detected. '{}' will be skipped.", + node.attribute((SVG_NS, "id")).unwrap_or_default() + ); + return Ok(()); + } + + parse_xml_node( + link, + node, + parent_id, + style_sheet, + true, + depth + 1, + doc, + id_map, + ) +} + +fn resolve_css<'a>( + xml: &'a roxmltree::Document<'a>, + style_sheet: Option<&'a str>, +) -> simplecss::StyleSheet<'a> { + let mut sheet = simplecss::StyleSheet::new(); + + // Injected style sheets do not override internal ones (we mimic the logic of rsvg-convert), + // so we need to parse it first. + if let Some(style_sheet) = style_sheet { + sheet.parse_more(style_sheet); + } + + for node in xml.descendants().filter(|n| n.has_tag_name("style")) { + match node.attribute("type") { + Some("text/css") => {} + Some(_) => continue, + None => {} + } + + let text = match node.text() { + Some(v) => v, + None => continue, + }; + + sheet.parse_more(text); + } + + sheet +} + +struct XmlNode<'a, 'input: 'a>(roxmltree::Node<'a, 'input>); + +impl simplecss::Element for XmlNode<'_, '_> { + fn parent_element(&self) -> Option { + self.0.parent_element().map(XmlNode) + } + + fn prev_sibling_element(&self) -> Option { + self.0.prev_sibling_element().map(XmlNode) + } + + fn has_local_name(&self, local_name: &str) -> bool { + self.0.tag_name().name() == local_name + } + + fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool { + match self.0.attribute(local_name) { + Some(value) => operator.matches(value), + None => false, + } + } + + fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool { + match class { + simplecss::PseudoClass::FirstChild => self.prev_sibling_element().is_none(), + // TODO: lang + _ => false, // Since we are querying a static SVG we can ignore other pseudo-classes. + } + } +} + +fn fix_recursive_patterns(doc: &mut Document) { + while let Some(node_id) = find_recursive_pattern(AId::Fill, doc) { + let idx = doc.get(node_id).attribute_id(AId::Fill).unwrap(); + doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none"); + } + + while let Some(node_id) = find_recursive_pattern(AId::Stroke, doc) { + let idx = doc.get(node_id).attribute_id(AId::Stroke).unwrap(); + doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none"); + } +} + +fn find_recursive_pattern(aid: AId, doc: &mut Document) -> Option { + for pattern_node in doc + .root() + .descendants() + .filter(|n| n.tag_name() == Some(EId::Pattern)) + { + for node in pattern_node.descendants() { + let value = match node.attribute(aid) { + Some(v) => v, + None => continue, + }; + + if let Ok(svgtypes::Paint::FuncIRI(link_id, _)) = svgtypes::Paint::from_str(value) { + if link_id == pattern_node.element_id() { + // If a pattern child has a link to the pattern itself + // then we have to replace it with `none`. + // Otherwise we will get endless loop/recursion and stack overflow. + return Some(node.id); + } else { + // Check that linked node children doesn't link this pattern. + if let Some(linked_node) = doc.element_by_id(link_id) { + for node2 in linked_node.descendants() { + let value2 = match node2.attribute(aid) { + Some(v) => v, + None => continue, + }; + + if let Ok(svgtypes::Paint::FuncIRI(link_id2, _)) = + svgtypes::Paint::from_str(value2) + { + if link_id2 == pattern_node.element_id() { + return Some(node2.id); + } + } + } + } + } + } + } + } + + None +} + +fn fix_recursive_links(eid: EId, aid: AId, doc: &mut Document) { + while let Some(node_id) = find_recursive_link(eid, aid, doc) { + let idx = doc.get(node_id).attribute_id(aid).unwrap(); + doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none"); + } +} + +fn find_recursive_link(eid: EId, aid: AId, doc: &Document) -> Option { + for node in doc + .root() + .descendants() + .filter(|n| n.tag_name() == Some(eid)) + { + for child in node.descendants() { + if let Some(link) = child.node_attribute(aid) { + if link == node { + // If an element child has a link to the element itself + // then we have to replace it with `none`. + // Otherwise we will get endless loop/recursion and stack overflow. + return Some(child.id); + } else { + // Check that linked node children doesn't link this element. + for node2 in link.descendants() { + if let Some(link2) = node2.node_attribute(aid) { + if link2 == node { + return Some(node2.id); + } + } + } + } + } + } + } + + None +} + +/// Detects cases like: +/// +/// ```xml +/// +/// +/// +/// +/// ``` +fn fix_recursive_fe_image(doc: &mut Document) { + let mut ids = Vec::new(); + for fe_node in doc + .root() + .descendants() + .filter(|n| n.tag_name() == Some(EId::FeImage)) + { + if let Some(link) = fe_node.node_attribute(AId::Href) { + if let Some(filter_uri) = link.attribute::<&str>(AId::Filter) { + let filter_id = fe_node.parent().unwrap().element_id(); + for func in svgtypes::FilterValueListParser::from(filter_uri).flatten() { + if let svgtypes::FilterValue::Url(url) = func { + if url == filter_id { + ids.push(link.id); + } + } + } + } + } + } + + for id in ids { + let idx = doc.get(id).attribute_id(AId::Filter).unwrap(); + doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none"); + } +} diff --git a/third_party/usvg/src/parser/svgtree/text.rs b/third_party/usvg/src/parser/svgtree/text.rs new file mode 100644 index 0000000000..11877da3f3 --- /dev/null +++ b/third_party/usvg/src/parser/svgtree/text.rs @@ -0,0 +1,366 @@ +// Copyright 2021 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![allow(clippy::comparison_chain)] + +use roxmltree::Error; + +use super::{AId, Document, EId, NodeId, NodeKind, SvgNode}; + +const XLINK_NS: &str = "http://www.w3.org/1999/xlink"; + +pub(crate) fn parse_svg_text_element<'input>( + parent: roxmltree::Node<'_, 'input>, + parent_id: NodeId, + style_sheet: &simplecss::StyleSheet, + doc: &mut Document<'input>, +) -> Result<(), Error> { + debug_assert_eq!(parent.tag_name().name(), "text"); + + let space = if doc.get(parent_id).has_attribute(AId::Space) { + get_xmlspace(doc, parent_id, XmlSpace::Default) + } else { + if let Some(node) = doc + .get(parent_id) + .ancestors() + .find(|n| n.has_attribute(AId::Space)) + { + get_xmlspace(doc, node.id, XmlSpace::Default) + } else { + XmlSpace::Default + } + }; + + parse_svg_text_element_impl(parent, parent_id, style_sheet, space, doc)?; + + trim_text_nodes(parent_id, space, doc); + Ok(()) +} + +fn parse_svg_text_element_impl<'input>( + parent: roxmltree::Node<'_, 'input>, + parent_id: NodeId, + style_sheet: &simplecss::StyleSheet, + space: XmlSpace, + doc: &mut Document<'input>, +) -> Result<(), Error> { + for node in parent.children() { + if node.is_text() { + let text = trim_text(node.text().unwrap(), space); + doc.append(parent_id, NodeKind::Text(text)); + continue; + } + + let mut tag_name = match super::parse::parse_tag_name(node) { + Some(v) => v, + None => continue, + }; + + if tag_name == EId::A { + // Treat links as simple text. + tag_name = EId::Tspan; + } + + if !matches!(tag_name, EId::Tspan | EId::Tref | EId::TextPath) { + continue; + } + + // `textPath` must be a direct `text` child. + if tag_name == EId::TextPath && parent.tag_name().name() != "text" { + continue; + } + + // We are converting `tref` into `tspan` to simplify later use. + let mut is_tref = false; + if tag_name == EId::Tref { + tag_name = EId::Tspan; + is_tref = true; + } + + let node_id = + super::parse::parse_svg_element(node, parent_id, tag_name, style_sheet, false, doc)?; + let space = get_xmlspace(doc, node_id, space); + + if is_tref { + let link_value = node + .attribute((XLINK_NS, "href")) + .or_else(|| node.attribute("href")); + + if let Some(href) = link_value { + if let Some(text) = resolve_tref_text(node.document(), href) { + let text = trim_text(&text, space); + doc.append(node_id, NodeKind::Text(text)); + } + } + } else { + parse_svg_text_element_impl(node, node_id, style_sheet, space, doc)?; + } + } + + Ok(()) +} + +fn resolve_tref_text(xml: &roxmltree::Document, href: &str) -> Option { + let id = svgtypes::IRI::from_str(href).ok()?.0; + + // Find linked element in the original tree. + let node = xml.descendants().find(|n| n.attribute("id") == Some(id))?; + + // `tref` should be linked to an SVG element. + super::parse::parse_tag_name(node)?; + + // 'All character data within the referenced element, including character data enclosed + // within additional markup, will be rendered.' + // + // So we don't care about attributes and everything. Just collecting text nodes data. + // + // Note: we have to filter nodes by `is_text()` first since `text()` will look up + // for text nodes in element children therefore we will get duplicates. + let text: String = node + .descendants() + .filter(|n| n.is_text()) + .filter_map(|n| n.text()) + .collect(); + if text.is_empty() { + None + } else { + Some(text) + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum XmlSpace { + Default, + Preserve, +} + +fn get_xmlspace(doc: &Document, node_id: NodeId, default: XmlSpace) -> XmlSpace { + match doc.get(node_id).attribute(AId::Space) { + Some("preserve") => XmlSpace::Preserve, + Some(_) => XmlSpace::Default, + _ => default, + } +} + +trait StrTrim { + fn remove_first_space(&mut self); + fn remove_last_space(&mut self); +} + +impl StrTrim for String { + fn remove_first_space(&mut self) { + debug_assert_eq!(self.chars().next().unwrap(), ' '); + self.drain(0..1); + } + + fn remove_last_space(&mut self) { + debug_assert_eq!(self.chars().next_back().unwrap(), ' '); + self.pop(); + } +} + +/// Prepares text nodes according to the spec: https://www.w3.org/TR/SVG11/text.html#WhiteSpace +/// +/// This function handles: +/// - 'xml:space' processing +/// - tabs and newlines removing/replacing +/// - spaces trimming +fn trim_text_nodes(text_elem_id: NodeId, xmlspace: XmlSpace, doc: &mut Document) { + let mut nodes = Vec::new(); // TODO: allocate only once + collect_text_nodes(doc.get(text_elem_id), 0, &mut nodes); + + // `trim` method has already collapsed all spaces into a single one, + // so we have to check only for one leading or trailing space. + + if nodes.len() == 1 { + // Process element with a single text node child. + + let node_id = nodes[0].0; + + if xmlspace == XmlSpace::Default { + if let NodeKind::Text(ref mut text) = doc.nodes[node_id.get_usize()].kind { + match text.len() { + 0 => {} // An empty string. Do nothing. + 1 => { + // If string has only one character and it's a space - clear this string. + if text.as_bytes()[0] == b' ' { + text.clear(); + } + } + _ => { + // 'text' has at least 2 bytes, so indexing is safe. + let c1 = text.as_bytes()[0]; + let c2 = text.as_bytes()[text.len() - 1]; + + if c1 == b' ' { + text.remove_first_space(); + } + + if c2 == b' ' { + text.remove_last_space(); + } + } + } + } + } else { + // Do nothing when xml:space=preserve. + } + } else if nodes.len() > 1 { + // Process element with many text node children. + + // We manage all text nodes as a single text node + // and trying to remove duplicated spaces across nodes. + // + // For example 'Text text text' + // is the same is 'Text text text' + + let mut i = 0; + let len = nodes.len() - 1; + let mut last_non_empty: Option = None; + while i < len { + // Process pairs. + let (mut node1_id, depth1) = nodes[i]; + let (node2_id, depth2) = nodes[i + 1]; + + if doc.get(node1_id).text().is_empty() { + if let Some(n) = last_non_empty { + node1_id = n; + } + } + + // Parent of the text node is always an element node and always exist, + // so unwrap is safe. + let xmlspace1 = get_xmlspace(doc, doc.get(node1_id).parent().unwrap().id, xmlspace); + let xmlspace2 = get_xmlspace(doc, doc.get(node2_id).parent().unwrap().id, xmlspace); + + // >text<..>text< + // 1 2 3 4 + let (c1, c2, c3, c4) = { + let text1 = doc.get(node1_id).text(); + let text2 = doc.get(node2_id).text(); + + let bytes1 = text1.as_bytes(); + let bytes2 = text2.as_bytes(); + + let c1 = bytes1.first().cloned(); + let c2 = bytes1.last().cloned(); + let c3 = bytes2.first().cloned(); + let c4 = bytes2.last().cloned(); + + (c1, c2, c3, c4) + }; + + // NOTE: xml:space processing is mostly an undefined behavior, + // because everyone do it differently. + // We're mimicking the Chrome behavior. + + // Remove space from the second text node if both nodes has bound spaces. + // From: 'Text text' + // To: 'Text text' + // + // See text-tspan-02-b.svg for details. + if depth1 < depth2 { + if c3 == Some(b' ') { + if xmlspace2 == XmlSpace::Default { + if let NodeKind::Text(ref mut text) = doc.nodes[node2_id.get_usize()].kind { + text.remove_first_space(); + } + } + } + } else { + if c2 == Some(b' ') && c2 == c3 { + if xmlspace1 == XmlSpace::Default && xmlspace2 == XmlSpace::Default { + if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind { + text.remove_last_space(); + } + } else { + if xmlspace1 == XmlSpace::Preserve && xmlspace2 == XmlSpace::Default { + if let NodeKind::Text(ref mut text) = + doc.nodes[node2_id.get_usize()].kind + { + text.remove_first_space(); + } + } + } + } + } + + let is_first = i == 0; + let is_last = i == len - 1; + + if is_first + && c1 == Some(b' ') + && xmlspace1 == XmlSpace::Default + && !doc.get(node1_id).text().is_empty() + { + // Remove a leading space from a first text node. + if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind { + text.remove_first_space(); + } + } else if is_last + && c4 == Some(b' ') + && !doc.get(node2_id).text().is_empty() + && xmlspace2 == XmlSpace::Default + { + // Remove a trailing space from a last text node. + // Also check that 'text2' is not empty already. + if let NodeKind::Text(ref mut text) = doc.nodes[node2_id.get_usize()].kind { + text.remove_last_space(); + } + } + + if is_last + && c2 == Some(b' ') + && !doc.get(node1_id).text().is_empty() + && doc.get(node2_id).text().is_empty() + && doc.get(node1_id).text().ends_with(' ') + { + if let NodeKind::Text(ref mut text) = doc.nodes[node1_id.get_usize()].kind { + text.remove_last_space(); + } + } + + if !doc.get(node1_id).text().trim().is_empty() { + last_non_empty = Some(node1_id); + } + + i += 1; + } + } + + // TODO: find a way to remove all empty text nodes +} + +fn collect_text_nodes(parent: SvgNode, depth: usize, nodes: &mut Vec<(NodeId, usize)>) { + for child in parent.children() { + if child.is_text() { + nodes.push((child.id, depth)); + } else if child.is_element() { + collect_text_nodes(child, depth + 1, nodes); + } + } +} + +fn trim_text(text: &str, space: XmlSpace) -> String { + let mut s = String::with_capacity(text.len()); + + let mut prev = '0'; + for c in text.chars() { + // \r, \n and \t should be converted into spaces. + let c = match c { + '\r' | '\n' | '\t' => ' ', + _ => c, + }; + + // Skip continuous spaces. + if space == XmlSpace::Default && c == ' ' && c == prev { + continue; + } + + prev = c; + + s.push(c); + } + + s +} diff --git a/third_party/usvg/src/parser/switch.rs b/third_party/usvg/src/parser/switch.rs new file mode 100644 index 0000000000..9825598ef3 --- /dev/null +++ b/third_party/usvg/src/parser/switch.rs @@ -0,0 +1,125 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use super::svgtree::{AId, SvgNode}; +use super::{converter, Options}; +use crate::{Group, Node}; + +// Full list can be found here: https://www.w3.org/TR/SVG11/feature.html +static FEATURES: &[&str] = &[ + "http://www.w3.org/TR/SVG11/feature#SVGDOM-static", + "http://www.w3.org/TR/SVG11/feature#SVG-static", + "http://www.w3.org/TR/SVG11/feature#CoreAttribute", // no xml:base and xml:lang + "http://www.w3.org/TR/SVG11/feature#Structure", + "http://www.w3.org/TR/SVG11/feature#BasicStructure", + "http://www.w3.org/TR/SVG11/feature#ContainerAttribute", // `enable-background` + "http://www.w3.org/TR/SVG11/feature#ConditionalProcessing", + "http://www.w3.org/TR/SVG11/feature#Image", + "http://www.w3.org/TR/SVG11/feature#Style", + // "http://www.w3.org/TR/SVG11/feature#ViewportAttribute", // `clip` and `overflow`, not yet + "http://www.w3.org/TR/SVG11/feature#Shape", + "http://www.w3.org/TR/SVG11/feature#Text", + "http://www.w3.org/TR/SVG11/feature#BasicText", + "http://www.w3.org/TR/SVG11/feature#PaintAttribute", // no color-interpolation and color-rendering + "http://www.w3.org/TR/SVG11/feature#BasicPaintAttribute", // no color-interpolation + "http://www.w3.org/TR/SVG11/feature#OpacityAttribute", + "http://www.w3.org/TR/SVG11/feature#GraphicsAttribute", + "http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute", + "http://www.w3.org/TR/SVG11/feature#Marker", + // "http://www.w3.org/TR/SVG11/feature#ColorProfile", // not yet + "http://www.w3.org/TR/SVG11/feature#Gradient", + "http://www.w3.org/TR/SVG11/feature#Pattern", + "http://www.w3.org/TR/SVG11/feature#Clip", + "http://www.w3.org/TR/SVG11/feature#BasicClip", + "http://www.w3.org/TR/SVG11/feature#Mask", + "http://www.w3.org/TR/SVG11/feature#Filter", + "http://www.w3.org/TR/SVG11/feature#BasicFilter", + // only xlink:href + "http://www.w3.org/TR/SVG11/feature#XlinkAttribute", + // "http://www.w3.org/TR/SVG11/feature#Font", + // "http://www.w3.org/TR/SVG11/feature#BasicFont", +]; + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) -> Option<()> { + let child = node + .children() + .find(|n| is_condition_passed(*n, state.opt))?; + if let Some(g) = converter::convert_group(node, state, false, cache, parent, &|cache, g| { + converter::convert_element(child, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); + } + + Some(()) +} + +pub(crate) fn is_condition_passed(node: SvgNode, opt: &Options) -> bool { + if !node.is_element() { + return false; + } + + if node.has_attribute(AId::RequiredExtensions) { + return false; + } + + // 'The value is a list of feature strings, with the individual values separated by white space. + // Determines whether all of the named features are supported by the user agent. + // Only feature strings defined in the Feature String appendix are allowed. + // If all of the given features are supported, then the attribute evaluates to true; + // otherwise, the current element and its children are skipped and thus will not be rendered.' + if let Some(features) = node.attribute::<&str>(AId::RequiredFeatures) { + for feature in features.split(' ') { + if !FEATURES.contains(&feature) { + return false; + } + } + } + + if !is_valid_sys_lang(node, opt) { + return false; + } + + true +} + +/// SVG spec 5.8.5 +fn is_valid_sys_lang(node: SvgNode, opt: &Options) -> bool { + // 'The attribute value is a comma-separated list of language names + // as defined in BCP 47.' + // + // But we support only simple cases like `en` or `en-US`. + // No one really uses this, especially with complex BCP 47 values. + if let Some(langs) = node.attribute::<&str>(AId::SystemLanguage) { + let mut has_match = false; + for lang in langs.split(',') { + let lang = lang.trim(); + + // 'Evaluates to `true` if one of the languages indicated by user preferences exactly + // equals one of the languages given in the value of this parameter.' + if opt.languages.iter().any(|v| v == lang) { + has_match = true; + break; + } + + // 'If one of the languages indicated by user preferences exactly equals a prefix + // of one of the languages given in the value of this parameter such that + // the first tag character following the prefix is `-`.' + if let Some(idx) = lang.bytes().position(|c| c == b'-') { + let lang_prefix = &lang[..idx]; + if opt.languages.iter().any(|v| v == lang_prefix) { + has_match = true; + break; + } + } + } + + has_match + } else { + true + } +} diff --git a/third_party/usvg/src/parser/text.rs b/third_party/usvg/src/parser/text.rs new file mode 100644 index 0000000000..743756a05c --- /dev/null +++ b/third_party/usvg/src/parser/text.rs @@ -0,0 +1,825 @@ +// Copyright 2019 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use kurbo::{ParamCurve, ParamCurveArclen}; +use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit}; + +use super::svgtree::{AId, EId, FromValue, SvgNode}; +use super::{converter, style, OptionLog}; +use crate::*; + +impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "start" => Some(TextAnchor::Start), + "middle" => Some(TextAnchor::Middle), + "end" => Some(TextAnchor::End), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "auto" => Some(AlignmentBaseline::Auto), + "baseline" => Some(AlignmentBaseline::Baseline), + "before-edge" => Some(AlignmentBaseline::BeforeEdge), + "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge), + "middle" => Some(AlignmentBaseline::Middle), + "central" => Some(AlignmentBaseline::Central), + "after-edge" => Some(AlignmentBaseline::AfterEdge), + "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge), + "ideographic" => Some(AlignmentBaseline::Ideographic), + "alphabetic" => Some(AlignmentBaseline::Alphabetic), + "hanging" => Some(AlignmentBaseline::Hanging), + "mathematical" => Some(AlignmentBaseline::Mathematical), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "auto" => Some(DominantBaseline::Auto), + "use-script" => Some(DominantBaseline::UseScript), + "no-change" => Some(DominantBaseline::NoChange), + "reset-size" => Some(DominantBaseline::ResetSize), + "ideographic" => Some(DominantBaseline::Ideographic), + "alphabetic" => Some(DominantBaseline::Alphabetic), + "hanging" => Some(DominantBaseline::Hanging), + "mathematical" => Some(DominantBaseline::Mathematical), + "central" => Some(DominantBaseline::Central), + "middle" => Some(DominantBaseline::Middle), + "text-after-edge" => Some(DominantBaseline::TextAfterEdge), + "text-before-edge" => Some(DominantBaseline::TextBeforeEdge), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "spacing" => Some(LengthAdjust::Spacing), + "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs), + _ => None, + } + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "normal" => Some(FontStyle::Normal), + "italic" => Some(FontStyle::Italic), + "oblique" => Some(FontStyle::Oblique), + _ => None, + } + } +} + +/// A text character position. +/// +/// _Character_ is a Unicode codepoint. +#[derive(Clone, Copy, Debug)] +struct CharacterPosition { + /// An absolute X axis position. + x: Option, + /// An absolute Y axis position. + y: Option, + /// A relative X axis offset. + dx: Option, + /// A relative Y axis offset. + dy: Option, +} + +pub(crate) fn convert( + text_node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) { + let pos_list = resolve_positions_list(text_node, state); + let rotate_list = resolve_rotate_list(text_node); + let writing_mode = convert_writing_mode(text_node); + + let chunks = collect_text_chunks(text_node, &pos_list, state, cache); + + let rendering_mode: TextRendering = text_node + .find_attribute(AId::TextRendering) + .unwrap_or(state.opt.text_rendering); + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let id = if state.parent_markers.is_empty() { + text_node.element_id().to_string() + } else { + String::new() + }; + + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + + let mut text = Text { + id, + rendering_mode, + dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(), + dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(), + rotate: rotate_list, + writing_mode, + chunks, + abs_transform: parent.abs_transform, + // All fields below will be reset by `text_to_paths`. + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + flattened: Box::new(Group::empty()), + layouted: vec![], + }; + + if text::convert(&mut text, &state.opt.font_resolver, cache).is_none() { + return; + } + + parent.children.push(Node::Text(Box::new(text))); +} + +struct IterState { + chars_count: usize, + chunk_bytes_count: usize, + split_chunk: bool, + text_flow: TextFlow, + chunks: Vec, +} + +fn collect_text_chunks( + text_node: SvgNode, + pos_list: &[CharacterPosition], + state: &converter::State, + cache: &mut converter::Cache, +) -> Vec { + let mut iter_state = IterState { + chars_count: 0, + chunk_bytes_count: 0, + split_chunk: false, + text_flow: TextFlow::Linear, + chunks: Vec::new(), + }; + + collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state); + + iter_state.chunks +} + +fn collect_text_chunks_impl( + parent: SvgNode, + pos_list: &[CharacterPosition], + state: &converter::State, + cache: &mut converter::Cache, + iter_state: &mut IterState, +) { + for child in parent.children() { + if child.is_element() { + if child.tag_name() == Some(EId::TextPath) { + if parent.tag_name() != Some(EId::Text) { + // `textPath` can be set only as a direct `text` element child. + iter_state.chars_count += count_chars(child); + continue; + } + + match resolve_text_flow(child, state) { + Some(v) => { + iter_state.text_flow = v; + } + None => { + // Skip an invalid text path and all it's children. + // We have to update the chars count, + // because `pos_list` was calculated including this text path. + iter_state.chars_count += count_chars(child); + continue; + } + } + + iter_state.split_chunk = true; + } + + collect_text_chunks_impl(child, pos_list, state, cache, iter_state); + + iter_state.text_flow = TextFlow::Linear; + + // Next char after `textPath` should be split too. + if child.tag_name() == Some(EId::TextPath) { + iter_state.split_chunk = true; + } + + continue; + } + + if !parent.is_visible_element(state.opt) { + iter_state.chars_count += child.text().chars().count(); + continue; + } + + let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default(); + + // TODO: what to do when <= 0? UB? + let font_size = super::units::resolve_font_size(parent, state); + let font_size = match NonZeroPositiveF32::new(font_size) { + Some(n) => n, + None => { + // Skip this span. + iter_state.chars_count += child.text().chars().count(); + continue; + } + }; + + let font = convert_font(parent, state); + + let raw_paint_order: svgtypes::PaintOrder = + parent.find_attribute(AId::PaintOrder).unwrap_or_default(); + let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order); + + let mut dominant_baseline = parent + .find_attribute(AId::DominantBaseline) + .unwrap_or_default(); + + // `no-change` means "use parent". + if dominant_baseline == DominantBaseline::NoChange { + dominant_baseline = parent + .parent_element() + .unwrap() + .find_attribute(AId::DominantBaseline) + .unwrap_or_default(); + } + + let mut apply_kerning = true; + #[allow(clippy::if_same_then_else)] + if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 { + apply_kerning = false; + } else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none") { + apply_kerning = false; + } + + let mut text_length = + parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state); + // Negative values should be ignored. + if let Some(n) = text_length { + if n < 0.0 { + text_length = None; + } + } + + let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default(); + + let span = TextSpan { + start: 0, + end: 0, + fill: style::resolve_fill(parent, true, state, cache), + stroke: style::resolve_stroke(parent, true, state, cache), + paint_order, + font, + font_size, + small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"), + apply_kerning, + decoration: resolve_decoration(parent, state, cache), + visible: visibility == Visibility::Visible, + dominant_baseline, + alignment_baseline: parent + .find_attribute(AId::AlignmentBaseline) + .unwrap_or_default(), + baseline_shift: convert_baseline_shift(parent, state), + letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0), + word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0), + text_length, + length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(), + }; + + let mut is_new_span = true; + for c in child.text().chars() { + let char_len = c.len_utf8(); + + // Create a new chunk if: + // - this is the first span (yes, position can be None) + // - text character has an absolute coordinate assigned to it (via x/y attribute) + // - `c` is the first char of the `textPath` + // - `c` is the first char after `textPath` + let is_new_chunk = pos_list[iter_state.chars_count].x.is_some() + || pos_list[iter_state.chars_count].y.is_some() + || iter_state.split_chunk + || iter_state.chunks.is_empty(); + + iter_state.split_chunk = false; + + if is_new_chunk { + iter_state.chunk_bytes_count = 0; + + let mut span2 = span.clone(); + span2.start = 0; + span2.end = char_len; + + iter_state.chunks.push(TextChunk { + x: pos_list[iter_state.chars_count].x, + y: pos_list[iter_state.chars_count].y, + anchor, + spans: vec![span2], + text_flow: iter_state.text_flow.clone(), + text: c.to_string(), + }); + } else if is_new_span { + // Add this span to the last text chunk. + let mut span2 = span.clone(); + span2.start = iter_state.chunk_bytes_count; + span2.end = iter_state.chunk_bytes_count + char_len; + + if let Some(chunk) = iter_state.chunks.last_mut() { + chunk.text.push(c); + chunk.spans.push(span2); + } + } else { + // Extend the last span. + if let Some(chunk) = iter_state.chunks.last_mut() { + chunk.text.push(c); + if let Some(span) = chunk.spans.last_mut() { + debug_assert_ne!(span.end, 0); + span.end += char_len; + } + } + } + + is_new_span = false; + iter_state.chars_count += 1; + iter_state.chunk_bytes_count += char_len; + } + } +} + +fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option { + let linked_node = node.attribute::(AId::Href)?; + let path = super::shapes::convert(linked_node, state)?; + + // The reference path's transform needs to be applied + let transform = linked_node.resolve_transform(AId::Transform, state); + let path = if !transform.is_identity() { + let mut path_copy = path.as_ref().clone(); + path_copy = path_copy.transform(transform)?; + Arc::new(path_copy) + } else { + path + }; + + let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default(); + let start_offset = if start_offset.unit == LengthUnit::Percent { + // 'If a percentage is given, then the `startOffset` represents + // a percentage distance along the entire path.' + let path_len = path_length(&path); + (path_len * (start_offset.number / 100.0)) as f32 + } else { + node.resolve_length(AId::StartOffset, state, 0.0) + }; + + let id = NonEmptyString::new(linked_node.element_id().to_string())?; + Some(TextFlow::Path(Arc::new(TextPath { + id, + start_offset, + path, + }))) +} + +fn convert_font(node: SvgNode, state: &converter::State) -> Font { + let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default(); + let stretch = conv_font_stretch(node); + let weight = resolve_font_weight(node); + + let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) + { + n.attribute(AId::FontFamily).unwrap_or("") + } else { + "" + }; + + let mut families = parse_font_families(font_families) + .ok() + .log_none(|| { + log::warn!( + "Failed to parse {} value: '{}'. Falling back to {}.", + AId::FontFamily, + font_families, + state.opt.font_family + ); + }) + .unwrap_or_default(); + + if families.is_empty() { + families.push(FontFamily::Named(state.opt.font_family.clone())); + } + + Font { + families, + style, + stretch, + weight, + } +} + +// TODO: properly resolve narrower/wider +fn conv_font_stretch(node: SvgNode) -> FontStretch { + if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) { + match n.attribute(AId::FontStretch).unwrap_or("") { + "narrower" | "condensed" => FontStretch::Condensed, + "ultra-condensed" => FontStretch::UltraCondensed, + "extra-condensed" => FontStretch::ExtraCondensed, + "semi-condensed" => FontStretch::SemiCondensed, + "semi-expanded" => FontStretch::SemiExpanded, + "wider" | "expanded" => FontStretch::Expanded, + "extra-expanded" => FontStretch::ExtraExpanded, + "ultra-expanded" => FontStretch::UltraExpanded, + _ => FontStretch::Normal, + } + } else { + FontStretch::Normal + } +} + +fn resolve_font_weight(node: SvgNode) -> u16 { + fn bound(min: usize, val: usize, max: usize) -> usize { + std::cmp::max(min, std::cmp::min(max, val)) + } + + let nodes: Vec<_> = node.ancestors().collect(); + let mut weight = 400; + for n in nodes.iter().rev().skip(1) { + // skip Root + weight = match n.attribute(AId::FontWeight).unwrap_or("") { + "normal" => 400, + "bold" => 700, + "100" => 100, + "200" => 200, + "300" => 300, + "400" => 400, + "500" => 500, + "600" => 600, + "700" => 700, + "800" => 800, + "900" => 900, + "bolder" => { + // By the CSS2 spec the default value should be 400 + // so `bolder` will result in 500. + // But Chrome and Inkscape will give us 700. + // Have no idea is it a bug or something, but + // we will follow such behavior for now. + let step = if weight == 400 { 300 } else { 100 }; + + bound(100, weight + step, 900) + } + "lighter" => { + // By the CSS2 spec the default value should be 400 + // so `lighter` will result in 300. + // But Chrome and Inkscape will give us 200. + // Have no idea is it a bug or something, but + // we will follow such behavior for now. + let step = if weight == 400 { 200 } else { 100 }; + + bound(100, weight - step, 900) + } + _ => weight, + }; + } + + weight as u16 +} + +/// Resolves text's character positions. +/// +/// This includes: x, y, dx, dy. +/// +/// # The character +/// +/// The first problem with this task is that the *character* itself +/// is basically undefined in the SVG spec. Sometimes it's an *XML character*, +/// sometimes a *glyph*, and sometimes just a *character*. +/// +/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537) +/// on the SVG working group that addresses this by stating that a character +/// is a Unicode code point. But it's not final. +/// +/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*. +/// +/// Anyway, we treat a character as a Unicode code point. +/// +/// # Algorithm +/// +/// To resolve positions, we have to iterate over descendant nodes and +/// if the current node is a `tspan` and has x/y/dx/dy attribute, +/// than the positions from this attribute should be assigned to the characters +/// of this `tspan` and it's descendants. +/// +/// Positions list can have more values than characters in the `tspan`, +/// so we have to clamp it, because values should not overlap, e.g.: +/// +/// (we ignore whitespaces for example purposes, +/// so the `text` content is `Text` and not `T ex t`) +/// +/// ```text +/// +/// a +/// +/// bc +/// +/// d +/// +/// ``` +/// +/// In this example, the `d` position should not be set to `30`. +/// And the result should be: `[None, 10, 20, None]` +/// +/// Another example: +/// +/// ```text +/// +/// +/// a +/// +/// bc +/// +/// +/// d +/// +/// ``` +/// +/// The result should be: `[100, 50, 120, None]` +fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec { + // Allocate a list that has all characters positions set to `None`. + let total_chars = count_chars(text_node); + let mut list = vec![ + CharacterPosition { + x: None, + y: None, + dx: None, + dy: None, + }; + total_chars + ]; + + let mut offset = 0; + for child in text_node.descendants() { + if child.is_element() { + // We must ignore text positions on `textPath`. + if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) { + continue; + } + + let child_chars = count_chars(child); + macro_rules! push_list { + ($aid:expr, $field:ident) => { + if let Some(num_list) = super::units::convert_list(child, $aid, state) { + // Note that we are using not the total count, + // but the amount of characters in the current `tspan` (with children). + let len = std::cmp::min(num_list.len(), child_chars); + for i in 0..len { + list[offset + i].$field = Some(num_list[i]); + } + } + }; + } + + push_list!(AId::X, x); + push_list!(AId::Y, y); + push_list!(AId::Dx, dx); + push_list!(AId::Dy, dy); + } else if child.is_text() { + // Advance the offset. + offset += child.text().chars().count(); + } + } + + list +} + +/// Resolves characters rotation. +/// +/// The algorithm is well explained +/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit). +/// +/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png) +/// +/// Note: this algorithm differs from the position resolving one. +fn resolve_rotate_list(text_node: SvgNode) -> Vec { + // Allocate a list that has all characters angles set to `0.0`. + let mut list = vec![0.0; count_chars(text_node)]; + let mut last = 0.0; + let mut offset = 0; + for child in text_node.descendants() { + if child.is_element() { + if let Some(rotate) = child.attribute::>(AId::Rotate) { + for i in 0..count_chars(child) { + if let Some(a) = rotate.get(i).cloned() { + list[offset + i] = a; + last = a; + } else { + // If the rotate list doesn't specify the rotation for + // this character - use the last one. + list[offset + i] = last; + } + } + } + } else if child.is_text() { + // Advance the offset. + offset += child.text().chars().count(); + } + } + + list +} + +/// Resolves node's `text-decoration` property. +fn resolve_decoration( + tspan: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, +) -> TextDecoration { + // Checks if a decoration is present in a single node. + fn find_decoration(node: SvgNode, value: &str) -> bool { + if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) { + str_value.split(' ').any(|v| v == value) + } else { + false + } + } + + // The algorithm is as follows: First, we check whether the given text decoration appears in ANY + // ancestor, i.e. it can also appear in ancestors outside of the element. If the text + // decoration is declared somewhere, it means that this tspan will have it. However, we still + // need to find the corresponding fill/stroke for it. To do this, we iterate through all + // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will + // stop at latest at the text node, and use its fill/stroke. + let mut gen_style = |text_decoration: &str| { + if !tspan + .ancestors() + .any(|n| find_decoration(n, text_decoration)) + { + return None; + } + + let mut fill_node = None; + let mut stroke_node = None; + + for node in tspan.ancestors() { + if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) { + fill_node = fill_node.map_or(Some(node), Some); + stroke_node = stroke_node.map_or(Some(node), Some); + break; + } + } + + Some(TextDecorationStyle { + fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)), + stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)), + }) + }; + + TextDecoration { + underline: gen_style("underline"), + overline: gen_style("overline"), + line_through: gen_style("line-through"), + } +} + +fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec { + let mut shift = Vec::new(); + let nodes: Vec<_> = node + .ancestors() + .take_while(|n| n.tag_name() != Some(EId::Text)) + .collect(); + for n in nodes { + if let Some(len) = n.try_attribute::(AId::BaselineShift) { + if len.unit == LengthUnit::Percent { + let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0); + shift.push(BaselineShift::Number(n)); + } else { + let n = super::units::convert_length( + len, + n, + AId::BaselineShift, + Units::ObjectBoundingBox, + state, + ); + shift.push(BaselineShift::Number(n)); + } + } else if let Some(s) = n.attribute(AId::BaselineShift) { + match s { + "sub" => shift.push(BaselineShift::Subscript), + "super" => shift.push(BaselineShift::Superscript), + _ => shift.push(BaselineShift::Baseline), + } + } + } + + if shift + .iter() + .all(|base| matches!(base, BaselineShift::Baseline)) + { + shift.clear(); + } + + shift +} + +fn count_chars(node: SvgNode) -> usize { + node.descendants() + .filter(|n| n.is_text()) + .fold(0, |w, n| w + n.text().chars().count()) +} + +/// Converts the writing mode. +/// +/// [SVG 2] references [CSS Writing Modes Level 3] for the definition of the +/// 'writing-mode' property, there are only two writing modes: +/// horizontal left-to-right and vertical right-to-left. +/// +/// That specification introduces new values for the property. The SVG 1.1 +/// values are obsolete but must still be supported by converting the specified +/// values to computed values as follows: +/// +/// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb` +/// - `tb`, `tb-rl` => `vertical-rl` +/// +/// The current `vertical-lr` behaves exactly the same as `vertical-rl`. +/// +/// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`. +/// And I'm not sure if its behaviour is correct. +/// +/// So we will ignore it as well, mainly because I have no idea how exactly +/// it should affect the rendering. +/// +/// [SVG 2]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty +/// [CSS Writing Modes Level 3]: https://www.w3.org/TR/css-writing-modes-3/#svg-writing-mode-css +fn convert_writing_mode(text_node: SvgNode) -> WritingMode { + if let Some(n) = text_node + .ancestors() + .find(|n| n.has_attribute(AId::WritingMode)) + { + match n.attribute(AId::WritingMode).unwrap_or("lr-tb") { + "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom, + _ => WritingMode::LeftToRight, + } + } else { + WritingMode::LeftToRight + } +} + +fn path_length(path: &tiny_skia_path::Path) -> f64 { + let mut prev_mx = path.points()[0].x; + let mut prev_my = path.points()[0].y; + let mut prev_x = prev_mx; + let mut prev_y = prev_my; + + fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { + let line = kurbo::Line::new( + kurbo::Point::new(px as f64, py as f64), + kurbo::Point::new(x as f64, y as f64), + ); + let p1 = line.eval(0.33); + let p2 = line.eval(0.66); + kurbo::CubicBez::new(line.p0, p1, p2, line.p1) + } + + let mut length = 0.0; + for seg in path.segments() { + let curve = match seg { + tiny_skia_path::PathSegment::MoveTo(p) => { + prev_mx = p.x; + prev_my = p.y; + prev_x = p.x; + prev_y = p.y; + continue; + } + tiny_skia_path::PathSegment::LineTo(p) => { + create_curve_from_line(prev_x, prev_y, p.x, p.y) + } + tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new( + kurbo::Point::new(prev_x as f64, prev_y as f64), + kurbo::Point::new(p1.x as f64, p1.y as f64), + kurbo::Point::new(p.x as f64, p.y as f64), + ) + .raise(), + tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new( + kurbo::Point::new(prev_x as f64, prev_y as f64), + kurbo::Point::new(p1.x as f64, p1.y as f64), + kurbo::Point::new(p2.x as f64, p2.y as f64), + kurbo::Point::new(p.x as f64, p.y as f64), + ), + tiny_skia_path::PathSegment::Close => { + create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) + } + }; + + length += curve.arclen(0.5); + prev_x = curve.p3.x as f32; + prev_y = curve.p3.y as f32; + } + + length +} diff --git a/third_party/usvg/src/parser/units.rs b/third_party/usvg/src/parser/units.rs new file mode 100644 index 0000000000..ce2c7dd247 --- /dev/null +++ b/third_party/usvg/src/parser/units.rs @@ -0,0 +1,145 @@ +// Copyright 2019 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use svgtypes::{Length, LengthUnit as Unit}; + +use super::converter; +use super::svgtree::{AId, SvgNode}; +use crate::Units; + +#[inline(never)] +pub(crate) fn convert_length( + length: Length, + node: SvgNode, + aid: AId, + object_units: Units, + state: &converter::State, +) -> f32 { + let dpi = state.opt.dpi; + let n = length.number as f32; + match length.unit { + Unit::None | Unit::Px => n, + Unit::Em => n * resolve_font_size(node, state), + Unit::Ex => n * resolve_font_size(node, state) / 2.0, + Unit::In => n * dpi, + Unit::Cm => n * dpi / 2.54, + Unit::Mm => n * dpi / 25.4, + Unit::Pt => n * dpi / 72.0, + Unit::Pc => n * dpi / 6.0, + Unit::Percent => { + if object_units == Units::ObjectBoundingBox { + n / 100.0 + } else { + let view_box = state.view_box; + + match aid { + AId::Cx + | AId::Dx + | AId::Fx + | AId::MarkerWidth + | AId::RefX + | AId::Rx + | AId::Width + | AId::X + | AId::X1 + | AId::X2 => convert_percent(length, view_box.width()), + AId::Cy + | AId::Dy + | AId::Fy + | AId::Height + | AId::MarkerHeight + | AId::RefY + | AId::Ry + | AId::Y + | AId::Y1 + | AId::Y2 => convert_percent(length, view_box.height()), + _ => { + let mut vb_len = view_box.width().powi(2) + view_box.height().powi(2); + vb_len = (vb_len / 2.0).sqrt(); + convert_percent(length, vb_len) + } + } + } + } + } +} + +pub(crate) fn convert_user_length( + length: Length, + node: SvgNode, + aid: AId, + state: &converter::State, +) -> f32 { + convert_length(length, node, aid, Units::UserSpaceOnUse, state) +} + +#[inline(never)] +pub(crate) fn convert_list(node: SvgNode, aid: AId, state: &converter::State) -> Option> { + if let Some(text) = node.attribute::<&str>(aid) { + let mut num_list = Vec::new(); + for length in svgtypes::LengthListParser::from(text).flatten() { + num_list.push(convert_user_length(length, node, aid, state)); + } + + Some(num_list) + } else { + None + } +} + +fn convert_percent(length: Length, base: f32) -> f32 { + base * (length.number as f32) / 100.0 +} + +#[inline(never)] +pub(crate) fn resolve_font_size(node: SvgNode, state: &converter::State) -> f32 { + let nodes: Vec<_> = node.ancestors().collect(); + let mut font_size = state.opt.font_size; + for n in nodes.iter().rev().skip(1) { + // skip Root + if let Some(length) = n.try_attribute::(AId::FontSize) { + let dpi = state.opt.dpi; + let n = length.number as f32; + font_size = match length.unit { + Unit::None | Unit::Px => n, + Unit::Em => n * font_size, + Unit::Ex => n * font_size / 2.0, + Unit::In => n * dpi, + Unit::Cm => n * dpi / 2.54, + Unit::Mm => n * dpi / 25.4, + Unit::Pt => n * dpi / 72.0, + Unit::Pc => n * dpi / 6.0, + Unit::Percent => { + // If `font-size` has percent units that it's value + // is relative to the parent node `font-size`. + length.number as f32 * font_size * 0.01 + } + } + } else if let Some(name) = n.attribute(AId::FontSize) { + font_size = convert_named_font_size(name, font_size); + } + } + + font_size +} + +fn convert_named_font_size(name: &str, parent_font_size: f32) -> f32 { + let factor = match name { + "xx-small" => -3, + "x-small" => -2, + "small" => -1, + "medium" => 0, + "large" => 1, + "x-large" => 2, + "xx-large" => 3, + "smaller" => -1, + "larger" => 1, + _ => { + log::warn!("Invalid 'font-size' value: '{}'.", name); + 0 + } + }; + + // 'On a computer screen a scaling factor of 1.2 is suggested between adjacent indexes.' + parent_font_size * 1.2f32.powi(factor) +} diff --git a/third_party/usvg/src/parser/use_node.rs b/third_party/usvg/src/parser/use_node.rs new file mode 100644 index 0000000000..74e540f465 --- /dev/null +++ b/third_party/usvg/src/parser/use_node.rs @@ -0,0 +1,370 @@ +// Copyright 2019 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use svgtypes::{Length, LengthUnit}; + +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, style}; +use crate::tree::ContextElement; +use crate::{Group, IsValidLength, Node, NonZeroRect, Path, Size, Transform, ViewBox}; + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) { + let child = match node.first_child() { + Some(v) => v, + None => return, + }; + + if state.parent_clip_path.is_some() && child.tag_name() == Some(EId::Symbol) { + // Ignore `symbol` referenced by `use` inside a `clipPath`. + // It will be ignored later anyway, but this will prevent + // a redundant `clipPath` creation (which is required for `symbol`). + return; + } + + let mut use_state = state.clone(); + use_state.context_element = Some(( + style::resolve_fill(node, true, state, cache).map(|mut f| { + f.context_element = Some(ContextElement::UseNode); + f + }), + style::resolve_stroke(node, true, state, cache).map(|mut s| { + s.context_element = Some(ContextElement::UseNode); + s + }), + )); + + // We require an original transformation to setup 'clipPath'. + let mut orig_ts = node.resolve_transform(AId::Transform, state); + let mut new_ts = Transform::default(); + + { + let x = node.convert_user_length(AId::X, &use_state, Length::zero()); + let y = node.convert_user_length(AId::Y, &use_state, Length::zero()); + new_ts = new_ts.pre_translate(x, y); + } + + let linked_to_symbol = child.tag_name() == Some(EId::Symbol); + + if linked_to_symbol { + // If a `use` element has a width/height attribute and references a symbol + // then relative units (like percentages) should be resolved relative + // to the width/height of the `use` element, and not the original SVG. + // This is why we need to (potentially) adapt the view box here. + use_state.view_box = { + let def = Length::new(100.0, LengthUnit::Percent); + let x = use_state.view_box.x(); + let y = use_state.view_box.y(); + + let width = if node.has_attribute(AId::Width) { + node.convert_user_length(AId::Width, &use_state, def) + } else { + use_state.view_box.width() + }; + + let height = if node.has_attribute(AId::Height) { + node.convert_user_length(AId::Height, &use_state, def) + } else { + use_state.view_box.height() + }; + + NonZeroRect::from_xywh(x, y, width, height) + // Fail silently if the rect is not valid. + .unwrap_or(use_state.view_box) + }; + + if let Some(ts) = viewbox_transform(node, child, &use_state) { + new_ts = new_ts.pre_concat(ts); + } + + if let Some(clip_rect) = get_clip_rect(node, child, &use_state) { + let mut g = clip_element(node, clip_rect, orig_ts, &use_state, cache); + g.abs_transform = parent.abs_transform; + + // Make group for `use`. + if let Some(mut g2) = + converter::convert_group(node, &use_state, true, cache, &mut g, &|cache, g2| { + convert_children(child, new_ts, &use_state, cache, false, g2); + }) + { + // We must reset transform, because it was already set + // to the group with clip-path. + g.is_context_element = true; + g2.id = String::new(); // Prevent ID duplication. + g2.transform = Transform::default(); + g.children.push(Node::Group(Box::new(g2))); + } + + if g.children.is_empty() { + return; + } + + g.calculate_bounding_boxes(); + parent.children.push(Node::Group(Box::new(g))); + return; + } + } + + orig_ts = orig_ts.pre_concat(new_ts); + + if linked_to_symbol { + // Make group for `use`. + if let Some(mut g) = + converter::convert_group(node, &use_state, false, cache, parent, &|cache, g| { + convert_children(child, orig_ts, &use_state, cache, false, g); + }) + { + g.is_context_element = true; + g.transform = Transform::default(); + parent.children.push(Node::Group(Box::new(g))); + } + } else { + let linked_to_svg = child.tag_name() == Some(EId::Svg); + if linked_to_svg { + // When a `use` element references a `svg` element, + // we have to remember `use` element size and use it + // instead of `svg` element size. + + let def = Length::new(100.0, LengthUnit::Percent); + // As per usual, the SVG spec doesn't clarify this edge case, + // but it seems like `use` size has to be reset by each `use`. + // Meaning if we have two nested `use` elements, where one had set `width` and + // other set `height`, we have to ignore the first `width`. + // + // Example: + // + // + // + // + // In this case `svg2` size is 80x100 and not 100x100. + use_state.use_size = (None, None); + + // Width and height can be set independently. + if node.has_attribute(AId::Width) { + use_state.use_size.0 = Some(node.convert_user_length(AId::Width, &use_state, def)); + } + if node.has_attribute(AId::Height) { + use_state.use_size.1 = Some(node.convert_user_length(AId::Height, &use_state, def)); + } + + convert_children(node, orig_ts, &use_state, cache, true, parent); + } else { + convert_children(node, orig_ts, &use_state, cache, true, parent); + } + } +} + +pub(crate) fn convert_svg( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, + parent: &mut Group, +) { + // We require original transformation to setup 'clipPath'. + let mut orig_ts = node.resolve_transform(AId::Transform, state); + let mut new_ts = Transform::default(); + + let x = node.convert_user_length(AId::X, state, Length::zero()); + let y = node.convert_user_length(AId::Y, state, Length::zero()); + new_ts = new_ts.pre_translate(x, y); + + if let Some(ts) = viewbox_transform(node, node, state) { + new_ts = new_ts.pre_concat(ts); + } + + // We have to create a new state which would have its viewBox set to the current SVG element. + // Note that we're not updating State::size - it's a completely different property. + let mut new_state = state.clone(); + new_state.view_box = { + if let Some(vb) = node.parse_viewbox() { + vb + } else { + // No `viewBox` attribute? Then use `x`, `y`, `width` and `height` instead. + let (mut w, mut h) = use_node_size(node, state); + + // If attributes `width` and/or `height` are provided on the `use` element, + // then these values will override the corresponding attributes + // on the `svg` in the generated tree. + w = state.use_size.0.unwrap_or(w); + h = state.use_size.1.unwrap_or(h); + + NonZeroRect::from_xywh(x, y, w, h).unwrap_or(state.view_box) + } + }; + + if let Some(clip_rect) = get_clip_rect(node, node, state) { + let mut g = clip_element(node, clip_rect, orig_ts, state, cache); + g.abs_transform = parent.abs_transform; + convert_children(node, new_ts, &new_state, cache, false, &mut g); + g.calculate_bounding_boxes(); + parent.children.push(Node::Group(Box::new(g))); + } else { + orig_ts = orig_ts.pre_concat(new_ts); + convert_children(node, orig_ts, &new_state, cache, false, parent); + } +} + +fn clip_element( + node: SvgNode, + clip_rect: NonZeroRect, + transform: Transform, + state: &converter::State, + cache: &mut converter::Cache, +) -> Group { + // We can't set `clip-path` on the element itself, + // because it will be affected by a possible transform. + // So we have to create an additional group. + + // Emulate a new viewport via clipPath. + // + // From: + // + // + // + // To: + // + // + // + // + // + // + // + // + + let mut clip_path = crate::ClipPath::empty(cache.gen_clip_path_id()); + + let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( + clip_rect.to_rect(), + ))) + .unwrap(); + path.fill = Some(crate::Fill::default()); + clip_path.root.children.push(Node::Path(Box::new(path))); + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let id = if state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + + Group { + id, + transform, + clip_path: Some(Arc::new(clip_path)), + ..Group::empty() + } +} + +fn convert_children( + node: SvgNode, + transform: Transform, + state: &converter::State, + cache: &mut converter::Cache, + is_context_element: bool, + parent: &mut Group, +) { + // Temporarily adjust absolute transform so `convert_group` would account for `transform`. + let old_abs_transform = parent.abs_transform; + parent.abs_transform = parent.abs_transform.pre_concat(transform); + + let required = !transform.is_identity(); + if let Some(mut g) = + converter::convert_group(node, state, required, cache, parent, &|cache, g| { + if state.parent_clip_path.is_some() { + converter::convert_clip_path_elements(node, state, cache, g); + } else { + converter::convert_children(node, state, cache, g); + } + }) + { + g.is_context_element = is_context_element; + g.transform = transform; + parent.children.push(Node::Group(Box::new(g))); + } + + parent.abs_transform = old_abs_transform; +} + +fn get_clip_rect( + use_node: SvgNode, + symbol_node: SvgNode, + state: &converter::State, +) -> Option { + // No need to clip elements with overflow:visible. + if matches!( + symbol_node.attribute(AId::Overflow), + Some("visible") | Some("auto") + ) { + return None; + } + + // A nested `svg` with only the `viewBox` attribute and no "rectangle" (x, y, width, height) + // should not be clipped. + if use_node.tag_name() == Some(EId::Svg) { + // Nested `svg` referenced by `use` still should be clipped, but by `use` bounds. + if state.use_size.0.is_none() && state.use_size.1.is_none() { + if !(use_node.has_attribute(AId::Width) && use_node.has_attribute(AId::Height)) { + return None; + } + } + } + + let (x, y, mut w, mut h) = { + let x = use_node.convert_user_length(AId::X, state, Length::zero()); + let y = use_node.convert_user_length(AId::Y, state, Length::zero()); + let (w, h) = use_node_size(use_node, state); + (x, y, w, h) + }; + + if use_node.tag_name() == Some(EId::Svg) { + // If attributes `width` and/or `height` are provided on the `use` element, + // then these values will override the corresponding attributes + // on the `svg` in the generated tree. + w = state.use_size.0.unwrap_or(w); + h = state.use_size.1.unwrap_or(h); + } + + if !w.is_valid_length() || !h.is_valid_length() { + return None; + } + + NonZeroRect::from_xywh(x, y, w, h) +} + +fn use_node_size(node: SvgNode, state: &converter::State) -> (f32, f32) { + let def = Length::new(100.0, LengthUnit::Percent); + let w = node.convert_user_length(AId::Width, state, def); + let h = node.convert_user_length(AId::Height, state, def); + (w, h) +} + +fn viewbox_transform( + node: SvgNode, + linked: SvgNode, + state: &converter::State, +) -> Option { + let (mut w, mut h) = use_node_size(node, state); + + if node.tag_name() == Some(EId::Svg) { + // If attributes `width` and/or `height` are provided on the `use` element, + // then these values will override the corresponding attributes + // on the `svg` in the generated tree. + w = state.use_size.0.unwrap_or(w); + h = state.use_size.1.unwrap_or(h); + } + + let size = Size::from_wh(w, h)?; + let rect = linked.parse_viewbox()?; + let aspect = linked + .attribute(AId::PreserveAspectRatio) + .unwrap_or_default(); + let view_box = ViewBox { rect, aspect }; + + Some(view_box.to_transform(size)) +} diff --git a/third_party/usvg/src/text/colr.rs b/third_party/usvg/src/text/colr.rs new file mode 100644 index 0000000000..b6d2ddf2b1 --- /dev/null +++ b/third_party/usvg/src/text/colr.rs @@ -0,0 +1,343 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::parser::OptionLog; +use rustybuzz::ttf_parser; + +struct Builder<'a>(&'a mut String); + +impl Builder<'_> { + fn finish(&mut self) { + if !self.0.is_empty() { + self.0.pop(); // remove trailing space + } + } +} + +impl ttf_parser::OutlineBuilder for Builder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "M {} {} ", x, y).unwrap(); + } + + fn line_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "L {} {} ", x, y).unwrap(); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap(); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap(); + } + + fn close(&mut self) { + self.0.push_str("Z "); + } +} + +trait XmlWriterExt { + fn write_color_attribute(&mut self, name: &str, ts: ttf_parser::RgbaColor); + fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform); + fn write_spread_method_attribute(&mut self, method: ttf_parser::colr::GradientExtend); +} + +impl XmlWriterExt for xmlwriter::XmlWriter { + fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) { + self.write_attribute_fmt( + name, + format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), + ); + } + + fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) { + if ts.is_default() { + return; + } + + self.write_attribute_fmt( + name, + format_args!( + "matrix({} {} {} {} {} {})", + ts.a, ts.b, ts.c, ts.d, ts.e, ts.f + ), + ); + } + + fn write_spread_method_attribute(&mut self, extend: ttf_parser::colr::GradientExtend) { + self.write_attribute( + "spreadMethod", + match extend { + ttf_parser::colr::GradientExtend::Pad => &"pad", + ttf_parser::colr::GradientExtend::Repeat => &"repeat", + ttf_parser::colr::GradientExtend::Reflect => &"reflect", + }, + ); + } +} + +// NOTE: This is only a best-effort translation of COLR into SVG. +pub(crate) struct GlyphPainter<'a> { + pub(crate) face: &'a ttf_parser::Face<'a>, + pub(crate) svg: &'a mut xmlwriter::XmlWriter, + pub(crate) path_buf: &'a mut String, + pub(crate) gradient_index: usize, + pub(crate) clip_path_index: usize, + pub(crate) palette_index: u16, + pub(crate) transform: ttf_parser::Transform, + pub(crate) outline_transform: ttf_parser::Transform, + pub(crate) transforms_stack: Vec, +} + +impl<'a> GlyphPainter<'a> { + fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) { + for stop in stops { + self.svg.start_element("stop"); + self.svg.write_attribute("offset", &stop.stop_offset); + self.svg.write_color_attribute("stop-color", stop.color); + let opacity = f32::from(stop.color.alpha) / 255.0; + self.svg.write_attribute("stop-opacity", &opacity); + self.svg.end_element(); + } + } + + fn paint_solid(&mut self, color: ttf_parser::RgbaColor) { + self.svg.start_element("path"); + self.svg.write_color_attribute("fill", color); + let opacity = f32::from(color.alpha) / 255.0; + self.svg.write_attribute("fill-opacity", &opacity); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) { + let gradient_id = format!("lg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + // TODO: We ignore x2, y2. Have to apply them somehow. + // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode + // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will + // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and + // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf + // we will see the actual spreadMode. We need to account for that somehow. + self.svg.start_element("linearGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("x1", &gradient.x0); + self.svg.write_attribute("y1", &gradient.y0); + self.svg.write_attribute("x2", &gradient.x1); + self.svg.write_attribute("y2", &gradient.y1); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) { + let gradient_id = format!("rg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + self.svg.start_element("radialGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("cx", &gradient.x1); + self.svg.write_attribute("cy", &gradient.y1); + self.svg.write_attribute("r", &gradient.r1); + self.svg.write_attribute("fr", &gradient.r0); + self.svg.write_attribute("fx", &gradient.x0); + self.svg.write_attribute("fy", &gradient.y0); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) { + println!("Warning: sweep gradients are not supported."); + } +} + +fn paint_transform( + outline_transform: ttf_parser::Transform, + transform: ttf_parser::Transform, +) -> ttf_parser::Transform { + let outline_transform = tiny_skia_path::Transform::from_row( + outline_transform.a, + outline_transform.b, + outline_transform.c, + outline_transform.d, + outline_transform.e, + outline_transform.f, + ); + + let gradient_transform = tiny_skia_path::Transform::from_row( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f, + ); + + let gradient_transform = outline_transform + .invert() + .log_none(|| log::warn!("Failed to calculate transform for gradient in glyph.")) + .unwrap_or_default() + .pre_concat(gradient_transform); + + ttf_parser::Transform { + a: gradient_transform.sx, + b: gradient_transform.ky, + c: gradient_transform.kx, + d: gradient_transform.sy, + e: gradient_transform.tx, + f: gradient_transform.ty, + } +} + +impl GlyphPainter<'_> { + fn clip_with_path(&mut self, path: &str) { + let clip_id = format!("cp{}", self.clip_path_index); + self.clip_path_index += 1; + + self.svg.start_element("clipPath"); + self.svg.write_attribute("id", &clip_id); + self.svg.start_element("path"); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", &path); + self.svg.end_element(); + self.svg.end_element(); + + self.svg.start_element("g"); + self.svg + .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); + } +} + +impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> { + fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) { + self.path_buf.clear(); + let mut builder = Builder(self.path_buf); + match self.face.outline_glyph(glyph_id, &mut builder) { + Some(v) => v, + None => return, + }; + builder.finish(); + + // We have to write outline using the current transform. + self.outline_transform = self.transform; + } + + fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) { + self.svg.start_element("g"); + + use ttf_parser::colr::CompositeMode; + // TODO: Need to figure out how to represent the other blend modes + // in SVG. + let mode = match mode { + CompositeMode::SourceOver => "normal", + CompositeMode::Screen => "screen", + CompositeMode::Overlay => "overlay", + CompositeMode::Darken => "darken", + CompositeMode::Lighten => "lighten", + CompositeMode::ColorDodge => "color-dodge", + CompositeMode::ColorBurn => "color-burn", + CompositeMode::HardLight => "hard-light", + CompositeMode::SoftLight => "soft-light", + CompositeMode::Difference => "difference", + CompositeMode::Exclusion => "exclusion", + CompositeMode::Multiply => "multiply", + CompositeMode::Hue => "hue", + CompositeMode::Saturation => "saturation", + CompositeMode::Color => "color", + CompositeMode::Luminosity => "luminosity", + _ => { + println!("Warning: unsupported blend mode: {:?}", mode); + "normal" + } + }; + self.svg.write_attribute_fmt( + "style", + format_args!("mix-blend-mode: {}; isolation: isolate", mode), + ); + } + + fn pop_layer(&mut self) { + self.svg.end_element(); // g + } + + fn push_transform(&mut self, transform: ttf_parser::Transform) { + self.transforms_stack.push(self.transform); + self.transform = ttf_parser::Transform::combine(self.transform, transform); + } + + fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) { + match paint { + ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color), + ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), + ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), + ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), + } + } + + fn pop_transform(&mut self) { + if let Some(ts) = self.transforms_stack.pop() { + self.transform = ts; + } + } + + fn push_clip(&mut self) { + self.clip_with_path(&self.path_buf.clone()); + } + + fn pop_clip(&mut self) { + self.svg.end_element(); + } + + fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) { + let x_min = clipbox.x_min; + let x_max = clipbox.x_max; + let y_min = clipbox.y_min; + let y_max = clipbox.y_max; + + let clip_path = format!( + "M {} {} L {} {} L {} {} L {} {} Z", + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ); + + self.clip_with_path(&clip_path); + } +} diff --git a/third_party/usvg/src/text/flatten.rs b/third_party/usvg/src/text/flatten.rs new file mode 100644 index 0000000000..89929a08e2 --- /dev/null +++ b/third_party/usvg/src/text/flatten.rs @@ -0,0 +1,324 @@ +// Copyright 2022 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::mem; +use std::sync::Arc; + +use fontdb::{Database, ID}; +use rustybuzz::ttf_parser; +use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat, RgbaColor}; +use tiny_skia_path::{NonZeroRect, Size, Transform}; +use xmlwriter::XmlWriter; + +use crate::text::colr::GlyphPainter; +use crate::*; + +fn resolve_rendering_mode(text: &Text) -> ShapeRendering { + match text.rendering_mode { + TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges, + TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision, + TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision, + } +} + +fn push_outline_paths( + span: &layout::Span, + builder: &mut tiny_skia_path::PathBuilder, + new_children: &mut Vec, + rendering_mode: ShapeRendering, +) { + let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new()); + + if let Some(path) = builder.finish().and_then(|p| { + Path::new( + String::new(), + span.visible, + span.fill.clone(), + span.stroke.clone(), + span.paint_order, + rendering_mode, + Arc::new(p), + Transform::default(), + ) + }) { + new_children.push(Node::Path(Box::new(path))); + } +} + +pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> { + let mut new_children = vec![]; + + let rendering_mode = resolve_rendering_mode(text); + + for span in &text.layouted { + if let Some(path) = span.overline.as_ref() { + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_children.push(Node::Path(Box::new(path))); + } + + if let Some(path) = span.underline.as_ref() { + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_children.push(Node::Path(Box::new(path))); + } + + // Instead of always processing each glyph separately, we always collect + // as many outline glyphs as possible by pushing them into the span_builder + // and only if we encounter a different glyph, or we reach the very end of the + // span to we push the actual outline paths into new_children. This way, we don't need + // to create a new path for every glyph if we have many consecutive glyphs + // with just outlines (which is the most common case). + let mut span_builder = tiny_skia_path::PathBuilder::new(); + + for glyph in &span.positioned_glyphs { + // A (best-effort conversion of a) COLR glyph. + if let Some(tree) = cache.fontdb_colr(glyph.font, glyph.id) { + let mut group = Group { + transform: glyph.colr_transform(), + ..Group::empty() + }; + // TODO: Probably need to update abs_transform of children? + group.children.push(Node::Group(Box::new(tree.root))); + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); + } + // An SVG glyph. Will return the usvg node containing the glyph descriptions. + else if let Some(node) = cache.fontdb_svg(glyph.font, glyph.id) { + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + + let mut group = Group { + transform: glyph.svg_transform(), + ..Group::empty() + }; + // TODO: Probably need to update abs_transform of children? + group.children.push(node); + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); + } + // A bitmap glyph. + else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id) { + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + + let transform = if img.is_sbix { + glyph.sbix_transform( + img.x as f32, + img.y as f32, + img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32, + img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32, + img.pixels_per_em as f32, + img.image.size.height(), + ) + } else { + glyph.cbdt_transform( + img.x as f32, + img.y as f32, + img.pixels_per_em as f32, + img.image.size.height(), + ) + }; + + let mut group = Group { + transform, + ..Group::empty() + }; + group.children.push(Node::Image(Box::new(img.image))); + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); + } else if let Some(outline) = cache + .fontdb_outline(glyph.font, glyph.id) + .and_then(|p| p.transform(glyph.outline_transform())) + { + span_builder.push_path(&outline); + } + } + + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + + if let Some(path) = span.line_through.as_ref() { + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_children.push(Node::Path(Box::new(path))); + } + } + + let mut group = Group { + id: text.id.clone(), + ..Group::empty() + }; + + for child in new_children { + group.children.push(child); + } + + group.calculate_bounding_boxes(); + let stroke_bbox = group.stroke_bounding_box().to_non_zero_rect()?; + Some((group, stroke_bbox)) +} + +struct PathBuilder { + builder: tiny_skia_path::PathBuilder, +} + +impl ttf_parser::OutlineBuilder for PathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.builder.move_to(x, y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.builder.line_to(x, y); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.builder.quad_to(x1, y1, x, y); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.builder.cubic_to(x1, y1, x2, y2, x, y); + } + + fn close(&mut self) { + self.builder.close(); + } +} + +pub(crate) trait DatabaseExt { + fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; + fn raster(&self, id: ID, glyph_id: GlyphId) -> Option; + fn svg(&self, id: ID, glyph_id: GlyphId) -> Option; + fn colr(&self, id: ID, glyph_id: GlyphId) -> Option; +} + +#[derive(Clone)] +pub(crate) struct BitmapImage { + image: Image, + x: i16, + y: i16, + pixels_per_em: u16, + glyph_bbox: Option, + is_sbix: bool, +} + +impl DatabaseExt for Database { + #[inline(never)] + fn outline(&self, id: ID, glyph_id: GlyphId) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + + let mut builder = PathBuilder { + builder: tiny_skia_path::PathBuilder::new(), + }; + + font.outline_glyph(glyph_id, &mut builder)?; + builder.builder.finish() + })? + } + + fn raster(&self, id: ID, glyph_id: GlyphId) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + let image = font.glyph_raster_image(glyph_id, u16::MAX)?; + + if image.format == RasterImageFormat::PNG { + let bitmap_image = BitmapImage { + image: Image { + id: String::new(), + visible: true, + size: Size::from_wh(image.width as f32, image.height as f32)?, + rendering_mode: ImageRendering::OptimizeQuality, + kind: ImageKind::PNG(Arc::new(image.data.into())), + abs_transform: Transform::default(), + abs_bounding_box: NonZeroRect::from_xywh( + 0.0, + 0.0, + image.width as f32, + image.height as f32, + )?, + }, + x: image.x, + y: image.y, + pixels_per_em: image.pixels_per_em, + glyph_bbox: font.glyph_bounding_box(glyph_id), + // ttf-parser always checks sbix first, so if this table exists, it was used. + is_sbix: font.tables().sbix.is_some(), + }; + + return Some(bitmap_image); + } + + None + })? + } + + fn svg(&self, id: ID, glyph_id: GlyphId) -> Option { + // TODO: Technically not 100% accurate because the SVG format in a OTF font + // is actually a subset/superset of a normal SVG, but it seems to work fine + // for Twitter Color Emoji, so might as well use what we already have. + + // TODO: Glyph records can contain the data for multiple glyphs. We should + // add a cache so we don't need to reparse the data every time. + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + let image = font.glyph_svg_image(glyph_id)?; + let tree = Tree::from_data(image.data, &Options::default()).ok()?; + + // Twitter Color Emoji seems to always have one SVG record per glyph, + // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky, + // but the best we have for now. + let node = if image.start_glyph_id == image.end_glyph_id { + Node::Group(Box::new(tree.root)) + } else { + tree.node_by_id(&format!("glyph{}", glyph_id.0)) + .log_none(|| { + log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0); + }) + .cloned()? + }; + + Some(node) + })? + } + + fn colr(&self, id: ID, glyph_id: GlyphId) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let face = ttf_parser::Face::parse(data, face_index).ok()?; + + let mut svg = XmlWriter::new(xmlwriter::Options::default()); + + svg.start_element("svg"); + svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); + svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + + let mut path_buf = String::with_capacity(256); + let gradient_index = 1; + let clip_path_index = 1; + + svg.start_element("g"); + + let mut glyph_painter = GlyphPainter { + face: &face, + svg: &mut svg, + path_buf: &mut path_buf, + gradient_index, + clip_path_index, + palette_index: 0, + transform: ttf_parser::Transform::default(), + outline_transform: ttf_parser::Transform::default(), + transforms_stack: vec![ttf_parser::Transform::default()], + }; + + face.paint_color_glyph( + glyph_id, + 0, + RgbaColor::new(0, 0, 0, 255), + &mut glyph_painter, + )?; + svg.end_element(); + + Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok() + })? + } +} diff --git a/third_party/usvg/src/text/layout.rs b/third_party/usvg/src/text/layout.rs new file mode 100644 index 0000000000..2261f66bb2 --- /dev/null +++ b/third_party/usvg/src/text/layout.rs @@ -0,0 +1,1746 @@ +// Copyright 2022 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroU16; +use std::sync::Arc; + +use fontdb::{Database, ID}; +use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; +use rustybuzz::ttf_parser; +use rustybuzz::ttf_parser::{GlyphId, Tag}; +use strict_num::NonZeroPositiveF32; +use tiny_skia_path::{NonZeroRect, Transform}; +use unicode_script::UnicodeScript; + +use crate::tree::{BBox, IsValidLength}; +use crate::{ + AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font, + FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor, + TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, WritingMode, +}; + +/// A glyph that has already been positioned correctly. +/// +/// Note that the transform already takes the font size into consideration, so applying the +/// transform to the outline of the glyphs is all that is necessary to display it correctly. +#[derive(Clone, Debug)] +pub struct PositionedGlyph { + /// Returns the transform of the glyph itself within the cluster. For example, + /// for zalgo text, it contains the transform to position the glyphs above/below + /// the main glyph. + glyph_ts: Transform, + /// Returns the transform of the whole cluster that the glyph is part of. + cluster_ts: Transform, + /// Returns the transform of the span that the glyph is a part of. + span_ts: Transform, + /// The units per em of the font the glyph belongs to. + units_per_em: u16, + /// The font size the glyph should be scaled to. + font_size: f32, + /// The ID of the glyph. + pub id: GlyphId, + /// The text from the original string that corresponds to that glyph. + pub text: String, + /// The ID of the font the glyph should be taken from. Can be used with the + /// [font database of the tree](crate::Tree::fontdb) this glyph is part of. + pub font: ID, +} + +impl PositionedGlyph { + /// Returns the transform of glyph. + pub fn transform(&self) -> Transform { + let sx = self.font_size / self.units_per_em as f32; + + self.span_ts + .pre_concat(self.cluster_ts) + .pre_concat(Transform::from_scale(sx, sx)) + .pre_concat(self.glyph_ts) + } + + /// Returns the transform of glyph, assuming that an outline + /// glyph is being used (i.e. from the `glyf` or `CFF/CFF2` table). + pub fn outline_transform(&self) -> Transform { + // Outlines are mirrored by default. + self.transform() + .pre_concat(Transform::from_scale(1.0, -1.0)) + } + + /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph + /// is being used. + pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform { + self.transform() + .pre_concat(Transform::from_scale( + self.units_per_em as f32 / pixels_per_em, + self.units_per_em as f32 / pixels_per_em, + )) + // Right now, the top-left corner of the image would be placed in + // on the "text cursor", but we want the bottom-left corner to be there, + // so we need to shift it up and also apply the x/y offset. + .pre_translate(x, -height - y) + } + + /// Returns the transform for the glyph, assuming that a sbix-based raster glyph + /// is being used. + pub fn sbix_transform( + &self, + x: f32, + y: f32, + x_min: f32, + y_min: f32, + pixels_per_em: f32, + height: f32, + ) -> Transform { + // In contrast to CBDT, we also need to look at the outline bbox of the glyph and add a shift if necessary. + let bbox_x_shift = -x_min; + + let bbox_y_shift = if y_min.approx_zero_ulps(4) { + // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift + // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows, + // this shift will not be applied. However, if this shift is not applied the emojis are a bit + // too high up when being together with other text, so we try to imitate this. + // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425 + // So whenever the y-shift is 0, we approximate this vertical shift that seems to be produced by it. + // This value seems to be pretty close to what is happening on MacOS. + // We can still remove this if it turns out to be a problem, but Apple Color Emoji is pretty + // much the only `sbix` font out there and they all seem to have a y-shift of 0, so it + // makes sense to keep it. + 0.128 * self.units_per_em as f32 + } else { + -y_min + }; + + self.transform() + .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift)) + .pre_concat(Transform::from_scale( + self.units_per_em as f32 / pixels_per_em, + self.units_per_em as f32 / pixels_per_em, + )) + // Right now, the top-left corner of the image would be placed in + // on the "text cursor", but we want the bottom-left corner to be there, + // so we need to shift it up and also apply the x/y offset. + .pre_translate(x, -height - y) + } + + /// Returns the transform for the glyph, assuming that an SVG glyph is + /// being used. + pub fn svg_transform(&self) -> Transform { + self.transform() + } + + /// Returns the transform for the glyph, assuming that a COLR glyph is + /// being used. + pub fn colr_transform(&self) -> Transform { + self.outline_transform() + } +} + +/// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and +/// visibility. +#[derive(Clone, Debug)] +pub struct Span { + /// The fill of the span. + pub fill: Option, + /// The stroke of the span. + pub stroke: Option, + /// The paint order of the span. + pub paint_order: PaintOrder, + /// The font size of the span. + pub font_size: NonZeroPositiveF32, + /// The visibility of the span. + pub visible: bool, + /// The glyphs that make up the span. + pub positioned_glyphs: Vec, + /// An underline text decoration of the span. + /// Needs to be rendered before all glyphs. + pub underline: Option, + /// An overline text decoration of the span. + /// Needs to be rendered before all glyphs. + pub overline: Option, + /// A line-through text decoration of the span. + /// Needs to be rendered after all glyphs. + pub line_through: Option, +} + +#[derive(Clone, Debug)] +struct GlyphCluster { + byte_idx: ByteIndex, + codepoint: char, + width: f32, + advance: f32, + ascent: f32, + descent: f32, + has_relative_shift: bool, + glyphs: Vec, + transform: Transform, + path_transform: Transform, + visible: bool, +} + +impl GlyphCluster { + pub(crate) fn height(&self) -> f32 { + self.ascent - self.descent + } + + pub(crate) fn transform(&self) -> Transform { + self.path_transform.post_concat(self.transform) + } +} + +pub(crate) fn layout_text( + text_node: &Text, + resolver: &FontResolver, + fontdb: &mut Arc, +) -> Option<(Vec, NonZeroRect)> { + let mut fonts_cache: FontsCache = HashMap::new(); + + for chunk in &text_node.chunks { + for span in &chunk.spans { + if !fonts_cache.contains_key(&span.font) { + if let Some(font) = + (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id)) + { + fonts_cache.insert(span.font.clone(), Arc::new(font)); + } + } + } + } + + let mut spans = vec![]; + let mut char_offset = 0; + let mut last_x = 0.0; + let mut last_y = 0.0; + let mut bbox = BBox::default(); + for chunk in &text_node.chunks { + let (x, y) = match chunk.text_flow { + TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)), + TextFlow::Path(_) => (0.0, 0.0), + }; + + let mut clusters = process_chunk(chunk, &fonts_cache, resolver, fontdb); + if clusters.is_empty() { + char_offset += chunk.text.chars().count(); + continue; + } + + apply_writing_mode(text_node.writing_mode, &mut clusters); + apply_letter_spacing(chunk, &mut clusters); + apply_word_spacing(chunk, &mut clusters); + + apply_length_adjust(chunk, &mut clusters); + let mut curr_pos = resolve_clusters_positions( + text_node, + chunk, + char_offset, + text_node.writing_mode, + &fonts_cache, + &mut clusters, + ); + + let mut text_ts = Transform::default(); + if text_node.writing_mode == WritingMode::TopToBottom { + if let TextFlow::Linear = chunk.text_flow { + text_ts = text_ts.pre_rotate_at(90.0, x, y); + } + } + + for span in &chunk.spans { + let font = match fonts_cache.get(&span.font) { + Some(v) => v, + None => continue, + }; + + let decoration_spans = collect_decoration_spans(span, &clusters); + + let mut span_ts = text_ts; + span_ts = span_ts.pre_translate(x, y); + if let TextFlow::Linear = chunk.text_flow { + let shift = resolve_baseline(span, font, text_node.writing_mode); + + // In case of a horizontal flow, shift transform and not clusters, + // because clusters can be rotated and an additional shift will lead + // to invalid results. + span_ts = span_ts.pre_translate(0.0, shift); + } + + let mut underline = None; + let mut overline = None; + let mut line_through = None; + + if let Some(decoration) = span.decoration.underline.clone() { + // TODO: No idea what offset should be used for top-to-bottom layout. + // There is + // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property + // but it doesn't go into details. + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.underline_position(span.font_size.get()), + WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0, + }; + + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + underline = Some(path); + } + } + + if let Some(decoration) = span.decoration.overline.clone() { + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.ascent(span.font_size.get()), + WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0, + }; + + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + overline = Some(path); + } + } + + if let Some(decoration) = span.decoration.line_through.clone() { + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()), + WritingMode::TopToBottom => 0.0, + }; + + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + line_through = Some(path); + } + } + + let mut fill = span.fill.clone(); + if let Some(ref mut fill) = fill { + // The `fill-rule` should be ignored. + // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder + // + // 'Since the fill-rule property does not apply to SVG text elements, + // the specific order of the subpaths within the equivalent path does not matter.' + fill.rule = FillRule::NonZero; + } + + if let Some((span_fragments, span_bbox)) = convert_span(span, &clusters, span_ts) { + bbox = bbox.expand(span_bbox); + + let positioned_glyphs = span_fragments + .into_iter() + .flat_map(|mut gc| { + let cluster_ts = gc.transform(); + gc.glyphs.iter_mut().for_each(|pg| { + pg.cluster_ts = cluster_ts; + pg.span_ts = span_ts; + }); + gc.glyphs + }) + .collect(); + + spans.push(Span { + fill, + stroke: span.stroke.clone(), + paint_order: span.paint_order, + font_size: span.font_size, + visible: span.visible, + positioned_glyphs, + underline, + overline, + line_through, + }); + } + } + + char_offset += chunk.text.chars().count(); + + if text_node.writing_mode == WritingMode::TopToBottom { + if let TextFlow::Linear = chunk.text_flow { + std::mem::swap(&mut curr_pos.0, &mut curr_pos.1); + } + } + + last_x = x + curr_pos.0; + last_y = y + curr_pos.1; + } + + let bbox = bbox.to_non_zero_rect()?; + + Some((spans, bbox)) +} + +fn convert_span( + span: &TextSpan, + clusters: &[GlyphCluster], + text_ts: Transform, +) -> Option<(Vec, NonZeroRect)> { + let mut span_clusters = vec![]; + let mut bboxes_builder = tiny_skia_path::PathBuilder::new(); + + for cluster in clusters { + if !cluster.visible { + continue; + } + + if span_contains(span, cluster.byte_idx) { + span_clusters.push(cluster.clone()); + } + + let mut advance = cluster.advance; + if advance <= 0.0 { + advance = 1.0; + } + + // We have to calculate text bbox using font metrics and not glyph shape. + if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) { + if let Some(r) = r.transform(cluster.transform()) { + bboxes_builder.push_rect(r.to_rect()); + } + } + } + + let mut bboxes = bboxes_builder.finish()?; + bboxes = bboxes.transform(text_ts)?; + let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?; + + Some((span_clusters, bbox)) +} + +fn collect_decoration_spans(span: &TextSpan, clusters: &[GlyphCluster]) -> Vec { + let mut spans = Vec::new(); + + let mut started = false; + let mut width = 0.0; + let mut transform = Transform::default(); + + for cluster in clusters { + if span_contains(span, cluster.byte_idx) { + if started && cluster.has_relative_shift { + started = false; + spans.push(DecorationSpan { width, transform }); + } + + if !started { + width = cluster.advance; + started = true; + transform = cluster.transform; + } else { + width += cluster.advance; + } + } else if started { + spans.push(DecorationSpan { width, transform }); + started = false; + } + } + + if started { + spans.push(DecorationSpan { width, transform }); + } + + spans +} + +pub(crate) fn convert_decoration( + dy: f32, + span: &TextSpan, + font: &ResolvedFont, + mut decoration: TextDecorationStyle, + decoration_spans: &[DecorationSpan], + transform: Transform, +) -> Option { + debug_assert!(!decoration_spans.is_empty()); + + let thickness = font.underline_thickness(span.font_size.get()); + + let mut builder = tiny_skia_path::PathBuilder::new(); + for dec_span in decoration_spans { + let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) { + Some(v) => v, + None => { + log::warn!("a decoration span has a malformed bbox"); + continue; + } + }; + + let ts = dec_span.transform.pre_translate(0.0, dy); + + let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect()); + path = match path.transform(ts) { + Some(v) => v, + None => continue, + }; + + builder.push_path(&path); + } + + let mut path_data = builder.finish()?; + path_data = path_data.transform(transform)?; + + Path::new( + String::new(), + span.visible, + decoration.fill.take(), + decoration.stroke.take(), + PaintOrder::default(), + ShapeRendering::default(), + Arc::new(path_data), + Transform::default(), + ) +} + +/// A text decoration span. +/// +/// Basically a horizontal line, that will be used for underline, overline and line-through. +/// It doesn't have a height, since it depends on the Font metrics. +#[derive(Clone, Copy)] +pub(crate) struct DecorationSpan { + pub(crate) width: f32, + pub(crate) transform: Transform, +} + +/// Resolves clusters positions. +/// +/// Mainly sets the `transform` property. +/// +/// Returns the last text position. The next text chunk should start from that position. +fn resolve_clusters_positions( + text: &Text, + chunk: &TextChunk, + char_offset: usize, + writing_mode: WritingMode, + fonts_cache: &FontsCache, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + match chunk.text_flow { + TextFlow::Linear => { + resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters) + } + TextFlow::Path(ref path) => resolve_clusters_positions_path( + text, + chunk, + char_offset, + path, + writing_mode, + fonts_cache, + clusters, + ), + } +} + +fn clusters_length(clusters: &[GlyphCluster]) -> f32 { + clusters.iter().fold(0.0, |w, cluster| w + cluster.advance) +} + +fn resolve_clusters_positions_horizontal( + text: &Text, + chunk: &TextChunk, + offset: usize, + writing_mode: WritingMode, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); + let mut y = 0.0; + + for cluster in clusters { + let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); + if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) { + if writing_mode == WritingMode::LeftToRight { + x += dx; + y += dy; + } else { + y -= dx; + x += dy; + } + cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); + } + + cluster.transform = cluster.transform.pre_translate(x, y); + + if let Some(angle) = text.rotate.get(cp).cloned() { + if !angle.approx_zero_ulps(4) { + cluster.transform = cluster.transform.pre_rotate(angle); + cluster.has_relative_shift = true; + } + } + + x += cluster.advance; + } + + (x, y) +} + +// Baseline resolving in SVG is a mess. +// Not only it's poorly documented, but as soon as you start mixing +// `dominant-baseline` and `alignment-baseline` each application/browser will produce +// different results. +// +// For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output. +// +// See `alignment_baseline_shift` method comment for more details. +pub(crate) fn resolve_baseline( + span: &TextSpan, + font: &ResolvedFont, + writing_mode: WritingMode, +) -> f32 { + let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get()); + + // TODO: support vertical layout as well + if writing_mode == WritingMode::LeftToRight { + if span.alignment_baseline == AlignmentBaseline::Auto + || span.alignment_baseline == AlignmentBaseline::Baseline + { + shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get()); + } else { + shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get()); + } + } + + shift +} + +fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 { + let mut shift = 0.0; + for baseline in baselines.iter().rev() { + match baseline { + BaselineShift::Baseline => {} + BaselineShift::Subscript => shift -= font.subscript_offset(font_size), + BaselineShift::Superscript => shift += font.superscript_offset(font_size), + BaselineShift::Number(n) => shift += n, + } + } + + shift +} + +fn resolve_clusters_positions_path( + text: &Text, + chunk: &TextChunk, + char_offset: usize, + path: &TextPath, + writing_mode: WritingMode, + fonts_cache: &FontsCache, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + let mut last_x = 0.0; + let mut last_y = 0.0; + + let mut dy = 0.0; + + // In the text path mode, chunk's x/y coordinates provide an additional offset along the path. + // The X coordinate is used in a horizontal mode, and Y in vertical. + let chunk_offset = match writing_mode { + WritingMode::LeftToRight => chunk.x.unwrap_or(0.0), + WritingMode::TopToBottom => chunk.y.unwrap_or(0.0), + }; + + let start_offset = + chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters)); + + let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset); + for (cluster, normal) in clusters.iter_mut().zip(normals) { + let (x, y, angle) = match normal { + Some(normal) => (normal.x, normal.y, normal.angle), + None => { + // Hide clusters that are outside the text path. + cluster.visible = false; + continue; + } + }; + + // We have to break a decoration line for each cluster during text-on-path. + cluster.has_relative_shift = true; + + let orig_ts = cluster.transform; + + // Clusters should be rotated by the x-midpoint x baseline position. + let half_width = cluster.width / 2.0; + cluster.transform = Transform::default(); + cluster.transform = cluster.transform.pre_translate(x - half_width, y); + cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0); + + let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); + dy += text.dy.get(cp).cloned().unwrap_or(0.0); + + let baseline_shift = chunk_span_at(chunk, cluster.byte_idx) + .map(|span| { + let font = match fonts_cache.get(&span.font) { + Some(v) => v, + None => return 0.0, + }; + -resolve_baseline(span, font, writing_mode) + }) + .unwrap_or(0.0); + + // Shift only by `dy` since we already applied `dx` + // during offset along the path calculation. + if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) { + let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64); + cluster.transform = cluster + .transform + .pre_translate(shift.x as f32, shift.y as f32); + } + + if let Some(angle) = text.rotate.get(cp).cloned() { + if !angle.approx_zero_ulps(4) { + cluster.transform = cluster.transform.pre_rotate(angle); + } + } + + // The possible `lengthAdjust` transform should be applied after text-on-path positioning. + cluster.transform = cluster.transform.pre_concat(orig_ts); + + last_x = x + cluster.advance; + last_y = y; + } + + (last_x, last_y) +} + +pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 { + match a { + TextAnchor::Start => 0.0, // Nothing. + TextAnchor::Middle => -text_width / 2.0, + TextAnchor::End => -text_width, + } +} + +pub(crate) struct PathNormal { + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) angle: f32, +} + +fn collect_normals( + text: &Text, + chunk: &TextChunk, + clusters: &[GlyphCluster], + path: &tiny_skia_path::Path, + char_offset: usize, + offset: f32, +) -> Vec> { + let mut offsets = Vec::with_capacity(clusters.len()); + let mut normals = Vec::with_capacity(clusters.len()); + { + let mut advance = offset; + for cluster in clusters { + // Clusters should be rotated by the x-midpoint x baseline position. + let half_width = cluster.width / 2.0; + + // Include relative position. + let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); + advance += text.dx.get(cp).cloned().unwrap_or(0.0); + + let offset = advance + half_width; + + // Clusters outside the path have no normals. + if offset < 0.0 { + normals.push(None); + } + + offsets.push(offset as f64); + advance += cluster.advance; + } + } + + let mut prev_mx = path.points()[0].x; + let mut prev_my = path.points()[0].y; + let mut prev_x = prev_mx; + let mut prev_y = prev_my; + + fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { + let line = kurbo::Line::new( + kurbo::Point::new(px as f64, py as f64), + kurbo::Point::new(x as f64, y as f64), + ); + let p1 = line.eval(0.33); + let p2 = line.eval(0.66); + kurbo::CubicBez { + p0: line.p0, + p1, + p2, + p3: line.p1, + } + } + + let mut length: f64 = 0.0; + for seg in path.segments() { + let curve = match seg { + tiny_skia_path::PathSegment::MoveTo(p) => { + prev_mx = p.x; + prev_my = p.y; + prev_x = p.x; + prev_y = p.y; + continue; + } + tiny_skia_path::PathSegment::LineTo(p) => { + create_curve_from_line(prev_x, prev_y, p.x, p.y) + } + tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez { + p0: kurbo::Point::new(prev_x as f64, prev_y as f64), + p1: kurbo::Point::new(p1.x as f64, p1.y as f64), + p2: kurbo::Point::new(p.x as f64, p.y as f64), + } + .raise(), + tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez { + p0: kurbo::Point::new(prev_x as f64, prev_y as f64), + p1: kurbo::Point::new(p1.x as f64, p1.y as f64), + p2: kurbo::Point::new(p2.x as f64, p2.y as f64), + p3: kurbo::Point::new(p.x as f64, p.y as f64), + }, + tiny_skia_path::PathSegment::Close => { + create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) + } + }; + + let arclen_accuracy = { + let base_arclen_accuracy = 0.5; + // Accuracy depends on a current scale. + // When we have a tiny path scaled by a large value, + // we have to increase out accuracy accordingly. + let (sx, sy) = text.abs_transform.get_scale(); + // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy. + base_arclen_accuracy / (sx * sy).sqrt().max(1.0) + }; + + let curve_len = curve.arclen(arclen_accuracy as f64); + + for offset in &offsets[normals.len()..] { + if *offset >= length && *offset <= length + curve_len { + let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64); + // some rounding error may occur, so we give offset a little tolerance + debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset)); + offset = offset.clamp(0.0, 1.0); + + let pos = curve.eval(offset); + let d = curve.deriv().eval(offset); + let d = kurbo::Vec2::new(-d.y, d.x); // tangent + let angle = d.atan2().to_degrees() - 90.0; + + normals.push(Some(PathNormal { + x: pos.x as f32, + y: pos.y as f32, + angle: angle as f32, + })); + + if normals.len() == offsets.len() { + break; + } + } + } + + length += curve_len; + prev_x = curve.p3.x as f32; + prev_y = curve.p3.y as f32; + } + + // If path ended and we still have unresolved normals - set them to `None`. + for _ in 0..(offsets.len() - normals.len()) { + normals.push(None); + } + + normals +} + +/// Converts a text chunk into a list of outlined clusters. +/// +/// This function will do the BIDI reordering, text shaping and glyphs outlining, +/// but not the text layouting. So all clusters are in the 0x0 position. +fn process_chunk( + chunk: &TextChunk, + fonts_cache: &FontsCache, + resolver: &FontResolver, + fontdb: &mut Arc, +) -> Vec { + // The way this function works is a bit tricky. + // + // The first problem is BIDI reordering. + // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous. + // + // For example: + // Hello שלום. + // + // Would be shaped as: + // H e l l o ש ל ו ם . (characters) + // 0 1 2 3 4 5 12 10 8 6 14 (cluster indices in UTF-8) + // --- --- (green span) + // + // As you can see, our continuous `lo של` span was split into two separated one. + // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black. + // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output. + // To properly handle this we simply shape the whole chunk. + // + // But this introduces another issue - what to do when we have multiple fonts? + // The easy solution would be to simply shape text with each font, + // where the first font output is used as a base one and all others overwrite it. + // This way in case of: + // Hello world + // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple. + // + // Well, it would work most of the time, but not always. + // This is because different fonts can produce different amount of glyphs for the same text. + // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`, + // but some can use `fi` (U+FB01) instead. + // Meaning that during merging we have to overwrite not individual glyphs, but clusters. + + // Glyph splitting assigns distinct glyphs to the same index in the original text, we need to + // store previously used indices to make sure we do not re-use the same index while overwriting + // span glyphs. + let mut positions = HashSet::new(); + + let mut glyphs = Vec::new(); + for span in &chunk.spans { + let font = match fonts_cache.get(&span.font) { + Some(v) => v.clone(), + None => continue, + }; + + let tmp_glyphs = shape_text( + &chunk.text, + font, + span.small_caps, + span.apply_kerning, + resolver, + fontdb, + ); + + // Do nothing with the first run. + if glyphs.is_empty() { + glyphs = tmp_glyphs; + continue; + } + + positions.clear(); + + // Overwrite span's glyphs. + let mut iter = tmp_glyphs.into_iter(); + while let Some(new_glyph) = iter.next() { + if !span_contains(span, new_glyph.byte_idx) { + continue; + } + + let Some(idx) = glyphs + .iter() + .position(|g| g.byte_idx == new_glyph.byte_idx) + .filter(|pos| !positions.contains(pos)) + else { + continue; + }; + + positions.insert(idx); + + let prev_cluster_len = glyphs[idx].cluster_len; + if prev_cluster_len < new_glyph.cluster_len { + // If the new font represents the same cluster with fewer glyphs + // then remove remaining glyphs. + for _ in 1..new_glyph.cluster_len { + glyphs.remove(idx + 1); + } + } else if prev_cluster_len > new_glyph.cluster_len { + // If the new font represents the same cluster with more glyphs + // then insert them after the current one. + for j in 1..prev_cluster_len { + if let Some(g) = iter.next() { + glyphs.insert(idx + j, g); + } + } + } + + glyphs[idx] = new_glyph; + } + } + + // Convert glyphs to clusters. + let mut clusters = Vec::new(); + for (range, byte_idx) in GlyphClusters::new(&glyphs) { + if let Some(span) = chunk_span_at(chunk, byte_idx) { + clusters.push(form_glyph_clusters( + &glyphs[range], + &chunk.text, + span.font_size.get(), + )); + } + } + + clusters +} + +fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear); + + for span in &chunk.spans { + let target_width = match span.text_length { + Some(v) => v, + None => continue, + }; + + let mut width = 0.0; + let mut cluster_indexes = Vec::new(); + for i in span.start..span.end { + if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) { + cluster_indexes.push(index); + } + } + // Complex scripts can have multi-codepoint clusters therefore we have to remove duplicates. + cluster_indexes.sort(); + cluster_indexes.dedup(); + + for i in &cluster_indexes { + // Use the original cluster `width` and not `advance`. + // This method essentially discards any `word-spacing` and `letter-spacing`. + width += clusters[*i].width; + } + + if cluster_indexes.is_empty() { + continue; + } + + if span.length_adjust == LengthAdjust::Spacing { + let factor = if cluster_indexes.len() > 1 { + (target_width - width) / (cluster_indexes.len() - 1) as f32 + } else { + 0.0 + }; + + for i in cluster_indexes { + clusters[i].advance = clusters[i].width + factor; + } + } else { + let factor = target_width / width; + // Prevent multiplying by zero. + if factor < 0.001 { + continue; + } + + for i in cluster_indexes { + clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0); + + // Technically just a hack to support the current text-on-path algorithm. + if !is_horizontal { + clusters[i].advance *= factor; + clusters[i].width *= factor; + } + } + } + } +} + +/// Rotates clusters according to +/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html). +fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) { + if writing_mode != WritingMode::TopToBottom { + return; + } + + for cluster in clusters { + let orientation = unicode_vo::char_orientation(cluster.codepoint); + if orientation == unicode_vo::Orientation::Upright { + let mut ts = Transform::default(); + // Position glyph in the center of vertical axis. + ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0); + // Rotate by 90 degrees in the center. + ts = ts.pre_rotate_at( + -90.0, + cluster.width / 2.0, + -(cluster.ascent + cluster.descent) / 2.0, + ); + + cluster.path_transform = ts; + + // Move "baseline" to the middle and make height equal to width. + cluster.ascent = cluster.width / 2.0; + cluster.descent = -cluster.width / 2.0; + } else { + // Could not find a spec that explains this, + // but this is how other applications are shifting the "rotated" characters + // in the top-to-bottom mode. + cluster.transform = cluster + .transform + .pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0); + } + } +} + +/// Applies the `letter-spacing` property to a text chunk clusters. +/// +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property). +fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + // At least one span should have a non-zero spacing. + if !chunk + .spans + .iter() + .any(|span| !span.letter_spacing.approx_zero_ulps(4)) + { + return; + } + + let num_clusters = clusters.len(); + for (i, cluster) in clusters.iter_mut().enumerate() { + // Spacing must be applied only to characters that belongs to the script + // that supports spacing. + // We are checking only the first code point, since it should be enough. + // https://www.w3.org/TR/css-text-3/#cursive-tracking + let script = cluster.codepoint.script(); + if script_supports_letter_spacing(script) { + if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { + // A space after the last cluster should be ignored, + // since it affects the bbox and text alignment. + if i != num_clusters - 1 { + cluster.advance += span.letter_spacing; + } + + // If the cluster advance became negative - clear it. + // This is an UB so we can do whatever we want, and we mimic Chrome's behavior. + if !cluster.advance.is_valid_length() { + cluster.width = 0.0; + cluster.advance = 0.0; + cluster.glyphs = vec![]; + } + } + } + } +} + +/// Applies the `word-spacing` property to a text chunk clusters. +/// +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing). +fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + // At least one span should have a non-zero spacing. + if !chunk + .spans + .iter() + .any(|span| !span.word_spacing.approx_zero_ulps(4)) + { + return; + } + + for cluster in clusters { + if is_word_separator_characters(cluster.codepoint) { + if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { + // Technically, word spacing 'should be applied half on each + // side of the character', but it doesn't affect us in any way, + // so we are ignoring this. + cluster.advance += span.word_spacing; + + // After word spacing, `advance` can be negative. + } + } + } +} + +fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster { + debug_assert!(!glyphs.is_empty()); + + let mut width = 0.0; + let mut x: f32 = 0.0; + + let mut positioned_glyphs = vec![]; + + for glyph in glyphs { + let sx = glyph.font.scale(font_size); + + // Apply offset. + // + // The first glyph in the cluster will have an offset from 0x0, + // but the later one will have an offset from the "current position". + // So we have to keep an advance. + // TODO: should be done only inside a single text span + let ts = Transform::from_translate(x + glyph.dx as f32, -glyph.dy as f32); + + positioned_glyphs.push(PositionedGlyph { + glyph_ts: ts, + // Will be set later. + cluster_ts: Transform::default(), + // Will be set later. + span_ts: Transform::default(), + units_per_em: glyph.font.units_per_em.get(), + font_size, + font: glyph.font.id, + text: glyph.text.clone(), + id: glyph.id, + }); + + x += glyph.width as f32; + + let glyph_width = glyph.width as f32 * sx; + if glyph_width > width { + width = glyph_width; + } + } + + let byte_idx = glyphs[0].byte_idx; + let font = glyphs[0].font.clone(); + GlyphCluster { + byte_idx, + codepoint: byte_idx.char_from(text), + width, + advance: width, + ascent: font.ascent(font_size), + descent: font.descent(font_size), + has_relative_shift: false, + transform: Transform::default(), + path_transform: Transform::default(), + glyphs: positioned_glyphs, + visible: true, + } +} + +pub(crate) trait DatabaseExt { + fn load_font(&self, id: ID) -> Option; + fn has_char(&self, id: ID, c: char) -> bool; +} + +impl DatabaseExt for Database { + #[inline(never)] + fn load_font(&self, id: ID) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + + let units_per_em = NonZeroU16::new(font.units_per_em())?; + + let ascent = font.ascender(); + let descent = font.descender(); + + let x_height = font + .x_height() + .and_then(|x| u16::try_from(x).ok()) + .and_then(NonZeroU16::new); + let x_height = match x_height { + Some(height) => height, + None => { + // If not set - fallback to height * 45%. + // 45% is what Firefox uses. + u16::try_from((f32::from(ascent - descent) * 0.45) as i32) + .ok() + .and_then(NonZeroU16::new)? + } + }; + + let line_through = font.strikeout_metrics(); + let line_through_position = match line_through { + Some(metrics) => metrics.position, + None => x_height.get() as i16 / 2, + }; + + let (underline_position, underline_thickness) = match font.underline_metrics() { + Some(metrics) => { + let thickness = u16::try_from(metrics.thickness) + .ok() + .and_then(NonZeroU16::new) + // `ttf_parser` guarantees that units_per_em is >= 16 + .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); + + (metrics.position, thickness) + } + None => ( + -(units_per_em.get() as i16) / 9, + NonZeroU16::new(units_per_em.get() / 12).unwrap(), + ), + }; + + // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). + let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; + let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; + if let Some(metrics) = font.subscript_metrics() { + subscript_offset = metrics.y_offset; + } + + if let Some(metrics) = font.superscript_metrics() { + superscript_offset = metrics.y_offset; + } + + Some(ResolvedFont { + id, + units_per_em, + ascent, + descent, + x_height, + underline_position, + underline_thickness, + line_through_position, + subscript_offset, + superscript_offset, + }) + })? + } + + #[inline(never)] + fn has_char(&self, id: ID, c: char) -> bool { + let res = self.with_face_data(id, |font_data, face_index| -> Option { + let font = ttf_parser::Face::parse(font_data, face_index).ok()?; + font.glyph_index(c)?; + Some(true) + }); + + res == Some(Some(true)) + } +} + +/// Text shaping with font fallback. +pub(crate) fn shape_text( + text: &str, + font: Arc, + small_caps: bool, + apply_kerning: bool, + resolver: &FontResolver, + fontdb: &mut Arc, +) -> Vec { + let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb) + .unwrap_or_default(); + + // Remember all fonts used for shaping. + let mut used_fonts = vec![font.id]; + + // Loop until all glyphs become resolved or until no more fonts are left. + 'outer: loop { + let mut missing = None; + for glyph in &glyphs { + if glyph.is_missing() { + missing = Some(glyph.byte_idx.char_from(text)); + break; + } + } + + if let Some(c) = missing { + let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb) + .and_then(|id| fontdb.load_font(id)) + { + Some(v) => Arc::new(v), + None => break 'outer, + }; + + // Shape again, using a new font. + let fallback_glyphs = shape_text_with_font( + text, + fallback_font.clone(), + small_caps, + apply_kerning, + fontdb, + ) + .unwrap_or_default(); + + let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing()); + if all_matched { + // Replace all glyphs when all of them were matched. + glyphs = fallback_glyphs; + break 'outer; + } + + // We assume, that shaping with an any font will produce the same amount of glyphs. + // This is incorrect, but good enough for now. + if glyphs.len() != fallback_glyphs.len() { + break 'outer; + } + + // TODO: Replace clusters and not glyphs. This should be more accurate. + + // Copy new glyphs. + for i in 0..glyphs.len() { + if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() { + glyphs[i] = fallback_glyphs[i].clone(); + } + } + + // Remember this font. + used_fonts.push(fallback_font.id); + } else { + break 'outer; + } + } + + // Warn about missing glyphs. + for glyph in &glyphs { + if glyph.is_missing() { + let c = glyph.byte_idx.char_from(text); + // TODO: print a full grapheme + log::warn!( + "No fonts with a {}/U+{:X} character were found.", + c, + c as u32 + ); + } + } + + glyphs +} + +/// Converts a text into a list of glyph IDs. +/// +/// This function will do the BIDI reordering and text shaping. +fn shape_text_with_font( + text: &str, + font: Arc, + small_caps: bool, + apply_kerning: bool, + fontdb: &fontdb::Database, +) -> Option> { + fontdb.with_face_data(font.id, |font_data, face_index| -> Option> { + let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?; + + let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr())); + let paragraph = &bidi_info.paragraphs[0]; + let line = paragraph.range.clone(); + + let mut glyphs = Vec::new(); + + let (levels, runs) = bidi_info.visual_runs(paragraph, line); + for run in runs.iter() { + let sub_text = &text[run.clone()]; + if sub_text.is_empty() { + continue; + } + + let ltr = levels[run.start].is_ltr(); + let hb_direction = if ltr { + rustybuzz::Direction::LeftToRight + } else { + rustybuzz::Direction::RightToLeft + }; + + let mut buffer = rustybuzz::UnicodeBuffer::new(); + buffer.push_str(sub_text); + buffer.set_direction(hb_direction); + + let mut features = Vec::new(); + if small_caps { + features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..)); + } + + if !apply_kerning { + features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..)); + } + + let output = rustybuzz::shape(&rb_font, &features, buffer); + + let positions = output.glyph_positions(); + let infos = output.glyph_infos(); + + for i in 0..output.len() { + let pos = positions[i]; + let info = infos[i]; + let idx = run.start + info.cluster as usize; + + let start = info.cluster as usize; + + let end = if ltr { + i.checked_add(1) + } else { + i.checked_sub(1) + } + .and_then(|last| infos.get(last)) + .map_or(sub_text.len(), |info| info.cluster as usize); + + glyphs.push(Glyph { + byte_idx: ByteIndex::new(idx), + cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? + text: sub_text[start..end].to_string(), + id: GlyphId(info.glyph_id as u16), + dx: pos.x_offset, + dy: pos.y_offset, + width: pos.x_advance, + font: font.clone(), + }); + } + } + + Some(glyphs) + })? +} + +/// An iterator over glyph clusters. +/// +/// Input: 0 2 2 2 3 4 4 5 5 +/// Result: 0 1 4 5 7 +pub(crate) struct GlyphClusters<'a> { + data: &'a [Glyph], + idx: usize, +} + +impl<'a> GlyphClusters<'a> { + pub(crate) fn new(data: &'a [Glyph]) -> Self { + GlyphClusters { data, idx: 0 } + } +} + +impl Iterator for GlyphClusters<'_> { + type Item = (std::ops::Range, ByteIndex); + + fn next(&mut self) -> Option { + if self.idx == self.data.len() { + return None; + } + + let start = self.idx; + let cluster = self.data[self.idx].byte_idx; + for g in &self.data[self.idx..] { + if g.byte_idx != cluster { + break; + } + + self.idx += 1; + } + + Some((start..self.idx, cluster)) + } +} + +/// Checks that selected script supports letter spacing. +/// +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking). +/// +/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64 +pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool { + use unicode_script::Script; + + !matches!( + script, + Script::Arabic + | Script::Syriac + | Script::Nko + | Script::Manichaean + | Script::Psalter_Pahlavi + | Script::Mandaic + | Script::Mongolian + | Script::Phags_Pa + | Script::Devanagari + | Script::Bengali + | Script::Gurmukhi + | Script::Modi + | Script::Sharada + | Script::Syloti_Nagri + | Script::Tirhuta + | Script::Ogham + ) +} + +/// A glyph. +/// +/// Basically, a glyph ID and it's metrics. +#[derive(Clone)] +pub(crate) struct Glyph { + /// The glyph ID in the font. + pub(crate) id: GlyphId, + + /// Position in bytes in the original string. + /// + /// We use it to match a glyph with a character in the text chunk and therefore with the style. + pub(crate) byte_idx: ByteIndex, + + // The length of the cluster in bytes. + pub(crate) cluster_len: usize, + + /// The text from the original string that corresponds to that glyph. + pub(crate) text: String, + + /// The glyph offset in font units. + pub(crate) dx: i32, + + /// The glyph offset in font units. + pub(crate) dy: i32, + + /// The glyph width / X-advance in font units. + pub(crate) width: i32, + + /// Reference to the source font. + /// + /// Each glyph can have it's own source font. + pub(crate) font: Arc, +} + +impl Glyph { + fn is_missing(&self) -> bool { + self.id.0 == 0 + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct ResolvedFont { + pub(crate) id: ID, + + units_per_em: NonZeroU16, + + // All values below are in font units. + ascent: i16, + descent: i16, + x_height: NonZeroU16, + + underline_position: i16, + underline_thickness: NonZeroU16, + + // line-through thickness should be the the same as underline thickness + // according to the TrueType spec: + // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize + line_through_position: i16, + + subscript_offset: i16, + superscript_offset: i16, +} + +pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> { + chunk + .spans + .iter() + .find(|&span| span_contains(span, byte_offset)) +} + +pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool { + byte_offset.value() >= span.start && byte_offset.value() < span.end +} + +/// Checks that the selected character is a word separator. +/// +/// According to: https://www.w3.org/TR/css-text-3/#word-separator +pub(crate) fn is_word_separator_characters(c: char) -> bool { + matches!( + c as u32, + 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F + ) +} + +impl ResolvedFont { + #[inline] + pub(crate) fn scale(&self, font_size: f32) -> f32 { + font_size / self.units_per_em.get() as f32 + } + + #[inline] + pub(crate) fn ascent(&self, font_size: f32) -> f32 { + self.ascent as f32 * self.scale(font_size) + } + + #[inline] + pub(crate) fn descent(&self, font_size: f32) -> f32 { + self.descent as f32 * self.scale(font_size) + } + + #[inline] + pub(crate) fn height(&self, font_size: f32) -> f32 { + self.ascent(font_size) - self.descent(font_size) + } + + #[inline] + pub(crate) fn x_height(&self, font_size: f32) -> f32 { + self.x_height.get() as f32 * self.scale(font_size) + } + + #[inline] + pub(crate) fn underline_position(&self, font_size: f32) -> f32 { + self.underline_position as f32 * self.scale(font_size) + } + + #[inline] + fn underline_thickness(&self, font_size: f32) -> f32 { + self.underline_thickness.get() as f32 * self.scale(font_size) + } + + #[inline] + pub(crate) fn line_through_position(&self, font_size: f32) -> f32 { + self.line_through_position as f32 * self.scale(font_size) + } + + #[inline] + fn subscript_offset(&self, font_size: f32) -> f32 { + self.subscript_offset as f32 * self.scale(font_size) + } + + #[inline] + fn superscript_offset(&self, font_size: f32) -> f32 { + self.superscript_offset as f32 * self.scale(font_size) + } + + fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 { + let alignment = match baseline { + DominantBaseline::Auto => AlignmentBaseline::Auto, + DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported + DominantBaseline::NoChange => AlignmentBaseline::Auto, // already resolved + DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported + DominantBaseline::Ideographic => AlignmentBaseline::Ideographic, + DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic, + DominantBaseline::Hanging => AlignmentBaseline::Hanging, + DominantBaseline::Mathematical => AlignmentBaseline::Mathematical, + DominantBaseline::Central => AlignmentBaseline::Central, + DominantBaseline::Middle => AlignmentBaseline::Middle, + DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge, + DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge, + }; + + self.alignment_baseline_shift(alignment, font_size) + } + + // The `alignment-baseline` property is a mess. + // + // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties) + // goes on and on about what this property suppose to do, but doesn't actually explain + // how it should be implemented. It's just a very verbose overview. + // + // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't. + // Same goes for basically every SVG library in existence. + // Meaning we have no idea how exactly it should be implemented. + // + // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`, + // `text-after-edge` and `ideographic` variants. Producing vastly different output. + // + // As per spec, a proper implementation should get baseline values from the font itself, + // using `BASE` and `bsln` TrueType tables. If those tables are not present, + // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts). + // And in the worst case scenario simply fallback to hardcoded values. + // + // Also, most fonts do not provide `BASE` and `bsln` tables to begin with. + // + // Again, as of Nov 2022, Chrome does only the latter: + // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153 + // + // Since baseline TrueType tables parsing and baseline synthesis are pretty hard, + // we do what Chrome does - use hardcoded values. And it seems like Safari does the same. + // + // + // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul, + // and it's far more complex now. Not sure if anyone actually supports it. + fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 { + match alignment { + AlignmentBaseline::Auto => 0.0, + AlignmentBaseline::Baseline => 0.0, + AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => { + self.ascent(font_size) + } + AlignmentBaseline::Middle => self.x_height(font_size) * 0.5, + AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5, + AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => { + self.descent(font_size) + } + AlignmentBaseline::Ideographic => self.descent(font_size), + AlignmentBaseline::Alphabetic => 0.0, + AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8, + AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5, + } + } +} + +pub(crate) type FontsCache = HashMap>; + +/// A read-only text index in bytes. +/// +/// Guarantee to be on a char boundary and in text bounds. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) struct ByteIndex(usize); + +impl ByteIndex { + fn new(i: usize) -> Self { + ByteIndex(i) + } + + pub(crate) fn value(&self) -> usize { + self.0 + } + + /// Converts byte position into a code point position. + pub(crate) fn code_point_at(&self, text: &str) -> usize { + text.char_indices() + .take_while(|(i, _)| *i != self.0) + .count() + } + + /// Converts byte position into a character. + pub(crate) fn char_from(&self, text: &str) -> char { + text[self.0..].chars().next().unwrap() + } +} diff --git a/third_party/usvg/src/text/mod.rs b/third_party/usvg/src/text/mod.rs new file mode 100644 index 0000000000..4b48274e17 --- /dev/null +++ b/third_party/usvg/src/text/mod.rs @@ -0,0 +1,216 @@ +// Copyright 2024 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use fontdb::{Database, ID}; +use svgtypes::FontFamily; + +use self::layout::DatabaseExt; +use crate::{Cache, Font, FontStretch, FontStyle, Text}; + +pub(crate) mod flatten; + +mod colr; +/// Provides access to the layout of a text node. +pub mod layout; + +/// A shorthand for [FontResolver]'s font selection function. +/// +/// This function receives a font specification (families + a style, weight, +/// stretch triple) and a font database and should return the ID of the font +/// that shall be used (if any). +/// +/// In the basic case, the function will search the existing fonts in the +/// database to find a good match, e.g. via +/// [`Database::query`](fontdb::Database::query). This is what the [default +/// implementation](FontResolver::default_font_selector) does. +/// +/// Users with more complex requirements can mutate the database to load +/// additional fonts dynamically. To perform mutation, it is recommended to call +/// `Arc::make_mut` on the provided database. (This call is not done outside of +/// the callback to not needless clone an underlying shared database if no +/// mutation will be performed.) It is important that the database is only +/// mutated additively. Removing fonts or replacing the entire database will +/// break things. +pub type FontSelectionFn<'a> = + Box) -> Option + Send + Sync + 'a>; + +/// A shorthand for [FontResolver]'s fallback selection function. +/// +/// This function receives a specific character, a list of already used fonts, +/// and a font database. It should return the ID of a font that +/// - is not any of the already used fonts +/// - is as close as possible to the first already used font (if any) +/// - supports the given character +/// +/// The function can search the existing database, but can also load additional +/// fonts dynamically. See the documentation of [`FontSelectionFn`] for more +/// details. +pub type FallbackSelectionFn<'a> = + Box) -> Option + Send + Sync + 'a>; + +/// A font resolver for `` elements. +/// +/// This type can be useful if you want to have an alternative font handling to +/// the default one. By default, only fonts specified upfront in +/// [`Options::fontdb`](crate::Options::fontdb) will be used. This type allows +/// you to load additional fonts on-demand and customize the font selection +/// process. +pub struct FontResolver<'a> { + /// Resolver function that will be used when selecting a specific font + /// for a generic [`Font`] specification. + pub select_font: FontSelectionFn<'a>, + + /// Resolver function that will be used when selecting a fallback font for a + /// character. + pub select_fallback: FallbackSelectionFn<'a>, +} + +impl Default for FontResolver<'_> { + fn default() -> Self { + FontResolver { + select_font: FontResolver::default_font_selector(), + select_fallback: FontResolver::default_fallback_selector(), + } + } +} + +impl FontResolver<'_> { + /// Creates a default font selection resolver. + /// + /// The default implementation forwards to + /// [`query`](fontdb::Database::query) on the font database specified in the + /// [`Options`](crate::Options). + pub fn default_font_selector() -> FontSelectionFn<'static> { + Box::new(move |font, fontdb| { + let mut name_list = Vec::new(); + for family in &font.families { + name_list.push(match family { + FontFamily::Serif => fontdb::Family::Serif, + FontFamily::SansSerif => fontdb::Family::SansSerif, + FontFamily::Cursive => fontdb::Family::Cursive, + FontFamily::Fantasy => fontdb::Family::Fantasy, + FontFamily::Monospace => fontdb::Family::Monospace, + FontFamily::Named(s) => fontdb::Family::Name(s), + }); + } + + // Use the default font as fallback. + name_list.push(fontdb::Family::Serif); + + let stretch = match font.stretch { + FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, + FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, + FontStretch::Condensed => fontdb::Stretch::Condensed, + FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, + FontStretch::Normal => fontdb::Stretch::Normal, + FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, + FontStretch::Expanded => fontdb::Stretch::Expanded, + FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, + FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, + }; + + let style = match font.style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + }; + + let query = fontdb::Query { + families: &name_list, + weight: fontdb::Weight(font.weight), + stretch, + style, + }; + + let id = fontdb.query(&query); + if id.is_none() { + log::warn!( + "No match for '{}' font-family.", + font.families + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", ") + ); + } + + id + }) + } + + /// Creates a default font fallback selection resolver. + /// + /// The default implementation searches through the entire `fontdb` + /// to find a font that has the correct style and supports the character. + pub fn default_fallback_selector() -> FallbackSelectionFn<'static> { + Box::new(|c, exclude_fonts, fontdb| { + let base_font_id = exclude_fonts[0]; + + // Iterate over fonts and check if any of them support the specified char. + for face in fontdb.faces() { + // Ignore fonts, that were used for shaping already. + if exclude_fonts.contains(&face.id) { + continue; + } + + // Check that the new face has the same style. + let base_face = fontdb.face(base_font_id)?; + if base_face.style != face.style + && base_face.weight != face.weight + && base_face.stretch != face.stretch + { + continue; + } + + if !fontdb.has_char(face.id, c) { + continue; + } + + let base_family = base_face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + let new_family = face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); + return Some(face.id); + } + + None + }) + } +} + +impl std::fmt::Debug for FontResolver<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("FontResolver { .. }") + } +} + +/// Convert a text into its paths. This is done in two steps: +/// 1. We convert the text into glyphs and position them according to the rules specified +/// in the SVG specification. While doing so, we also calculate the text bbox (which +/// is not based on the outlines of a glyph, but instead the glyph metrics as well +/// as decoration spans). +/// 2. We convert all of the positioned glyphs into outlines. +pub(crate) fn convert(text: &mut Text, resolver: &FontResolver, cache: &mut Cache) -> Option<()> { + let (text_fragments, bbox) = layout::layout_text(text, resolver, &mut cache.fontdb)?; + text.layouted = text_fragments; + text.bounding_box = bbox.to_rect(); + text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect(); + + let (group, stroke_bbox) = flatten::flatten(text, cache)?; + text.flattened = Box::new(group); + text.stroke_bounding_box = stroke_bbox.to_rect(); + text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect(); + + Some(()) +} diff --git a/third_party/usvg/src/tree/filter.rs b/third_party/usvg/src/tree/filter.rs new file mode 100644 index 0000000000..f89d399d39 --- /dev/null +++ b/third_party/usvg/src/tree/filter.rs @@ -0,0 +1,1061 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! SVG filter types. + +use strict_num::PositiveF32; + +use crate::{BlendMode, Color, Group, NonEmptyString, NonZeroF32, NonZeroRect, Opacity}; + +/// A filter element. +/// +/// `filter` element in the SVG. +#[derive(Debug)] +pub struct Filter { + pub(crate) id: NonEmptyString, + pub(crate) rect: NonZeroRect, + pub(crate) primitives: Vec, +} + +impl Filter { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Filter region. + /// + /// `x`, `y`, `width` and `height` in the SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// A list of filter primitives. + pub fn primitives(&self) -> &[Primitive] { + &self.primitives + } +} + +/// A filter primitive element. +#[derive(Clone, Debug)] +pub struct Primitive { + pub(crate) rect: NonZeroRect, + pub(crate) color_interpolation: ColorInterpolation, + pub(crate) result: String, + pub(crate) kind: Kind, +} + +impl Primitive { + /// Filter subregion. + /// + /// `x`, `y`, `width` and `height` in the SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// Color interpolation mode. + /// + /// `color-interpolation-filters` in the SVG. + pub fn color_interpolation(&self) -> ColorInterpolation { + self.color_interpolation + } + + /// Assigned name for this filter primitive. + /// + /// `result` in the SVG. + pub fn result(&self) -> &str { + &self.result + } + + /// Filter primitive kind. + pub fn kind(&self) -> &Kind { + &self.kind + } +} + +/// A filter kind. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum Kind { + Blend(Blend), + ColorMatrix(ColorMatrix), + ComponentTransfer(ComponentTransfer), + Composite(Composite), + ConvolveMatrix(ConvolveMatrix), + DiffuseLighting(DiffuseLighting), + DisplacementMap(DisplacementMap), + DropShadow(DropShadow), + Flood(Flood), + GaussianBlur(GaussianBlur), + Image(Image), + Merge(Merge), + Morphology(Morphology), + Offset(Offset), + SpecularLighting(SpecularLighting), + Tile(Tile), + Turbulence(Turbulence), +} + +impl Kind { + /// Checks that `FilterKind` has a specific input. + pub fn has_input(&self, input: &Input) -> bool { + match self { + Kind::Blend(ref fe) => fe.input1 == *input || fe.input2 == *input, + Kind::ColorMatrix(ref fe) => fe.input == *input, + Kind::ComponentTransfer(ref fe) => fe.input == *input, + Kind::Composite(ref fe) => fe.input1 == *input || fe.input2 == *input, + Kind::ConvolveMatrix(ref fe) => fe.input == *input, + Kind::DiffuseLighting(ref fe) => fe.input == *input, + Kind::DisplacementMap(ref fe) => fe.input1 == *input || fe.input2 == *input, + Kind::DropShadow(ref fe) => fe.input == *input, + Kind::Flood(_) => false, + Kind::GaussianBlur(ref fe) => fe.input == *input, + Kind::Image(_) => false, + Kind::Merge(ref fe) => fe.inputs.iter().any(|i| i == input), + Kind::Morphology(ref fe) => fe.input == *input, + Kind::Offset(ref fe) => fe.input == *input, + Kind::SpecularLighting(ref fe) => fe.input == *input, + Kind::Tile(ref fe) => fe.input == *input, + Kind::Turbulence(_) => false, + } + } +} + +/// Identifies input for a filter primitive. +#[allow(missing_docs)] +#[derive(Clone, PartialEq, Debug)] +pub enum Input { + SourceGraphic, + SourceAlpha, + Reference(String), +} + +/// A color interpolation mode. +/// +/// The default is `ColorInterpolation::LinearRGB`. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug, Default)] +pub enum ColorInterpolation { + SRGB, + #[default] + LinearRGB, +} + +/// A blend filter primitive. +/// +/// `feBlend` element in the SVG. +#[derive(Clone, Debug)] +pub struct Blend { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) mode: BlendMode, +} + +impl Blend { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input1(&self) -> &Input { + &self.input1 + } + + /// Identifies input for the given filter primitive. + /// + /// `in2` in the SVG. + pub fn input2(&self) -> &Input { + &self.input2 + } + + /// A blending mode. + /// + /// `mode` in the SVG. + pub fn mode(&self) -> BlendMode { + self.mode + } +} + +/// A color matrix filter primitive. +/// +/// `feColorMatrix` element in the SVG. +#[derive(Clone, Debug)] +pub struct ColorMatrix { + pub(crate) input: Input, + pub(crate) kind: ColorMatrixKind, +} + +impl ColorMatrix { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A matrix kind. + /// + /// `type` in the SVG. + pub fn kind(&self) -> &ColorMatrixKind { + &self.kind + } +} + +/// A color matrix filter primitive kind. +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub enum ColorMatrixKind { + Matrix(Vec), // Guarantee to have 20 numbers. + Saturate(PositiveF32), + HueRotate(f32), + LuminanceToAlpha, +} + +impl Default for ColorMatrixKind { + fn default() -> Self { + ColorMatrixKind::Matrix(vec![ + 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + ]) + } +} + +/// A component-wise remapping filter primitive. +/// +/// `feComponentTransfer` element in the SVG. +#[derive(Clone, Debug)] +pub struct ComponentTransfer { + pub(crate) input: Input, + pub(crate) func_r: TransferFunction, + pub(crate) func_g: TransferFunction, + pub(crate) func_b: TransferFunction, + pub(crate) func_a: TransferFunction, +} + +impl ComponentTransfer { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// `feFuncR` in the SVG. + pub fn func_r(&self) -> &TransferFunction { + &self.func_r + } + + /// `feFuncG` in the SVG. + pub fn func_g(&self) -> &TransferFunction { + &self.func_g + } + + /// `feFuncB` in the SVG. + pub fn func_b(&self) -> &TransferFunction { + &self.func_b + } + + /// `feFuncA` in the SVG. + pub fn func_a(&self) -> &TransferFunction { + &self.func_a + } +} + +/// A transfer function used by `FeComponentTransfer`. +/// +/// +#[derive(Clone, Debug)] +pub enum TransferFunction { + /// Keeps a component as is. + Identity, + + /// Applies a linear interpolation to a component. + /// + /// The number list can be empty. + Table(Vec), + + /// Applies a step function to a component. + /// + /// The number list can be empty. + Discrete(Vec), + + /// Applies a linear shift to a component. + #[allow(missing_docs)] + Linear { slope: f32, intercept: f32 }, + + /// Applies an exponential shift to a component. + #[allow(missing_docs)] + Gamma { + amplitude: f32, + exponent: f32, + offset: f32, + }, +} + +/// A composite filter primitive. +/// +/// `feComposite` element in the SVG. +#[derive(Clone, Debug)] +pub struct Composite { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) operator: CompositeOperator, +} + +impl Composite { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input1(&self) -> &Input { + &self.input1 + } + + /// Identifies input for the given filter primitive. + /// + /// `in2` in the SVG. + pub fn input2(&self) -> &Input { + &self.input2 + } + + /// A compositing operation. + /// + /// `operator` in the SVG. + pub fn operator(&self) -> CompositeOperator { + self.operator + } +} + +/// An images compositing operation. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum CompositeOperator { + Over, + In, + Out, + Atop, + Xor, + Arithmetic { k1: f32, k2: f32, k3: f32, k4: f32 }, +} + +/// A matrix convolution filter primitive. +/// +/// `feConvolveMatrix` element in the SVG. +#[derive(Clone, Debug)] +pub struct ConvolveMatrix { + pub(crate) input: Input, + pub(crate) matrix: ConvolveMatrixData, + pub(crate) divisor: NonZeroF32, + pub(crate) bias: f32, + pub(crate) edge_mode: EdgeMode, + pub(crate) preserve_alpha: bool, +} + +impl ConvolveMatrix { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A convolve matrix. + pub fn matrix(&self) -> &ConvolveMatrixData { + &self.matrix + } + + /// A matrix divisor. + /// + /// `divisor` in the SVG. + pub fn divisor(&self) -> NonZeroF32 { + self.divisor + } + + /// A kernel matrix bias. + /// + /// `bias` in the SVG. + pub fn bias(&self) -> f32 { + self.bias + } + + /// An edges processing mode. + /// + /// `edgeMode` in the SVG. + pub fn edge_mode(&self) -> EdgeMode { + self.edge_mode + } + + /// An alpha preserving flag. + /// + /// `preserveAlpha` in the SVG. + pub fn preserve_alpha(&self) -> bool { + self.preserve_alpha + } +} + +/// A convolve matrix representation. +/// +/// Used primarily by [`ConvolveMatrix`]. +#[derive(Clone, Debug)] +pub struct ConvolveMatrixData { + pub(crate) target_x: u32, + pub(crate) target_y: u32, + pub(crate) columns: u32, + pub(crate) rows: u32, + pub(crate) data: Vec, +} + +impl ConvolveMatrixData { + /// Returns a matrix's X target. + /// + /// `targetX` in the SVG. + pub fn target_x(&self) -> u32 { + self.target_x + } + + /// Returns a matrix's Y target. + /// + /// `targetY` in the SVG. + pub fn target_y(&self) -> u32 { + self.target_y + } + + /// Returns a number of columns in the matrix. + /// + /// Part of the `order` attribute in the SVG. + pub fn columns(&self) -> u32 { + self.columns + } + + /// Returns a number of rows in the matrix. + /// + /// Part of the `order` attribute in the SVG. + pub fn rows(&self) -> u32 { + self.rows + } + + /// The actual matrix. + pub fn data(&self) -> &[f32] { + &self.data + } +} + +impl ConvolveMatrixData { + /// Creates a new `ConvolveMatrixData`. + /// + /// Returns `None` when: + /// + /// - `columns` * `rows` != `data.len()` + /// - `target_x` >= `columns` + /// - `target_y` >= `rows` + pub(crate) fn new( + target_x: u32, + target_y: u32, + columns: u32, + rows: u32, + data: Vec, + ) -> Option { + if (columns * rows) as usize != data.len() || target_x >= columns || target_y >= rows { + return None; + } + + Some(ConvolveMatrixData { + target_x, + target_y, + columns, + rows, + data, + }) + } + + /// Returns a matrix value at the specified position. + /// + /// # Panics + /// + /// - When position is out of bounds. + pub fn get(&self, x: u32, y: u32) -> f32 { + self.data[(y * self.columns + x) as usize] + } +} + +/// An edges processing mode. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum EdgeMode { + None, + Duplicate, + Wrap, +} + +/// A displacement map filter primitive. +/// +/// `feDisplacementMap` element in the SVG. +#[derive(Clone, Debug)] +pub struct DisplacementMap { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) scale: f32, + pub(crate) x_channel_selector: ColorChannel, + pub(crate) y_channel_selector: ColorChannel, +} + +impl DisplacementMap { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input1(&self) -> &Input { + &self.input1 + } + + /// Identifies input for the given filter primitive. + /// + /// `in2` in the SVG. + pub fn input2(&self) -> &Input { + &self.input2 + } + + /// Scale factor. + /// + /// `scale` in the SVG. + pub fn scale(&self) -> f32 { + self.scale + } + + /// Indicates a source color channel along the X-axis. + /// + /// `xChannelSelector` in the SVG. + pub fn x_channel_selector(&self) -> ColorChannel { + self.x_channel_selector + } + + /// Indicates a source color channel along the Y-axis. + /// + /// `yChannelSelector` in the SVG. + pub fn y_channel_selector(&self) -> ColorChannel { + self.y_channel_selector + } +} + +/// A color channel. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum ColorChannel { + R, + G, + B, + A, +} + +/// A drop shadow filter primitive. +/// +/// This is essentially `feGaussianBlur`, `feOffset` and `feFlood` joined together. +/// +/// `feDropShadow` element in the SVG. +#[derive(Clone, Debug)] +pub struct DropShadow { + pub(crate) input: Input, + pub(crate) dx: f32, + pub(crate) dy: f32, + pub(crate) std_dev_x: PositiveF32, + pub(crate) std_dev_y: PositiveF32, + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl DropShadow { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// The amount to offset the input graphic along the X-axis. + pub fn dx(&self) -> f32 { + self.dx + } + + /// The amount to offset the input graphic along the Y-axis. + pub fn dy(&self) -> f32 { + self.dy + } + + /// A standard deviation along the X-axis. + /// + /// `stdDeviation` in the SVG. + pub fn std_dev_x(&self) -> PositiveF32 { + self.std_dev_x + } + + /// A standard deviation along the Y-axis. + /// + /// `stdDeviation` in the SVG. + pub fn std_dev_y(&self) -> PositiveF32 { + self.std_dev_y + } + + /// A flood color. + /// + /// `flood-color` in the SVG. + pub fn color(&self) -> Color { + self.color + } + + /// A flood opacity. + /// + /// `flood-opacity` in the SVG. + pub fn opacity(&self) -> Opacity { + self.opacity + } +} + +/// A flood filter primitive. +/// +/// `feFlood` element in the SVG. +#[derive(Clone, Copy, Debug)] +pub struct Flood { + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl Flood { + /// A flood color. + /// + /// `flood-color` in the SVG. + pub fn color(&self) -> Color { + self.color + } + + /// A flood opacity. + /// + /// `flood-opacity` in the SVG. + pub fn opacity(&self) -> Opacity { + self.opacity + } +} + +/// A Gaussian blur filter primitive. +/// +/// `feGaussianBlur` element in the SVG. +#[derive(Clone, Debug)] +pub struct GaussianBlur { + pub(crate) input: Input, + pub(crate) std_dev_x: PositiveF32, + pub(crate) std_dev_y: PositiveF32, +} + +impl GaussianBlur { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A standard deviation along the X-axis. + /// + /// `stdDeviation` in the SVG. + pub fn std_dev_x(&self) -> PositiveF32 { + self.std_dev_x + } + + /// A standard deviation along the Y-axis. + /// + /// `stdDeviation` in the SVG. + pub fn std_dev_y(&self) -> PositiveF32 { + self.std_dev_y + } +} + +/// An image filter primitive. +/// +/// `feImage` element in the SVG. +#[derive(Clone, Debug)] +pub struct Image { + pub(crate) root: Group, +} + +impl Image { + /// `feImage` children. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// A diffuse lighting filter primitive. +/// +/// `feDiffuseLighting` element in the SVG. +#[derive(Clone, Debug)] +pub struct DiffuseLighting { + pub(crate) input: Input, + pub(crate) surface_scale: f32, + pub(crate) diffuse_constant: f32, + pub(crate) lighting_color: Color, + pub(crate) light_source: LightSource, +} + +impl DiffuseLighting { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A surface scale. + /// + /// `surfaceScale` in the SVG. + pub fn surface_scale(&self) -> f32 { + self.surface_scale + } + + /// A diffuse constant. + /// + /// `diffuseConstant` in the SVG. + pub fn diffuse_constant(&self) -> f32 { + self.diffuse_constant + } + + /// A lighting color. + /// + /// `lighting-color` in the SVG. + pub fn lighting_color(&self) -> Color { + self.lighting_color + } + + /// A light source. + pub fn light_source(&self) -> LightSource { + self.light_source + } +} + +/// A specular lighting filter primitive. +/// +/// `feSpecularLighting` element in the SVG. +#[derive(Clone, Debug)] +pub struct SpecularLighting { + pub(crate) input: Input, + pub(crate) surface_scale: f32, + pub(crate) specular_constant: f32, + pub(crate) specular_exponent: f32, + pub(crate) lighting_color: Color, + pub(crate) light_source: LightSource, +} + +impl SpecularLighting { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A surface scale. + /// + /// `surfaceScale` in the SVG. + pub fn surface_scale(&self) -> f32 { + self.surface_scale + } + + /// A specular constant. + /// + /// `specularConstant` in the SVG. + pub fn specular_constant(&self) -> f32 { + self.specular_constant + } + + /// A specular exponent. + /// + /// Should be in 1..128 range. + /// + /// `specularExponent` in the SVG. + pub fn specular_exponent(&self) -> f32 { + self.specular_exponent + } + + /// A lighting color. + /// + /// `lighting-color` in the SVG. + pub fn lighting_color(&self) -> Color { + self.lighting_color + } + + /// A light source. + pub fn light_source(&self) -> LightSource { + self.light_source + } +} + +/// A light source kind. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug)] +pub enum LightSource { + DistantLight(DistantLight), + PointLight(PointLight), + SpotLight(SpotLight), +} + +/// A distant light source. +/// +/// `feDistantLight` element in the SVG. +#[derive(Clone, Copy, Debug)] +pub struct DistantLight { + /// Direction angle for the light source on the XY plane (clockwise), + /// in degrees from the x axis. + /// + /// `azimuth` in the SVG. + pub azimuth: f32, + + /// Direction angle for the light source from the XY plane towards the z axis, in degrees. + /// + /// `elevation` in the SVG. + pub elevation: f32, +} + +/// A point light source. +/// +/// `fePointLight` element in the SVG. +#[derive(Clone, Copy, Debug)] +pub struct PointLight { + /// X location for the light source. + /// + /// `x` in the SVG. + pub x: f32, + + /// Y location for the light source. + /// + /// `y` in the SVG. + pub y: f32, + + /// Z location for the light source. + /// + /// `z` in the SVG. + pub z: f32, +} + +/// A spot light source. +/// +/// `feSpotLight` element in the SVG. +#[derive(Clone, Copy, Debug)] +pub struct SpotLight { + /// X location for the light source. + /// + /// `x` in the SVG. + pub x: f32, + + /// Y location for the light source. + /// + /// `y` in the SVG. + pub y: f32, + + /// Z location for the light source. + /// + /// `z` in the SVG. + pub z: f32, + + /// X point at which the light source is pointing. + /// + /// `pointsAtX` in the SVG. + pub points_at_x: f32, + + /// Y point at which the light source is pointing. + /// + /// `pointsAtY` in the SVG. + pub points_at_y: f32, + + /// Z point at which the light source is pointing. + /// + /// `pointsAtZ` in the SVG. + pub points_at_z: f32, + + /// Exponent value controlling the focus for the light source. + /// + /// `specularExponent` in the SVG. + pub specular_exponent: PositiveF32, + + /// A limiting cone which restricts the region where the light is projected. + /// + /// `limitingConeAngle` in the SVG. + pub limiting_cone_angle: Option, +} + +/// A merge filter primitive. +/// +/// `feMerge` element in the SVG. +#[derive(Clone, Debug)] +pub struct Merge { + pub(crate) inputs: Vec, +} + +impl Merge { + /// List of input layers that should be merged. + /// + /// List of `feMergeNode`'s in the SVG. + pub fn inputs(&self) -> &[Input] { + &self.inputs + } +} + +/// A morphology filter primitive. +/// +/// `feMorphology` element in the SVG. +#[derive(Clone, Debug)] +pub struct Morphology { + pub(crate) input: Input, + pub(crate) operator: MorphologyOperator, + pub(crate) radius_x: PositiveF32, + pub(crate) radius_y: PositiveF32, +} + +impl Morphology { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// A filter operator. + /// + /// `operator` in the SVG. + pub fn operator(&self) -> MorphologyOperator { + self.operator + } + + /// A filter radius along the X-axis. + /// + /// A value of zero disables the effect of the given filter primitive. + /// + /// `radius` in the SVG. + pub fn radius_x(&self) -> PositiveF32 { + self.radius_x + } + + /// A filter radius along the Y-axis. + /// + /// A value of zero disables the effect of the given filter primitive. + /// + /// `radius` in the SVG. + pub fn radius_y(&self) -> PositiveF32 { + self.radius_y + } +} + +/// A morphology operation. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum MorphologyOperator { + Erode, + Dilate, +} + +/// An offset filter primitive. +/// +/// `feOffset` element in the SVG. +#[derive(Clone, Debug)] +pub struct Offset { + pub(crate) input: Input, + pub(crate) dx: f32, + pub(crate) dy: f32, +} + +impl Offset { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } + + /// The amount to offset the input graphic along the X-axis. + pub fn dx(&self) -> f32 { + self.dx + } + + /// The amount to offset the input graphic along the Y-axis. + pub fn dy(&self) -> f32 { + self.dy + } +} + +/// A tile filter primitive. +/// +/// `feTile` element in the SVG. +#[derive(Clone, Debug)] +pub struct Tile { + pub(crate) input: Input, +} + +impl Tile { + /// Identifies input for the given filter primitive. + /// + /// `in` in the SVG. + pub fn input(&self) -> &Input { + &self.input + } +} + +/// A turbulence generation filter primitive. +/// +/// `feTurbulence` element in the SVG. +#[derive(Clone, Copy, Debug)] +pub struct Turbulence { + pub(crate) base_frequency_x: PositiveF32, + pub(crate) base_frequency_y: PositiveF32, + pub(crate) num_octaves: u32, + pub(crate) seed: i32, + pub(crate) stitch_tiles: bool, + pub(crate) kind: TurbulenceKind, +} + +impl Turbulence { + /// Identifies the base frequency for the noise function. + /// + /// `baseFrequency` in the SVG. + pub fn base_frequency_x(&self) -> PositiveF32 { + self.base_frequency_x + } + + /// Identifies the base frequency for the noise function. + /// + /// `baseFrequency` in the SVG. + pub fn base_frequency_y(&self) -> PositiveF32 { + self.base_frequency_y + } + + /// Identifies the number of octaves for the noise function. + /// + /// `numOctaves` in the SVG. + pub fn num_octaves(&self) -> u32 { + self.num_octaves + } + + /// The starting number for the pseudo random number generator. + /// + /// `seed` in the SVG. + pub fn seed(&self) -> i32 { + self.seed + } + + /// Smooth transitions at the border of tiles. + /// + /// `stitchTiles` in the SVG. + pub fn stitch_tiles(&self) -> bool { + self.stitch_tiles + } + + /// Indicates whether the filter primitive should perform a noise or turbulence function. + /// + /// `type` in the SVG. + pub fn kind(&self) -> TurbulenceKind { + self.kind + } +} + +/// A turbulence kind for the `feTurbulence` filter. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum TurbulenceKind { + FractalNoise, + Turbulence, +} diff --git a/third_party/usvg/src/tree/geom.rs b/third_party/usvg/src/tree/geom.rs new file mode 100644 index 0000000000..32ad8e7931 --- /dev/null +++ b/third_party/usvg/src/tree/geom.rs @@ -0,0 +1,193 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use strict_num::ApproxEqUlps; +use svgtypes::{Align, AspectRatio}; +pub use tiny_skia_path::{NonZeroRect, Rect, Size, Transform}; + +/// Approximate zero equality comparisons. +pub trait ApproxZeroUlps: ApproxEqUlps { + /// Checks if the number is approximately zero. + fn approx_zero_ulps(&self, ulps: ::U) -> bool; +} + +impl ApproxZeroUlps for f32 { + fn approx_zero_ulps(&self, ulps: i32) -> bool { + self.approx_eq_ulps(&0.0, ulps) + } +} + +impl ApproxZeroUlps for f64 { + fn approx_zero_ulps(&self, ulps: i64) -> bool { + self.approx_eq_ulps(&0.0, ulps) + } +} + +/// Checks that the current number is > 0. +pub(crate) trait IsValidLength { + /// Checks that the current number is > 0. + fn is_valid_length(&self) -> bool; +} + +impl IsValidLength for f32 { + #[inline] + fn is_valid_length(&self) -> bool { + *self > 0.0 && self.is_finite() + } +} + +impl IsValidLength for f64 { + #[inline] + fn is_valid_length(&self) -> bool { + *self > 0.0 && self.is_finite() + } +} + +/// View box. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ViewBox { + /// Value of the `viewBox` attribute. + pub rect: NonZeroRect, + + /// Value of the `preserveAspectRatio` attribute. + pub aspect: AspectRatio, +} + +impl ViewBox { + /// Converts `viewBox` into `Transform`. + pub fn to_transform(&self, img_size: Size) -> Transform { + let vr = self.rect; + + let sx = img_size.width() / vr.width(); + let sy = img_size.height() / vr.height(); + + let (sx, sy) = if self.aspect.align == Align::None { + (sx, sy) + } else { + let s = if self.aspect.slice { + if sx < sy { + sy + } else { + sx + } + } else { + if sx > sy { + sy + } else { + sx + } + }; + + (s, s) + }; + + let x = -vr.x() * sx; + let y = -vr.y() * sy; + let w = img_size.width() - vr.width() * sx; + let h = img_size.height() - vr.height() * sy; + + let (tx, ty) = aligned_pos(self.aspect.align, x, y, w, h); + Transform::from_row(sx, 0.0, 0.0, sy, tx, ty) + } +} + +/// A bounding box calculator. +#[derive(Clone, Copy, Debug)] +pub(crate) struct BBox { + left: f32, + top: f32, + right: f32, + bottom: f32, +} + +impl From for BBox { + fn from(r: Rect) -> Self { + Self { + left: r.left(), + top: r.top(), + right: r.right(), + bottom: r.bottom(), + } + } +} + +impl From for BBox { + fn from(r: NonZeroRect) -> Self { + Self { + left: r.left(), + top: r.top(), + right: r.right(), + bottom: r.bottom(), + } + } +} + +impl Default for BBox { + fn default() -> Self { + Self { + left: f32::MAX, + top: f32::MAX, + right: f32::MIN, + bottom: f32::MIN, + } + } +} + +impl BBox { + /// Checks if the bounding box is default, i.e. invalid. + pub fn is_default(&self) -> bool { + self.left == f32::MAX + && self.top == f32::MAX + && self.right == f32::MIN + && self.bottom == f32::MIN + } + + /// Expand the bounding box to the specified bounds. + #[must_use] + pub fn expand(&self, r: impl Into) -> Self { + self.expand_impl(r.into()) + } + + fn expand_impl(&self, r: Self) -> Self { + Self { + left: self.left.min(r.left), + top: self.top.min(r.top), + right: self.right.max(r.right), + bottom: self.bottom.max(r.bottom), + } + } + + /// Converts a bounding box into [`Rect`]. + pub fn to_rect(&self) -> Option { + if !self.is_default() { + Rect::from_ltrb(self.left, self.top, self.right, self.bottom) + } else { + None + } + } + + /// Converts a bounding box into [`NonZeroRect`]. + pub fn to_non_zero_rect(&self) -> Option { + if !self.is_default() { + NonZeroRect::from_ltrb(self.left, self.top, self.right, self.bottom) + } else { + None + } + } +} + +/// Returns object aligned position. +pub(crate) fn aligned_pos(align: Align, x: f32, y: f32, w: f32, h: f32) -> (f32, f32) { + match align { + Align::None => (x, y), + Align::XMinYMin => (x, y), + Align::XMidYMin => (x + w / 2.0, y), + Align::XMaxYMin => (x + w, y), + Align::XMinYMid => (x, y + h / 2.0), + Align::XMidYMid => (x + w / 2.0, y + h / 2.0), + Align::XMaxYMid => (x + w, y + h / 2.0), + Align::XMinYMax => (x, y + h), + Align::XMidYMax => (x + w / 2.0, y + h), + Align::XMaxYMax => (x + w, y + h), + } +} diff --git a/third_party/usvg/src/tree/mod.rs b/third_party/usvg/src/tree/mod.rs new file mode 100644 index 0000000000..e3486663dd --- /dev/null +++ b/third_party/usvg/src/tree/mod.rs @@ -0,0 +1,1875 @@ +// Copyright 2019 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +pub mod filter; +mod geom; +mod text; + +use std::sync::Arc; + +pub use strict_num::{self, ApproxEqUlps, NonZeroPositiveF32, NormalizedF32, PositiveF32}; + +pub use tiny_skia_path; + +pub use self::geom::*; +pub use self::text::*; + +use crate::OptionLog; + +/// An alias to `NormalizedF32`. +pub type Opacity = NormalizedF32; + +// Must not be clone-able to preserve ID uniqueness. +#[derive(Debug)] +pub(crate) struct NonEmptyString(String); + +impl NonEmptyString { + pub(crate) fn new(string: String) -> Option { + if string.trim().is_empty() { + return None; + } + + Some(NonEmptyString(string)) + } + + pub(crate) fn get(&self) -> &str { + &self.0 + } + + pub(crate) fn take(self) -> String { + self.0 + } +} + +/// A non-zero `f32`. +/// +/// Just like `f32` but immutable and guarantee to never be zero. +#[derive(Clone, Copy, Debug)] +pub struct NonZeroF32(f32); + +impl NonZeroF32 { + /// Creates a new `NonZeroF32` value. + #[inline] + pub fn new(n: f32) -> Option { + if n.approx_eq_ulps(&0.0, 4) { + None + } else { + Some(NonZeroF32(n)) + } + } + + /// Returns an underlying value. + #[inline] + pub fn get(&self) -> f32 { + self.0 + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Units { + UserSpaceOnUse, + ObjectBoundingBox, +} + +// `Units` cannot have a default value, because it changes depending on an element. + +/// A visibility property. +/// +/// `visibility` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Visibility { + Visible, + Hidden, + Collapse, +} + +impl Default for Visibility { + fn default() -> Self { + Self::Visible + } +} + +/// A shape rendering method. +/// +/// `shape-rendering` attribute in the SVG. +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub enum ShapeRendering { + OptimizeSpeed, + CrispEdges, + GeometricPrecision, +} + +impl ShapeRendering { + /// Checks if anti-aliasing should be enabled. + pub fn use_shape_antialiasing(self) -> bool { + match self { + ShapeRendering::OptimizeSpeed => false, + ShapeRendering::CrispEdges => false, + ShapeRendering::GeometricPrecision => true, + } + } +} + +impl Default for ShapeRendering { + fn default() -> Self { + Self::GeometricPrecision + } +} + +impl std::str::FromStr for ShapeRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeSpeed" => Ok(ShapeRendering::OptimizeSpeed), + "crispEdges" => Ok(ShapeRendering::CrispEdges), + "geometricPrecision" => Ok(ShapeRendering::GeometricPrecision), + _ => Err("invalid"), + } + } +} + +/// A text rendering method. +/// +/// `text-rendering` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum TextRendering { + OptimizeSpeed, + OptimizeLegibility, + GeometricPrecision, +} + +impl Default for TextRendering { + fn default() -> Self { + Self::OptimizeLegibility + } +} + +impl std::str::FromStr for TextRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeSpeed" => Ok(TextRendering::OptimizeSpeed), + "optimizeLegibility" => Ok(TextRendering::OptimizeLegibility), + "geometricPrecision" => Ok(TextRendering::GeometricPrecision), + _ => Err("invalid"), + } + } +} + +/// An image rendering method. +/// +/// `image-rendering` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum ImageRendering { + OptimizeQuality, + OptimizeSpeed, + // The following can only appear as presentation attributes. + Smooth, + HighQuality, + CrispEdges, + Pixelated, +} + +impl Default for ImageRendering { + fn default() -> Self { + Self::OptimizeQuality + } +} + +impl std::str::FromStr for ImageRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeQuality" => Ok(ImageRendering::OptimizeQuality), + "optimizeSpeed" => Ok(ImageRendering::OptimizeSpeed), + "smooth" => Ok(ImageRendering::Smooth), + "high-quality" => Ok(ImageRendering::HighQuality), + "crisp-edges" => Ok(ImageRendering::CrispEdges), + "pixelated" => Ok(ImageRendering::Pixelated), + _ => Err("invalid"), + } + } +} + +/// A blending mode property. +/// +/// `mix-blend-mode` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +} + +impl Default for BlendMode { + fn default() -> Self { + Self::Normal + } +} + +/// A spread method. +/// +/// `spreadMethod` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum SpreadMethod { + Pad, + Reflect, + Repeat, +} + +impl Default for SpreadMethod { + fn default() -> Self { + Self::Pad + } +} + +/// A generic gradient. +#[derive(Debug)] +pub struct BaseGradient { + pub(crate) id: NonEmptyString, + pub(crate) units: Units, // used only during parsing + pub(crate) transform: Transform, + pub(crate) spread_method: SpreadMethod, + pub(crate) stops: Vec, +} + +impl BaseGradient { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Gradient transform. + /// + /// `gradientTransform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Gradient spreading method. + /// + /// `spreadMethod` in SVG. + pub fn spread_method(&self) -> SpreadMethod { + self.spread_method + } + + /// A list of `stop` elements. + pub fn stops(&self) -> &[Stop] { + &self.stops + } +} + +/// A linear gradient. +/// +/// `linearGradient` element in SVG. +#[derive(Debug)] +pub struct LinearGradient { + pub(crate) base: BaseGradient, + pub(crate) x1: f32, + pub(crate) y1: f32, + pub(crate) x2: f32, + pub(crate) y2: f32, +} + +impl LinearGradient { + /// `x1` coordinate. + pub fn x1(&self) -> f32 { + self.x1 + } + + /// `y1` coordinate. + pub fn y1(&self) -> f32 { + self.y1 + } + + /// `x2` coordinate. + pub fn x2(&self) -> f32 { + self.x2 + } + + /// `y2` coordinate. + pub fn y2(&self) -> f32 { + self.y2 + } +} + +impl std::ops::Deref for LinearGradient { + type Target = BaseGradient; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +/// A radial gradient. +/// +/// `radialGradient` element in SVG. +#[derive(Debug)] +pub struct RadialGradient { + pub(crate) base: BaseGradient, + pub(crate) cx: f32, + pub(crate) cy: f32, + pub(crate) r: PositiveF32, + pub(crate) fx: f32, + pub(crate) fy: f32, +} + +impl RadialGradient { + /// `cx` coordinate. + pub fn cx(&self) -> f32 { + self.cx + } + + /// `cy` coordinate. + pub fn cy(&self) -> f32 { + self.cy + } + + /// Gradient radius. + pub fn r(&self) -> PositiveF32 { + self.r + } + + /// `fx` coordinate. + pub fn fx(&self) -> f32 { + self.fx + } + + /// `fy` coordinate. + pub fn fy(&self) -> f32 { + self.fy + } +} + +impl std::ops::Deref for RadialGradient { + type Target = BaseGradient; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +/// An alias to `NormalizedF32`. +pub type StopOffset = NormalizedF32; + +/// Gradient's stop element. +/// +/// `stop` element in SVG. +#[derive(Clone, Copy, Debug)] +pub struct Stop { + pub(crate) offset: StopOffset, + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl Stop { + /// Gradient stop offset. + /// + /// `offset` in SVG. + pub fn offset(&self) -> StopOffset { + self.offset + } + + /// Gradient stop color. + /// + /// `stop-color` in SVG. + pub fn color(&self) -> Color { + self.color + } + + /// Gradient stop opacity. + /// + /// `stop-opacity` in SVG. + pub fn opacity(&self) -> Opacity { + self.opacity + } +} + +/// A pattern element. +/// +/// `pattern` element in SVG. +#[derive(Debug)] +pub struct Pattern { + pub(crate) id: NonEmptyString, + pub(crate) units: Units, // used only during parsing + pub(crate) content_units: Units, // used only during parsing + pub(crate) transform: Transform, + pub(crate) rect: NonZeroRect, + pub(crate) view_box: Option, + pub(crate) root: Group, +} + +impl Pattern { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Pattern transform. + /// + /// `patternTransform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Pattern rectangle. + /// + /// `x`, `y`, `width` and `height` in SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// Pattern children. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// An alias to `NonZeroPositiveF32`. +pub type StrokeWidth = NonZeroPositiveF32; + +/// A `stroke-miterlimit` value. +/// +/// Just like `f32` but immutable and guarantee to be >=1.0. +#[derive(Clone, Copy, Debug)] +pub struct StrokeMiterlimit(f32); + +impl StrokeMiterlimit { + /// Creates a new `StrokeMiterlimit` value. + #[inline] + pub fn new(n: f32) -> Self { + debug_assert!(n.is_finite()); + debug_assert!(n >= 1.0); + + let n = if !(n >= 1.0) { 1.0 } else { n }; + + StrokeMiterlimit(n) + } + + /// Returns an underlying value. + #[inline] + pub fn get(&self) -> f32 { + self.0 + } +} + +impl Default for StrokeMiterlimit { + #[inline] + fn default() -> Self { + StrokeMiterlimit::new(4.0) + } +} + +impl From for StrokeMiterlimit { + #[inline] + fn from(n: f32) -> Self { + Self::new(n) + } +} + +impl PartialEq for StrokeMiterlimit { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.approx_eq_ulps(&other.0, 4) + } +} + +/// A line cap. +/// +/// `stroke-linecap` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Default for LineCap { + fn default() -> Self { + Self::Butt + } +} + +/// A line join. +/// +/// `stroke-linejoin` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LineJoin { + Miter, + MiterClip, + Round, + Bevel, +} + +impl Default for LineJoin { + fn default() -> Self { + Self::Miter + } +} + +/// A stroke style. +#[derive(Clone, Debug)] +pub struct Stroke { + pub(crate) paint: Paint, + pub(crate) dasharray: Option>, + pub(crate) dashoffset: f32, + pub(crate) miterlimit: StrokeMiterlimit, + pub(crate) opacity: Opacity, + pub(crate) width: StrokeWidth, + pub(crate) linecap: LineCap, + pub(crate) linejoin: LineJoin, + // Whether the current stroke needs to be resolved relative + // to a context element. + pub(crate) context_element: Option, +} + +impl Stroke { + /// Stroke paint. + pub fn paint(&self) -> &Paint { + &self.paint + } + + /// Stroke dash array. + pub fn dasharray(&self) -> Option<&[f32]> { + self.dasharray.as_deref() + } + + /// Stroke dash offset. + pub fn dashoffset(&self) -> f32 { + self.dashoffset + } + + /// Stroke miter limit. + pub fn miterlimit(&self) -> StrokeMiterlimit { + self.miterlimit + } + + /// Stroke opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Stroke width. + pub fn width(&self) -> StrokeWidth { + self.width + } + + /// Stroke linecap. + pub fn linecap(&self) -> LineCap { + self.linecap + } + + /// Stroke linejoin. + pub fn linejoin(&self) -> LineJoin { + self.linejoin + } + + /// Converts into a `tiny_skia_path::Stroke` type. + pub fn to_tiny_skia(&self) -> tiny_skia_path::Stroke { + let mut stroke = tiny_skia_path::Stroke { + width: self.width.get(), + miter_limit: self.miterlimit.get(), + line_cap: match self.linecap { + LineCap::Butt => tiny_skia_path::LineCap::Butt, + LineCap::Round => tiny_skia_path::LineCap::Round, + LineCap::Square => tiny_skia_path::LineCap::Square, + }, + line_join: match self.linejoin { + LineJoin::Miter => tiny_skia_path::LineJoin::Miter, + LineJoin::MiterClip => tiny_skia_path::LineJoin::MiterClip, + LineJoin::Round => tiny_skia_path::LineJoin::Round, + LineJoin::Bevel => tiny_skia_path::LineJoin::Bevel, + }, + // According to the spec, dash should not be accounted during + // bbox calculation. + dash: None, + }; + + if let Some(ref list) = self.dasharray { + stroke.dash = tiny_skia_path::StrokeDash::new(list.clone(), self.dashoffset); + } + + stroke + } +} + +/// A fill rule. +/// +/// `fill-rule` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum FillRule { + NonZero, + EvenOdd, +} + +impl Default for FillRule { + fn default() -> Self { + Self::NonZero + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum ContextElement { + /// The current context element is a use node. Since we can get + /// the bounding box of a use node only once we have converted + /// all elements, we need to fix the transform and units of + /// the stroke/fill after converting the whole tree. + UseNode, + /// The current context element is a path node (i.e. only applicable + /// if we draw the marker of a path). Since we already know the bounding + /// box of the path when rendering the markers, we can convert them directly, + /// so we do it while parsing. + PathNode(Transform, Option), +} + +/// A fill style. +#[derive(Clone, Debug)] +pub struct Fill { + pub(crate) paint: Paint, + pub(crate) opacity: Opacity, + pub(crate) rule: FillRule, + // Whether the current fill needs to be resolved relative + // to a context element. + pub(crate) context_element: Option, +} + +impl Fill { + /// Fill paint. + pub fn paint(&self) -> &Paint { + &self.paint + } + + /// Fill opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Fill rule. + pub fn rule(&self) -> FillRule { + self.rule + } +} + +impl Default for Fill { + fn default() -> Self { + Fill { + paint: Paint::Color(Color::black()), + opacity: Opacity::ONE, + rule: FillRule::default(), + context_element: None, + } + } +} + +/// A 8-bit RGB color. +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, +} + +impl Color { + /// Constructs a new `Color` from RGB values. + #[inline] + pub fn new_rgb(red: u8, green: u8, blue: u8) -> Color { + Color { red, green, blue } + } + + /// Constructs a new `Color` set to black. + #[inline] + pub fn black() -> Color { + Color::new_rgb(0, 0, 0) + } + + /// Constructs a new `Color` set to white. + #[inline] + pub fn white() -> Color { + Color::new_rgb(255, 255, 255) + } +} + +/// A paint style. +/// +/// `paint` value type in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum Paint { + Color(Color), + LinearGradient(Arc), + RadialGradient(Arc), + Pattern(Arc), +} + +impl PartialEq for Paint { + #[inline] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Color(lc), Self::Color(rc)) => lc == rc, + (Self::LinearGradient(ref lg1), Self::LinearGradient(ref lg2)) => Arc::ptr_eq(lg1, lg2), + (Self::RadialGradient(ref rg1), Self::RadialGradient(ref rg2)) => Arc::ptr_eq(rg1, rg2), + (Self::Pattern(ref p1), Self::Pattern(ref p2)) => Arc::ptr_eq(p1, p2), + _ => false, + } + } +} + +/// A clip-path element. +/// +/// `clipPath` element in SVG. +#[derive(Debug)] +pub struct ClipPath { + pub(crate) id: NonEmptyString, + pub(crate) transform: Transform, + pub(crate) clip_path: Option>, + pub(crate) root: Group, +} + +impl ClipPath { + pub(crate) fn empty(id: NonEmptyString) -> Self { + ClipPath { + id, + transform: Transform::default(), + clip_path: None, + root: Group::empty(), + } + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Clip path transform. + /// + /// `transform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Additional clip path. + /// + /// `clip-path` in SVG. + pub fn clip_path(&self) -> Option<&ClipPath> { + self.clip_path.as_deref() + } + + /// Clip path children. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// A mask type. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum MaskType { + /// Indicates that the luminance values of the mask should be used. + Luminance, + /// Indicates that the alpha values of the mask should be used. + Alpha, +} + +impl Default for MaskType { + fn default() -> Self { + Self::Luminance + } +} + +/// A mask element. +/// +/// `mask` element in SVG. +#[derive(Debug)] +pub struct Mask { + pub(crate) id: NonEmptyString, + pub(crate) rect: NonZeroRect, + pub(crate) kind: MaskType, + pub(crate) mask: Option>, + pub(crate) root: Group, +} + +impl Mask { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Mask rectangle. + /// + /// `x`, `y`, `width` and `height` in SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// Mask type. + /// + /// `mask-type` in SVG. + pub fn kind(&self) -> MaskType { + self.kind + } + + /// Additional mask. + /// + /// `mask` in SVG. + pub fn mask(&self) -> Option<&Mask> { + self.mask.as_deref() + } + + /// Mask children. + /// + /// A mask can have no children, in which case the whole element should be masked out. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// Node's kind. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum Node { + Group(Box), + Path(Box), + Image(Box), + Text(Box), +} + +impl Node { + /// Returns node's ID. + pub fn id(&self) -> &str { + match self { + Node::Group(ref e) => e.id.as_str(), + Node::Path(ref e) => e.id.as_str(), + Node::Image(ref e) => e.id.as_str(), + Node::Text(ref e) => e.id.as_str(), + } + } + + /// Returns node's absolute transform. + /// + /// This method is cheap since absolute transforms are already resolved. + pub fn abs_transform(&self) -> Transform { + match self { + Node::Group(ref group) => group.abs_transform(), + Node::Path(ref path) => path.abs_transform(), + Node::Image(ref image) => image.abs_transform(), + Node::Text(ref text) => text.abs_transform(), + } + } + + /// Returns node's bounding box in object coordinates, if any. + pub fn bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.bounding_box(), + Node::Path(ref path) => path.bounding_box(), + Node::Image(ref image) => image.bounding_box(), + Node::Text(ref text) => text.bounding_box(), + } + } + + /// Returns node's bounding box in canvas coordinates, if any. + pub fn abs_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.abs_bounding_box(), + Node::Path(ref path) => path.abs_bounding_box(), + Node::Image(ref image) => image.abs_bounding_box(), + Node::Text(ref text) => text.abs_bounding_box(), + } + } + + /// Returns node's bounding box, including stroke, in object coordinates, if any. + pub fn stroke_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.stroke_bounding_box(), + Node::Path(ref path) => path.stroke_bounding_box(), + // Image cannot be stroked. + Node::Image(ref image) => image.bounding_box(), + Node::Text(ref text) => text.stroke_bounding_box(), + } + } + + /// Returns node's bounding box, including stroke, in canvas coordinates, if any. + pub fn abs_stroke_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.abs_stroke_bounding_box(), + Node::Path(ref path) => path.abs_stroke_bounding_box(), + // Image cannot be stroked. + Node::Image(ref image) => image.abs_bounding_box(), + Node::Text(ref text) => text.abs_stroke_bounding_box(), + } + } + + /// Element's "layer" bounding box in canvas units, if any. + /// + /// For most nodes this is just `abs_bounding_box`, + /// but for groups this is `abs_layer_bounding_box`. + /// + /// See [`Group::layer_bounding_box`] for details. + pub fn abs_layer_bounding_box(&self) -> Option { + match self { + Node::Group(ref group) => Some(group.abs_layer_bounding_box()), + // Hor/ver path without stroke can return None. This is expected. + Node::Path(ref path) => path.abs_bounding_box().to_non_zero_rect(), + Node::Image(ref image) => image.abs_bounding_box().to_non_zero_rect(), + Node::Text(ref text) => text.abs_bounding_box().to_non_zero_rect(), + } + } + + /// Calls a closure for each subroot this `Node` has. + /// + /// The [`Tree::root`](Tree::root) field contain only render-able SVG elements. + /// But some elements, specifically clip paths, masks, patterns and feImage + /// can store their own SVG subtrees. + /// And while one can access them manually, it's pretty verbose. + /// This methods allows looping over _all_ SVG elements present in the `Tree`. + /// + /// # Example + /// + /// ```no_run + /// fn all_nodes(parent: &usvg::Group) { + /// for node in parent.children() { + /// // do stuff... + /// + /// if let usvg::Node::Group(ref g) = node { + /// all_nodes(g); + /// } + /// + /// // handle subroots as well + /// node.subroots(|subroot| all_nodes(subroot)); + /// } + /// } + /// ``` + pub fn subroots(&self, mut f: F) { + match self { + Node::Group(ref group) => group.subroots(&mut f), + Node::Path(ref path) => path.subroots(&mut f), + Node::Image(ref image) => image.subroots(&mut f), + Node::Text(ref text) => text.subroots(&mut f), + } + } +} + +/// A group container. +/// +/// The preprocessor will remove all groups that don't impact rendering. +/// Those that left is just an indicator that a new canvas should be created. +/// +/// `g` element in SVG. +#[derive(Clone, Debug)] +pub struct Group { + pub(crate) id: String, + pub(crate) transform: Transform, + pub(crate) abs_transform: Transform, + pub(crate) opacity: Opacity, + pub(crate) blend_mode: BlendMode, + pub(crate) isolate: bool, + pub(crate) clip_path: Option>, + /// Whether the group is a context element (i.e. a use node) + pub(crate) is_context_element: bool, + pub(crate) mask: Option>, + pub(crate) filters: Vec>, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, + pub(crate) layer_bounding_box: NonZeroRect, + pub(crate) abs_layer_bounding_box: NonZeroRect, + pub(crate) children: Vec, +} + +impl Group { + pub(crate) fn empty() -> Self { + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + Group { + id: String::new(), + transform: Transform::default(), + abs_transform: Transform::default(), + opacity: Opacity::ONE, + blend_mode: BlendMode::Normal, + isolate: false, + clip_path: None, + mask: None, + filters: Vec::new(), + is_context_element: false, + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + abs_layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + children: Vec::new(), + } + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element's transform. + /// + /// This is a relative transform. The one that is set via the `transform` attribute in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including group's transform. + /// + /// Note that subroots, like clipPaths, masks and patterns, have their own root transform, + /// which isn't affected by the node that references this subroot. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Group opacity. + /// + /// After the group is rendered we should combine + /// it with a parent group using the specified opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Group blend mode. + /// + /// `mix-blend-mode` in SVG. + pub fn blend_mode(&self) -> BlendMode { + self.blend_mode + } + + /// Group isolation. + /// + /// `isolation` in SVG. + pub fn isolate(&self) -> bool { + self.isolate + } + + /// Element's clip path. + pub fn clip_path(&self) -> Option<&ClipPath> { + self.clip_path.as_deref() + } + + /// Element's mask. + pub fn mask(&self) -> Option<&Mask> { + self.mask.as_deref() + } + + /// Element's filters. + pub fn filters(&self) -> &[Arc] { + &self.filters + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + /// + /// Can be set to `None` in case of an empty group. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Similar to `bounding_box`, but includes stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in user coordinates. + /// + /// Similar to `abs_bounding_box`, but includes stroke. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + /// Element's "layer" bounding box in object units. + /// + /// Conceptually, this is `stroke_bounding_box` expanded and/or clipped + /// by `filters_bounding_box`, but also including all the children. + /// This is the bounding box `resvg` will later use to allocate layers/pixmaps + /// during isolated groups rendering. + /// + /// Only groups have it, because only groups can have filters. + /// For other nodes layer bounding box is the same as stroke bounding box. + /// + /// Unlike other bounding boxes, cannot have zero size. + /// + /// Returns 0x0x1x1 for empty groups. + pub fn layer_bounding_box(&self) -> NonZeroRect { + self.layer_bounding_box + } + + /// Element's "layer" bounding box in canvas units. + pub fn abs_layer_bounding_box(&self) -> NonZeroRect { + self.abs_layer_bounding_box + } + + /// Group's children. + pub fn children(&self) -> &[Node] { + &self.children + } + + /// Checks if this group should be isolated during rendering. + pub fn should_isolate(&self) -> bool { + self.isolate + || self.opacity != Opacity::ONE + || self.clip_path.is_some() + || self.mask.is_some() + || !self.filters.is_empty() + || self.blend_mode != BlendMode::Normal // TODO: probably not needed? + } + + /// Returns `true` if the group has any children. + pub fn has_children(&self) -> bool { + !self.children.is_empty() + } + + /// Calculates a node's filter bounding box. + /// + /// Filters with `objectBoundingBox` and missing or zero `bounding_box` would be ignored. + /// + /// Note that a filter region can act like a clipping rectangle, + /// therefore this function can produce a bounding box smaller than `bounding_box`. + /// + /// Returns `None` when then group has no filters. + /// + /// This function is very fast, that's why we do not store this bbox as a `Group` field. + pub fn filters_bounding_box(&self) -> Option { + let mut full_region = BBox::default(); + for filter in &self.filters { + full_region = full_region.expand(filter.rect); + } + + full_region.to_non_zero_rect() + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let Some(ref clip) = self.clip_path { + f(&clip.root); + + if let Some(ref sub_clip) = clip.clip_path { + f(&sub_clip.root); + } + } + + if let Some(ref mask) = self.mask { + f(&mask.root); + + if let Some(ref sub_mask) = mask.mask { + f(&sub_mask.root); + } + } + + for filter in &self.filters { + for primitive in &filter.primitives { + if let filter::Kind::Image(ref image) = primitive.kind { + f(image.root()); + } + } + } + } +} + +/// Representation of the [`paint-order`] property. +/// +/// `usvg` will handle `markers` automatically, +/// therefore we provide only `fill` and `stroke` variants. +/// +/// [`paint-order`]: https://www.w3.org/TR/SVG2/painting.html#PaintOrder +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub enum PaintOrder { + FillAndStroke, + StrokeAndFill, +} + +impl Default for PaintOrder { + fn default() -> Self { + Self::FillAndStroke + } +} + +/// A path element. +#[derive(Clone, Debug)] +pub struct Path { + pub(crate) id: String, + pub(crate) visible: bool, + pub(crate) fill: Option, + pub(crate) stroke: Option, + pub(crate) paint_order: PaintOrder, + pub(crate) rendering_mode: ShapeRendering, + pub(crate) data: Arc, + pub(crate) abs_transform: Transform, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, +} + +impl Path { + pub(crate) fn new_simple(data: Arc) -> Option { + Self::new( + String::new(), + true, + None, + None, + PaintOrder::default(), + ShapeRendering::default(), + data, + Transform::default(), + ) + } + + pub(crate) fn new( + id: String, + visible: bool, + fill: Option, + stroke: Option, + paint_order: PaintOrder, + rendering_mode: ShapeRendering, + data: Arc, + abs_transform: Transform, + ) -> Option { + let bounding_box = data.compute_tight_bounds()?; + let stroke_bounding_box = + Path::calculate_stroke_bbox(stroke.as_ref(), &data).unwrap_or(bounding_box); + + let abs_bounding_box: Rect; + let abs_stroke_bounding_box: Rect; + if abs_transform.has_skew() { + // TODO: avoid re-alloc + let path2 = data.as_ref().clone(); + let path2 = path2.transform(abs_transform)?; + abs_bounding_box = path2.compute_tight_bounds()?; + abs_stroke_bounding_box = + Path::calculate_stroke_bbox(stroke.as_ref(), &path2).unwrap_or(abs_bounding_box); + } else { + // A transform without a skew can be performed just on a bbox. + abs_bounding_box = bounding_box.transform(abs_transform)?; + abs_stroke_bounding_box = stroke_bounding_box.transform(abs_transform)?; + } + + Some(Path { + id, + visible, + fill, + stroke, + paint_order, + rendering_mode, + data, + abs_transform, + bounding_box, + abs_bounding_box, + stroke_bounding_box, + abs_stroke_bounding_box, + }) + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element visibility. + pub fn is_visible(&self) -> bool { + self.visible + } + + /// Fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// Stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } + + /// Fill and stroke paint order. + /// + /// Since markers will be replaced with regular nodes automatically, + /// `usvg` doesn't provide the `markers` order type. It's was already done. + /// + /// `paint-order` in SVG. + pub fn paint_order(&self) -> PaintOrder { + self.paint_order + } + + /// Rendering mode. + /// + /// `shape-rendering` in SVG. + pub fn rendering_mode(&self) -> ShapeRendering { + self.rendering_mode + } + + // TODO: find a better name + /// Segments list. + /// + /// All segments are in absolute coordinates. + pub fn data(&self) -> &tiny_skia_path::Path { + self.data.as_ref() + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Will have the same value as `bounding_box` when path has no stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in canvas coordinates. + /// + /// Will have the same value as `abs_bounding_box` when path has no stroke. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + fn calculate_stroke_bbox(stroke: Option<&Stroke>, path: &tiny_skia_path::Path) -> Option { + let mut stroke = stroke?.to_tiny_skia(); + // According to the spec, dash should not be accounted during bbox calculation. + stroke.dash = None; + + // TODO: avoid for round and bevel caps + + // Expensive, but there is not much we can do about it. + if let Some(stroked_path) = path.stroke(&stroke, 1.0) { + return stroked_path.compute_tight_bounds(); + } + + None + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let Some(Paint::Pattern(ref patt)) = self.fill.as_ref().map(|f| &f.paint) { + f(patt.root()); + } + if let Some(Paint::Pattern(ref patt)) = self.stroke.as_ref().map(|f| &f.paint) { + f(patt.root()); + } + } +} + +/// An embedded image kind. +#[derive(Clone)] +pub enum ImageKind { + /// A reference to raw JPEG data. Should be decoded by the caller. + JPEG(Arc>), + /// A reference to raw PNG data. Should be decoded by the caller. + PNG(Arc>), + /// A reference to raw GIF data. Should be decoded by the caller. + GIF(Arc>), + /// A reference to raw WebP data. Should be decoded by the caller. + WEBP(Arc>), + /// A preprocessed SVG tree. Can be rendered as is. + SVG(Tree), +} + +impl ImageKind { + pub(crate) fn actual_size(&self) -> Option { + match self { + ImageKind::JPEG(ref data) + | ImageKind::PNG(ref data) + | ImageKind::GIF(ref data) + | ImageKind::WEBP(ref data) => imagesize::blob_size(data) + .ok() + .and_then(|size| Size::from_wh(size.width as f32, size.height as f32)) + .log_none(|| log::warn!("Image has an invalid size. Skipped.")), + ImageKind::SVG(ref svg) => Some(svg.size), + } + } +} + +impl std::fmt::Debug for ImageKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ImageKind::JPEG(_) => f.write_str("ImageKind::JPEG(..)"), + ImageKind::PNG(_) => f.write_str("ImageKind::PNG(..)"), + ImageKind::GIF(_) => f.write_str("ImageKind::GIF(..)"), + ImageKind::WEBP(_) => f.write_str("ImageKind::WEBP(..)"), + ImageKind::SVG(_) => f.write_str("ImageKind::SVG(..)"), + } + } +} + +/// A raster image element. +/// +/// `image` element in SVG. +#[derive(Clone, Debug)] +pub struct Image { + pub(crate) id: String, + pub(crate) visible: bool, + pub(crate) size: Size, + pub(crate) rendering_mode: ImageRendering, + pub(crate) kind: ImageKind, + pub(crate) abs_transform: Transform, + pub(crate) abs_bounding_box: NonZeroRect, +} + +impl Image { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element visibility. + pub fn is_visible(&self) -> bool { + self.visible + } + + /// The actual image size. + /// + /// This is not `width` and `height` attributes, + /// but rather the actual PNG/JPEG/GIF/SVG image size. + pub fn size(&self) -> Size { + self.size + } + + /// Rendering mode. + /// + /// `image-rendering` in SVG. + pub fn rendering_mode(&self) -> ImageRendering { + self.rendering_mode + } + + /// Image data. + pub fn kind(&self) -> &ImageKind { + &self.kind + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + pub fn bounding_box(&self) -> Rect { + self.size.to_rect(0.0, 0.0).unwrap() + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box.to_rect() + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let ImageKind::SVG(ref tree) = self.kind { + f(&tree.root); + } + } +} + +/// A nodes tree container. +#[allow(missing_debug_implementations)] +#[derive(Clone, Debug)] +pub struct Tree { + pub(crate) size: Size, + pub(crate) root: Group, + pub(crate) linear_gradients: Vec>, + pub(crate) radial_gradients: Vec>, + pub(crate) patterns: Vec>, + pub(crate) clip_paths: Vec>, + pub(crate) masks: Vec>, + pub(crate) filters: Vec>, + #[cfg(feature = "text")] + pub(crate) fontdb: Arc, +} + +impl Tree { + /// Image size. + /// + /// Size of an image that should be created to fit the SVG. + /// + /// `width` and `height` in SVG. + pub fn size(&self) -> Size { + self.size + } + + /// The root element of the SVG tree. + pub fn root(&self) -> &Group { + &self.root + } + + /// Returns a renderable node by ID. + /// + /// If an empty ID is provided, than this method will always return `None`. + pub fn node_by_id(&self, id: &str) -> Option<&Node> { + if id.is_empty() { + return None; + } + + node_by_id(&self.root, id) + } + + /// Checks if the current tree has any text nodes. + pub fn has_text_nodes(&self) -> bool { + has_text_nodes(&self.root) + } + + /// Returns a list of all unique [`LinearGradient`]s in the tree. + pub fn linear_gradients(&self) -> &[Arc] { + &self.linear_gradients + } + + /// Returns a list of all unique [`RadialGradient`]s in the tree. + pub fn radial_gradients(&self) -> &[Arc] { + &self.radial_gradients + } + + /// Returns a list of all unique [`Pattern`]s in the tree. + pub fn patterns(&self) -> &[Arc] { + &self.patterns + } + + /// Returns a list of all unique [`ClipPath`]s in the tree. + pub fn clip_paths(&self) -> &[Arc] { + &self.clip_paths + } + + /// Returns a list of all unique [`Mask`]s in the tree. + pub fn masks(&self) -> &[Arc] { + &self.masks + } + + /// Returns a list of all unique [`Filter`](filter::Filter)s in the tree. + pub fn filters(&self) -> &[Arc] { + &self.filters + } + + /// Returns the font database that applies to all text nodes in the tree. + #[cfg(feature = "text")] + pub fn fontdb(&self) -> &Arc { + &self.fontdb + } + + pub(crate) fn collect_paint_servers(&mut self) { + loop_over_paint_servers(&self.root, &mut |paint| match paint { + Paint::Color(_) => {} + Paint::LinearGradient(lg) => { + if !self + .linear_gradients + .iter() + .any(|other| Arc::ptr_eq(lg, other)) + { + self.linear_gradients.push(lg.clone()); + } + } + Paint::RadialGradient(rg) => { + if !self + .radial_gradients + .iter() + .any(|other| Arc::ptr_eq(rg, other)) + { + self.radial_gradients.push(rg.clone()); + } + } + Paint::Pattern(patt) => { + if !self.patterns.iter().any(|other| Arc::ptr_eq(patt, other)) { + self.patterns.push(patt.clone()); + } + } + }); + } +} + +fn node_by_id<'a>(parent: &'a Group, id: &str) -> Option<&'a Node> { + for child in &parent.children { + if child.id() == id { + return Some(child); + } + + if let Node::Group(ref g) = child { + if let Some(n) = node_by_id(g, id) { + return Some(n); + } + } + } + + None +} + +fn has_text_nodes(root: &Group) -> bool { + for node in &root.children { + if let Node::Text(_) = node { + return true; + } + + let mut has_text = false; + + if let Node::Image(ref image) = node { + if let ImageKind::SVG(ref tree) = image.kind { + if has_text_nodes(&tree.root) { + has_text = true; + } + } + } + + node.subroots(|subroot| has_text |= has_text_nodes(subroot)); + + if has_text { + return true; + } + } + + false +} + +fn loop_over_paint_servers(parent: &Group, f: &mut dyn FnMut(&Paint)) { + fn push(paint: Option<&Paint>, f: &mut dyn FnMut(&Paint)) { + if let Some(paint) = paint { + f(paint); + } + } + + for node in &parent.children { + match node { + Node::Group(ref group) => loop_over_paint_servers(group, f), + Node::Path(ref path) => { + push(path.fill.as_ref().map(|f| &f.paint), f); + push(path.stroke.as_ref().map(|f| &f.paint), f); + } + Node::Image(_) => {} + // Flattened text would be used instead. + Node::Text(_) => {} + } + + node.subroots(|subroot| loop_over_paint_servers(subroot, f)); + } +} + +impl Group { + pub(crate) fn collect_clip_paths(&self, clip_paths: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + if let Some(ref clip) = g.clip_path { + if !clip_paths.iter().any(|other| Arc::ptr_eq(clip, other)) { + clip_paths.push(clip.clone()); + } + + if let Some(ref sub_clip) = clip.clip_path { + if !clip_paths.iter().any(|other| Arc::ptr_eq(sub_clip, other)) { + clip_paths.push(sub_clip.clone()); + } + } + } + } + + node.subroots(|subroot| subroot.collect_clip_paths(clip_paths)); + + if let Node::Group(ref g) = node { + g.collect_clip_paths(clip_paths); + } + } + } + + pub(crate) fn collect_masks(&self, masks: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + if let Some(ref mask) = g.mask { + if !masks.iter().any(|other| Arc::ptr_eq(mask, other)) { + masks.push(mask.clone()); + } + + if let Some(ref sub_mask) = mask.mask { + if !masks.iter().any(|other| Arc::ptr_eq(sub_mask, other)) { + masks.push(sub_mask.clone()); + } + } + } + } + + node.subroots(|subroot| subroot.collect_masks(masks)); + + if let Node::Group(ref g) = node { + g.collect_masks(masks); + } + } + } + + pub(crate) fn collect_filters(&self, filters: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + for filter in g.filters() { + if !filters.iter().any(|other| Arc::ptr_eq(filter, other)) { + filters.push(filter.clone()); + } + } + } + + node.subroots(|subroot| subroot.collect_filters(filters)); + + if let Node::Group(ref g) = node { + g.collect_filters(filters); + } + } + } + + pub(crate) fn calculate_object_bbox(&mut self) -> Option { + let mut bbox = BBox::default(); + for child in &self.children { + let mut c_bbox = child.bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + bbox = bbox.expand(c_bbox); + } + + bbox.to_non_zero_rect() + } + + pub(crate) fn calculate_bounding_boxes(&mut self) -> Option<()> { + let mut bbox = BBox::default(); + let mut abs_bbox = BBox::default(); + let mut stroke_bbox = BBox::default(); + let mut abs_stroke_bbox = BBox::default(); + let mut layer_bbox = BBox::default(); + for child in &self.children { + { + let mut c_bbox = child.bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + bbox = bbox.expand(c_bbox); + } + + abs_bbox = abs_bbox.expand(child.abs_bounding_box()); + + { + let mut c_bbox = child.stroke_bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + stroke_bbox = stroke_bbox.expand(c_bbox); + } + + abs_stroke_bbox = abs_stroke_bbox.expand(child.abs_stroke_bounding_box()); + + if let Node::Group(ref group) = child { + let r = group.layer_bounding_box; + if let Some(r) = r.transform(group.transform) { + layer_bbox = layer_bbox.expand(r); + } + } else { + // Not a group - no need to transform. + layer_bbox = layer_bbox.expand(child.stroke_bounding_box()); + } + } + + // `bbox` can be None for empty groups, but we still have to + // calculate `layer_bounding_box after` it. + if let Some(bbox) = bbox.to_rect() { + self.bounding_box = bbox; + self.abs_bounding_box = abs_bbox.to_rect()?; + self.stroke_bounding_box = stroke_bbox.to_rect()?; + self.abs_stroke_bounding_box = abs_stroke_bbox.to_rect()?; + } + + // Filter bbox has a higher priority than layers bbox. + if let Some(filter_bbox) = self.filters_bounding_box() { + self.layer_bounding_box = filter_bbox; + } else { + self.layer_bounding_box = layer_bbox.to_non_zero_rect()?; + } + + self.abs_layer_bounding_box = self.layer_bounding_box.transform(self.abs_transform)?; + + Some(()) + } +} diff --git a/third_party/usvg/src/tree/text.rs b/third_party/usvg/src/tree/text.rs new file mode 100644 index 0000000000..10898bb3c8 --- /dev/null +++ b/third_party/usvg/src/tree/text.rs @@ -0,0 +1,654 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use strict_num::NonZeroPositiveF32; +pub use svgtypes::FontFamily; + +#[cfg(feature = "text")] +use crate::layout::Span; +use crate::{Fill, Group, NonEmptyString, PaintOrder, Rect, Stroke, TextRendering, Transform}; + +/// A font stretch property. +#[allow(missing_docs)] +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +pub enum FontStretch { + UltraCondensed, + ExtraCondensed, + Condensed, + SemiCondensed, + Normal, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl Default for FontStretch { + #[inline] + fn default() -> Self { + Self::Normal + } +} + +#[cfg(feature = "text")] +impl From for FontStretch { + fn from(stretch: fontdb::Stretch) -> Self { + match stretch { + fontdb::Stretch::UltraCondensed => FontStretch::UltraCondensed, + fontdb::Stretch::ExtraCondensed => FontStretch::ExtraCondensed, + fontdb::Stretch::Condensed => FontStretch::Condensed, + fontdb::Stretch::SemiCondensed => FontStretch::SemiCondensed, + fontdb::Stretch::Normal => FontStretch::Normal, + fontdb::Stretch::SemiExpanded => FontStretch::SemiExpanded, + fontdb::Stretch::Expanded => FontStretch::Expanded, + fontdb::Stretch::ExtraExpanded => FontStretch::ExtraExpanded, + fontdb::Stretch::UltraExpanded => FontStretch::UltraExpanded, + } + } +} + +#[cfg(feature = "text")] +impl From for fontdb::Stretch { + fn from(stretch: FontStretch) -> Self { + match stretch { + FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, + FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, + FontStretch::Condensed => fontdb::Stretch::Condensed, + FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, + FontStretch::Normal => fontdb::Stretch::Normal, + FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, + FontStretch::Expanded => fontdb::Stretch::Expanded, + FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, + FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, + } + } +} + +/// A font style property. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum FontStyle { + /// A face that is neither italic not obliqued. + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Default for FontStyle { + #[inline] + fn default() -> FontStyle { + Self::Normal + } +} + +#[cfg(feature = "text")] +impl From for FontStyle { + fn from(style: fontdb::Style) -> Self { + match style { + fontdb::Style::Normal => FontStyle::Normal, + fontdb::Style::Italic => FontStyle::Italic, + fontdb::Style::Oblique => FontStyle::Oblique, + } + } +} + +#[cfg(feature = "text")] +impl From for fontdb::Style { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + } + } +} + +/// Text font properties. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct Font { + pub(crate) families: Vec, + pub(crate) style: FontStyle, + pub(crate) stretch: FontStretch, + pub(crate) weight: u16, +} + +impl Font { + /// A list of family names. + /// + /// Never empty. Uses `usvg::Options::font_family` as fallback. + pub fn families(&self) -> &[FontFamily] { + &self.families + } + + /// A font style. + pub fn style(&self) -> FontStyle { + self.style + } + + /// A font stretch. + pub fn stretch(&self) -> FontStretch { + self.stretch + } + + /// A font width. + pub fn weight(&self) -> u16 { + self.weight + } +} + +/// A dominant baseline property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum DominantBaseline { + Auto, + UseScript, + NoChange, + ResetSize, + Ideographic, + Alphabetic, + Hanging, + Mathematical, + Central, + Middle, + TextAfterEdge, + TextBeforeEdge, +} + +impl Default for DominantBaseline { + fn default() -> Self { + Self::Auto + } +} + +/// An alignment baseline property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum AlignmentBaseline { + Auto, + Baseline, + BeforeEdge, + TextBeforeEdge, + Middle, + Central, + AfterEdge, + TextAfterEdge, + Ideographic, + Alphabetic, + Hanging, + Mathematical, +} + +impl Default for AlignmentBaseline { + fn default() -> Self { + Self::Auto + } +} + +/// A baseline shift property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BaselineShift { + Baseline, + Subscript, + Superscript, + Number(f32), +} + +impl Default for BaselineShift { + #[inline] + fn default() -> BaselineShift { + BaselineShift::Baseline + } +} + +/// A length adjust property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LengthAdjust { + Spacing, + SpacingAndGlyphs, +} + +impl Default for LengthAdjust { + fn default() -> Self { + Self::Spacing + } +} + +/// A text span decoration style. +/// +/// In SVG, text decoration and text it's applied to can have different styles. +/// So you can have black text and green underline. +/// +/// Also, in SVG you can specify text decoration stroking. +#[derive(Clone, Debug)] +pub struct TextDecorationStyle { + pub(crate) fill: Option, + pub(crate) stroke: Option, +} + +impl TextDecorationStyle { + /// A fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// A stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } +} + +/// A text span decoration. +#[derive(Clone, Debug)] +pub struct TextDecoration { + pub(crate) underline: Option, + pub(crate) overline: Option, + pub(crate) line_through: Option, +} + +impl TextDecoration { + /// An optional underline and its style. + pub fn underline(&self) -> Option<&TextDecorationStyle> { + self.underline.as_ref() + } + + /// An optional overline and its style. + pub fn overline(&self) -> Option<&TextDecorationStyle> { + self.overline.as_ref() + } + + /// An optional line-through and its style. + pub fn line_through(&self) -> Option<&TextDecorationStyle> { + self.line_through.as_ref() + } +} + +/// A text style span. +/// +/// Spans do not overlap inside a text chunk. +#[derive(Clone, Debug)] +pub struct TextSpan { + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) fill: Option, + pub(crate) stroke: Option, + pub(crate) paint_order: PaintOrder, + pub(crate) font: Font, + pub(crate) font_size: NonZeroPositiveF32, + pub(crate) small_caps: bool, + pub(crate) apply_kerning: bool, + pub(crate) decoration: TextDecoration, + pub(crate) dominant_baseline: DominantBaseline, + pub(crate) alignment_baseline: AlignmentBaseline, + pub(crate) baseline_shift: Vec, + pub(crate) visible: bool, + pub(crate) letter_spacing: f32, + pub(crate) word_spacing: f32, + pub(crate) text_length: Option, + pub(crate) length_adjust: LengthAdjust, +} + +impl TextSpan { + /// A span start in bytes. + /// + /// Offset is relative to the parent text chunk and not the parent text element. + pub fn start(&self) -> usize { + self.start + } + + /// A span end in bytes. + /// + /// Offset is relative to the parent text chunk and not the parent text element. + pub fn end(&self) -> usize { + self.end + } + + /// A fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// A stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } + + /// A paint order style. + pub fn paint_order(&self) -> PaintOrder { + self.paint_order + } + + /// A font. + pub fn font(&self) -> &Font { + &self.font + } + + /// A font size. + pub fn font_size(&self) -> NonZeroPositiveF32 { + self.font_size + } + + /// Indicates that small caps should be used. + /// + /// Set by `font-variant="small-caps"` + pub fn small_caps(&self) -> bool { + self.small_caps + } + + /// Indicates that a kerning should be applied. + /// + /// Supports both `kerning` and `font-kerning` properties. + pub fn apply_kerning(&self) -> bool { + self.apply_kerning + } + + /// A span decorations. + pub fn decoration(&self) -> &TextDecoration { + &self.decoration + } + + /// A span dominant baseline. + pub fn dominant_baseline(&self) -> DominantBaseline { + self.dominant_baseline + } + + /// A span alignment baseline. + pub fn alignment_baseline(&self) -> AlignmentBaseline { + self.alignment_baseline + } + + /// A list of all baseline shift that should be applied to this span. + /// + /// Ordered from `text` element down to the actual `span` element. + pub fn baseline_shift(&self) -> &[BaselineShift] { + &self.baseline_shift + } + + /// A visibility property. + pub fn is_visible(&self) -> bool { + self.visible + } + + /// A letter spacing property. + pub fn letter_spacing(&self) -> f32 { + self.letter_spacing + } + + /// A word spacing property. + pub fn word_spacing(&self) -> f32 { + self.word_spacing + } + + /// A text length property. + pub fn text_length(&self) -> Option { + self.text_length + } + + /// A length adjust property. + pub fn length_adjust(&self) -> LengthAdjust { + self.length_adjust + } +} + +/// A text chunk anchor property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum TextAnchor { + Start, + Middle, + End, +} + +impl Default for TextAnchor { + fn default() -> Self { + Self::Start + } +} + +/// A path used by text-on-path. +#[derive(Debug)] +pub struct TextPath { + pub(crate) id: NonEmptyString, + pub(crate) start_offset: f32, + pub(crate) path: Arc, +} + +impl TextPath { + /// Element's ID. + /// + /// Taken from the SVG itself. + pub fn id(&self) -> &str { + self.id.get() + } + + /// A text offset in SVG coordinates. + /// + /// Percentage values already resolved. + pub fn start_offset(&self) -> f32 { + self.start_offset + } + + /// A path. + pub fn path(&self) -> &tiny_skia_path::Path { + &self.path + } +} + +/// A text chunk flow property. +#[derive(Clone, Debug)] +pub enum TextFlow { + /// A linear layout. + /// + /// Includes left-to-right, right-to-left and top-to-bottom. + Linear, + /// A text-on-path layout. + Path(Arc), +} + +/// A text chunk. +/// +/// Text alignment and BIDI reordering can only be done inside a text chunk. +#[derive(Clone, Debug)] +pub struct TextChunk { + pub(crate) x: Option, + pub(crate) y: Option, + pub(crate) anchor: TextAnchor, + pub(crate) spans: Vec, + pub(crate) text_flow: TextFlow, + pub(crate) text: String, +} + +impl TextChunk { + /// An absolute X axis offset. + pub fn x(&self) -> Option { + self.x + } + + /// An absolute Y axis offset. + pub fn y(&self) -> Option { + self.y + } + + /// A text anchor. + pub fn anchor(&self) -> TextAnchor { + self.anchor + } + + /// A list of text chunk style spans. + pub fn spans(&self) -> &[TextSpan] { + &self.spans + } + + /// A text chunk flow. + pub fn text_flow(&self) -> TextFlow { + self.text_flow.clone() + } + + /// A text chunk actual text. + pub fn text(&self) -> &str { + &self.text + } +} + +/// A writing mode. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum WritingMode { + LeftToRight, + TopToBottom, +} + +/// A text element. +/// +/// `text` element in SVG. +#[derive(Clone, Debug)] +pub struct Text { + pub(crate) id: String, + pub(crate) rendering_mode: TextRendering, + pub(crate) dx: Vec, + pub(crate) dy: Vec, + pub(crate) rotate: Vec, + pub(crate) writing_mode: WritingMode, + pub(crate) chunks: Vec, + pub(crate) abs_transform: Transform, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, + pub(crate) flattened: Box, + #[cfg(feature = "text")] + pub(crate) layouted: Vec, +} + +impl Text { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Rendering mode. + /// + /// `text-rendering` in SVG. + pub fn rendering_mode(&self) -> TextRendering { + self.rendering_mode + } + + /// A relative X axis offsets. + /// + /// One offset for each Unicode codepoint. Aka `char` in Rust. + pub fn dx(&self) -> &[f32] { + &self.dx + } + + /// A relative Y axis offsets. + /// + /// One offset for each Unicode codepoint. Aka `char` in Rust. + pub fn dy(&self) -> &[f32] { + &self.dy + } + + /// A list of rotation angles. + /// + /// One angle for each Unicode codepoint. Aka `char` in Rust. + pub fn rotate(&self) -> &[f32] { + &self.rotate + } + + /// A writing mode. + pub fn writing_mode(&self) -> WritingMode { + self.writing_mode + } + + /// A list of text chunks. + pub fn chunks(&self) -> &[TextChunk] { + &self.chunks + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's text bounding box. + /// + /// Text bounding box is special in SVG and doesn't represent + /// tight bounds of the element's content. + /// You can find more about it + /// [here](https://razrfalcon.github.io/notes-on-svg-parsing/text/bbox.html). + /// + /// `objectBoundingBox` in SVG terms. Meaning it isn't affected by parent transforms. + /// + /// Returns `None` when the `text` build feature was disabled. + /// This is because we have to perform a text layout before calculating a bounding box. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's text bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Similar to `bounding_box`, but includes stroke. + /// + /// Will have the same value as `bounding_box` when path has no stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in canvas coordinates. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + /// Text converted into paths, ready to render. + /// + /// Note that this is only a + /// "best-effort" attempt: The text will be converted into group/paths/image + /// primitives, so that they can be rendered with the existing infrastructure. + /// This process is in general lossless and should lead to correct output, with + /// two notable exceptions: + /// 1. For glyphs based on the `SVG` table, only glyphs that are pure SVG 1.1/2.0 + /// are supported. Glyphs that make use of features in the OpenType specification + /// that are not part of the original SVG specification are not supported. + /// 2. For glyphs based on the `COLR` table, there are a certain number of features + /// that are not (correctly) supported, such as conical + /// gradients, certain gradient transforms and some blend modes. But this shouldn't + /// cause any issues in 95% of the cases, as most of those are edge cases. + /// If the two above are not acceptable, then you will need to implement your own + /// glyph rendering logic based on the layouted glyphs (see the `layouted` method). + pub fn flattened(&self) -> &Group { + &self.flattened + } + + /// The positioned glyphs and decoration spans of the text. + /// + /// This should only be used if you need more low-level access + /// to the glyphs that make up the text. If you just need the + /// outlines of the text, you should use `flattened` instead. + #[cfg(feature = "text")] + pub fn layouted(&self) -> &[Span] { + &self.layouted + } + + pub(crate) fn subroots(&self, f: &mut dyn FnMut(&Group)) { + f(&self.flattened); + } +} diff --git a/third_party/usvg/src/writer.rs b/third_party/usvg/src/writer.rs new file mode 100644 index 0000000000..e475a059bb --- /dev/null +++ b/third_party/usvg/src/writer.rs @@ -0,0 +1,1582 @@ +// Copyright 2023 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::fmt::Display; +use std::io::Write; + +use svgtypes::{parse_font_families, FontFamily}; +use xmlwriter::XmlWriter; + +use crate::parser::{AId, EId}; +use crate::*; + +impl Tree { + /// Writes `usvg::Tree` back to SVG. + pub fn to_string(&self, opt: &WriteOptions) -> String { + convert(self, opt) + } +} + +/// Checks that type has a default value. +trait IsDefault: Default { + /// Checks that type has a default value. + fn is_default(&self) -> bool; +} + +impl IsDefault for T { + #[inline] + fn is_default(&self) -> bool { + *self == Self::default() + } +} + +/// XML writing options. +#[derive(Clone, Debug)] +pub struct WriteOptions { + /// Used to add a custom prefix to each element ID during writing. + pub id_prefix: Option, + + /// Do not convert text into paths. + /// + /// Default: false + pub preserve_text: bool, + + /// Set the coordinates numeric precision. + /// + /// Smaller precision can lead to a malformed output in some cases. + /// + /// Default: 8 + pub coordinates_precision: u8, + + /// Set the transform values numeric precision. + /// + /// Smaller precision can lead to a malformed output in some cases. + /// + /// Default: 8 + pub transforms_precision: u8, + + /// Use single quote marks instead of double quote. + /// + /// # Examples + /// + /// Before: + /// + /// ```text + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// ``` + /// + /// Default: disabled + pub use_single_quote: bool, + + /// Set XML nodes indention. + /// + /// # Examples + /// + /// `Indent::None` + /// Before: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// ``` + /// + /// Default: 4 spaces + pub indent: Indent, + + /// Set XML attributes indention. + /// + /// # Examples + /// + /// `Indent::Spaces(2)` + /// + /// Before: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// Default: `None` + pub attributes_indent: Indent, +} + +impl Default for WriteOptions { + fn default() -> Self { + Self { + id_prefix: Default::default(), + preserve_text: false, + coordinates_precision: 8, + transforms_precision: 8, + use_single_quote: false, + indent: Indent::Spaces(4), + attributes_indent: Indent::None, + } + } +} + +pub(crate) fn convert(tree: &Tree, opt: &WriteOptions) -> String { + let mut xml = XmlWriter::new(xmlwriter::Options { + use_single_quote: opt.use_single_quote, + indent: opt.indent, + attributes_indent: opt.attributes_indent, + }); + + xml.start_svg_element(EId::Svg); + xml.write_svg_attribute(AId::Width, &tree.size.width()); + xml.write_svg_attribute(AId::Height, &tree.size.height()); + xml.write_attribute("xmlns", "http://www.w3.org/2000/svg"); + if has_xlink(&tree.root) { + xml.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + } + + xml.start_svg_element(EId::Defs); + write_defs(tree, opt, &mut xml); + xml.end_element(); + + write_elements(&tree.root, false, opt, &mut xml); + + xml.end_document() +} + +fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { + let mut written_fe_image_nodes: Vec = Vec::new(); + for filter in tree.filters() { + for fe in &filter.primitives { + if let filter::Kind::Image(ref img) = fe.kind { + if let Some(child) = img.root().children.first() { + if !written_fe_image_nodes.iter().any(|id| id == child.id()) { + write_element(child, false, opt, xml); + written_fe_image_nodes.push(child.id().to_string()); + } + } + } + } + + xml.start_svg_element(EId::Filter); + xml.write_id_attribute(filter.id(), opt); + xml.write_rect_attrs(filter.rect); + xml.write_units( + AId::FilterUnits, + Units::UserSpaceOnUse, + Units::ObjectBoundingBox, + ); + + for fe in &filter.primitives { + match fe.kind { + filter::Kind::DropShadow(ref shadow) => { + xml.start_svg_element(EId::FeDropShadow); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &shadow.input); + xml.write_attribute_fmt( + AId::StdDeviation.to_str(), + format_args!("{} {}", shadow.std_dev_x.get(), shadow.std_dev_y.get()), + ); + xml.write_svg_attribute(AId::Dx, &shadow.dx); + xml.write_svg_attribute(AId::Dy, &shadow.dy); + xml.write_color(AId::FloodColor, shadow.color); + xml.write_svg_attribute(AId::FloodOpacity, &shadow.opacity.get()); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::GaussianBlur(ref blur) => { + xml.start_svg_element(EId::FeGaussianBlur); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &blur.input); + xml.write_attribute_fmt( + AId::StdDeviation.to_str(), + format_args!("{} {}", blur.std_dev_x.get(), blur.std_dev_y.get()), + ); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Offset(ref offset) => { + xml.start_svg_element(EId::FeOffset); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &offset.input); + xml.write_svg_attribute(AId::Dx, &offset.dx); + xml.write_svg_attribute(AId::Dy, &offset.dy); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Blend(ref blend) => { + xml.start_svg_element(EId::FeBlend); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &blend.input1); + xml.write_filter_input(AId::In2, &blend.input2); + xml.write_svg_attribute( + AId::Mode, + match blend.mode { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + }, + ); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Flood(ref flood) => { + xml.start_svg_element(EId::FeFlood); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_color(AId::FloodColor, flood.color); + xml.write_svg_attribute(AId::FloodOpacity, &flood.opacity.get()); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Composite(ref composite) => { + xml.start_svg_element(EId::FeComposite); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &composite.input1); + xml.write_filter_input(AId::In2, &composite.input2); + xml.write_svg_attribute( + AId::Operator, + match composite.operator { + filter::CompositeOperator::Over => "over", + filter::CompositeOperator::In => "in", + filter::CompositeOperator::Out => "out", + filter::CompositeOperator::Atop => "atop", + filter::CompositeOperator::Xor => "xor", + filter::CompositeOperator::Arithmetic { .. } => "arithmetic", + }, + ); + + if let filter::CompositeOperator::Arithmetic { k1, k2, k3, k4 } = + composite.operator + { + xml.write_svg_attribute(AId::K1, &k1); + xml.write_svg_attribute(AId::K2, &k2); + xml.write_svg_attribute(AId::K3, &k3); + xml.write_svg_attribute(AId::K4, &k4); + } + + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Merge(ref merge) => { + xml.start_svg_element(EId::FeMerge); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_svg_attribute(AId::Result, &fe.result); + for input in &merge.inputs { + xml.start_svg_element(EId::FeMergeNode); + xml.write_filter_input(AId::In, input); + xml.end_element(); + } + + xml.end_element(); + } + filter::Kind::Tile(ref tile) => { + xml.start_svg_element(EId::FeTile); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &tile.input); + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::Image(ref img) => { + xml.start_svg_element(EId::FeImage); + xml.write_filter_primitive_attrs(filter.rect(), fe); + if let Some(child) = img.root.children.first() { + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + xml.write_attribute_fmt( + "xlink:href", + format_args!("#{}{}", prefix, child.id()), + ); + } + + xml.write_svg_attribute(AId::Result, &fe.result); + xml.end_element(); + } + filter::Kind::ComponentTransfer(ref transfer) => { + xml.start_svg_element(EId::FeComponentTransfer); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &transfer.input); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_filter_transfer_function(EId::FeFuncR, &transfer.func_r); + xml.write_filter_transfer_function(EId::FeFuncG, &transfer.func_g); + xml.write_filter_transfer_function(EId::FeFuncB, &transfer.func_b); + xml.write_filter_transfer_function(EId::FeFuncA, &transfer.func_a); + + xml.end_element(); + } + filter::Kind::ColorMatrix(ref matrix) => { + xml.start_svg_element(EId::FeColorMatrix); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &matrix.input); + xml.write_svg_attribute(AId::Result, &fe.result); + + match matrix.kind { + filter::ColorMatrixKind::Matrix(ref values) => { + xml.write_svg_attribute(AId::Type, "matrix"); + xml.write_numbers(AId::Values, values); + } + filter::ColorMatrixKind::Saturate(value) => { + xml.write_svg_attribute(AId::Type, "saturate"); + xml.write_svg_attribute(AId::Values, &value.get()); + } + filter::ColorMatrixKind::HueRotate(angle) => { + xml.write_svg_attribute(AId::Type, "hueRotate"); + xml.write_svg_attribute(AId::Values, &angle); + } + filter::ColorMatrixKind::LuminanceToAlpha => { + xml.write_svg_attribute(AId::Type, "luminanceToAlpha"); + } + } + + xml.end_element(); + } + filter::Kind::ConvolveMatrix(ref matrix) => { + xml.start_svg_element(EId::FeConvolveMatrix); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &matrix.input); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_attribute_fmt( + AId::Order.to_str(), + format_args!("{} {}", matrix.matrix.columns, matrix.matrix.rows), + ); + xml.write_numbers(AId::KernelMatrix, &matrix.matrix.data); + xml.write_svg_attribute(AId::Divisor, &matrix.divisor.get()); + xml.write_svg_attribute(AId::Bias, &matrix.bias); + xml.write_svg_attribute(AId::TargetX, &matrix.matrix.target_x); + xml.write_svg_attribute(AId::TargetY, &matrix.matrix.target_y); + xml.write_svg_attribute( + AId::EdgeMode, + match matrix.edge_mode { + filter::EdgeMode::None => "none", + filter::EdgeMode::Duplicate => "duplicate", + filter::EdgeMode::Wrap => "wrap", + }, + ); + xml.write_svg_attribute( + AId::PreserveAlpha, + if matrix.preserve_alpha { + "true" + } else { + "false" + }, + ); + + xml.end_element(); + } + filter::Kind::Morphology(ref morphology) => { + xml.start_svg_element(EId::FeMorphology); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &morphology.input); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_svg_attribute( + AId::Operator, + match morphology.operator { + filter::MorphologyOperator::Erode => "erode", + filter::MorphologyOperator::Dilate => "dilate", + }, + ); + xml.write_attribute_fmt( + AId::Radius.to_str(), + format_args!( + "{} {}", + morphology.radius_x.get(), + morphology.radius_y.get() + ), + ); + + xml.end_element(); + } + filter::Kind::DisplacementMap(ref map) => { + xml.start_svg_element(EId::FeDisplacementMap); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_filter_input(AId::In, &map.input1); + xml.write_filter_input(AId::In2, &map.input2); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_svg_attribute(AId::Scale, &map.scale); + + let mut write_channel = |c, aid| { + xml.write_svg_attribute( + aid, + match c { + filter::ColorChannel::R => "R", + filter::ColorChannel::G => "G", + filter::ColorChannel::B => "B", + filter::ColorChannel::A => "A", + }, + ); + }; + write_channel(map.x_channel_selector, AId::XChannelSelector); + write_channel(map.y_channel_selector, AId::YChannelSelector); + + xml.end_element(); + } + filter::Kind::Turbulence(ref turbulence) => { + xml.start_svg_element(EId::FeTurbulence); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_attribute_fmt( + AId::BaseFrequency.to_str(), + format_args!( + "{} {}", + turbulence.base_frequency_x.get(), + turbulence.base_frequency_y.get() + ), + ); + xml.write_svg_attribute(AId::NumOctaves, &turbulence.num_octaves); + xml.write_svg_attribute(AId::Seed, &turbulence.seed); + xml.write_svg_attribute( + AId::StitchTiles, + match turbulence.stitch_tiles { + true => "stitch", + false => "noStitch", + }, + ); + xml.write_svg_attribute( + AId::Type, + match turbulence.kind { + filter::TurbulenceKind::FractalNoise => "fractalNoise", + filter::TurbulenceKind::Turbulence => "turbulence", + }, + ); + + xml.end_element(); + } + filter::Kind::DiffuseLighting(ref light) => { + xml.start_svg_element(EId::FeDiffuseLighting); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); + xml.write_svg_attribute(AId::DiffuseConstant, &light.diffuse_constant); + xml.write_color(AId::LightingColor, light.lighting_color); + write_light_source(&light.light_source, xml); + + xml.end_element(); + } + filter::Kind::SpecularLighting(ref light) => { + xml.start_svg_element(EId::FeSpecularLighting); + xml.write_filter_primitive_attrs(filter.rect(), fe); + xml.write_svg_attribute(AId::Result, &fe.result); + + xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); + xml.write_svg_attribute(AId::SpecularConstant, &light.specular_constant); + xml.write_svg_attribute(AId::SpecularExponent, &light.specular_exponent); + xml.write_color(AId::LightingColor, light.lighting_color); + write_light_source(&light.light_source, xml); + + xml.end_element(); + } + }; + } + + xml.end_element(); + } +} + +fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { + for lg in tree.linear_gradients() { + xml.start_svg_element(EId::LinearGradient); + xml.write_id_attribute(lg.id(), opt); + xml.write_svg_attribute(AId::X1, &lg.x1); + xml.write_svg_attribute(AId::Y1, &lg.y1); + xml.write_svg_attribute(AId::X2, &lg.x2); + xml.write_svg_attribute(AId::Y2, &lg.y2); + write_base_grad(&lg.base, opt, xml); + xml.end_element(); + } + + for rg in tree.radial_gradients() { + xml.start_svg_element(EId::RadialGradient); + xml.write_id_attribute(rg.id(), opt); + xml.write_svg_attribute(AId::Cx, &rg.cx); + xml.write_svg_attribute(AId::Cy, &rg.cy); + xml.write_svg_attribute(AId::R, &rg.r.get()); + xml.write_svg_attribute(AId::Fx, &rg.fx); + xml.write_svg_attribute(AId::Fy, &rg.fy); + write_base_grad(&rg.base, opt, xml); + xml.end_element(); + } + + for pattern in tree.patterns() { + xml.start_svg_element(EId::Pattern); + xml.write_id_attribute(pattern.id(), opt); + xml.write_rect_attrs(pattern.rect); + xml.write_units(AId::PatternUnits, pattern.units, Units::ObjectBoundingBox); + xml.write_units( + AId::PatternContentUnits, + pattern.content_units, + Units::UserSpaceOnUse, + ); + xml.write_transform(AId::PatternTransform, pattern.transform, opt); + + write_elements(&pattern.root, false, opt, xml); + + xml.end_element(); + } + + if tree.has_text_nodes() { + write_text_path_paths(&tree.root, opt, xml); + } + + write_filters(tree, opt, xml); + + for clip in tree.clip_paths() { + xml.start_svg_element(EId::ClipPath); + xml.write_id_attribute(clip.id(), opt); + xml.write_transform(AId::Transform, clip.transform, opt); + + if let Some(ref clip) = clip.clip_path { + xml.write_func_iri(AId::ClipPath, clip.id(), opt); + } + + write_elements(&clip.root, true, opt, xml); + + xml.end_element(); + } + + for mask in tree.masks() { + xml.start_svg_element(EId::Mask); + xml.write_id_attribute(mask.id(), opt); + if mask.kind == MaskType::Alpha { + xml.write_svg_attribute(AId::MaskType, "alpha"); + } + xml.write_units( + AId::MaskUnits, + Units::UserSpaceOnUse, + Units::ObjectBoundingBox, + ); + xml.write_rect_attrs(mask.rect); + + if let Some(ref mask) = mask.mask { + xml.write_func_iri(AId::Mask, mask.id(), opt); + } + + write_elements(&mask.root, false, opt, xml); + + xml.end_element(); + } +} + +fn write_text_path_paths(parent: &Group, opt: &WriteOptions, xml: &mut XmlWriter) { + for node in &parent.children { + if let Node::Group(ref group) = node { + write_text_path_paths(group, opt, xml); + } else if let Node::Text(ref text) = node { + for chunk in &text.chunks { + if let TextFlow::Path(ref text_path) = chunk.text_flow { + let path = Path::new( + text_path.id().to_string(), + true, + None, + None, + PaintOrder::default(), + ShapeRendering::default(), + text_path.path.clone(), + Transform::default(), + ); + if let Some(ref path) = path { + write_path(path, false, Transform::default(), None, opt, xml); + } + } + } + } + + node.subroots(|subroot| write_text_path_paths(subroot, opt, xml)); + } +} + +fn write_elements(parent: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + for n in &parent.children { + write_element(n, is_clip_path, opt, xml); + } +} + +fn write_element(node: &Node, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + match node { + Node::Path(ref p) => { + write_path(p, is_clip_path, Transform::default(), None, opt, xml); + } + Node::Image(ref img) => { + xml.start_svg_element(EId::Image); + if !img.id.is_empty() { + xml.write_id_attribute(&img.id, opt); + } + + xml.write_svg_attribute(AId::Width, &img.size().width()); + xml.write_svg_attribute(AId::Height, &img.size().height()); + + xml.write_visibility(img.visible); + + match img.rendering_mode { + ImageRendering::OptimizeQuality => {} + ImageRendering::OptimizeSpeed => { + xml.write_svg_attribute(AId::ImageRendering, "optimizeSpeed"); + } + ImageRendering::Smooth => { + xml.write_attribute(AId::Style.to_str(), "image-rendering:smooth"); + } + ImageRendering::HighQuality => { + xml.write_attribute(AId::Style.to_str(), "image-rendering:high-quality"); + } + ImageRendering::CrispEdges => { + xml.write_attribute(AId::Style.to_str(), "image-rendering:crisp-edges"); + } + ImageRendering::Pixelated => { + xml.write_attribute(AId::Style.to_str(), "image-rendering:pixelated"); + } + } + + xml.write_image_data(&img.kind); + + xml.end_element(); + } + Node::Group(ref g) => { + write_group_element(g, is_clip_path, opt, xml); + } + Node::Text(ref text) => { + if opt.preserve_text { + xml.start_svg_element(EId::Text); + + if !text.id.is_empty() { + xml.write_id_attribute(&text.id, opt); + } + + xml.write_attribute("xml:space", "preserve"); + + match text.writing_mode { + WritingMode::LeftToRight => {} + WritingMode::TopToBottom => xml.write_svg_attribute(AId::WritingMode, "tb"), + } + + match text.rendering_mode { + TextRendering::OptimizeSpeed => { + xml.write_svg_attribute(AId::TextRendering, "optimizeSpeed"); + } + TextRendering::GeometricPrecision => { + xml.write_svg_attribute(AId::TextRendering, "geometricPrecision"); + } + TextRendering::OptimizeLegibility => {} + } + + if text.rotate.iter().any(|r| *r != 0.0) { + xml.write_numbers(AId::Rotate, &text.rotate); + } + + if text.dx.iter().any(|dx| *dx != 0.0) { + xml.write_numbers(AId::Dx, &text.dx); + } + + if text.dy.iter().any(|dy| *dy != 0.0) { + xml.write_numbers(AId::Dy, &text.dy); + } + + xml.set_preserve_whitespaces(true); + + for chunk in &text.chunks { + if let TextFlow::Path(text_path) = &chunk.text_flow { + xml.start_svg_element(EId::TextPath); + + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + xml.write_attribute_fmt( + "xlink:href", + format_args!("#{}{}", prefix, text_path.id()), + ); + + if text_path.start_offset != 0.0 { + xml.write_svg_attribute(AId::StartOffset, &text_path.start_offset); + } + } + + xml.start_svg_element(EId::Tspan); + + if let Some(x) = chunk.x { + xml.write_svg_attribute(AId::X, &x); + } + + if let Some(y) = chunk.y { + xml.write_svg_attribute(AId::Y, &y); + } + + match chunk.anchor { + TextAnchor::Start => {} + TextAnchor::Middle => xml.write_svg_attribute(AId::TextAnchor, "middle"), + TextAnchor::End => xml.write_svg_attribute(AId::TextAnchor, "end"), + } + + for span in &chunk.spans { + let decorations: Vec<_> = [ + ("underline", &span.decoration.underline), + ("line-through", &span.decoration.line_through), + ("overline", &span.decoration.overline), + ] + .iter() + .filter_map(|&(key, option_value)| { + option_value.as_ref().map(|value| (key, value)) + }) + .collect(); + + // Decorations need to be dumped BEFORE we write the actual span data + // (so that for example stroke color of span doesn't affect the text + // itself while baseline shifts need to be written after (since they are + // affected by the font size) + for (deco_name, deco) in &decorations { + xml.start_svg_element(EId::Tspan); + xml.write_svg_attribute(AId::TextDecoration, deco_name); + write_fill(&deco.fill, false, opt, xml); + write_stroke(&deco.stroke, opt, xml); + } + + write_span(is_clip_path, opt, xml, chunk, span); + + // End for each tspan we needed to create for decorations + for _ in &decorations { + xml.end_element(); + } + } + xml.end_element(); + + // End textPath element + if matches!(&chunk.text_flow, TextFlow::Path(_)) { + xml.end_element(); + } + } + + xml.end_element(); + xml.set_preserve_whitespaces(false); + } else { + write_group_element(text.flattened(), is_clip_path, opt, xml); + } + } + } +} + +fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + if is_clip_path { + // The `clipPath` element in SVG doesn't allow groups, only shapes and text. + // The problem is that in `usvg` we can set a `clip-path` only on groups. + // So in cases when a `clipPath` child has a `clip-path` as well, + // it would be inside a group. And we have to skip this group during writing. + // + // Basically, the following SVG: + // + // + // + // + // + // will be represented in usvg as: + // + // + // + // + // + // + // + // + // Same with text. Text elements will be converted into groups, + // but only the group's children should be written. + for child in &g.children { + if let Node::Path(ref path) = child { + let clip_id = g.clip_path.as_ref().map(|cp| cp.id().to_string()); + write_path( + path, + is_clip_path, + g.transform, + clip_id.as_deref(), + opt, + xml, + ); + } + } + return; + } + + xml.start_svg_element(EId::G); + if !g.id.is_empty() { + xml.write_id_attribute(&g.id, opt); + }; + + if let Some(ref clip) = g.clip_path { + xml.write_func_iri(AId::ClipPath, clip.id(), opt); + } + + if let Some(ref mask) = g.mask { + xml.write_func_iri(AId::Mask, mask.id(), opt); + } + + if !g.filters.is_empty() { + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + let ids: Vec<_> = g + .filters + .iter() + .map(|filter| format!("url(#{}{})", prefix, filter.id())) + .collect(); + xml.write_svg_attribute(AId::Filter, &ids.join(" ")); + } + + if g.opacity != Opacity::ONE { + xml.write_svg_attribute(AId::Opacity, &g.opacity.get()); + } + + xml.write_transform(AId::Transform, g.transform, opt); + + if g.blend_mode != BlendMode::Normal || g.isolate { + let blend_mode = match g.blend_mode { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + }; + + // For reasons unknown, `mix-blend-mode` and `isolation` must be written + // as `style` attribute. + let isolation = if g.isolate { "isolate" } else { "auto" }; + xml.write_attribute_fmt( + AId::Style.to_str(), + format_args!("mix-blend-mode:{};isolation:{}", blend_mode, isolation), + ); + } + + write_elements(g, false, opt, xml); + + xml.end_element(); +} + +trait XmlWriterExt { + fn start_svg_element(&mut self, id: EId); + fn write_svg_attribute(&mut self, id: AId, value: &V); + fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions); + fn write_color(&mut self, id: AId, color: Color); + fn write_units(&mut self, id: AId, units: Units, def: Units); + fn write_transform(&mut self, id: AId, units: Transform, opt: &WriteOptions); + fn write_visibility(&mut self, value: bool); + fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions); + fn write_rect_attrs(&mut self, r: NonZeroRect); + fn write_numbers(&mut self, aid: AId, list: &[f32]); + fn write_image_data(&mut self, kind: &ImageKind); + fn write_filter_input(&mut self, id: AId, input: &filter::Input); + fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive); + fn write_filter_transfer_function(&mut self, eid: EId, fe: &filter::TransferFunction); +} + +impl XmlWriterExt for XmlWriter { + #[inline(never)] + fn start_svg_element(&mut self, id: EId) { + self.start_element(id.to_str()); + } + + #[inline(never)] + fn write_svg_attribute(&mut self, id: AId, value: &V) { + self.write_attribute(id.to_str(), value); + } + + #[inline(never)] + fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions) { + debug_assert!(!id.is_empty()); + + if let Some(ref prefix) = opt.id_prefix { + let full_id = format!("{}{}", prefix, id); + self.write_attribute("id", &full_id); + } else { + self.write_attribute("id", id); + } + } + + #[inline(never)] + fn write_color(&mut self, id: AId, c: Color) { + static CHARS: &[u8] = b"0123456789abcdef"; + + #[inline] + fn int2hex(n: u8) -> (u8, u8) { + (CHARS[(n >> 4) as usize], CHARS[(n & 0xf) as usize]) + } + + let (r1, r2) = int2hex(c.red); + let (g1, g2) = int2hex(c.green); + let (b1, b2) = int2hex(c.blue); + + self.write_attribute_raw(id.to_str(), |buf| { + buf.extend_from_slice(&[b'#', r1, r2, g1, g2, b1, b2]); + }); + } + + // TODO: simplify + fn write_units(&mut self, id: AId, units: Units, def: Units) { + if units != def { + self.write_attribute( + id.to_str(), + match units { + Units::UserSpaceOnUse => "userSpaceOnUse", + Units::ObjectBoundingBox => "objectBoundingBox", + }, + ); + } + } + + fn write_transform(&mut self, id: AId, ts: Transform, opt: &WriteOptions) { + if !ts.is_default() { + self.write_attribute_raw(id.to_str(), |buf| { + buf.extend_from_slice(b"matrix("); + write_num(ts.sx, buf, opt.transforms_precision); + buf.push(b' '); + write_num(ts.ky, buf, opt.transforms_precision); + buf.push(b' '); + write_num(ts.kx, buf, opt.transforms_precision); + buf.push(b' '); + write_num(ts.sy, buf, opt.transforms_precision); + buf.push(b' '); + write_num(ts.tx, buf, opt.transforms_precision); + buf.push(b' '); + write_num(ts.ty, buf, opt.transforms_precision); + buf.extend_from_slice(b")"); + }); + } + } + + fn write_visibility(&mut self, value: bool) { + if !value { + self.write_attribute(AId::Visibility.to_str(), "hidden"); + } + } + + fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions) { + debug_assert!(!id.is_empty()); + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + self.write_attribute_fmt(aid.to_str(), format_args!("url(#{}{})", prefix, id)); + } + + fn write_rect_attrs(&mut self, r: NonZeroRect) { + self.write_svg_attribute(AId::X, &r.x()); + self.write_svg_attribute(AId::Y, &r.y()); + self.write_svg_attribute(AId::Width, &r.width()); + self.write_svg_attribute(AId::Height, &r.height()); + } + + fn write_numbers(&mut self, aid: AId, list: &[f32]) { + self.write_attribute_raw(aid.to_str(), |buf| { + for n in list { + buf.write_fmt(format_args!("{} ", n)).unwrap(); + } + + if !list.is_empty() { + buf.pop(); + } + }); + } + + fn write_filter_input(&mut self, id: AId, input: &filter::Input) { + self.write_attribute( + id.to_str(), + match input { + filter::Input::SourceGraphic => "SourceGraphic", + filter::Input::SourceAlpha => "SourceAlpha", + filter::Input::Reference(ref s) => s, + }, + ); + } + + fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive) { + if parent_rect.x() != fe.rect().x() { + self.write_svg_attribute(AId::X, &fe.rect().x()); + } + if parent_rect.y() != fe.rect().y() { + self.write_svg_attribute(AId::Y, &fe.rect().y()); + } + if parent_rect.width() != fe.rect().width() { + self.write_svg_attribute(AId::Width, &fe.rect().width()); + } + if parent_rect.height() != fe.rect().height() { + self.write_svg_attribute(AId::Height, &fe.rect().height()); + } + + self.write_attribute( + AId::ColorInterpolationFilters.to_str(), + match fe.color_interpolation { + filter::ColorInterpolation::SRGB => "sRGB", + filter::ColorInterpolation::LinearRGB => "linearRGB", + }, + ); + } + + fn write_filter_transfer_function(&mut self, eid: EId, fe: &filter::TransferFunction) { + self.start_svg_element(eid); + + match fe { + filter::TransferFunction::Identity => { + self.write_svg_attribute(AId::Type, "identity"); + } + filter::TransferFunction::Table(ref values) => { + self.write_svg_attribute(AId::Type, "table"); + self.write_numbers(AId::TableValues, values); + } + filter::TransferFunction::Discrete(ref values) => { + self.write_svg_attribute(AId::Type, "discrete"); + self.write_numbers(AId::TableValues, values); + } + filter::TransferFunction::Linear { slope, intercept } => { + self.write_svg_attribute(AId::Type, "linear"); + self.write_svg_attribute(AId::Slope, &slope); + self.write_svg_attribute(AId::Intercept, &intercept); + } + filter::TransferFunction::Gamma { + amplitude, + exponent, + offset, + } => { + self.write_svg_attribute(AId::Type, "gamma"); + self.write_svg_attribute(AId::Amplitude, &litude); + self.write_svg_attribute(AId::Exponent, &exponent); + self.write_svg_attribute(AId::Offset, &offset); + } + } + + self.end_element(); + } + + fn write_image_data(&mut self, kind: &ImageKind) { + let svg_string; + let (mime, data) = match kind { + ImageKind::JPEG(ref data) => ("jpeg", data.as_slice()), + ImageKind::PNG(ref data) => ("png", data.as_slice()), + ImageKind::GIF(ref data) => ("gif", data.as_slice()), + ImageKind::WEBP(ref data) => ("webp", data.as_slice()), + ImageKind::SVG(ref tree) => { + svg_string = tree.to_string(&WriteOptions::default()); + ("svg+xml", svg_string.as_bytes()) + } + }; + + self.write_attribute_raw("xlink:href", |buf| { + buf.extend_from_slice(b"data:image/"); + buf.extend_from_slice(mime.as_bytes()); + buf.extend_from_slice(b";base64, "); + + let mut enc = + base64::write::EncoderWriter::new(buf, &base64::engine::general_purpose::STANDARD); + enc.write_all(data).unwrap(); + enc.finish().unwrap(); + }); + } +} + +fn has_xlink(parent: &Group) -> bool { + for node in &parent.children { + match node { + Node::Group(ref g) => { + for filter in &g.filters { + if filter + .primitives + .iter() + .any(|p| matches!(p.kind, filter::Kind::Image(_))) + { + return true; + } + } + + if let Some(ref mask) = g.mask { + if has_xlink(mask.root()) { + return true; + } + + if let Some(ref sub_mask) = mask.mask { + if has_xlink(&sub_mask.root) { + return true; + } + } + } + + if has_xlink(g) { + return true; + } + } + Node::Image(_) => { + return true; + } + Node::Text(ref text) => { + if text + .chunks + .iter() + .any(|t| matches!(t.text_flow, TextFlow::Path(_))) + { + return true; + } + } + _ => {} + } + + let mut present = false; + node.subroots(|root| present |= has_xlink(root)); + if present { + return true; + } + } + + false +} + +fn write_base_grad(g: &BaseGradient, opt: &WriteOptions, xml: &mut XmlWriter) { + xml.write_units(AId::GradientUnits, g.units, Units::ObjectBoundingBox); + xml.write_transform(AId::GradientTransform, g.transform, opt); + + match g.spread_method { + SpreadMethod::Pad => {} + SpreadMethod::Reflect => xml.write_svg_attribute(AId::SpreadMethod, "reflect"), + SpreadMethod::Repeat => xml.write_svg_attribute(AId::SpreadMethod, "repeat"), + } + + for s in &g.stops { + xml.start_svg_element(EId::Stop); + xml.write_svg_attribute(AId::Offset, &s.offset.get()); + xml.write_color(AId::StopColor, s.color); + if s.opacity != Opacity::ONE { + xml.write_svg_attribute(AId::StopOpacity, &s.opacity.get()); + } + + xml.end_element(); + } +} + +fn write_path( + path: &Path, + is_clip_path: bool, + path_transform: Transform, + clip_path: Option<&str>, + opt: &WriteOptions, + xml: &mut XmlWriter, +) { + xml.start_svg_element(EId::Path); + if !path.id.is_empty() { + xml.write_id_attribute(&path.id, opt); + } + + write_fill(&path.fill, is_clip_path, opt, xml); + write_stroke(&path.stroke, opt, xml); + + xml.write_visibility(path.visible); + + if path.paint_order == PaintOrder::StrokeAndFill { + xml.write_svg_attribute(AId::PaintOrder, "stroke"); + } + + match path.rendering_mode { + ShapeRendering::OptimizeSpeed => { + xml.write_svg_attribute(AId::ShapeRendering, "optimizeSpeed"); + } + ShapeRendering::CrispEdges => xml.write_svg_attribute(AId::ShapeRendering, "crispEdges"), + ShapeRendering::GeometricPrecision => {} + } + + if let Some(id) = clip_path { + xml.write_func_iri(AId::ClipPath, id, opt); + } + + xml.write_transform(AId::Transform, path_transform, opt); + + xml.write_attribute_raw("d", |buf| { + use tiny_skia_path::PathSegment; + + for seg in path.data.segments() { + match seg { + PathSegment::MoveTo(p) => { + buf.extend_from_slice(b"M "); + write_num(p.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.y, buf, opt.coordinates_precision); + buf.push(b' '); + } + PathSegment::LineTo(p) => { + buf.extend_from_slice(b"L "); + write_num(p.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.y, buf, opt.coordinates_precision); + buf.push(b' '); + } + PathSegment::QuadTo(p1, p) => { + buf.extend_from_slice(b"Q "); + write_num(p1.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p1.y, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.y, buf, opt.coordinates_precision); + buf.push(b' '); + } + PathSegment::CubicTo(p1, p2, p) => { + buf.extend_from_slice(b"C "); + write_num(p1.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p1.y, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p2.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p2.y, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.x, buf, opt.coordinates_precision); + buf.push(b' '); + write_num(p.y, buf, opt.coordinates_precision); + buf.push(b' '); + } + PathSegment::Close => { + buf.extend_from_slice(b"Z "); + } + } + } + + buf.pop(); + }); + + xml.end_element(); +} + +fn write_fill(fill: &Option, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + if let Some(ref fill) = fill { + write_paint(AId::Fill, &fill.paint, opt, xml); + + if fill.opacity != Opacity::ONE { + xml.write_svg_attribute(AId::FillOpacity, &fill.opacity.get()); + } + + if !fill.rule.is_default() { + let name = if is_clip_path { + AId::ClipRule + } else { + AId::FillRule + }; + + xml.write_svg_attribute(name, "evenodd"); + } + } else { + xml.write_svg_attribute(AId::Fill, "none"); + } +} + +fn write_stroke(stroke: &Option, opt: &WriteOptions, xml: &mut XmlWriter) { + if let Some(ref stroke) = stroke { + write_paint(AId::Stroke, &stroke.paint, opt, xml); + + if stroke.opacity != Opacity::ONE { + xml.write_svg_attribute(AId::StrokeOpacity, &stroke.opacity.get()); + } + + if !stroke.dashoffset.approx_zero_ulps(4) { + xml.write_svg_attribute(AId::StrokeDashoffset, &stroke.dashoffset); + } + + if !stroke.miterlimit.is_default() { + xml.write_svg_attribute(AId::StrokeMiterlimit, &stroke.miterlimit.get()); + } + + if stroke.width.get() != 1.0 { + xml.write_svg_attribute(AId::StrokeWidth, &stroke.width.get()); + } + + match stroke.linecap { + LineCap::Butt => {} + LineCap::Round => xml.write_svg_attribute(AId::StrokeLinecap, "round"), + LineCap::Square => xml.write_svg_attribute(AId::StrokeLinecap, "square"), + } + + match stroke.linejoin { + LineJoin::Miter => {} + LineJoin::MiterClip => xml.write_svg_attribute(AId::StrokeLinejoin, "miter-clip"), + LineJoin::Round => xml.write_svg_attribute(AId::StrokeLinejoin, "round"), + LineJoin::Bevel => xml.write_svg_attribute(AId::StrokeLinejoin, "bevel"), + } + + if let Some(ref array) = stroke.dasharray { + xml.write_numbers(AId::StrokeDasharray, array); + } + } else { + // Always set `stroke` to `none` to override the parent value. + // In 99.9% of the cases it's redundant, but a group with `filter` with `StrokePaint` + // will set `stroke`, which will interfere with children nodes. + xml.write_svg_attribute(AId::Stroke, "none"); + } +} + +fn write_paint(aid: AId, paint: &Paint, opt: &WriteOptions, xml: &mut XmlWriter) { + match paint { + Paint::Color(c) => xml.write_color(aid, *c), + Paint::LinearGradient(ref lg) => { + xml.write_func_iri(aid, lg.id(), opt); + } + Paint::RadialGradient(ref rg) => { + xml.write_func_iri(aid, rg.id(), opt); + } + Paint::Pattern(ref patt) => { + xml.write_func_iri(aid, patt.id(), opt); + } + } +} + +fn write_light_source(light: &filter::LightSource, xml: &mut XmlWriter) { + match light { + filter::LightSource::DistantLight(ref light) => { + xml.start_svg_element(EId::FeDistantLight); + xml.write_svg_attribute(AId::Azimuth, &light.azimuth); + xml.write_svg_attribute(AId::Elevation, &light.elevation); + } + filter::LightSource::PointLight(ref light) => { + xml.start_svg_element(EId::FePointLight); + xml.write_svg_attribute(AId::X, &light.x); + xml.write_svg_attribute(AId::Y, &light.y); + xml.write_svg_attribute(AId::Z, &light.z); + } + filter::LightSource::SpotLight(ref light) => { + xml.start_svg_element(EId::FeSpotLight); + xml.write_svg_attribute(AId::X, &light.x); + xml.write_svg_attribute(AId::Y, &light.y); + xml.write_svg_attribute(AId::Z, &light.z); + xml.write_svg_attribute(AId::PointsAtX, &light.points_at_x); + xml.write_svg_attribute(AId::PointsAtY, &light.points_at_y); + xml.write_svg_attribute(AId::PointsAtZ, &light.points_at_z); + xml.write_svg_attribute(AId::SpecularExponent, &light.specular_exponent); + if let Some(ref n) = light.limiting_cone_angle { + xml.write_svg_attribute(AId::LimitingConeAngle, n); + } + } + } + + xml.end_element(); +} + +static POW_VEC: &[f32] = &[ + 1.0, + 10.0, + 100.0, + 1_000.0, + 10_000.0, + 100_000.0, + 1_000_000.0, + 10_000_000.0, + 100_000_000.0, + 1_000_000_000.0, + 10_000_000_000.0, + 100_000_000_000.0, + 1_000_000_000_000.0, +]; + +fn write_num(num: f32, buf: &mut Vec, precision: u8) { + // If number is an integer, it's faster to write it as i32. + if num.fract().approx_zero_ulps(4) { + write!(buf, "{}", num as i32).unwrap(); + return; + } + + // Round numbers up to the specified precision to prevent writing + // ugly numbers like 29.999999999999996. + // It's not 100% correct, but differences are insignificant. + // + // Note that at least in Rust 1.64 the number formatting in debug and release modes + // can be slightly different. So having a lower precision makes + // our output and tests reproducible. + let v = (num * POW_VEC[precision as usize]).round() / POW_VEC[precision as usize]; + + write!(buf, "{}", v).unwrap(); +} + +/// Write all of the tspan attributes except for decorations. +fn write_span( + is_clip_path: bool, + opt: &WriteOptions, + xml: &mut XmlWriter, + chunk: &TextChunk, + span: &TextSpan, +) { + xml.start_svg_element(EId::Tspan); + + let font_family_to_str = |font_family: &FontFamily| match font_family { + FontFamily::Monospace => "monospace".to_string(), + FontFamily::Serif => "serif".to_string(), + FontFamily::SansSerif => "sans-serif".to_string(), + FontFamily::Cursive => "cursive".to_string(), + FontFamily::Fantasy => "fantasy".to_string(), + FontFamily::Named(s) => { + // Only quote if absolutely necessary + match parse_font_families(s) { + Ok(_) => s.clone(), + Err(_) => { + if opt.use_single_quote { + format!("\"{}\"", s) + } else { + format!("'{}'", s) + } + } + } + } + }; + + if !span.font.families.is_empty() { + let families = span + .font + .families + .iter() + .map(font_family_to_str) + .collect::>() + .join(", "); + xml.write_svg_attribute(AId::FontFamily, &families); + } + + match span.font.style { + FontStyle::Normal => {} + FontStyle::Italic => xml.write_svg_attribute(AId::FontStyle, "italic"), + FontStyle::Oblique => xml.write_svg_attribute(AId::FontStyle, "oblique"), + } + + if span.font.weight != 400 { + xml.write_svg_attribute(AId::FontWeight, &span.font.weight); + } + + if span.font.stretch != FontStretch::Normal { + let name = match span.font.stretch { + FontStretch::Condensed => "condensed", + FontStretch::ExtraCondensed => "extra-condensed", + FontStretch::UltraCondensed => "ultra-condensed", + FontStretch::SemiCondensed => "semi-condensed", + FontStretch::Expanded => "expanded", + FontStretch::SemiExpanded => "semi-expanded", + FontStretch::ExtraExpanded => "extra-expanded", + FontStretch::UltraExpanded => "ultra-expanded", + FontStretch::Normal => unreachable!(), + }; + xml.write_svg_attribute(AId::FontStretch, name); + } + + xml.write_svg_attribute(AId::FontSize, &span.font_size); + + xml.write_visibility(span.visible); + + if span.letter_spacing != 0.0 { + xml.write_svg_attribute(AId::LetterSpacing, &span.letter_spacing); + } + + if span.word_spacing != 0.0 { + xml.write_svg_attribute(AId::WordSpacing, &span.word_spacing); + } + + if let Some(text_length) = span.text_length { + xml.write_svg_attribute(AId::TextLength, &text_length); + } + + if span.length_adjust == LengthAdjust::SpacingAndGlyphs { + xml.write_svg_attribute(AId::LengthAdjust, "spacingAndGlyphs"); + } + + if span.small_caps { + xml.write_svg_attribute(AId::FontVariant, "small-caps"); + } + + if span.paint_order == PaintOrder::StrokeAndFill { + xml.write_svg_attribute(AId::PaintOrder, "stroke fill"); + } + + if !span.apply_kerning { + xml.write_attribute("style", "font-kerning:none"); + } + + if span.dominant_baseline != DominantBaseline::Auto { + let name = match span.dominant_baseline { + DominantBaseline::UseScript => "use-script", + DominantBaseline::NoChange => "no-change", + DominantBaseline::ResetSize => "reset-size", + DominantBaseline::TextBeforeEdge => "text-before-edge", + DominantBaseline::Middle => "middle", + DominantBaseline::Central => "central", + DominantBaseline::TextAfterEdge => "text-after-edge", + DominantBaseline::Ideographic => "ideographic", + DominantBaseline::Alphabetic => "alphabetic", + DominantBaseline::Hanging => "hanging", + DominantBaseline::Mathematical => "mathematical", + DominantBaseline::Auto => unreachable!(), + }; + xml.write_svg_attribute(AId::DominantBaseline, name); + } + + if span.alignment_baseline != AlignmentBaseline::Auto { + let name = match span.alignment_baseline { + AlignmentBaseline::Baseline => "baseline", + AlignmentBaseline::BeforeEdge => "before-edge", + AlignmentBaseline::TextBeforeEdge => "text-before-edge", + AlignmentBaseline::Middle => "middle", + AlignmentBaseline::Central => "central", + AlignmentBaseline::AfterEdge => "after-edge", + AlignmentBaseline::TextAfterEdge => "text-after-edge", + AlignmentBaseline::Ideographic => "ideographic", + AlignmentBaseline::Alphabetic => "alphabetic", + AlignmentBaseline::Hanging => "hanging", + AlignmentBaseline::Mathematical => "mathematical", + AlignmentBaseline::Auto => unreachable!(), + }; + xml.write_svg_attribute(AId::AlignmentBaseline, name); + } + + write_fill(&span.fill, is_clip_path, opt, xml); + write_stroke(&span.stroke, opt, xml); + + for baseline_shift in &span.baseline_shift { + xml.start_svg_element(EId::Tspan); + match baseline_shift { + BaselineShift::Baseline => {} + BaselineShift::Number(num) => xml.write_svg_attribute(AId::BaselineShift, num), + BaselineShift::Subscript => xml.write_svg_attribute(AId::BaselineShift, "sub"), + BaselineShift::Superscript => xml.write_svg_attribute(AId::BaselineShift, "super"), + } + } + + let cur_text = &chunk.text[span.start..span.end]; + + xml.write_text(&cur_text.replace('&', "&")); + + // End for each tspan we needed to create for baseline_shift + for _ in &span.baseline_shift { + xml.end_element(); + } + + xml.end_element(); +} diff --git a/third_party/usvg/tests/files/clip-path-with-complex-text-expected.svg b/third_party/usvg/tests/files/clip-path-with-complex-text-expected.svg new file mode 100644 index 0000000000..60743f1120 --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-complex-text-expected.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/clip-path-with-complex-text.svg b/third_party/usvg/tests/files/clip-path-with-complex-text.svg new file mode 100644 index 0000000000..4fd28a1c10 --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-complex-text.svg @@ -0,0 +1,13 @@ + + + + + + + + + Text + + + diff --git a/third_party/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg b/third_party/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg new file mode 100644 index 0000000000..5f685f2f78 --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/clip-path-with-object-units-multi-use.svg b/third_party/usvg/tests/files/clip-path-with-object-units-multi-use.svg new file mode 100644 index 0000000000..ddc3c339f4 --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/third_party/usvg/tests/files/clip-path-with-text-expected.svg b/third_party/usvg/tests/files/clip-path-with-text-expected.svg new file mode 100644 index 0000000000..6a095e7630 --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-text-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/third_party/usvg/tests/files/clip-path-with-text.svg b/third_party/usvg/tests/files/clip-path-with-text.svg new file mode 100644 index 0000000000..71f2348a2d --- /dev/null +++ b/third_party/usvg/tests/files/clip-path-with-text.svg @@ -0,0 +1,9 @@ + + + + + + Text + + + diff --git a/third_party/usvg/tests/files/ellipse-simple-case-expected.svg b/third_party/usvg/tests/files/ellipse-simple-case-expected.svg new file mode 100644 index 0000000000..50aa29c5e5 --- /dev/null +++ b/third_party/usvg/tests/files/ellipse-simple-case-expected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/third_party/usvg/tests/files/ellipse-simple-case.svg b/third_party/usvg/tests/files/ellipse-simple-case.svg new file mode 100644 index 0000000000..81c3492c96 --- /dev/null +++ b/third_party/usvg/tests/files/ellipse-simple-case.svg @@ -0,0 +1,4 @@ + + + + diff --git a/third_party/usvg/tests/files/filter-id-with-prefix-expected.svg b/third_party/usvg/tests/files/filter-id-with-prefix-expected.svg new file mode 100644 index 0000000000..baffe59b72 --- /dev/null +++ b/third_party/usvg/tests/files/filter-id-with-prefix-expected.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/filter-id-with-prefix.svg b/third_party/usvg/tests/files/filter-id-with-prefix.svg new file mode 100644 index 0000000000..252a3a878a --- /dev/null +++ b/third_party/usvg/tests/files/filter-id-with-prefix.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party/usvg/tests/files/filter-with-object-units-multi-use-expected.svg b/third_party/usvg/tests/files/filter-with-object-units-multi-use-expected.svg new file mode 100644 index 0000000000..59947b6fee --- /dev/null +++ b/third_party/usvg/tests/files/filter-with-object-units-multi-use-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/filter-with-object-units-multi-use.svg b/third_party/usvg/tests/files/filter-with-object-units-multi-use.svg new file mode 100644 index 0000000000..1947879473 --- /dev/null +++ b/third_party/usvg/tests/files/filter-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg b/third_party/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg new file mode 100644 index 0000000000..b885c28869 --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-clip-path-for-symbol.svg b/third_party/usvg/tests/files/generate-id-clip-path-for-symbol.svg new file mode 100644 index 0000000000..4386d6234a --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-clip-path-for-symbol.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-filter-function-v1-expected.svg b/third_party/usvg/tests/files/generate-id-filter-function-v1-expected.svg new file mode 100644 index 0000000000..be5817e283 --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-filter-function-v1-expected.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-filter-function-v1.svg b/third_party/usvg/tests/files/generate-id-filter-function-v1.svg new file mode 100644 index 0000000000..74d93d6369 --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-filter-function-v1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-filter-function-v2-expected.svg b/third_party/usvg/tests/files/generate-id-filter-function-v2-expected.svg new file mode 100644 index 0000000000..066d40aeff --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-filter-function-v2-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/generate-id-filter-function-v2.svg b/third_party/usvg/tests/files/generate-id-filter-function-v2.svg new file mode 100644 index 0000000000..92cc0e104d --- /dev/null +++ b/third_party/usvg/tests/files/generate-id-filter-function-v2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/mask-with-object-units-multi-use-expected.svg b/third_party/usvg/tests/files/mask-with-object-units-multi-use-expected.svg new file mode 100644 index 0000000000..98c391b973 --- /dev/null +++ b/third_party/usvg/tests/files/mask-with-object-units-multi-use-expected.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/mask-with-object-units-multi-use.svg b/third_party/usvg/tests/files/mask-with-object-units-multi-use.svg new file mode 100644 index 0000000000..4d5478ea25 --- /dev/null +++ b/third_party/usvg/tests/files/mask-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/third_party/usvg/tests/files/optimize-paths-without-markers-expected.svg b/third_party/usvg/tests/files/optimize-paths-without-markers-expected.svg new file mode 100644 index 0000000000..abcf9ba23c --- /dev/null +++ b/third_party/usvg/tests/files/optimize-paths-without-markers-expected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party/usvg/tests/files/optimize-paths-without-markers.svg b/third_party/usvg/tests/files/optimize-paths-without-markers.svg new file mode 100644 index 0000000000..3430f7d9b1 --- /dev/null +++ b/third_party/usvg/tests/files/optimize-paths-without-markers.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/third_party/usvg/tests/files/path-simple-case-expected.svg b/third_party/usvg/tests/files/path-simple-case-expected.svg new file mode 100644 index 0000000000..4b8c723c9e --- /dev/null +++ b/third_party/usvg/tests/files/path-simple-case-expected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/third_party/usvg/tests/files/path-simple-case.svg b/third_party/usvg/tests/files/path-simple-case.svg new file mode 100644 index 0000000000..bbac550600 --- /dev/null +++ b/third_party/usvg/tests/files/path-simple-case.svg @@ -0,0 +1,3 @@ + + + diff --git a/third_party/usvg/tests/files/preserve-id-clip-path-v1-expected.svg b/third_party/usvg/tests/files/preserve-id-clip-path-v1-expected.svg new file mode 100644 index 0000000000..d38c8788ae --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-clip-path-v1-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-clip-path-v1.svg b/third_party/usvg/tests/files/preserve-id-clip-path-v1.svg new file mode 100644 index 0000000000..3cba0ca357 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-clip-path-v1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-clip-path-v2-expected.svg b/third_party/usvg/tests/files/preserve-id-clip-path-v2-expected.svg new file mode 100644 index 0000000000..4a1afc37bd --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-clip-path-v2-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-clip-path-v2.svg b/third_party/usvg/tests/files/preserve-id-clip-path-v2.svg new file mode 100644 index 0000000000..d629ba90d7 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-clip-path-v2.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-fe-image-expected.svg b/third_party/usvg/tests/files/preserve-id-fe-image-expected.svg new file mode 100644 index 0000000000..34f589c70b --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-fe-image-expected.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg b/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg new file mode 100644 index 0000000000..7bff1915be --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity.svg b/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity.svg new file mode 100644 index 0000000000..24be35e082 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-fe-image-with-opacity.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-fe-image.svg b/third_party/usvg/tests/files/preserve-id-fe-image.svg new file mode 100644 index 0000000000..c1d51b558d --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-fe-image.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-filter-expected.svg b/third_party/usvg/tests/files/preserve-id-filter-expected.svg new file mode 100644 index 0000000000..092ed764a8 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-filter-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-filter.svg b/third_party/usvg/tests/files/preserve-id-filter.svg new file mode 100644 index 0000000000..a4b2830ef7 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-filter.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg b/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg new file mode 100644 index 0000000000..aaf08b0df2 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg b/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg new file mode 100644 index 0000000000..2f4a7362ad --- /dev/null +++ b/third_party/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-clip-path-expected.svg b/third_party/usvg/tests/files/preserve-text-in-clip-path-expected.svg new file mode 100644 index 0000000000..be65d4e035 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-clip-path-expected.svg @@ -0,0 +1,11 @@ + + + + + abcdefghijklmnopqrstuvwxyz + + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-clip-path.svg b/third_party/usvg/tests/files/preserve-text-in-clip-path.svg new file mode 100644 index 0000000000..ef7ee7a330 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-clip-path.svg @@ -0,0 +1,14 @@ + + + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-mask-expected.svg b/third_party/usvg/tests/files/preserve-text-in-mask-expected.svg new file mode 100644 index 0000000000..0df53edeb9 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-mask-expected.svg @@ -0,0 +1,12 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-mask.svg b/third_party/usvg/tests/files/preserve-text-in-mask.svg new file mode 100644 index 0000000000..33f6f31ab4 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-mask.svg @@ -0,0 +1,12 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-pattern-expected.svg b/third_party/usvg/tests/files/preserve-text-in-pattern-expected.svg new file mode 100644 index 0000000000..2c37596772 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-pattern-expected.svg @@ -0,0 +1,8 @@ + + + + H + + + + diff --git a/third_party/usvg/tests/files/preserve-text-in-pattern.svg b/third_party/usvg/tests/files/preserve-text-in-pattern.svg new file mode 100644 index 0000000000..54c0b2fa0c --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-in-pattern.svg @@ -0,0 +1,7 @@ + + + H + + + diff --git a/third_party/usvg/tests/files/preserve-text-multiple-font-families-expected.svg b/third_party/usvg/tests/files/preserve-text-multiple-font-families-expected.svg new file mode 100644 index 0000000000..2d6e7d314f --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-multiple-font-families-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/preserve-text-multiple-font-families.svg b/third_party/usvg/tests/files/preserve-text-multiple-font-families.svg new file mode 100644 index 0000000000..71fe961dfa --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-multiple-font-families.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/preserve-text-on-path-expected.svg b/third_party/usvg/tests/files/preserve-text-on-path-expected.svg new file mode 100644 index 0000000000..cf58902048 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-on-path-expected.svg @@ -0,0 +1,7 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + diff --git a/third_party/usvg/tests/files/preserve-text-on-path.svg b/third_party/usvg/tests/files/preserve-text-on-path.svg new file mode 100644 index 0000000000..5d4392ee52 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-on-path.svg @@ -0,0 +1,15 @@ + + + + + + + + + + abcdefghijklmnopqrstuvwxyz + + + diff --git a/third_party/usvg/tests/files/preserve-text-simple-case-expected.svg b/third_party/usvg/tests/files/preserve-text-simple-case-expected.svg new file mode 100644 index 0000000000..4ca409156f --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-simple-case-expected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-simple-case.svg b/third_party/usvg/tests/files/preserve-text-simple-case.svg new file mode 100644 index 0000000000..573bf4224d --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-simple-case.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg b/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg new file mode 100644 index 0000000000..dd8b5a34fd --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration.svg b/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration.svg new file mode 100644 index 0000000000..d1c8111fdd --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-complex-text-decoration.svg @@ -0,0 +1,12 @@ + + + + + + + Text + + + + diff --git a/third_party/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg b/third_party/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg new file mode 100644 index 0000000000..058a379919 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/preserve-text-with-dx-and-dy.svg b/third_party/usvg/tests/files/preserve-text-with-dx-and-dy.svg new file mode 100644 index 0000000000..4ce13d3e96 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-dx-and-dy.svg @@ -0,0 +1,6 @@ + + + Text + + diff --git a/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg b/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg new file mode 100644 index 0000000000..6808a8e8af --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg @@ -0,0 +1,4 @@ + + + A B C D E F + diff --git a/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg b/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg new file mode 100644 index 0000000000..a8b5aefc82 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg @@ -0,0 +1,25 @@ + + + + + + + A + + B + + C + + D + + E + + F + + + + + + + diff --git a/third_party/usvg/tests/files/preserve-text-with-rotate-expected.svg b/third_party/usvg/tests/files/preserve-text-with-rotate-expected.svg new file mode 100644 index 0000000000..70f2e13693 --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-rotate-expected.svg @@ -0,0 +1,4 @@ + + + Some long Text + diff --git a/third_party/usvg/tests/files/preserve-text-with-rotate.svg b/third_party/usvg/tests/files/preserve-text-with-rotate.svg new file mode 100644 index 0000000000..11eaa4940f --- /dev/null +++ b/third_party/usvg/tests/files/preserve-text-with-rotate.svg @@ -0,0 +1,7 @@ + + + Some long + Text + + diff --git a/third_party/usvg/tests/files/resolve-css-style-expected.svg b/third_party/usvg/tests/files/resolve-css-style-expected.svg new file mode 100644 index 0000000000..d1902da58f --- /dev/null +++ b/third_party/usvg/tests/files/resolve-css-style-expected.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/third_party/usvg/tests/files/resolve-css-style.svg b/third_party/usvg/tests/files/resolve-css-style.svg new file mode 100644 index 0000000000..2a8e14624e --- /dev/null +++ b/third_party/usvg/tests/files/resolve-css-style.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/third_party/usvg/tests/files/text-simple-case-expected.svg b/third_party/usvg/tests/files/text-simple-case-expected.svg new file mode 100644 index 0000000000..4ca409156f --- /dev/null +++ b/third_party/usvg/tests/files/text-simple-case-expected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party/usvg/tests/files/text-simple-case.svg b/third_party/usvg/tests/files/text-simple-case.svg new file mode 100644 index 0000000000..84dd249695 --- /dev/null +++ b/third_party/usvg/tests/files/text-simple-case.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/third_party/usvg/tests/files/text-with-generated-gradients-expected.svg b/third_party/usvg/tests/files/text-with-generated-gradients-expected.svg new file mode 100644 index 0000000000..b6fb94465f --- /dev/null +++ b/third_party/usvg/tests/files/text-with-generated-gradients-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/third_party/usvg/tests/files/text-with-generated-gradients.svg b/third_party/usvg/tests/files/text-with-generated-gradients.svg new file mode 100644 index 0000000000..82da709424 --- /dev/null +++ b/third_party/usvg/tests/files/text-with-generated-gradients.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Hello مرحبا. + + diff --git a/third_party/usvg/tests/parser.rs b/third_party/usvg/tests/parser.rs new file mode 100644 index 0000000000..7b34806d86 --- /dev/null +++ b/third_party/usvg/tests/parser.rs @@ -0,0 +1,549 @@ +// Copyright 2018 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use tiny_skia_path::Rect; +use usvg::Color; + +#[test] +fn clippath_with_invalid_child() { + let svg = " + + + + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + // clipPath is invalid and should be removed together with rect. + assert!(!tree.root().has_children()); +} + +#[test] +fn stylesheet_injection() { + let svg = " + + + + + + + +"; + + let stylesheet = "rect { fill: red }".to_string(); + + let options = usvg::Options { + style_sheet: Some(stylesheet), + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_str(&svg, &options).unwrap(); + + let usvg::Node::Path(ref first) = &tree.root().children()[0] else { + unreachable!() + }; + + // Only the rects with no CSS attributes should be overridden. + assert_eq!( + first.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref second) = &tree.root().children()[1] else { + unreachable!() + }; + assert_eq!( + second.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[2] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(0, 128, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[3] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(0, 128, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[3] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(0, 128, 0)) + ); +} + +#[test] +fn stylesheet_injection_with_important() { + let svg = " + + + + + + + +"; + + let stylesheet = "rect { fill: red !important }".to_string(); + + let options = usvg::Options { + style_sheet: Some(stylesheet), + ..usvg::Options::default() + }; + + let tree = usvg::Tree::from_str(&svg, &options).unwrap(); + + let usvg::Node::Path(ref first) = &tree.root().children()[0] else { + unreachable!() + }; + + // All rects should be overridden, since we use `important`. + assert_eq!( + first.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref second) = &tree.root().children()[1] else { + unreachable!() + }; + assert_eq!( + second.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[2] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[3] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); + + let usvg::Node::Path(ref third) = &tree.root().children()[4] else { + unreachable!() + }; + assert_eq!( + third.fill().unwrap().paint(), + &usvg::Paint::Color(Color::new_rgb(255, 0, 0)) + ); +} + +#[test] +fn simplify_paths() { + let svg = " + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + let path = &tree.root().children()[0]; + match path { + usvg::Node::Path(ref path) => { + // Make sure we have MLZ and not MLZZZ + assert_eq!(path.data().verbs().len(), 3); + } + _ => unreachable!(), + }; +} + +#[test] +fn size_detection_1() { + let svg = ""; + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(10.0, 20.0).unwrap()); +} + +#[test] +fn size_detection_2() { + let svg = + ""; + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(30.0, 40.0).unwrap()); +} + +#[test] +fn size_detection_3() { + let svg = + ""; + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(5.0, 20.0).unwrap()); +} + +#[test] +fn size_detection_4() { + let svg = " + + + + "; + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap()); + assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap()); +} + +#[test] +fn size_detection_5() { + let svg = ""; + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(100.0, 100.0).unwrap()); +} + +#[test] +fn invalid_size_1() { + let svg = ""; + let result = usvg::Tree::from_str(&svg, &usvg::Options::default()); + assert!(result.is_err()); +} + +#[test] +fn tree_is_send_and_sync() { + fn ensure_send_and_sync() {} + ensure_send_and_sync::(); +} + +#[test] +fn path_transform() { + let svg = " + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.root().children().len(), 1); + + let group_node = &tree.root().children()[0]; + assert!(matches!(group_node, usvg::Node::Group(_))); + assert_eq!( + group_node.abs_transform(), + usvg::Transform::from_translate(10.0, 0.0) + ); + + let group = match group_node { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!( + path.abs_transform(), + usvg::Transform::from_translate(10.0, 0.0) + ); +} + +#[test] +fn path_transform_nested() { + let svg = " + + + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.root().children().len(), 1); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!( + group_node1.abs_transform(), + usvg::Transform::from_translate(20.0, 0.0) + ); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!( + group_node2.abs_transform(), + usvg::Transform::from_translate(30.0, 0.0) + ); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!( + path.abs_transform(), + usvg::Transform::from_translate(30.0, 0.0) + ); +} + +#[test] +fn path_transform_in_symbol_no_clip() { + let svg = " + + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "use1"); + assert_eq!(group_node1.abs_transform(), usvg::Transform::default()); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!( + group_node2.abs_transform(), + usvg::Transform::from_translate(20.0, 0.0) + ); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!( + path.abs_transform(), + usvg::Transform::from_translate(20.0, 0.0) + ); +} + +#[test] +fn path_transform_in_symbol_with_clip() { + let svg = " + + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + // + // + // + // + // + // + // + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "use1"); + assert_eq!(group_node1.abs_transform(), usvg::Transform::default()); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.abs_transform(), usvg::Transform::default()); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node3 = &group2.children()[0]; + assert!(matches!(group_node3, usvg::Node::Group(_))); + assert_eq!( + group_node3.abs_transform(), + usvg::Transform::from_translate(20.0, 0.0) + ); + + let group3 = match group_node3 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group3.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!( + path.abs_transform(), + usvg::Transform::from_translate(20.0, 0.0) + ); +} + +#[test] +fn path_transform_in_svg() { + let svg = " + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + // + // + // + // + // + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "g1"); + assert_eq!( + group_node1.abs_transform(), + usvg::Transform::from_translate(100.0, 150.0) + ); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.id(), "svg1"); + assert_eq!( + group_node2.abs_transform(), + usvg::Transform::from_translate(100.0, 150.0) + ); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!( + path.abs_transform(), + usvg::Transform::from_translate(100.0, 150.0) + ); +} + +#[test] +fn svg_without_xmlns() { + let svg = " + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(100.0, 100.0).unwrap()); +} + +#[test] +fn image_bbox_with_parent_transform() { + let svg = " + + + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + + let usvg::Node::Group(group_node1) = &tree.root().children()[0] else { + unreachable!() + }; + let usvg::Node::Group(group_node2) = &group_node1.children()[0] else { + unreachable!() + }; + let usvg::Node::Image(image_node) = &group_node2.children()[0] else { + unreachable!() + }; + + assert_eq!( + image_node.abs_bounding_box(), + Rect::from_xywh(35.0, 35.0, 50.0, 50.0).unwrap() + ); +} + +#[test] +fn no_text_nodes() { + let svg = " + + + + + + "; + + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); + assert!(!tree.has_text_nodes()); +} diff --git a/third_party/usvg/tests/write.rs b/third_party/usvg/tests/write.rs new file mode 100644 index 0000000000..3c1641636d --- /dev/null +++ b/third_party/usvg/tests/write.rs @@ -0,0 +1,215 @@ +// Copyright 2023 the Resvg Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::sync::Arc; + +use once_cell::sync::Lazy; + +static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_fonts_dir("../../fixtures/fonts"); + + fontdb.set_serif_family("Noto Serif"); + fontdb.set_sans_serif_family("Noto Sans"); + fontdb.set_monospace_family("Noto Mono"); + + Arc::new(fontdb) +}); + +fn resave(name: &str) { + resave_impl(name, None, false); +} + +fn resave_with_text(name: &str) { + resave_impl(name, None, true); +} + +fn resave_with_prefix(name: &str, id_prefix: &str) { + resave_impl(name, Some(id_prefix.to_string()), false); +} + +fn resave_impl(name: &str, id_prefix: Option, preserve_text: bool) { + let input_svg = std::fs::read_to_string(format!("tests/files/{}.svg", name)).unwrap(); + + let tree = { + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + ..Default::default() + }; + usvg::Tree::from_str(&input_svg, &opt).unwrap() + }; + let xml_opt = usvg::WriteOptions { + id_prefix, + preserve_text, + coordinates_precision: 4, // Reduce noise and file size. + transforms_precision: 4, + ..usvg::WriteOptions::default() + }; + let output_svg = tree.to_string(&xml_opt); + + // std::fs::write( + // format!("tests/files/{}-expected.svg", name), + // output_svg.clone(), + // ) + // .unwrap(); + + let expected_svg = + std::fs::read_to_string(format!("tests/files/{}-expected.svg", name)).unwrap(); + // Do not use `assert_eq` because it produces an unreadable output. + assert!(output_svg == expected_svg); +} + +#[test] +fn path_simple_case() { + resave("path-simple-case"); +} + +#[test] +fn ellipse_simple_case() { + resave("ellipse-simple-case"); +} + +#[test] +fn text_simple_case() { + resave("text-simple-case"); +} + +#[test] +fn preserve_id_filter() { + resave("preserve-id-filter"); +} + +#[test] +fn preserve_id_fe_image() { + resave("preserve-id-fe-image"); +} + +#[test] +fn preserve_id_fe_image_with_opacity() { + resave("preserve-id-fe-image-with-opacity"); +} + +#[test] +fn generate_filter_id_function_v1() { + resave("generate-id-filter-function-v1"); +} + +#[test] +fn generate_filter_id_function_v2() { + resave("generate-id-filter-function-v2"); +} + +#[test] +fn filter_id_with_prefix() { + resave_with_prefix("filter-id-with-prefix", "prefix-"); +} + +#[test] +fn filter_with_object_units_multi_use() { + resave("filter-with-object-units-multi-use"); +} + +#[test] +fn preserve_id_clip_path_v1() { + resave("preserve-id-clip-path-v1"); +} + +#[test] +fn preserve_id_clip_path_v2() { + resave("preserve-id-clip-path-v2"); +} + +#[test] +fn preserve_id_for_clip_path_in_pattern() { + resave("preserve-id-for-clip-path-in-pattern"); +} + +#[test] +fn generate_id_clip_path_for_symbol() { + resave("generate-id-clip-path-for-symbol"); +} + +#[test] +fn clip_path_with_text() { + resave("clip-path-with-text"); +} + +#[test] +fn clip_path_with_complex_text() { + resave("clip-path-with-complex-text"); +} + +#[test] +fn clip_path_with_object_units_multi_use() { + resave("clip-path-with-object-units-multi-use"); +} + +#[test] +fn mask_with_object_units_multi_use() { + resave("mask-with-object-units-multi-use"); +} + +#[test] +fn text_with_generated_gradients() { + resave("text-with-generated-gradients"); +} + +#[test] +fn preserve_text_multiple_font_families() { + resave_with_text("preserve-text-multiple-font-families"); +} + +#[test] +fn preserve_text_on_path() { + resave_with_text("preserve-text-on-path"); +} + +#[test] +fn preserve_text_in_clip_path() { + resave_with_text("preserve-text-in-clip-path"); +} + +#[test] +fn preserve_text_in_mask() { + resave_with_text("preserve-text-in-mask"); +} + +#[test] +fn preserve_text_in_pattern() { + resave_with_text("preserve-text-in-pattern"); +} + +#[test] +fn preserve_text_simple_case() { + resave("preserve-text-simple-case"); +} + +#[test] +fn preserve_text_with_dx_and_dy() { + resave_with_text("preserve-text-with-dx-and-dy"); +} + +#[test] +fn preserve_text_with_rotate() { + resave_with_text("preserve-text-with-rotate"); +} + +#[test] +fn preserve_text_with_complex_text_decoration() { + resave_with_text("preserve-text-with-complex-text-decoration"); +} + +#[test] +fn preserve_text_with_nested_baseline_shift() { + resave_with_text("preserve-text-with-nested-baseline-shift"); +} + +#[test] +fn optimize_paths_without_markers() { + resave("optimize-paths-without-markers"); +} + +#[test] +fn resolve_style() { + resave("resolve-css-style"); +}