from lxml import etree as ET import copy # Để tạo bản sao sâu của rPr import os import traceback # Để in chi tiết lỗi # --- Namespaces (giữ nguyên) --- ns = { 'a': "http://schemas.openxmlformats.org/drawingml/2006/main", 'p': "http://schemas.openxmlformats.org/presentationml/2006/main", 'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships", 'dgm': 'http://schemas.openxmlformats.org/drawingml/2006/diagram', 'pr': 'http://schemas.openxmlformats.org/package/2006/relationships' } # --- Đăng ký namespace (giữ nguyên) --- for prefix, uri in ns.items(): if prefix != 'pr': ET.register_namespace(prefix, uri) def _get_paragraph_details(p_element): """ Helper function to extract merged text and the first rPr associated with text from a given element. Handles text within and . Args: p_element (ET.Element): The element. Returns: tuple | None: (merged_text, first_rPr_with_text) if text exists, else None. """ paragraph_text_parts = [] first_rPr_with_text = None found_first_rpr = False # Cờ để chỉ tìm rPr đầu tiên một lần # Duyệt qua các con TRỰC TIẾP của để xử lý for child_elem in p_element: current_rpr = None found_text_in_child = None # Trường hợp 1: Run thông thường () if child_elem.tag == f"{{{ns['a']}}}r": # Tìm text bên trong run (dùng .// an toàn cho run lồng nhau nếu có) t_elem = child_elem.find('.//a:t', ns) if t_elem is not None and t_elem.text is not None: found_text_in_child = t_elem.text # Tìm rPr của run này current_rpr = child_elem.find('.//a:rPr', ns) # Dùng .// # Trường hợp 2: Field () elif child_elem.tag == f"{{{ns['a']}}}fld": # Tìm text là con TRỰC TIẾP của field t_elem = child_elem.find('./a:t', ns) if t_elem is not None and t_elem.text is not None: found_text_in_child = t_elem.text # Tìm rPr là con TRỰC TIẾP của field current_rpr = child_elem.find('./a:rPr', ns) # Xử lý nếu tìm thấy text trong child hiện tại (hoặc hoặc ) if found_text_in_child is not None: paragraph_text_parts.append(found_text_in_child) # Nếu chưa lưu rPr đầu tiên, lưu rPr của child hiện tại if not found_first_rpr: first_rPr_with_text = current_rpr # Lưu rPr tìm được (có thể là None) found_first_rpr = True # Đánh dấu đã tìm thấy # Chỉ trả về kết quả nếu paragraph thực sự có nội dung text if paragraph_text_parts: merged_text = "".join(paragraph_text_parts).strip() if merged_text: # Trả về text đã ghép và rPr đầu tiên tìm thấy (có thể là None) return (merged_text, first_rPr_with_text) return None # Không có text trong paragraph này hoặc text rỗng # --- Hàm trích xuất chính (Trả về list các tuple chi tiết paragraph) --- def extract_text_from_slide(slide_file): """ Trích xuất chi tiết từ từng thẻ trong file slide XML. Args: slide_file (str): Đường dẫn đến file slide XML. Returns: list: Một list các tuple, mỗi tuple có dạng: (paragraph_text, first_rPr_in_paragraph) - paragraph_text (str): Toàn bộ text trong các con cháu của , đã được ghép và strip(). - first_rPr_in_paragraph (ET.Element | None): Phần tử của đầu tiên có chứa text trong đó. Là None nếu run đầu tiên có text không có thẻ , hoặc nếu không có text nào trong paragraph. Trả về list rỗng nếu có lỗi hoặc không tìm thấy paragraph nào có text. """ # print(f"--- Bắt đầu trích xuất chi tiết từng từ file: {slide_file} ---") extracted_data = [] # Danh sách kết quả cuối cùng if not os.path.exists(slide_file): print(f"Lỗi: File không tồn tại: {slide_file}") print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi) ---") return extracted_data try: tree = ET.parse(slide_file) root = tree.getroot() except ET.ParseError as e: print(f"Lỗi parse XML file {slide_file}: {e}") print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse) ---") return extracted_data except Exception as e: print(f"Lỗi không xác định khi parse {slide_file}: {e}") # traceback.print_exc() print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse không xác định) ---") return extracted_data try: processed_txBody_elements = set() elements_to_check = [] # 1. Thu thập các container có thể chứa txBody for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp) for grpSp in root.findall('.//p:spTree/p:grpSp', ns): for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp) for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc) # Thêm tìm kiếm khác nếu cần # 2. Duyệt qua container, tìm txBody, rồi xử lý từng bên trong for container in elements_to_check: txBody = container.find('./p:txBody', ns) if txBody is None: txBody = container.find('./a:txBody', ns) if txBody is not None and txBody not in processed_txBody_elements: # Tìm TẤT CẢ các thẻ là con TRỰC TIẾP của txBody này paragraphs = txBody.findall('a:p', ns) for p_elem in paragraphs: # Gọi hàm helper để lấy chi tiết của paragraph này details = _get_paragraph_details(p_elem) # Nếu paragraph có nội dung text, thêm tuple vào kết quả if details: extracted_data.append(details) processed_txBody_elements.add(txBody) except Exception as e: print(f"Lỗi khi tìm kiếm hoặc trích xuất chi tiết : {e}") return extracted_data def replace_text_in_slide(xml_file_path, list_of_translated_paragraph_data): """ Thay thế văn bản trong file XML slide, ghi đè file gốc. *** Logic mới: *** - Giảm cỡ chữ đi 0.85 lần. - Nếu text > 20 chars: Loại bỏ định dạng bold (giữ nguyên case). - Nếu text <= 20 chars: Giữ nguyên định dạng bold gốc (và case). Args: xml_file_path (str): Đường dẫn file XML slide gốc (sẽ bị ghi đè). list_of_translated_paragraph_data (list): List các tuple (translated_paragraph_text, original_first_rPr_in_paragraph). Returns: bool: True nếu thành công (ghi file), False nếu có lỗi. """ # print(f"\n--- Bắt đầu thay thế PARAGRAPH (ghi đè, logic length/bold) trong file: {os.path.basename(xml_file_path)} ---") processed_p_count = 0 if not os.path.exists(xml_file_path): print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.") return False try: tree = ET.parse(xml_file_path) root = tree.getroot() # --- TÌM và LỌC THEO CÙNG LOGIC NHƯ EXTRACT --- paragraphs_to_modify = [] processed_txBody_elements = set() elements_to_check = [] for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp) for grpSp in root.findall('.//p:spTree/p:grpSp', ns): for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp) for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc) for container in elements_to_check: txBody = container.find('./p:txBody', ns) if txBody is None: txBody = container.find('./a:txBody', ns) if txBody is not None and txBody not in processed_txBody_elements: paragraphs = txBody.findall('a:p', ns) for p_elem in paragraphs: has_actual_text = False elements_with_text = p_elem.findall('.//a:r/a:t', ns) + p_elem.findall('.//a:fld/a:t', ns) for t in elements_with_text: if t.text and t.text.strip(): has_actual_text = True; break if has_actual_text: paragraphs_to_modify.append(p_elem) processed_txBody_elements.add(txBody) # --- Kiểm tra số lượng khớp --- num_paragraphs_found = len(paragraphs_to_modify) num_data_items = len(list_of_translated_paragraph_data) if num_paragraphs_found == 0: # print(f"Thông báo [...]: Không tìm thấy nào có text để thay thế.") if num_data_items > 0: print(f"Cảnh báo: Đã cung cấp {num_data_items} mục dữ liệu nhưng không có nào để áp dụng.") # print(f"--- Kết thúc xử lý (không thay đổi): {os.path.basename(xml_file_path)} ---") return True if num_paragraphs_found != num_data_items: print(f"CẢNH BÁO [...]: Số lượng ({num_paragraphs_found}) KHÔNG KHỚP dữ liệu dịch ({num_data_items}).") num_items_to_process = min(num_paragraphs_found, num_data_items) print(f"=> Sẽ chỉ xử lý {num_items_to_process} mục đầu tiên.") else: num_items_to_process = num_paragraphs_found # --- Lặp và thực hiện thay thế --- for i in range(num_items_to_process): try: p_elem_to_modify = paragraphs_to_modify[i] translated_text, rpr_to_use_original = list_of_translated_paragraph_data[i] p_id = hex(id(p_elem_to_modify)) # --- 1. Xử lý text ban đầu (chỉ strip) --- cleaned_translated_text = translated_text.strip() if isinstance(translated_text, str) else "" # --- 2. Chuẩn bị rPr cuối cùng (bắt đầu bằng copy hoặc trống) --- final_rpr = None if rpr_to_use_original is not None and ET.iselement(rpr_to_use_original) and rpr_to_use_original.tag == f"{{{ns['a']}}}rPr": try: final_rpr = copy.deepcopy(rpr_to_use_original) except Exception as clone_e: print(f"Lỗi sao chép rPr gốc cho index {i} (ID {p_id}): {clone_e}") final_rpr = ET.Element(f"{{{ns['a']}}}rPr") else: final_rpr = ET.Element(f"{{{ns['a']}}}rPr") # --- 3. Luôn giảm cỡ chữ (nếu có) --- original_sz_str = final_rpr.get('sz') if original_sz_str: try: original_sz = int(original_sz_str) new_sz = max(100, int(original_sz * 0.85)) final_rpr.set('sz', str(new_sz)) except ValueError: print(f"Cảnh báo: Không thể chuyển đổi sz='{original_sz_str}' thành số nguyên cho p_id {p_id}.") # --- 4. Áp dụng logic độ dài cho bold (KHÔNG ĐỔI CASE) --- if len(cleaned_translated_text) > 10: # Dài > 20: BỎ BOLD (nếu có) final_rpr.attrib.pop('b', None) # Xóa thuộc tính bold # print(f"Debug: Text > 20 chars for p_id {p_id}. Removed bold.") # else: # Ngắn <= 20: Giữ lại thuộc tính 'b' gốc (đã có trong final_rpr nếu có) # print(f"Debug: Text <= 20 chars for p_id {p_id}. Kept original bold.") # --- 5. Xóa nội dung cũ (run và field) --- runs_to_remove = p_elem_to_modify.findall('a:r', ns) fields_to_remove = p_elem_to_modify.findall('a:fld', ns) for elem_to_remove in runs_to_remove + fields_to_remove: try: p_elem_to_modify.remove(elem_to_remove) except ValueError: pass # --- 6. Tạo nội dung mới (nếu text không rỗng) --- if cleaned_translated_text: new_r = ET.Element(f"{{{ns['a']}}}r") new_r.insert(0, final_rpr) # Chèn rPr đã xử lý new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t") new_t.text = cleaned_translated_text # Chèn text gốc (đã strip) # Chèn run mới end_para_rpr = p_elem_to_modify.find('./a:endParaRPr', ns) insert_index = -1 if end_para_rpr is not None: try: insert_index = list(p_elem_to_modify).index(end_para_rpr) except ValueError: insert_index = -1 if insert_index != -1: p_elem_to_modify.insert(insert_index, new_r) else: p_elem_to_modify.append(new_r) processed_p_count += 1 except (IndexError, ValueError, TypeError) as data_err: print(f"Lỗi lấy dữ liệu tại index {i}: {data_err}. Bỏ qua mục này.") except Exception as p_replace_err: p_id_err = hex(id(paragraphs_to_modify[i])) if i < len(paragraphs_to_modify) else "N/A" print(f"Lỗi khi xử lý thay thế cho tại index {i} (ID {p_id_err}): {p_replace_err}") # --- Lưu cây XML --- try: tree.write(xml_file_path, encoding='utf-8', xml_declaration=True, pretty_print=True) except TypeError: tree.write(xml_file_path, encoding='utf-8', xml_declaration=True) return True except ET.ParseError as pe: print(f"Lỗi parse XML file '{xml_file_path}': {pe}"); return False except IOError as ioe: print(f"Lỗi I/O với file '{xml_file_path}': {ioe}"); return False except Exception as e: print(f"Lỗi nghiêm trọng: {e}"); traceback.print_exc(); return False # -------------------------- # 2. Xử lý SmartArt # -------------------------- def get_smartart_data_file(rels_file, base_path): """ Đọc file .rels và tìm relationship có Type là diagramData, trả về đường dẫn đầy đủ đến file data*.xml của SmartArt. (Không thay đổi đáng kể) """ try: if not os.path.exists(rels_file): # print(f"Thông báo: File rels không tồn tại: {rels_file}") # Có thể bỏ qua log này return None tree = ET.parse(rels_file) root = tree.getroot() # Sử dụng ns['pr'] for rel in root.findall('pr:Relationship', ns): target = rel.attrib.get('Target') rel_type = rel.attrib.get('Type') # Kiểm tra Type chính xác if rel_type == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData' and target: target_fixed = target.replace("../", "") full_target_path = os.path.join(base_path, target_fixed) absolute_path = os.path.normpath(full_target_path) if os.path.exists(absolute_path): return absolute_path else: print(f"Cảnh báo: Tìm thấy relationship SmartArt nhưng file target không tồn tại: {absolute_path}") return None except ET.ParseError as e: print(f"Lỗi parse XML file rels {rels_file}: {e}") return None except Exception as e: print(f"Lỗi khi xử lý file rels {rels_file}: {e}") # traceback.print_exc() return None def extract_text_from_smartart(xml_file_path): """ Trích xuất văn bản tổng hợp từ mỗi đoạn có chứa text trong file XML SmartArt. Args: xml_file_path (str): Đường dẫn đến file XML SmartArt. Returns: list: Một list các tuple (paragraph_text, first_rPr_in_paragraph). paragraph_text là toàn bộ text trong các con cháu của . first_rPr_in_paragraph là element của đầu tiên có chứa text trong đó. Trả về list rỗng nếu lỗi. """ paragraph_data = [] try: tree = ET.parse(xml_file_path) root = tree.getroot() # Tìm tất cả các đoạn trong cây XML (thường nằm trong ) # Sử dụng .// để tìm ở mọi cấp độ sâu trong các cấu trúc SmartArt for p_elem in root.findall('.//a:p', ns): combined_text = "" first_rPr = None found_first_rpr_in_p = False # Cờ cho rPr đầu tiên trong đoạn p này # Tìm tất cả các run bên trong đoạn hiện tại for r_elem in p_elem.findall('.//a:r', ns): t_element = r_elem.find('.//a:t', ns) # Tìm text trong run if t_element is not None and t_element.text is not None: current_text = t_element.text combined_text += current_text # Nối text từ các run # Lấy rPr của run đầu tiên có text trong đoạn p này if not found_first_rpr_in_p and current_text.strip(): rPr_element = r_elem.find('.//a:rPr', ns) first_rPr = rPr_element # Lưu trữ element rPr (có thể là None) found_first_rpr_in_p = True # Sau khi duyệt hết các run trong , thêm vào kết quả nếu có text cleaned_text = combined_text.strip() if cleaned_text: paragraph_data.append((cleaned_text, first_rPr)) except FileNotFoundError: print(f"Lỗi: Không tìm thấy file XML '{xml_file_path}'.") return [] except ET.ParseError as pe: print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}") return [] except Exception as e: print(f"Lỗi không xác định khi trích xuất text theo đoạn từ file '{xml_file_path}': {e}") traceback.print_exc() return [] return paragraph_data # --- Hàm thay thế theo từng đoạn --- def replace_text_in_smartart(xml_file_path, list_of_translated_paragraph_data, output_xml_file_path): """ Thay thế văn bản trong file XML SmartArt dựa trên dữ liệu đoạn đã dịch. Mỗi mục dịch sẽ thay thế nội dung text của một tương ứng, đặt toàn bộ text dịch vào một run duy nhất với định dạng rPr được cung cấp. Args: xml_file_path (str): Đường dẫn file XML gốc. list_of_translated_paragraph_data (list): List các tuple (translated_paragraph_text, original_first_rPr_in_paragraph). output_xml_file_path (str): Đường dẫn file XML đầu ra. Returns: bool: True nếu thành công, False nếu lỗi. """ p_index_for_data = 0 # Index để lấy dữ liệu dịch processed_p_count = 0 # Đếm số đoạn đã được xử lý (thay thế) if not output_xml_file_path: output_xml_file_path = xml_file_path try: tree = ET.parse(xml_file_path) root = tree.getroot() # Tạo parent map để xóa element an toàn khi dùng findall với './/' parent_map = {c: p for p in root.iter() for c in p} # Tìm lại tất cả các theo cùng thứ tự như khi trích xuất paragraphs_in_order = root.findall('.//a:p', ns) # Lọc ra những đoạn mà ban đầu có chứa text để khớp với logic trích xuất paragraphs_to_modify = [] for p_elem in paragraphs_in_order: has_actual_text = False for t in p_elem.findall('.//a:t', ns): if t.text and t.text.strip(): has_actual_text = True break if has_actual_text: paragraphs_to_modify.append(p_elem) # Kiểm tra số lượng khớp if len(paragraphs_to_modify) != len(list_of_translated_paragraph_data): print(f"Cảnh báo [File: {os.path.basename(xml_file_path)}]: Số lượng có text ({len(paragraphs_to_modify)}) " f"không khớp số lượng dữ liệu dịch ({len(list_of_translated_paragraph_data)}). Thay thế có thể sai lệch.") # Quyết định số lượng sẽ xử lý num_items_to_process = min(len(paragraphs_to_modify), len(list_of_translated_paragraph_data)) else: num_items_to_process = len(paragraphs_to_modify) # Duyệt qua các cần sửa đổi for i in range(num_items_to_process): p_elem = paragraphs_to_modify[i] translated_text, original_first_rPr = list_of_translated_paragraph_data[p_index_for_data] cleaned_translated_text = translated_text.strip() if translated_text else "" # --- Xóa các run cũ bên trong này --- # Sử dụng .// để nhất quán với extraction, cần parent map để xóa runs_to_remove = p_elem.findall('.//a:r', ns) for r_elem in runs_to_remove: parent = parent_map.get(r_elem) if parent is not None: try: # Cập nhật parent map nếu cấu trúc thay đổi động (ít khả năng ở đây) # parent_map = {c: p for p in root.iter() for c in p} parent.remove(r_elem) except ValueError: pass # Bỏ qua nếu không tìm thấy để xóa # else: # r_elem không có parent trong map (hiếm) if cleaned_translated_text: new_r = ET.Element(f"{{{ns['a']}}}r") # Tạo run mới # Áp dụng rPr gốc (đã deepcopy) cho run mới applied_rPr = False if original_first_rPr is not None and ET.iselement(original_first_rPr): # *** Thêm kiểm tra thẻ rPr ở đây cho an toàn *** if original_first_rPr.tag == f"{{{ns['a']}}}rPr": try: cloned_rPr = copy.deepcopy(original_first_rPr) new_r.insert(0, cloned_rPr) # Chèn rPr vào đầu run applied_rPr = True except Exception as clone_e: print(f"Lỗi sao chép rPr cho index {i} (data index {p_index_for_data}): {clone_e}") else: print(f"Cảnh báo: Thẻ rPr gốc không phải cho p_elem index {i}. Thẻ: {original_first_rPr.tag}") if not applied_rPr: ET.SubElement(new_r, f"{{{ns['a']}}}rPr") # Thêm rPr trống nếu cần # Thêm text vào run new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t") new_t.text = cleaned_translated_text # --- SỬA ĐỔI QUAN TRỌNG: Chèn run mới vào đúng vị trí --- # Tìm phần tử là con TRỰC TIẾP của p_elem end_para_rpr = p_elem.find('./a:endParaRPr', ns) if end_para_rpr is not None: # Nếu tìm thấy, lấy danh sách con hiện tại và tìm index của nó try: children_list = list(p_elem) insert_index = children_list.index(end_para_rpr) # Chèn run mới *ngay trước* endParaRPr p_elem.insert(insert_index, new_r) # print(f"Inserted new_r at index {insert_index} before endParaRPr for p_elem {i}") except ValueError: # Hiếm khi xảy ra nếu find() hoạt động đúng, nhưng là fallback print(f"Cảnh báo: Không tìm thấy index của endParaRPr dù đã find thấy. Appending new_r cho p_elem {i}.") p_elem.append(new_r) else: # Nếu không có endParaRPr, append vào cuối là hành vi chấp nhận được p_elem.append(new_r) # print(f"Appended new_r (no endParaRPr found) for p_elem {i}") # Nếu cleaned_translated_text rỗng, đoạn sẽ bị trống (đã xóa hết ) p_index_for_data += 1 # Chuyển sang dữ liệu dịch tiếp theo processed_p_count += 1 # Tăng số đoạn đã xử lý # print(f"Thông tin [File: {os.path.basename(xml_file_path)}]: Đã xử lý {processed_p_count} đoạn .") if p_index_for_data < len(list_of_translated_paragraph_data): print(f"Cảnh báo [File: {os.path.basename(xml_file_path)}]: Còn {len(list_of_translated_paragraph_data) - p_index_for_data} " f"mục dữ liệu dịch chưa được sử dụng do số lượng không đủ.") # --- Lưu cây XML đã sửa đổi --- for prefix, uri in ns.items(): ET.register_namespace(prefix, uri) tree.write(output_xml_file_path, encoding='utf-8', xml_declaration=True) # print(f"Đã lưu SmartArt cập nhật (theo đoạn) vào: {output_xml_file_path}") return True except FileNotFoundError: print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.") return False except ET.ParseError as pe: print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}") return False except IOError as ioe: print(f"Lỗi I/O khi ghi file '{output_xml_file_path}': {ioe}") return False except Exception as e: print(f"Lỗi nghiêm trọng trong quá trình thay thế text SmartArt (theo đoạn) file '{xml_file_path}': {e}") traceback.print_exc() return False