import json import base64 import io from com.sun.star.beans import PropertyValue import tempfile import os from com.sun.star.beans import PropertyValue from uno import ByteSequence, systemPathToFileUrl from com.sun.star.text.TextContentAnchorType import AS_CHARACTER, AT_PARAGRAPH def Replace(bookmark_name_value_map): """ 替换所有书签 Args: bookmark_name_value_map: 书签名称和新值的映射 """ doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() for bookmark_name, new_value in bookmark_name_value_map.items(): if bookmarks.hasByName(bookmark_name): bookmark = bookmarks.getByName(bookmark_name) anchor = bookmark.getAnchor() anchor.setString(new_value) def ReplaceOne(bookmark_obj_collection, bookmark_name, new_value): """ 替换一个书签 Args: bookmark_obj_collection: 书签对象集合 bookmark_name: 书签名称 new_value: 新值 """ if bookmark_obj_collection.hasByName(bookmark_name): bookmark = bookmark_obj_collection.getByName(bookmark_name) anchor = bookmark.getAnchor() anchor.setString(new_value) def ReplaceWithJSON(json_str): """ 替换所有书签 Args: json_str: 书签名称和新值的映射的JSON字符串 """ bookmark_name_value_map = json.loads(json_str) Replace(bookmark_name_value_map) def QueryAll(): """ 查询所有书签,表格中的书签获取对应表格index 返回结果格式: { "text": [], "table": [ { "tableIndex": 表格索引, "bookmark": [书签名称列表] } ] } """ doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() bookmark_names = bookmarks.getElementNames() filtered_bookmarks = [ bk_name for bk_name in bookmark_names if not bk_name.startswith("_") and ":" not in bk_name ] result = {"text": [], "table": []} for bk_name in filtered_bookmarks: result["text"].append(bk_name) return result def QueryAllWithJSON(): return returnWithJSON(QueryAll()) def QueryBookmarkPositionInTable(tables, bookmarks): """ 查询书签在表格中的位置。 参数: tables: 表格集合 (XIndexAccess) bookmarks: 书签集合 (XIndexAccess) 返回: 一个字典,格式为 {'书签名': {'tableIndex': 表格索引, 'row': 行号}} """ result = {} # 遍历所有书签 for b in range(bookmarks.getCount()): bookmark = bookmarks.getByIndex(b) bookmark_name = bookmark.getName() x_range = bookmark.getAnchor() # 获取书签的锚点 x_text = x_range.getText() # 获取书签所在的文本对象 # 遍历所有表格 for t in range(tables.getCount()): table = tables.getByIndex(t) # 遍历表格中的每一行 rows = table.getRows().getCount() for i in range(rows): # 动态确定当前行的最大列数 max_cols = 0 while True: try: cell = table.getCellByPosition(max_cols, i) max_cols += 1 except Exception: break for j in range(max_cols): cell = table.getCellByPosition(j, i) cell_text = cell.getText() if cell_text == x_text: result[bookmark_name] = { 'tableIndex': t, 'row': i, 'col': j } break else: continue break else: continue break return result def InsertRowWithJSON(json_str): """ 插入行 """ data = json.loads(json_str) InsertRow(data["location_bookmark_name"], data["data"], data["start_row_index"]) def InsertRow(location_bookmark_name, data, start_row_index=-1): """ 表格插入行 Args: location_bookmark_name: 用于定位表格的书签 data: 二维数组,用于填充表格数据 start_row_index: 起始行位置,默认为-1,即在表格末尾插入 """ doc = XSCRIPTCONTEXT.getDocument() tables = doc.getTextTables() bookmarks = doc.getBookmarks() bookmark_in_table_position = QueryBookmarkPositionInTable(tables, bookmarks) if location_bookmark_name not in bookmark_in_table_position: raise ValueError(f"未找到书签 {location_bookmark_name} 对应的表格") handle_table_index = bookmark_in_table_position[location_bookmark_name][ "tableIndex" ] try: handle_table = tables.getByIndex(handle_table_index) except IndexError: raise IndexError(f"表格索引 {handle_table_index} 超出范围") col_count = handle_table.getColumns().getCount() row_count = handle_table.getRows().getCount() if not data or len(data) == 0: return try: row_count = handle_table.getRows().getCount() rows_to_insert = len(data) if rows_to_insert > 0: # 确定插入位置 if start_row_index != -1 and 0 <= start_row_index < row_count: insert_pos = start_row_index + 1 # 在指定行下方插入 else: insert_pos = row_count # 插入到表格末尾 # 批量插入所有新行 handle_table.getRows().insertByIndex(insert_pos, rows_to_insert) # 填充数据到新插入的行 for data_row_idx, row_data in enumerate(data): target_row = insert_pos + data_row_idx # 计算目标行索引 for col_idx, cell_value in enumerate(row_data): cell = handle_table.getCellByPosition(col_idx, target_row) cell.setString(str(cell_value)) except Exception as e: raise RuntimeError(f"err: {str(e)}") def BatchInsertRowWithContentControl(data_array): """ 批量插入行 """ doc = XSCRIPTCONTEXT.getDocument() tables = doc.getTextTables() bookmarks = doc.getBookmarks() bookmark_in_table_position = QueryBookmarkPositionInTable(tables, bookmarks) for arr_obj in data_array: location_bookmark_name = arr_obj.get("location_bookmark_name") data = arr_obj.get("data") start_row_index = arr_obj.get("start_row_index", -1) # 默认值为 -1 if location_bookmark_name not in bookmark_in_table_position: raise ValueError(f"未找到书签 {location_bookmark_name} 对应的表格") handle_table_index = bookmark_in_table_position[location_bookmark_name][ "tableIndex" ] try: handle_table = tables.getByIndex(handle_table_index) except IndexError: raise IndexError(f"表格索引 {handle_table_index} 超出范围") col_count = handle_table.getColumns().getCount() row_count = handle_table.getRows().getCount() if not data or len(data) == 0: return # if any(len(row) != col_count for row in data): # raise ValueError(f"数据列数不匹配,表格有 {col_count} 列") try: row_count = handle_table.getRows().getCount() rows_to_insert = len(data) if rows_to_insert > 0: # 确定插入位置 if start_row_index != -1 and 0 <= start_row_index < row_count: insert_pos = start_row_index + 1 # 在指定行下方插入 else: insert_pos = row_count # 插入到表格末尾 # 批量插入所有新行 handle_table.getRows().insertByIndex(insert_pos, rows_to_insert) # 填充数据到新插入的行 for data_row_idx, row_data in enumerate(data): target_row = insert_pos + data_row_idx # 计算目标行索引 for col_idx, cell_value in enumerate(row_data): cell = handle_table.getCellByPosition(col_idx, target_row) cell_text = cell.getText() # === 清空单元格 === cell_text.setString("") # === 创建内容控件 === content_control = doc.createInstance( "com.sun.star.text.ContentControl" ) # === 插入控件到单元格 === cursor = cell_text.createTextCursor() cell_text.insertTextContent(cursor, content_control, False) # === 设置控件内容 === cc_text = content_control.getText() cc_cursor = cc_text.createTextCursor() cc_cursor.setString(str(cell_value)) except Exception as e: raise RuntimeError(f"插入行时发生错误: {str(e)}") def BatchInsertRow(data_array): """ 批量插入行 """ doc = XSCRIPTCONTEXT.getDocument() tables = doc.getTextTables() bookmarks = doc.getBookmarks() bookmark_in_table_position = QueryBookmarkPositionInTable(tables, bookmarks) for arr_obj in data_array: location_bookmark_name = arr_obj.get("location_bookmark_name") data = arr_obj.get("data") start_row_index = arr_obj.get("start_row_index", -1) # 默认值为 -1 if location_bookmark_name not in bookmark_in_table_position: raise ValueError(f"未找到书签 {location_bookmark_name} 对应的表格") handle_table_index = bookmark_in_table_position[location_bookmark_name][ "tableIndex" ] try: handle_table = tables.getByIndex(handle_table_index) except IndexError: raise IndexError(f"表格索引 {handle_table_index} 超出范围") col_count = handle_table.getColumns().getCount() row_count = handle_table.getRows().getCount() if not data or len(data) == 0: return # if any(len(row) != col_count for row in data): # raise ValueError(f"数据列数不匹配,表格有 {col_count} 列") try: row_count = handle_table.getRows().getCount() rows_to_insert = len(data) if rows_to_insert > 0: # 确定插入位置 if start_row_index != -1 and 0 <= start_row_index < row_count: insert_pos = start_row_index + 1 # 在指定行下方插入 else: insert_pos = row_count # 插入到表格末尾 # 批量插入所有新行 handle_table.getRows().insertByIndex(insert_pos, rows_to_insert) # 填充数据到新插入的行 for data_row_idx, row_data in enumerate(data): target_row = insert_pos + data_row_idx # 计算目标行索引 for col_idx, cell_value in enumerate(row_data): cell = handle_table.getCellByPosition(col_idx, target_row) cell.setString(str(cell_value)) except Exception as e: raise RuntimeError(f"插入行时发生错误: {str(e)}") def DeleteRowWithJSON(json_str): """ 删除行 """ data = json.loads(json_str) DeleteRow( data["location_bookmark_name"], data["start_row_index"], data["delete_row_count"], ) def DeleteRow(location_bookmark_name, start_row_index, delete_row_count=-1): """ 删除表格行 Args: location_bookmark_name: 用于定位表格的书签 start_row_index: 起始行位置(删除行包含本行) delete_row_count: 删除的行数 """ doc = XSCRIPTCONTEXT.getDocument() tables = doc.getTextTables() bookmarks = doc.getBookmarks() bookmark_in_table_position = QueryBookmarkPositionInTable(tables, bookmarks) if location_bookmark_name not in bookmark_in_table_position: raise ValueError(f"未找到书签 {location_bookmark_name} 对应的表格") handle_table_index = bookmark_in_table_position[location_bookmark_name][ "tableIndex" ] handle_table = tables.getByIndex(handle_table_index) row_count = handle_table.getRows().getCount() if start_row_index < 0 or start_row_index >= row_count: raise ValueError(f"起始行索引 {start_row_index} 超出范围") # 如果未指定删除行数或超出范围,则删除剩余所有行 if start_row_index + delete_row_count > row_count or delete_row_count == -1: delete_row_count = row_count - start_row_index try: handle_table.getRows().removeByIndex(start_row_index, delete_row_count) except Exception as e: raise RuntimeError(f"err: {str(e)}") def LocateBookmark(bookmark_name): """ 定位书签并选中内容 Args: bookmark_name: 要定位的书签名称 """ doc = XSCRIPTCONTEXT.getDocument() if False == doc.getBookmarks().hasByName(bookmark_name): return bookmark = doc.getBookmarks().getByName(bookmark_name) try: # 获取书签的文本范围锚点 anchor = bookmark.getAnchor() # 获取文档控制器 controller = doc.getCurrentController() # 将视图光标移动到书签位置并选中 view_cursor = controller.getViewCursor() view_cursor.gotoRange(anchor, False) # False表示不扩展选区 # 如果需要高亮显示(可选) controller.select(anchor) # 确保窗口可见(针对隐藏窗口的情况) try: window = controller.getFrame().getContainerWindow() window.setVisible(True) window.toFront() except Exception: pass except Exception as e: raise RuntimeError(f"定位书签失败: {str(e)}") from e def InsertBookmark(bookmark_name): """ 选中内容插入书签,如果没有选中任何内容,插入点书签 Args: bookmark_name: 要插入的书签名称 """ doc = XSCRIPTCONTEXT.getDocument() controller = doc.getCurrentController() # 获取当前选区 selection = controller.getSelection() # 检查是否已存在同名书签,如果存在则先删除 bookmarks = doc.getBookmarks() if bookmarks.hasByName(bookmark_name): bookmarks.getByName(bookmark_name).dispose() try: # 创建书签 if selection.getCount() > 0: # 尝试获取选区的第一个元素 range_to_bookmark = selection.getByIndex(0) # 创建书签并附加到选区 bookmark = doc.createInstance("com.sun.star.text.Bookmark") bookmark.attach(range_to_bookmark) bookmark.setName(bookmark_name) else: # 没有选中内容,创建点书签 cursor = controller.getViewCursor() text_cursor = cursor.getText().createTextCursorByRange(cursor) # 创建书签并附加到光标位置 bookmark = doc.createInstance("com.sun.star.text.Bookmark") bookmark.attach(text_cursor) bookmark.setName(bookmark_name) except Exception as e: raise RuntimeError(f"插入书签失败: {str(e)}") from e def UpdateBookmark(bookmark_name, new_value): """ 修改书签名称(通过删除旧书签 + 插入新书签实现) Args: bookmark_name: 原书签名称 new_value: 新书签名称 """ doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() if not bookmarks.hasByName(bookmark_name): raise ValueError(f"书签 '{bookmark_name}' 不存在") if bookmarks.hasByName(new_value): raise ValueError(f"书签 '{new_value}' 已存在") # 获取旧书签的锚点位置 old_bookmark = bookmarks.getByName(bookmark_name) old_bookmark.setName(new_value) def DeleteBookmark(bookmark_name): """ 删除书签但保留内容(通过断开书签与锚点的关联) """ doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() if not bookmarks.hasByName(bookmark_name): return bookmark = bookmarks.getByName(bookmark_name) if hasattr(bookmark, "Anchor"): try: anchor = bookmark.Anchor anchor.getText().removeTextContent(bookmark) except Exception as e: pass def CreateTable(location_bookmark_name, data): """ 创建表格 Args: location_bookmark_name: 用于定位表格的书签 data: 二维数组,用于填充表格数据 """ def ReplaceTextAndInsertTableRow(json_str): """ 替换文本和表格 """ data = json.loads(json_str) Replace(data["text"]) BatchInsertRow(data["table"]) ReplaceBookmarkWithImage(data["image"]) def ReplaceTextAndInsertTableRowWithContentControl(json_str): data = json.loads(json_str) Replace(data["text"]) ReplaceBookmarkWithImage(data["image"]) BatchInsertRowWithContentControl(data["table"]) ReplaceBookmarksWithControls({}, ExtractBookmarkNames(data)) def returnWithJSON(data): return json.dumps(data, ensure_ascii=False) def SaveDocument(): # 获取当前文档对象 model = XSCRIPTCONTEXT.getDocument() try: # 检查文档是否支持保存(XStorable 接口) from com.sun.star.frame import XStorable xstorable = model.uno_getAdapter(XStorable) # 调用保存方法(覆盖原文件) xstorable.store() return True except Exception as e: print("err:", e) return False def ReplaceBookmarksWithControls(bookmark_name_value_map={}, exclude_bookmark_names=[]): # 获取文档对象 doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() # 过滤包含下划线前缀的书签 bookmark_names = [name for name in bookmarks.getElementNames() if not name.startswith("_")] # 遍历所有需要处理的书签 for name in bookmark_names: try: # 检查是否需要排除 if name in exclude_bookmark_names: continue # 检查书签是否存在 if not bookmarks.hasByName(name): continue # 获取书签对象及其锚点 bookmark = bookmarks.getByName(name) anchor = bookmark.getAnchor() # 获取书签的原始内容 original_text = anchor.getString() replace_value = bookmark_name_value_map.get(name, original_text) # === 第1步:创建内容控件 === content_control = doc.createInstance("com.sun.star.text.ContentControl") # 获取书签所在文本对象并清空原内容 text = anchor.getText() anchor.setString("") # 插入内容控件 text.insertTextContent(anchor, content_control, True) # 设置控件内容 cc_cursor = content_control.getText().createTextCursor() cc_cursor.setString(replace_value) # === 第2步:清理旧书签 === bookmark.dispose() # === 第3步:创建新书签 === cc_anchor = content_control.getText().getAnchor() new_bookmark = doc.createInstance("com.sun.star.text.Bookmark") new_bookmark.setName(name) new_bookmark.attach(cc_anchor) except Exception as e: print(f"处理书签 '{name}' 时出错: {e}") def NextEditableZone(current_index): """ 根据索引定位名称包含'permission'的书签(支持循环) Args: current_index (int/str): 从 0 开始的索引,支持字符串或整数类型 """ # 强制转换为整数 ----------------------------- try: current_index = int(current_index) except (ValueError, TypeError): print(f"错误:无法将 {current_index} 转换为整数") return doc = XSCRIPTCONTEXT.getDocument() controller = doc.getCurrentController() view_cursor = controller.getViewCursor() bookmarks = doc.getBookmarks() bookmark_names = bookmarks.getElementNames() # 过滤包含permission的书签(不区分大小写) permission_bookmarks = [name for name in bookmark_names if not name.startswith("_")] editable_anchors = [] for bm_name in permission_bookmarks: try: bookmark = bookmarks.getByName(bm_name) anchor = bookmark.getAnchor() editable_anchors.append(anchor) except Exception as e: print(f"err '{bm_name}' e: {e}") continue # 无匹配书签时直接返回 if not editable_anchors: return # 计算有效索引(循环逻辑) total = len(editable_anchors) effective_index = current_index % total # 自动处理越界 # 定位到目标书签 target_anchor = editable_anchors[effective_index] try: # 跳转到书签起始位置 view_cursor.gotoRange(target_anchor, False) # 选中整个书签范围 controller.select(target_anchor) # 窗口可见性处理 try: window = controller.getFrame().getContainerWindow() window.setVisible(True) window.toFront() except Exception: pass except Exception as e: raise RuntimeError(f"err: {str(e)}") from e def ReplaceBookmarkWithImage(data_array): """ 替换书签为图片,同时保留书签 参数: data_array: 包含书签和图片信息的字典数组 """ # 获取当前文档和相关服务 doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() smgr = XSCRIPTCONTEXT.getComponentContext().getServiceManager() graphic_provider = smgr.createInstanceWithContext("com.sun.star.graphic.GraphicProvider", XSCRIPTCONTEXT.getComponentContext()) if not graphic_provider: raise RuntimeError("无法初始化 GraphicProvider 服务") for data in data_array: bookmark_name = data["bookmarkName"] image_data = data["imageData"] width = data["width"] height = data["height"] file_type = data["fileType"] # 检查书签是否存在 if bookmark_name not in bookmarks: print(f"错误: 书签 '{bookmark_name}' 不存在") continue try: # 解码 Base64 数据 image_bytes = base64.b64decode(image_data) if not image_bytes: raise ValueError("Base64 数据为空或无效") # 获取书签的文本范围和光标 bookmark = bookmarks[bookmark_name] text_range = bookmark.getAnchor() text_cursor = text_range.getText().createTextCursorByRange(text_range) # 记录书签位置并删除书签内容 text = text_range.getText() text_cursor.setString("") # 删除书签范围内的文本 # 使用临时文件方式插入图片 temp_file_path = None try: with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_type}") as temp_file: temp_file.write(image_bytes) temp_file_path = temp_file.name file_url = systemPathToFileUrl(temp_file_path) url_prop = PropertyValue() url_prop.Name = "URL" url_prop.Value = file_url graphic_properties = (url_prop,) graphic = graphic_provider.queryGraphic(graphic_properties) if not graphic: raise RuntimeError(f"无法为书签 '{bookmark_name}' 生成图形对象") # 创建图形对象 graphic_object = doc.createInstance("com.sun.star.text.TextGraphicObject") graphic_object.Graphic = graphic graphic_object.Width = width graphic_object.Height = height # 设置锚定方式 graphic_object.AnchorType = AS_CHARACTER # 锚定为字符 # 插入图片 text.insertTextContent(text_cursor, graphic_object, False) if bookmark_name not in doc.getBookmarks(): # 重新创建书签以覆盖图片位置 bookmark_range = text.createTextCursorByRange(text_range) new_bookmark = doc.createInstance("com.sun.star.text.Bookmark") new_bookmark.Name = bookmark_name text.insertTextContent(bookmark_range, new_bookmark, False) finally: # 清理临时文件 if temp_file_path and os.path.exists(temp_file_path): os.unlink(temp_file_path) except Exception as e: continue def ExtractBookmarkNames(data): """ 从 data["image"] 数组中提取所有 bookmarkName,组成一个字符串数组 参数: data: 包含 "image" 键的字典,其值为图片对象数组 返回: list: 包含所有 bookmarkName 的字符串数组 """ if "image" not in data or not isinstance(data["image"], list): return [] # 使用列表推导式提取 bookmarkName bookmark_names = [item["bookmarkName"] for item in data["image"] if "bookmarkName" in item] return bookmark_names def GetBookmarksInLists(): """ 返回文档中所有位于列表中的书签 Returns: list: 位于列表中的书签名称列表 """ doc = XSCRIPTCONTEXT.getDocument() bookmarks = doc.getBookmarks() bookmarks_in_lists = [] # 遍历所有书签 for bookmark in bookmarks: bookmark_name = bookmark.getName() anchor = bookmark.getAnchor() # 判断书签的锚点是否在列表中 if anchor.supportsService("com.sun.star.text.Paragraph"): paragraph = anchor if paragraph.getPropertySetInfo().hasPropertyByName("NumberingStyleName"): numbering_style = paragraph.getPropertyValue("NumberingStyleName") if numbering_style: bookmarks_in_lists.append(bookmark_name) return bookmarks_in_lists g_exportedScripts = ( Replace, ReplaceWithJSON, QueryAll, QueryAllWithJSON, InsertRow, InsertRowWithJSON, DeleteRow, DeleteRowWithJSON, ReplaceTextAndInsertTableRow, ReplaceBookmarksWithControls, ReplaceTextAndInsertTableRowWithContentControl, SaveDocument, NextEditableZone, GetBookmarksInLists, )