Skip to content

Parsers

This module contains the internal parsing functions that convert binary chunk data into Python dataclasses.

Main Parser

parse_project

parse_project(aep_file_path: str | PathLike[str]) -> Project

Parse an After Effects (.aep) project file.

Parameters:

Name Type Description Default
aep_file_path str | PathLike[str]

path to the project file

required
Source code in src/aep_parser/parsers/project.py
def parse_project(aep_file_path: str | os.PathLike[str]) -> Project:
    """
    Parse an After Effects (.aep) project file.

    Args:
        aep_file_path: path to the project file
    """
    file_path = os.fspath(aep_file_path)
    with Aep.from_file(file_path) as aep:
        root_chunks = aep.data.chunks

        root_folder_chunk = find_by_list_type(chunks=root_chunks, list_type="Fold")
        nnhd_chunk = find_by_type(chunks=root_chunks, chunk_type="nnhd")
        nnhd_data = nnhd_chunk.data

        project = Project(
            bits_per_channel=map_bits_per_channel(
                get_enum_value(nnhd_data.bits_per_channel)
            ),
            effect_names=_get_effect_names(root_chunks),
            expression_engine=_get_expression_engine(root_chunks),  # CC 2019+
            file=file_path,
            footage_timecode_display_start_type=map_footage_timecode_display_start_type(
                get_enum_value(nnhd_data.footage_timecode_display_start_type)
            ),
            frame_rate=nnhd_data.frame_rate,
            frames_count_type=map_frames_count_type(
                get_enum_value(nnhd_data.frames_count_type)
            ),
            project_items={},
            time_display_type=map_time_display_type(
                get_enum_value(nnhd_data.time_display_type)
            ),
        )

        project.xmp_packet = ET.fromstring(aep.xmp_packet)
        software_agents = project.xmp_packet.findall(
            path=SOFTWARE_AGENT_XPATH, namespaces=XMP_NAMESPACES
        )
        if software_agents:
            project.ae_version = software_agents[-1].text

        root_folder = parse_folder(
            is_root=True,
            child_chunks=root_folder_chunk.data.chunks,
            project=project,
            item_id=0,
            item_name="root",
            label=Aep.Label(0),
            parent_folder=None,
            comment="",
        )
        project.project_items[0] = root_folder

        # Link layers to their source items and parent layers
        for composition in project.compositions:
            # Build layer lookup by id for this composition
            layers_by_id = {layer.layer_id: layer for layer in composition.layers}
            for layer in composition.layers:
                if layer.layer_type == Aep.LayerType.footage:
                    layer.source = project.project_items.get(layer.source_id)
                if layer.parent_id is not None:
                    layer.parent = layers_by_id.get(layer.parent_id)

        return project

Item Parsers

parse_composition

parse_composition(child_chunks: list[Chunk], item_id: int, item_name: str, label: Label, parent_folder: Folder | None, comment: str) -> CompItem

Parse a composition item. Args: child_chunks: child chunks of the composition LIST chunk. item_id: The unique item ID. item_name: The composition name. label: The label color. Colors are represented by their number (0 for None, or 1 to 16 for one of the preset colors in the Labels preferences). parent_folder: The composition's parent folder. comment: The composition comment.

Source code in src/aep_parser/parsers/composition.py
def parse_composition(
    child_chunks: list[Aep.Chunk],
    item_id: int,
    item_name: str,
    label: Aep.Label,
    parent_folder: Folder | None,
    comment: str,
) -> CompItem:
    """
    Parse a composition item.
    Args:
        child_chunks: child chunks of the composition LIST chunk.
        item_id: The unique item ID.
        item_name: The composition name.
        label: The label color. Colors are represented by their number (0 for
            None, or 1 to 16 for one of the preset colors in the Labels
            preferences).
        parent_folder: The composition's parent folder.
        comment: The composition comment.
    """
    cdta_chunk = find_by_type(chunks=child_chunks, chunk_type="cdta")
    cdta_data = cdta_chunk.data

    # Normalize bg_color from 0-255 to 0-1 range to match ExtendScript output
    bg_color = [c / 255 for c in cdta_data.bg_color]

    composition = CompItem(
        comment=comment,
        item_id=item_id,
        label=label,
        name=item_name,
        type_name="Composition",
        parent_folder=parent_folder,
        duration=cdta_data.duration,
        frame_duration=int(
            cdta_data.frame_duration
        ),  # in JSX API, this value is 1 / frame_rate (the duration of a frame). Here, duration * frame_rate
        frame_rate=cdta_data.frame_rate,
        height=cdta_data.height,
        pixel_aspect=cdta_data.pixel_aspect,
        width=cdta_data.width,
        bg_color=bg_color,
        frame_blending=cdta_data.frame_blending,
        hide_shy_layers=cdta_data.hide_shy_layers,
        layers=[],
        markers=[],
        motion_blur=cdta_data.motion_blur,
        motion_blur_adaptive_sample_limit=cdta_data.motion_blur_adaptive_sample_limit,
        motion_blur_samples_per_frame=cdta_data.motion_blur_samples_per_frame,
        preserve_nested_frame_rate=cdta_data.preserve_nested_frame_rate,
        preserve_nested_resolution=cdta_data.preserve_nested_resolution,
        shutter_angle=cdta_data.shutter_angle,
        shutter_phase=cdta_data.shutter_phase,
        resolution_factor=cdta_data.resolution_factor,
        time_scale=cdta_data.time_scale,
        in_point=cdta_data.in_point,
        frame_in_point=int(cdta_data.frame_in_point),
        out_point=cdta_data.out_point,
        frame_out_point=int(cdta_data.frame_out_point),
        frame_time=int(cdta_data.frame_time),
        time=cdta_data.time,
        display_start_time=cdta_data.display_start_time,
        display_start_frame=int(cdta_data.display_start_frame),
    )

    composition.markers = _get_markers(
        child_chunks=child_chunks,
        composition=composition,
    )

    # Parse composition's layers
    layer_sub_chunks = filter_by_list_type(chunks=child_chunks, list_type="Layr")
    for layer_chunk in layer_sub_chunks:
        layer = parse_layer(
            layer_chunk=layer_chunk,
            composition=composition,
        )
        composition.layers.append(layer)

    return composition

parse_footage

parse_footage(child_chunks: list[Chunk], item_id: int, item_name: str, label: Label, parent_folder: Folder | None, comment: str) -> FootageItem

Parse a footage item. Args: child_chunks: The footage item child chunks. item_id: The item's unique id. item_name: The item's name. label: The label color. Colors are represented by their number (0 for None, or 1 to 16 for one of the preset colors in the Labels preferences). parent_folder: The item's parent folder. comment: The item's comment.

Source code in src/aep_parser/parsers/footage.py
def parse_footage(
    child_chunks: list[Aep.Chunk],
    item_id: int,
    item_name: str,
    label: Aep.Label,
    parent_folder: Folder | None,
    comment: str,
) -> FootageItem:
    """
    Parse a footage item.
    Args:
        child_chunks: The footage item child chunks.
        item_id: The item's unique id.
        item_name: The item's name.
        label: The label color. Colors are represented by their number (0 for
            None, or 1 to 16 for one of the preset colors in the Labels
            preferences).
        parent_folder: The item's parent folder.
        comment: The item's comment.
    """
    pin_chunk = find_by_list_type(chunks=child_chunks, list_type="Pin ")
    pin_child_chunks = pin_chunk.data.chunks
    sspc_chunk = find_by_type(chunks=pin_child_chunks, chunk_type="sspc")
    opti_chunk = find_by_type(chunks=pin_child_chunks, chunk_type="opti")
    sspc_data = sspc_chunk.data
    opti_data = opti_chunk.data

    asset_type = opti_data.asset_type
    start_frame = sspc_data.start_frame
    end_frame = sspc_data.end_frame

    # Common source attributes from sspc
    source_attrs = {
        "has_alpha": sspc_data.has_alpha,
        "alpha_mode": map_alpha_mode(
            get_enum_value(sspc_data.alpha_mode_raw), sspc_data.has_alpha
        ),
        "invert_alpha": sspc_data.invert_alpha,
        "field_separation_type": map_field_separation_type(
            get_enum_value(sspc_data.field_separation_type_raw),
            get_enum_value(sspc_data.field_order),
        ),
        "high_quality_field_separation": sspc_data.high_quality_field_separation != 0,
        "loop": sspc_data.loop,
        "conform_frame_rate": sspc_data.conform_frame_rate,
        "is_still": sspc_data.duration == 0,
        # premul_color: RGB bytes (0-255) converted to floats (0.0-1.0)
        "premul_color": [
            sspc_data.premul_color_r / 255.0,
            sspc_data.premul_color_g / 255.0,
            sspc_data.premul_color_b / 255.0,
        ],
    }

    if not asset_type:
        asset_type = "placeholder"
        item_name = opti_data.placeholder_name
        main_source = PlaceholderSource(**source_attrs)
    elif asset_type == "Soli":
        asset_type = "solid"
        item_name = opti_data.solid_name
        color = [opti_data.red, opti_data.green, opti_data.blue, opti_data.alpha]
        main_source = SolidSource(color=color, **source_attrs)
    else:
        asset_type = "file"
        main_source = _parse_file_source(pin_child_chunks, source_attrs)

        # If start frame or end frame is undefined, try to get it from the filenames
        if 0xFFFFFFFF in (start_frame, end_frame):
            first_file_numbers = re.findall(r"\d+", main_source.file_names[0])
            last_file_numbers = re.findall(r"\d+", main_source.file_names[-1])
            if len(main_source.file_names) == 1:
                start_frame = end_frame = int(first_file_numbers[-1])
            else:
                for first, last in zip(
                    reversed(first_file_numbers), reversed(last_file_numbers)
                ):
                    if first != last:
                        start_frame = int(first)
                        end_frame = int(last)

        if not item_name:
            item_name = os.path.basename(main_source.file)

    item = FootageItem(
        comment=comment,
        item_id=item_id,
        label=label,
        name=item_name,
        parent_folder=parent_folder,
        type_name="Footage",
        duration=sspc_data.duration,
        frame_duration=int(sspc_data.frame_duration),
        frame_rate=sspc_data.frame_rate,
        height=sspc_data.height,
        pixel_aspect=sspc_data.pixel_aspect,
        width=sspc_data.width,
        main_source=main_source,
        asset_type=asset_type,
        end_frame=end_frame,
        start_frame=start_frame,
    )
    return item

parse_folder

parse_folder(is_root: bool, child_chunks: list[Chunk], project: Project, item_id: int, item_name: str, label: Label, parent_folder: Folder | None, comment: str) -> Folder

Parse a folder item.

This function cannot be moved to its own file as it calls parse_item, which can call parse_folder.

Parameters:

Name Type Description Default
is_root bool

Whether the folder is the root folder (ID 0).

required
child_chunks list[Chunk]

child chunks of the folder LIST chunk.

required
project Project

The project.

required
item_id int

The unique item ID.

required
item_name str

The folder name.

required
label Label

The label color. Colors are represented by their number (0 for None, or 1 to 16 for one of the preset colors in the Labels preferences).

required
parent_folder Folder | None

The folder's parent folder.

required
comment str

The folder comment.

required
Source code in src/aep_parser/parsers/item.py
def parse_folder(
    is_root: bool,
    child_chunks: list[Aep.Chunk],
    project: Project,
    item_id: int,
    item_name: str,
    label: Aep.Label,
    parent_folder: Folder | None,
    comment: str,
) -> Folder:
    """
    Parse a folder item.

    This function cannot be moved to its own file as it calls `parse_item`,
    which can call `parse_folder`.

    Args:
        is_root: Whether the folder is the root folder (ID 0).
        child_chunks: child chunks of the folder LIST chunk.
        project: The project.
        item_id: The unique item ID.
        item_name: The folder name.
        label: The label color. Colors are represented by their number (0 for
            None, or 1 to 16 for one of the preset colors in the Labels
            preferences).
        parent_folder: The folder's parent folder.
        comment: The folder comment.
    """
    folder = Folder(
        comment=comment,
        item_id=item_id,
        label=label,
        name=item_name,
        type_name="Folder",
        parent_folder=parent_folder,
        folder_items=[],
    )
    # Get folder contents
    if is_root:
        child_item_chunks = filter_by_list_type(chunks=child_chunks, list_type="Item")
    else:
        sfdr_chunk = find_by_list_type(chunks=child_chunks, list_type="Sfdr")
        child_item_chunks = filter_by_list_type(
            chunks=sfdr_chunk.data.chunks, list_type="Item"
        )
    for child_item_chunk in child_item_chunks:
        child_item = parse_item(
            item_chunk=child_item_chunk, project=project, parent_folder=folder
        )
        folder.folder_items.append(child_item.item_id)

    return folder

Layer Parser

parse_layer

parse_layer(layer_chunk: Chunk, composition: CompItem) -> Layer

Parse a composition layer.

This layer is an instance of an item in a composition. Some information can only be found on the source item. To access it, use source_item = layer.source.

Parameters:

Name Type Description Default
layer_chunk Chunk

The LIST chunk to parse.

required
composition CompItem

The composition.

required

Returns:

Type Description
Layer

An AVLayer for most layers, or a LightLayer for light layers.

Source code in src/aep_parser/parsers/layer.py
def parse_layer(layer_chunk: Aep.Chunk, composition: CompItem) -> Layer:
    """
    Parse a composition layer.

    This layer is an instance of an item in a composition. Some information can
    only be found on the source item. To access it, use `source_item = layer.source`.

    Args:
        layer_chunk: The LIST chunk to parse.
        composition: The composition.

    Returns:
        An AVLayer for most layers, or a LightLayer for light layers.
    """
    child_chunks = layer_chunk.data.chunks

    comment = get_comment(child_chunks)

    ldta_chunk = find_by_type(chunks=child_chunks, chunk_type="ldta")
    name_chunk = find_by_type(chunks=child_chunks, chunk_type="Utf8")
    name = str_contents(name_chunk)

    ldta_data = ldta_chunk.data
    layer_type = ldta_data.layer_type
    try:
        # ExtendScript stretch is a percentage: 100 = normal, 200 = half speed, -100 = reverse
        stretch = float(ldta_data.stretch_dividend) / ldta_data.stretch_divisor * 100
    except ZeroDivisionError:
        stretch = None

    # Calculate absolute in_point and out_point from relative binary values
    # Binary stores in_point/out_point relative to start_time
    in_point = ldta_data.start_time + ldta_data.in_point
    out_point = ldta_data.start_time + ldta_data.out_point

    # Clamp out_point to composition duration (ExtendScript behavior)
    # Layers cannot extend past the composition's end
    out_point = min(out_point, composition.duration)

    # Parse property groups early to compute time_remap_enabled
    root_tdgp_chunk = find_by_list_type(chunks=child_chunks, list_type="tdgp")
    tdgp_map = get_chunks_by_match_name(root_tdgp_chunk)

    # Time remap is enabled when "ADBE Time Remapping" property has keyframes
    time_remap_props = tdgp_map.get("ADBE Time Remapping", [])
    time_remap_enabled = (
        property_has_keyframes(time_remap_props[0]) if time_remap_props else False
    )

    # Parse transform stack
    transform_tdgp = tdgp_map.get("ADBE Transform Group", [])
    transform = []
    if transform_tdgp:
        transform_prop = parse_property_group(
            tdgp_chunk=transform_tdgp[0],
            group_match_name="ADBE Transform Group",
            time_scale=composition.time_scale,
        )
        transform = transform_prop.properties

    # Parse effects stack
    effects_tdgp = tdgp_map.get("ADBE Effect Parade", [])
    effects = []
    if effects_tdgp:
        effects_prop = parse_property_group(
            tdgp_chunk=effects_tdgp[0],
            group_match_name="ADBE Effect Parade",
            time_scale=composition.time_scale,
        )
        effects = effects_prop.properties

    # Parse text layer properties
    text_tdgp = tdgp_map.get("ADBE Text Properties", [])
    text = None
    if text_tdgp:
        text = parse_property_group(
            tdgp_chunk=text_tdgp[0],
            group_match_name="ADBE Text Properties",
            time_scale=composition.time_scale,
        )

    # Parse markers
    markers_mrst = tdgp_map.get("ADBE Marker", [])
    markers = []
    if markers_mrst:
        markers = parse_markers(
            mrst_chunk=markers_mrst[0],
            group_match_name="ADBE Marker",
            time_scale=composition.time_scale,
            frame_rate=composition.frame_rate,
        )

    # Base Layer attributes (shared by all layer types)
    layer_attrs = {
        "auto_orient": map_auto_orient_type(
            auto_orient_along_path=ldta_data.auto_orient_along_path,
            camera_or_poi_auto_orient=ldta_data.camera_or_poi_auto_orient,
            three_d_layer=ldta_data.three_d_layer,
            characters_toward_camera=ldta_data.characters_toward_camera,
        ),
        "comment": comment,
        "containing_comp": composition,
        "effects": effects,
        "enabled": ldta_data.enabled,
        "frame_in_point": int(round(in_point * composition.frame_rate)),
        "frame_out_point": int(round(out_point * composition.frame_rate)),
        "frame_start_time": int(round(ldta_data.start_time * composition.frame_rate)),
        "in_point": in_point,
        "label": ldta_data.label,
        "layer_id": ldta_data.layer_id,
        "layer_type": layer_type,
        "locked": ldta_data.locked,
        "markers": markers,
        "name": name,
        "null_layer": ldta_data.null_layer,
        "out_point": out_point,
        "parent_id": ldta_data.parent_id,
        "shy": ldta_data.shy,
        "solo": ldta_data.solo,
        "start_time": ldta_data.start_time,
        "stretch": stretch,
        "text": text,
        "time": 0,
        "transform": transform,
    }

    # Additional AVLayer attributes
    av_layer_attrs = {
        "adjustment_layer": ldta_data.adjustment_layer,
        "audio_enabled": ldta_data.audio_enabled,
        "blending_mode": map_blending_mode(get_enum_value(ldta_data.blending_mode)),
        "collapse_transformation": ldta_data.collapse_transformation,
        "effects_active": ldta_data.effects_active,
        "environment_layer": ldta_data.environment_layer,
        "frame_blending": ldta_data.frame_blending,
        "frame_blending_type": map_frame_blending_type(
            get_enum_value(ldta_data.frame_blending_type),
            ldta_data.frame_blending,
        ),
        "guide_layer": ldta_data.guide_layer,
        "motion_blur": ldta_data.motion_blur,
        "preserve_transparency": bool(ldta_data.preserve_transparency),
        "quality": map_layer_quality(get_enum_value(ldta_data.quality)),
        "sampling_quality": map_layer_sampling_quality(
            get_enum_value(ldta_data.sampling_quality)
        ),
        "source_id": ldta_data.source_id,
        "three_d_layer": ldta_data.three_d_layer,
        "time_remap_enabled": time_remap_enabled,
        "track_matte_type": map_track_matte_type(
            get_enum_value(ldta_data.track_matte_type)
        ),
    }

    # Create the appropriate layer type
    layer_type_name = getattr(layer_type, "name", None)
    if layer_type_name == "light":
        layer: Layer = LightLayer(
            **layer_attrs,
            light_type=map_light_type(get_enum_value(ldta_data.light_type)),
        )
    elif layer_type_name == "camera":
        layer = CameraLayer(**layer_attrs)
    elif layer_type_name == "shape":
        layer = ShapeLayer(**layer_attrs, **av_layer_attrs)
    elif layer_type_name == "text":
        layer = TextLayer(**layer_attrs, **av_layer_attrs)
    else:
        layer = AVLayer(**layer_attrs, **av_layer_attrs)

    return layer

Property Parsers

parse_property_group

parse_property_group(tdgp_chunk: Chunk, group_match_name: str, time_scale: float) -> PropertyGroup

Parse a property group.

Parameters:

Name Type Description Default
tdgp_chunk Chunk

The TDGP chunk to parse.

required
group_match_name str

A special name for the property used to build unique naming paths. The match name is not displayed, but you can refer to it in scripts. Every property has a unique match-name identifier. Match names are stable from version to version regardless of the display name (the name attribute value) or any changes to the application. Unlike the display name, it is not localized. An indexed group (PropertyBase.propertyType == Aep.PropertyType.indexed_group) may not have a name value, but always has a match_name value.

required
time_scale float

The time scale of the parent composition, used as a divisor for some frame values.

required
Source code in src/aep_parser/parsers/property.py
def parse_property_group(
    tdgp_chunk: Aep.Chunk, group_match_name: str, time_scale: float
) -> PropertyGroup:
    """
    Parse a property group.

    Args:
        tdgp_chunk: The TDGP chunk to parse.
        group_match_name: A special name for the property used to build unique
            naming paths. The match name is not displayed, but you can refer
            to it in scripts. Every property has a unique match-name
            identifier. Match names are stable from version to version
            regardless of the display name (the name attribute value) or any
            changes to the application. Unlike the display name, it is not
            localized. An indexed group (PropertyBase.propertyType ==
            Aep.PropertyType.indexed_group) may not have a name value, but
            always has a match_name value.
        time_scale: The time scale of the parent composition, used as a divisor
            for some frame values.
    """
    nice_name = MATCH_NAME_TO_NICE_NAME.get(group_match_name, group_match_name)

    properties = []
    chunks_by_sub_prop = get_chunks_by_match_name(tdgp_chunk)
    for match_name, sub_prop_chunks in chunks_by_sub_prop.items():
        first_chunk = sub_prop_chunks[0]
        if first_chunk.data.list_type == "tdgp":
            sub_prop = parse_property_group(
                tdgp_chunk=first_chunk,
                group_match_name=match_name,
                time_scale=time_scale,
            )
        elif first_chunk.data.list_type == "sspc":
            sub_prop = parse_effect(
                sspc_chunk=first_chunk,
                group_match_name=match_name,
                time_scale=time_scale,
            )
        elif first_chunk.data.list_type == "tdbs":
            sub_prop = parse_property(
                tdbs_chunk=first_chunk,
                match_name=match_name,
                time_scale=time_scale,
            )
        elif first_chunk.data.list_type == "otst":
            sub_prop = parse_orientation(
                otst_chunk=first_chunk,
                match_name=match_name,
                time_scale=time_scale,
            )
        elif first_chunk.data.list_type == "btds":
            sub_prop = parse_text_document(
                btds_chunk=first_chunk,
                match_name=match_name,
                time_scale=time_scale,
            )
        else:
            raise NotImplementedError(
                f"Cannot parse {first_chunk.data.list_type} property"
            )
        properties.append(sub_prop)

    prop_group = PropertyGroup(
        match_name=group_match_name,
        name=nice_name,
        is_effect=False,
        properties=properties,
    )

    return prop_group

parse_property

parse_property(tdbs_chunk: Chunk, match_name: str, time_scale: float) -> Property

Parse a property.

Parameters:

Name Type Description Default
tdbs_chunk Chunk

The TDBS chunk to parse.

required
match_name str

A special name for the property used to build unique naming paths. The match name is not displayed, but you can refer to it in scripts. Every property has a unique match-name identifier. Match names are stable from version to version regardless of the display name (the name attribute value) or any changes to the application. Unlike the display name, it is not localized.

required
time_scale float

The time scale of the parent composition, used as a divisor for some frame values.

required
Source code in src/aep_parser/parsers/property.py
def parse_property(
    tdbs_chunk: Aep.Chunk,
    match_name: str,
    time_scale: float,
) -> Property:
    """
    Parse a property.

    Args:
        tdbs_chunk: The TDBS chunk to parse.
        match_name: A special name for the property used to build unique
            naming paths. The match name is not displayed, but you can refer
            to it in scripts. Every property has a unique match-name
            identifier. Match names are stable from version to version
            regardless of the display name (the name attribute value) or any
            changes to the application. Unlike the display name, it is not
            localized.
        time_scale: The time scale of the parent composition, used as a divisor
            for some frame values.
    """
    tdbs_child_chunks = tdbs_chunk.data.chunks

    # Get property settings from tdsb chunk
    tdsb_chunk = find_by_type(chunks=tdbs_child_chunks, chunk_type="tdsb")
    tdsb_data = tdsb_chunk.data
    locked_ratio = tdsb_data.locked_ratio
    enabled = tdsb_data.enabled
    dimensions_separated = tdsb_data.dimensions_separated

    # Get nice name if user-defined
    nice_name = _get_nice_name(tdbs_chunk)
    name = nice_name or MATCH_NAME_TO_NICE_NAME.get(match_name, match_name)

    # Get property type info from tdb4 chunk
    tdb4_chunk = find_by_type(chunks=tdbs_child_chunks, chunk_type="tdb4")
    tdb4_data = tdb4_chunk.data
    is_spatial = tdb4_data.is_spatial
    expression_enabled = tdb4_data.expression_enabled
    animated = tdb4_data.animated
    dimensions = tdb4_data.dimensions
    integer = tdb4_data.integer
    vector = tdb4_data.vector
    no_value = tdb4_data.no_value
    color = tdb4_data.color

    # Determine property control and value types
    property_control_type = Aep.PropertyControlType.unknown
    property_value_type = Aep.PropertyValueType.unknown

    if no_value:
        property_value_type = Aep.PropertyValueType.no_value
    if color:
        property_control_type = Aep.PropertyControlType.color
        property_value_type = Aep.PropertyValueType.color
    elif integer:
        property_control_type = Aep.PropertyControlType.boolean
        property_value_type = Aep.PropertyValueType.one_d
    elif vector:
        if dimensions == 1:
            property_control_type = Aep.PropertyControlType.scalar
            property_value_type = Aep.PropertyValueType.one_d
        elif dimensions == 2:
            property_control_type = Aep.PropertyControlType.two_d
            property_value_type = (
                Aep.PropertyValueType.two_d_spatial
                if is_spatial
                else Aep.PropertyValueType.two_d
            )
        elif dimensions == 3:
            property_control_type = Aep.PropertyControlType.three_d
            property_value_type = (
                Aep.PropertyValueType.three_d_spatial
                if is_spatial
                else Aep.PropertyValueType.three_d
            )

    if property_control_type == Aep.PropertyControlType.unknown:
        print(
            f"Could not determine type for property {match_name}"
            f" | nice_name: {nice_name}"
            f" | dimensions: {dimensions}"
            f" | animated: {animated}"
            f" | integer: {integer}"
            f" | is_spatial: {is_spatial}"
            f" | vector: {vector}"
            f" | no_value: {no_value}"
            f" | color: {color}"
        )

    # Get property value
    value = None
    cdat_chunk = find_by_type(chunks=tdbs_child_chunks, chunk_type="cdat")
    if cdat_chunk is not None:
        value = cdat_chunk.data.value[:dimensions]

    # Get property expression
    expression = None
    utf8_chunk = find_by_type(chunks=tdbs_child_chunks, chunk_type="Utf8")
    if utf8_chunk is not None:
        expression = str_contents(utf8_chunk).splitlines()

    # Get property keyframes
    keyframes = _parse_keyframes(tdbs_child_chunks, time_scale, is_spatial)

    return Property(
        match_name=match_name,
        name=name,
        enabled=enabled,
        property_control_type=property_control_type,
        expression=expression,
        expression_enabled=expression_enabled,
        property_value_type=property_value_type,
        value=value,
        dimensions_separated=dimensions_separated,
        is_spatial=is_spatial,
        locked_ratio=locked_ratio,
        keyframes=keyframes,
        animated=animated,
        dimensions=dimensions,
        integer=integer,
        vector=vector,
        no_value=no_value,
        color=color,
    )

parse_effect

parse_effect(sspc_chunk: Chunk, group_match_name: str, time_scale: float) -> PropertyGroup

Parse an effect.

Parameters:

Name Type Description Default
sspc_chunk Chunk

The SSPC chunk to parse.

required
group_match_name str

A special name for the property used to build unique naming paths. The match name is not displayed, but you can refer to it in scripts. Every property has a unique match-name identifier. Match names are stable from version to version regardless of the display name (the name attribute value) or any changes to the application. Unlike the display name, it is not localized. An indexed group (PropertyBase.propertyType == Aep.PropertyType.indexed_group) may not have a name value, but always has a match_name value.

required
time_scale float

The time scale of the parent composition, used as a divisor for some frame values.

required
Source code in src/aep_parser/parsers/property.py
def parse_effect(
    sspc_chunk: Aep.Chunk, group_match_name: str, time_scale: float
) -> PropertyGroup:
    """
    Parse an effect.

    Args:
        sspc_chunk: The SSPC chunk to parse.
        group_match_name: A special name for the property used to build unique
            naming paths. The match name is not displayed, but you can refer
            to it in scripts. Every property has a unique match-name
            identifier. Match names are stable from version to version
            regardless of the display name (the name attribute value) or any
            changes to the application. Unlike the display name, it is not
            localized. An indexed group (PropertyBase.propertyType ==
            Aep.PropertyType.indexed_group) may not have a name value, but
            always has a match_name value.
        time_scale: The time scale of the parent composition, used as a divisor
            for some frame values.
    """
    sspc_child_chunks = sspc_chunk.data.chunks
    fnam_chunk = find_by_type(chunks=sspc_child_chunks, chunk_type="fnam")
    utf8_chunk = fnam_chunk.data.chunk
    tdgp_chunk = find_by_list_type(chunks=sspc_child_chunks, list_type="tdgp")
    nice_name = _get_nice_name(tdgp_chunk) or str_contents(utf8_chunk)

    # Get effect parameter definitions from parT chunk
    param_defs: dict[str, dict] = {}
    part_chunk = find_by_list_type(chunks=sspc_child_chunks, list_type="parT")
    if part_chunk:
        chunks_by_parameter = get_chunks_by_match_name(part_chunk)
        for index, (match_name, parameter_chunks) in enumerate(
            chunks_by_parameter.items()
        ):
            # Skip first, it describes parent
            if index == 0:
                continue
            param_defs[match_name] = _parse_effect_parameter_def(parameter_chunks)

    # Parse properties and merge with parameter definitions
    properties: list[Property] = []
    chunks_by_property = get_chunks_by_match_name(tdgp_chunk)
    for match_name, prop_chunks in chunks_by_property.items():
        first_chunk = prop_chunks[0]
        if first_chunk.data.list_type == "tdbs":
            prop = parse_property(
                tdbs_chunk=first_chunk,
                match_name=match_name,
                time_scale=time_scale,
            )
            # Merge parameter definition if available
            if match_name in param_defs:
                param_def = param_defs[match_name]
                prop.name = param_def.get("name") or prop.name
                prop.property_control_type = param_def.get(
                    "property_control_type", prop.property_control_type
                )
                prop.property_value_type = param_def.get(
                    "property_value_type", prop.property_value_type
                )
                prop.last_value = param_def.get("last_value")
                prop.default_value = param_def.get("default_value")
                prop.min_value = param_def.get("min_value") or prop.min_value
                prop.max_value = param_def.get("max_value") or prop.max_value
                prop.nb_options = param_def.get("nb_options")
                prop.property_parameters = param_def.get("property_parameters")
            properties.append(prop)
        elif first_chunk.data.list_type == "tdgp":
            # Encountered with "ADBE FreePin3" effect (Obsolete > Puppet)
            pass
        else:
            raise NotImplementedError(
                f"Cannot parse parameter value : {first_chunk.data.list_type}"
            )

    return PropertyGroup(
        match_name=group_match_name,
        name=nice_name,
        is_effect=True,
        properties=properties,
    )

parse_markers

parse_markers(mrst_chunk: Chunk, group_match_name: str, time_scale: float, frame_rate: float) -> list[Marker]

Parse markers.

Parameters:

Name Type Description Default
mrst_chunk Chunk

The MRST chunk to parse.

required
group_match_name str

A special name for the property used to build unique naming paths. The match name is not displayed, but you can refer to it in scripts. Every property has a unique match-name identifier. Match names are stable from version to version regardless of the display name (the name attribute value) or any changes to the application. Unlike the display name, it is not localized. An indexed group (PropertyBase.propertyType == Aep.PropertyType.indexed_group) may not have a name value, but always has a match_name value.

required
time_scale float

The time scale of the parent composition, used as a divisor for some frame values.

required
frame_rate float

The frame rate of the parent composition, used to compute marker duration in seconds.

required
Source code in src/aep_parser/parsers/property.py
def parse_markers(
    mrst_chunk: Aep.Chunk, group_match_name: str, time_scale: float, frame_rate: float
) -> list[Marker]:
    """
    Parse markers.

    Args:
        mrst_chunk: The MRST chunk to parse.
        group_match_name: A special name for the property used to build unique
            naming paths. The match name is not displayed, but you can refer
            to it in scripts. Every property has a unique match-name
            identifier. Match names are stable from version to version
            regardless of the display name (the name attribute value) or any
            changes to the application. Unlike the display name, it is not
            localized. An indexed group (PropertyBase.propertyType ==
            Aep.PropertyType.indexed_group) may not have a name value, but
            always has a match_name value.
        time_scale: The time scale of the parent composition, used as a divisor
            for some frame values.
        frame_rate: The frame rate of the parent composition, used to compute
            marker duration in seconds.
    """
    tdbs_chunk = find_by_list_type(chunks=mrst_chunk.data.chunks, list_type="tdbs")
    # Get keyframes (markers time)
    marker_group = parse_property(
        tdbs_chunk=tdbs_chunk,
        match_name=group_match_name,
        time_scale=time_scale,
    )
    mrky_chunk = find_by_list_type(chunks=mrst_chunk.data.chunks, list_type="mrky")
    # Get each marker with its frame_time
    nmrd_chunks = filter_by_list_type(chunks=mrky_chunk.data.chunks, list_type="Nmrd")
    markers = []
    for i, nmrd_chunk in enumerate(nmrd_chunks):
        frame_time = marker_group.keyframes[i].frame_time
        marker = parse_marker(
            nmrd_chunk=nmrd_chunk,
            frame_rate=frame_rate,
            frame_time=frame_time,
        )
        markers.append(marker)
    return markers