:"/\\|?*' return re.sub('[{}]'.format(re.escape(invalid_chars)), '_', name) def normalize_part_name(part_name): """ Remove a"> :"/\\|?*' return re.sub('[{}]'.format(re.escape(invalid_chars)), '_', name) def normalize_part_name(part_name): """ Remove a"> :"/\\|?*' return re.sub('[{}]'.format(re.escape(invalid_chars)), '_', name) def normalize_part_name(part_name): """ Remove a">
import clr
import sys
import re
import os

from collections import defaultdict

clr.AddReference("System")
clr.AddReference("Microsoft.Office.Interop.Excel")

from Microsoft.Office.Interop import Excel

################################################################
# Minimal MsoTriState "enum" for AddPicture in .NET Excel interop
################################################################
class MsoTriState:
    msoFalse = 0
    msoTrue = -1

###########################################################
# 1) UTILITY FUNCTIONS
###########################################################

def clean_file_name(name):
    """
    Replace any invalid Windows file-system characters with underscores.
    """
    if not isinstance(name, basestring):
        # Fallback if name is None or some other non-string
        return "NoName"

    invalid_chars = r'<>:"/\\\\|?*'
    return re.sub('[{}]'.format(re.escape(invalid_chars)), '_', name)

def normalize_part_name(part_name):
    """
    Remove angle-bracket instance tags like <1>, <2>, etc.
    """
    return re.sub(r"<\\d+>", "", part_name)

def debug_print(*args):
    """
    Simple helper so we can add debugging lines. 
    Example usage: debug_print("Message:", value)
    """
    msg = " ".join(str(a) for a in args)
    print(msg)

###########################################################
# 2) PROMPT FOR THUMBNAIL SIZE & SAVE FOLDER
###########################################################

Win = Windows()  # Provided by Alibre Script environment
Option = [
    ['Thumbnail Size',  WindowsInputTypes.Real,   100],
    ['Save Folder',     WindowsInputTypes.Folder, None],
]
Values = Win.OptionsDialog("Image from Assembly", Option, 100)
if Values is None:
    sys.exit()  # user canceled

dimension_thumb = Values[0]  # e.g. 100
save_path      = Values[1]  # user-chosen folder

if not os.path.isdir(save_path):
    raise EnvironmentError("The selected folder does not exist: " + save_path)

debug_print("### DEBUG PRINT ###  Using dimension:", dimension_thumb)
debug_print("### DEBUG PRINT ###  Thumbnails and Excel will be in:", save_path)

###########################################################
# 3) DETECT ASSEMBLY OR PART
###########################################################
def detect_type():
    """
    Returns a tuple: (obj, typeString) 
    where typeString is 'Assembly' or 'Part'.
    """
    try:
        if hasattr(CurrentAssembly(), 'Parts'):
            return (CurrentAssembly(), 'Assembly')
        else:
            raise Exception("Not a valid assembly")
    except:
        if hasattr(CurrentPart(), 'Name'):
            return (CurrentPart(), 'Part')
        else:
            raise Exception("Document is neither valid Part nor Assembly")

###########################################################
# 4) ENUMERATE PARTS IN AN ASSEMBLY & COUNT QUANTITIES
###########################################################
Assembly_Name = None
Assembly_DNum = None

def count_parts_in_assembly(assembly):
    """
    Returns a dict { normalizedName : quantity }.
    Also sets global Assembly_Name and Assembly_DNum from the top-level.
    """
    parts_count = defaultdict(int)

    def process_asm(asm):
        global Assembly_Name
        global Assembly_DNum

        Assembly_Name = asm.Name or "Assembly"
        Assembly_DNum = asm.DocumentNumber or "NoDocNum"

        # Count top-level parts
        for p in asm.Parts:
            pname = normalize_part_name(p.Name)
            parts_count[pname] += 1

        # Sub-assemblies
        for sa in asm.SubAssemblies:
            sname = normalize_part_name(sa.Name)
            parts_count[sname] += 1
            process_asm(sa)  # recursion

    process_asm(assembly)
    return parts_count

###########################################################
# 5) THUMBNAIL GENERATION
###########################################################
def generate_thumbnails_with_quantities(assembly, parts_count, folderpath):
    """
    Saves a single thumbnail for each unique part/sub-assembly:
        {CleanedPartName};{CleanedDocNumber};{Quantity}.jpg
    """
    saved_thumbnails = set()

    def save_thumb(name, docnum, quantity, item_obj):
        """
        Actually do the SaveThumbnail to folderpath, with debug prints.
        """
        n_safe = name or "NoName"
        d_safe = docnum or "NoDocNum"

        c_name   = clean_file_name(n_safe)
        c_docnum = clean_file_name(d_safe)

        final_filename = "{};{};{}.jpg".format(c_name, c_docnum, quantity)
        path_out = os.path.join(folderpath, final_filename)

        geometry_hash = getattr(item_obj, 'GeometryHash', id(item_obj))
        unique_id = (final_filename, geometry_hash)

        if unique_id not in saved_thumbnails:
            debug_print("### DEBUG PRINT ###  Saving thumbnail:", path_out)
            item_obj.SaveThumbnail(path_out, dimension_thumb, dimension_thumb)
            saved_thumbnails.add(unique_id)
        else:
            debug_print("### DEBUG PRINT ###  Skipped duplicate thumbnail:", final_filename)

    def traverse_asm(asm):
        # parts
        for p in asm.Parts:
            p_name = normalize_part_name(p.Name)
            p_doc  = p.DocumentNumber
            p_qty  = parts_count[p_name]
            save_thumb(p_name, p_doc, p_qty, p)

        # sub-assemblies
        for sa in asm.SubAssemblies:
            sa_name = normalize_part_name(sa.Name)
            sa_doc  = sa.DocumentNumber
            sa_qty  = parts_count[sa_name]
            save_thumb(sa_name, sa_doc, sa_qty, sa)

            traverse_asm(sa)

    traverse_asm(assembly)

###########################################################
# 6) BUILD THE EXCEL WORKSHEET WITH IMAGES
###########################################################
def GenerateExcelWithImagesNET(image_directory):
    """
    1) Loops over all .jpg/.png/etc. in 'image_directory'.
    2) Splits filename on semicolons: "PartName;DocNum;Qty.jpg".
    3) Writes name, docnum, qty in columns A,B,D.
    4) Embeds the image in column C.
    5) Saves Excel as: "<Assembly_DNum> <Assembly_Name>.xlsx".
    """
    debug_print("### DEBUG PRINT ###  Scanning directory for thumbnails:", image_directory)

    excel = Excel.ApplicationClass()
    excel.Visible = False

    workbook = excel.Workbooks.Add()
    sheet = workbook.Worksheets[1]

    # HEADERS
    sheet.Cells[1, 1].Value2 = "Part Name"
    sheet.Cells[1, 2].Value2 = "Doc #"
    sheet.Cells[1, 3].Value2 = "Image"
    sheet.Cells[1, 4].Value2 = "Quantity"
    sheet.Cells[1, 5].Value2 = "Purchased"
    sheet.Cells[1, 6].Value2 = "Ready"

    sheet.Columns("C").ColumnWidth = 15

    row = 2
    valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif')

    # We'll make images about 75 points high/wide
    cell_size_points = 75

    all_files = os.listdir(image_directory)
    if not all_files:
        debug_print("### DEBUG PRINT ###  WARNING: No files found in folder:", image_directory)

    for filename in all_files:
        fn_lower = filename.lower()
        if fn_lower.endswith(valid_extensions):
            debug_print("### DEBUG PRINT ###  Found image file:", filename)

            base_no_ext, ext = os.path.splitext(filename)
            parts = base_no_ext.split(';')
            if len(parts) < 3:
                debug_print("### DEBUG PRINT ###  Skipping file (not enough semicolons):", filename)
                continue

            parted_name   = parts[0]
            parted_docnum = parts[1]
            parted_qty_str = parts[2]

            try:
                parted_qty = int(parted_qty_str)
            except:
                parted_qty = 1

            # Write data in columns A,B,D
            sheet.Cells[row, 1].Value2 = parted_name
            sheet.Cells[row, 2].Value2 = parted_docnum
            sheet.Cells[row, 4].Value2 = parted_qty

            # Row height for image
            sheet.Rows[row].RowHeight = cell_size_points

            image_path = os.path.join(image_directory, filename)
            left = sheet.Cells[row, 3].Left
            top  = sheet.Cells[row, 3].Top

            # Insert the picture
            picture = sheet.Shapes.AddPicture(
                Filename=image_path,
                LinkToFile=MsoTriState.msoFalse,
                SaveWithDocument=MsoTriState.msoTrue,
                Left=left,
                Top=top,
                Width=cell_size_points,
                Height=cell_size_points
            )

            row += 1

    sheet.Columns("A").AutoFit()
    sheet.Columns("B").AutoFit()

    global Assembly_DNum
    global Assembly_Name

    # If we never set them => single Part fallback
    if not Assembly_DNum:
        Assembly_DNum = "PART"
    if not Assembly_Name:
        Assembly_Name = "SINGLE"

    # Clean them for final path:
    Assembly_DNum  = clean_file_name(Assembly_DNum)
    Assembly_Name  = clean_file_name(Assembly_Name)

    excel_file_path = os.path.join(image_directory,
        "{} {}.xlsx".format(Assembly_DNum, Assembly_Name)
    )

    debug_print("### DEBUG PRINT ###  Saving Excel to:", excel_file_path)

    workbook.SaveAs(excel_file_path)
    workbook.Close(False)
    excel.Quit()

    debug_print("### DEBUG PRINT ###  Excel file with images created at:", excel_file_path)

###########################################################
# 7) MAIN
###########################################################
def Main():
    try:
        doc_obj, doc_type = detect_type()
    except Exception as exc:
        print("Error detecting Part or Assembly:", exc)
        return

    debug_print("### DEBUG PRINT ###  Detected type:", doc_type)

    if doc_type == 'Part':
        debug_print("### DEBUG PRINT ###  Single Part scenario.")
        part = doc_obj
        c_name  = clean_file_name(part.Name or "Part")
        c_docno = clean_file_name(part.DocumentNumber or "NoDocNum")
        final_filename = "{};{};1.jpg".format(c_name, c_docno)
        path_out = os.path.join(save_path, final_filename)

        debug_print("### DEBUG PRINT ###  Saving Part thumbnail:", path_out)
        part.SaveThumbnail(path_out, dimension_thumb, dimension_thumb)

    else:
        debug_print("### DEBUG PRINT ###  Assembly scenario: Counting parts.")
        parts_count = count_parts_in_assembly(doc_obj)

        debug_print("### DEBUG PRINT ###  Generating thumbnails with doc # + quantity.")
        generate_thumbnails_with_quantities(doc_obj, parts_count, save_path)

    debug_print("### DEBUG PRINT ###  Building Excel sheet from generated images.")
    GenerateExcelWithImagesNET(save_path)

Main()