libreoffice-python-script/BookmarkOP.py
2025-03-28 11:54:53 +08:00

815 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
)