mirrored 9 minutes ago
0
Eric PateyFix crash caused by referencing an unbound local variable. (#128) Co-authored-by: Eric Patey <>bf3f054
import logging
import xml.etree.ElementTree as ET
import zipfile
from math import sqrt

from pptx import Presentation
from pptx.util import Inches

logger = logging.getLogger("desktopenv.metric.slides")


def check_presenter_console_disable(config_file_path):
    try:
        tree = ET.parse(config_file_path)
        root = tree.getroot()

        namespaces = {
            'oor': 'http://openoffice.org/2001/registry'
        }

        for item in root.findall(
                ".//item[@oor:path='/org.openoffice.Office.Impress/Misc/Start']/prop[@oor:name='EnablePresenterScreen']",
                namespaces):
            # Check if the value of the configuration item indicates that the presenter console has been disabled
            presenter_screen_enabled = item.find('value').text
            if presenter_screen_enabled.lower() == 'false':
                return 1.
            else:
                return 0.
        return 0.
    except Exception as e:
        logger.error(f"Error: {e}")
        return 0.


def check_image_stretch_and_center(modified_ppt, original_ppt):
    # fixme: this func is overfit to this example libreoffice_impress
    # Load the presentations
    original_pres = Presentation(original_ppt)
    modified_pres = Presentation(modified_ppt)

    # Get the first slide of each presentation
    original_slide = original_pres.slides[0]
    modified_slide = modified_pres.slides[0]

    # Get the image on the first slide of each presentation
    original_slide_images = [shape for shape in original_slide.shapes if shape.shape_type == 13]
    modified_slide_images = [shape for shape in modified_slide.shapes if shape.shape_type == 13]

    the_image = original_slide_images[0]

    the_modified_image = None

    # Get the images that modified in width and height
    for modified_image in modified_slide_images:
        if the_image.image.blob == modified_image.image.blob:
            the_modified_image = modified_image

    if the_modified_image is None:
        return 0.

    if (abs(the_modified_image.width - original_pres.slide_width) > Inches(0.5) or
            abs(the_modified_image.height - original_pres.slide_height) > Inches(0.5) or
            abs(the_modified_image.left - (original_pres.slide_width - the_modified_image.width) / 2) > Inches(0.5) or
            abs(the_modified_image.top - (original_pres.slide_height - the_modified_image.height) / 2) > Inches(0.5)):
        return 0.

    return 1.


def is_red_color(color):
    # judge if the color is red
    return color and color.rgb == (255, 0, 0)


def get_master_placeholder_color(prs):
    # get the color of the placeholder
    masters = prs.slide_masters
    for idx, master in enumerate(masters):
        for placeholder in master.placeholders:
            if placeholder.has_text_frame and placeholder.text == "<number>":
                text_frame = placeholder.text_frame

                if text_frame.paragraphs:
                    first_paragraph = text_frame.paragraphs[0]
                    return first_paragraph.font.color
    return None


def check_slide_numbers_color(pptx_file_path):
    presentation = Presentation(pptx_file_path)

    for i, slide in enumerate(presentation.slides):
        for shape in slide.shapes:
            # check if the shape is a text box
            if hasattr(shape, "text"):
                if shape.text.isdigit():
                    # "SlidePlaceholder" is the name of the placeholder in the master slide
                    page_number_text = shape.text
                    font_color = get_master_placeholder_color(presentation)
                    return 1 if font_color is not None and is_red_color(font_color) else 0


# import numpy as np
# from PIL import Image
# from skimage.metrics import structural_similarity as ssim

# def compare_images(image1_path, image2_path):
#     # You would call this function with the paths to the two images you want to compare:
#     # score = compare_images('path_to_image1', 'path_to_image2')
#     # print("Similarity score:", score)

#     if not image1_path or not image2_path:
#         return 0

#     # Open the images and convert to grayscale
#     image1 = Image.open(image1_path).convert('L')
#     image2 = Image.open(image2_path).convert('L')

#     # Resize images to the smaller one's size for comparison
#     image1_size = image1.size
#     image2_size = image2.size
#     new_size = min(image1_size, image2_size)

#     image1 = image1.resize(new_size, Image.Resampling.LANCZOS)
#     image2 = image2.resize(new_size, Image.Resampling.LANCZOS)

#     # Convert images to numpy arrays
#     image1_array = np.array(image1)
#     image2_array = np.array(image2)

#     # Calculate SSIM between two images
#     similarity_index = ssim(image1_array, image2_array)

#     return similarity_index

def compare_pptx_files(file1_path, file2_path, **options):
    # todo: not strictly match since not all information is compared because we cannot get the info through pptx
    prs1 = Presentation(file1_path)
    prs2 = Presentation(file2_path)

    examine_number_of_slides = options.get("examine_number_of_slides", True)
    examine_shape = options.get("examine_shape", True)
    examine_text = options.get("examine_text", True)
    examine_indent = options.get("examine_indent", True)
    examine_font_name = options.get("examine_font_name", True)
    examine_font_size = options.get("examine_font_size", True)
    examine_font_bold = options.get("examine_font_bold", True)
    examine_font_italic = options.get("examine_font_italic", True)
    examine_color_rgb = options.get("examine_color_rgb", True)
    examine_font_underline = options.get("examine_font_underline", True)
    examine_strike_through = options.get("examine_strike_through", True)
    examine_alignment = options.get("examine_alignment", True)
    examine_title_bottom_position = options.get("examine_title_bottom_position", False)
    examine_table_bottom_position = options.get("examine_table_bottom_position", False)
    examine_right_position = options.get("examine_right_position", False)
    examine_top_position = options.get("examine_top_position", False)
    examine_shape_for_shift_size = options.get("examine_shape_for_shift_size", False)
    examine_image_size = options.get("examine_image_size", False)
    examine_modify_height = options.get("examine_modify_height", False)
    examine_bullets = options.get("examine_bullets", True)
    examine_background_color = options.get("examine_background_color", True)
    examine_note = options.get("examine_note", True)

    # compare the number of slides
    if len(prs1.slides) != len(prs2.slides) and examine_number_of_slides:
        return 0

    slide_idx = 0
    # compare the content of each slide
    for slide1, slide2 in zip(prs1.slides, prs2.slides):
        slide_idx += 1

        def get_slide_background_color(slide):
            # background = slide.background
            # if background.fill.background():
            #     return background.fill.fore_color.rgb
            # else:
            #     return None
            fill = slide.background.fill
            if fill.type == 1:
                return fill.fore_color.rgb
            elif fill.type == 5:
                master_fill = slide.slide_layout.slide_master.background.fill
                if master_fill.type == 1:
                    return master_fill.fore_color.rgb
                else:
                    return None
            else:
                return None

        if get_slide_background_color(slide1) != get_slide_background_color(slide2) and examine_background_color:
            return 0

        def get_slide_notes(slide):
            notes_slide = slide.notes_slide
            if notes_slide:
                return notes_slide.notes_text_frame.text
            else:
                return None

        if get_slide_notes(slide1).strip() != get_slide_notes(slide2).strip() and examine_note:
            return 0

        # check if the number of slides is the same
        if len(slide1.shapes) != len(slide2.shapes):
            return 0

        # check if the shapes are the same
        for shape1, shape2 in zip(slide1.shapes, slide2.shapes):
            if examine_title_bottom_position:
                if hasattr(shape1, "text") and hasattr(shape2, "text") and shape1.text == shape2.text:
                    if shape1.text == "Product Comparison" and (shape1.top <= shape2.top or shape1.top < 3600000):
                        return 0
                elif shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    return 0

            if examine_table_bottom_position:
                if slide_idx == 3 and shape1.shape_type == 19 and shape2.shape_type == 19:
                    if shape1.top <= shape2.top or shape1.top < 3600000:
                        return 0
                elif shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    return 0

            if examine_right_position:
                if slide_idx == 2 and not hasattr(shape1, "text") and not hasattr(shape2, "text"):
                    if shape1.left <= shape2.left or shape1.left < 4320000:
                        return 0

            if examine_top_position:
                if slide_idx == 2 and shape1.shape_type == 13 and shape2.shape_type == 13:
                    if shape1.top >= shape2.top or shape1.top > 1980000:
                        return 0
                elif shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    return 0

            if examine_shape_for_shift_size:
                if shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    if not (hasattr(shape1, "text") and hasattr(shape2,
                                                                "text") and shape1.text == shape2.text and shape1.text == "Elaborate on what you want to discuss."):
                        return 0

            if (
                    shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height) and examine_shape:
                return 0

            if examine_image_size:
                if shape1.shape_type == 13 and shape2.shape_type == 13:
                    if shape1.width != shape2.width or shape1.height != shape2.height:
                        return 0
                elif shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    return 0

            if examine_modify_height:
                if not hasattr(shape1, "text") and not hasattr(shape2,
                                                               "text") or shape1.shape_type == 5 and shape2.shape_type == 5:
                    if shape1.height != shape2.height:
                        return 0
                elif shape1.left != shape2.left or shape1.top != shape2.top or shape1.width != shape2.width or shape1.height != shape2.height:
                    return 0

            if hasattr(shape1, "text") and hasattr(shape2, "text"):
                if shape1.text.strip() != shape2.text.strip() and examine_text:
                    return 0

                    # check if the paragraphs are the same
                for para1, para2 in zip(shape1.text_frame.paragraphs, shape2.text_frame.paragraphs):
                    if para1.alignment != para2.alignment and examine_alignment:
                        return 0

                    # check if the runs are the same
                    if para1.text != para2.text and examine_text:
                        return 0

                    if para1.level != para2.level and examine_indent:
                        return 0

                    for run1, run2 in zip(para1.runs, para2.runs):

                        # check if the font properties are the same                        
                        if run1.font.name != run2.font.name and examine_font_name:
                            return 0

                        if run1.font.size != run2.font.size and examine_font_size:
                            return 0

                        if run1.font.bold != run2.font.bold and examine_font_bold:
                            return 0

                        if run1.font.italic != run2.font.italic and examine_font_italic:
                            return 0

                        if hasattr(run1.font.color, "rgb") and hasattr(run2.font.color, "rgb"):
                            if run1.font.color.rgb != run2.font.color.rgb and examine_color_rgb:
                                return 0

                        if run1.font.underline != run2.font.underline and examine_font_underline:
                            return 0

                        if run1.font._element.attrib.get('strike', 'noStrike') != run2.font._element.attrib.get(
                                'strike', 'noStrike') and examine_strike_through:
                            return 0

                        def _extract_bullets(xml_data):
                            root = ET.fromstring(xml_data)

                            namespaces = {
                                'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
                                'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
                            }

                            bullets = []

                            for paragraph in root.findall('.//a:p', namespaces):
                                pPr = paragraph.find('a:pPr', namespaces)
                                if pPr is not None:
                                    lvl = pPr.get('lvl')
                                    buChar = pPr.find('a:buChar', namespaces)
                                    char = buChar.get('char') if buChar is not None else "No Bullet"
                                    buClr = pPr.find('a:buClr/a:srgbClr', namespaces)
                                    color = buClr.get('val') if buClr is not None else "No Color"
                                else:
                                    lvl = "No Level"
                                    char = "No Bullet"
                                    color = "No Color"

                                text = "".join(t.text for t in paragraph.findall('.//a:t', namespaces))

                                bullets.append((lvl, char, text, color))

                            return bullets

                        if examine_bullets and _extract_bullets(run1.part.blob.decode('utf-8')) != _extract_bullets(
                                run2.part.blob.decode('utf-8')):
                            return 0

                    # fixme: Actually there are more properties to be compared, we can add them later via parsing the xml data

    return 1


def check_strikethrough(pptx_path, rules):
    # Load the presentation
    presentation = Presentation(pptx_path)

    slide_index_s = rules["slide_index_s"]
    shape_index_s = rules["shape_index_s"]
    paragraph_index_s = rules["paragraph_index_s"]

    try:
        for slide_index in slide_index_s:
            # Get the slide
            slide = presentation.slides[slide_index]

            for shape_index in shape_index_s:
                # Get the text box
                paragraphs = slide.shapes[shape_index].text_frame.paragraphs

                for paragraph_index in paragraph_index_s:
                    paragraph = paragraphs[paragraph_index]
                    run = paragraph.runs[0]
                    if 'strike' not in run.font._element.attrib:
                        return 0


    except Exception as e:
        logger.error(f"Error: {e}")
        return 0

    return 1


def check_slide_orientation_Portrait(pptx_path):
    presentation = Presentation(pptx_path)

    slide_height = presentation.slide_height
    slide_width = presentation.slide_width

    if slide_width < slide_height:
        return 1
    return 0


def evaluate_presentation_fill_to_rgb_distance(pptx_file, rules):
    rgb = rules["rgb"]

    try:
        original_rgb = rules["original_rgb"]
    except:
        original_rgb = None

    def slide_fill_distance_to_rgb(_slide, _rgb, _original_rgb):
        fill = _slide.background.fill
        if fill.type == 1:
            r1, g1, b1 = fill.fore_color.rgb
            r2, g2, b2 = _rgb

            if _original_rgb is not None:
                r3, g3, b3 = _original_rgb
                if r1 == r3 and g1 == g3 and b1 == b3:
                    return 1

            return sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) / sqrt(255 ** 2 + 255 ** 2 + 255 ** 2)
        elif fill.type == 5:
            master_fill = _slide.slide_layout.slide_master.background.fill
            if master_fill.type == 1:
                r1, g1, b1 = master_fill.fore_color.rgb
            else:
                return 1
            r2, g2, b2 = _rgb

            if _original_rgb is not None:
                r3, g3, b3 = _original_rgb
                if r1 == r3 and g1 == g3 and b1 == b3:
                    return 1

            return sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) / sqrt(255 ** 2 + 255 ** 2 + 255 ** 2)

        return 1

    prs = Presentation(pptx_file)
    similarity = 1 - sum(slide_fill_distance_to_rgb(slide, rgb, original_rgb) for slide in prs.slides) / len(prs.slides)
    return similarity


def check_left_panel(accessibility_tree):
    namespaces = {
        'st': 'uri:deskat:state.at-spi.gnome.org',
        'cp': 'uri:deskat:component.at-spi.gnome.org'
    }

    root = ET.fromstring(accessibility_tree)

    for root_pane in root.iter('root-pane'):
        for panel in root_pane.iter('panel'):
            for split_pane in panel.iter('split-pane'):
                # Get the left panel
                if split_pane.attrib.get("{{{}}}parentcoord".format(namespaces['cp'])) == "(0, 0)":
                    # Get the visible attribute
                    visible = split_pane.attrib.get("{{{}}}visible".format(namespaces['st']))
                    if visible:
                        # decide if it is left panel
                        return 1.

    return 0.


def check_transition(pptx_file, rules):
    slide_idx = rules['slide_idx']
    transition_type = rules['transition_type']

    # Use the zipfile module to open the .pptx file
    with zipfile.ZipFile(pptx_file, 'r') as zip_ref:
        # Get the slide XML file
        slide_name = 'ppt/slides/slide{}.xml'.format(slide_idx + 1)
        try:
            zip_ref.getinfo(slide_name)
        except KeyError:
            # Slide does not exist
            return 0.

        with zip_ref.open(slide_name) as slide_file:
            # 解析XML
            tree = ET.parse(slide_file)
            root = tree.getroot()

            # XML namespace
            namespaces = {
                'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
                'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
            }

            # Search for the transition element
            transition = root.find('.//p:transition', namespaces)
            if transition is not None:
                # Check if the transition is an expected transition
                dissolve = transition.find('.//p:{}'.format(transition_type), namespaces)
                if dissolve is not None:
                    return 1.
                else:
                    return 0.
            else:
                return 0.


def check_page_number_colors(pptx_file, rules):
    color = rules["color"]

    def is_red(rgb_str, threshold=50):
        r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
        return r > g + threshold and r > b + threshold

    def is_blue(rgb_str, threshold=50):
        r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
        return b > g + threshold and b > r + threshold

    def is_green(rgb_str, threshold=50):
        r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
        return g > r + threshold and g > b + threshold

    def is_black(rgb_str, threshold=50):
        r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
        return r < threshold and g < threshold and b < threshold

    with zipfile.ZipFile(pptx_file, 'r') as zip_ref:
        slide_master_name = 'ppt/slideMasters/slideMaster1.xml'
        with zip_ref.open(slide_master_name) as slide_master_file:
            tree = ET.parse(slide_master_file)
            root = tree.getroot()

            namespaces = {
                'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
                'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
            }

            color_elems = root.findall('.//a:solidFill//a:srgbClr', namespaces)
            slides_color_val = color_elems[-2].get('val')

    if slides_color_val is None:
        return 0
    elif color == "red" and not is_red(slides_color_val):
        return 0
    elif color == "blue" and not is_blue(slides_color_val):
        return 0
    elif color == "green" and not is_green(slides_color_val):
        return 0
    elif color == "black" and not is_black(slides_color_val):
        return 0

    return 1


def check_auto_saving_time(pptx_file, rules):
    minutes = rules["minutes"]

    # open and parse xml file
    try:
        tree = ET.parse(pptx_file)
        root = tree.getroot()

        # Traverse the XML tree to find the autosave time setting
        autosave_time = None
        for item in root.findall(".//item"):
            # Check the path attribute
            path = item.get('{http://openoffice.org/2001/registry}path')
            if path == "/org.openoffice.Office.Common/Save/Document":
                # Once the correct item is found, look for the prop element with the name "AutoSaveTimeIntervall"
                for prop in item.findall(".//prop"):
                    name = prop.get('{http://openoffice.org/2001/registry}name')
                    if name == "AutoSaveTimeIntervall":
                        # Extract the value of the autosave time interval
                        autosave_time = prop.find(".//value").text
                        break

        if autosave_time is None:
            return 0
        else:
            autosave_time = int(autosave_time)
            if autosave_time == minutes:
                return 1
            else:
                return 0

    except ET.ParseError as e:
        logger.error(f"Error parsing XML: {e}")
    except FileNotFoundError:
        logger.error(f"File not found: {pptx_file}")