无需使用InDesign,如何通过C#或Python代码合并多个.IDML文件?
无InDesign合并多个IDML文件的实现方案
C# 实现方案
核心思路
IDML是标准ZIP压缩包,内部包含XML格式的文档结构、页面布局和资源文件。合并步骤如下:
- 批量解压所有待合并的IDML文件
- 合并
Stories目录下的所有故事XML(文本内容核心) - 合并
Spreads目录下的页面布局XML - 更新
DesignMap.xml,将新增的故事和页面加入文档引用 - 重新打包所有文件为新的IDML
具体代码实现
需要引用System.IO.Compression和System.Xml命名空间:
using System; using System.IO; using System.IO.Compression; using System.Xml; using System.Collections.Generic; class IdmlMerger { static void Main(string[] args) { // 待合并的IDML文件路径列表 string[] idmlPaths = { "file1.idml", "file2.idml", "file3.idml" }; string outputPath = "merged_output.idml"; MergeIdmlFiles(idmlPaths, outputPath); } static void MergeIdmlFiles(string[] idmlPaths, string outputPath) { string tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempRoot); List<XmlDocument> storyDocs = new List<XmlDocument>(); List<XmlDocument> spreadDocs = new List<XmlDocument>(); Dictionary<string, string> resourceFiles = new Dictionary<string, string>(); foreach (string idmlPath in idmlPaths) { string extractDir = Path.Combine(tempRoot, Path.GetFileNameWithoutExtension(idmlPath)); ZipFile.ExtractToDirectory(idmlPath, extractDir); // 收集并重命名Stories string storiesDir = Path.Combine(extractDir, "Stories"); foreach (string storyFile in Directory.GetFiles(storiesDir, "*.xml")) { XmlDocument doc = new XmlDocument(); doc.Load(storyFile); XmlNode storyNode = doc.SelectSingleNode("//Story"); string newId = $"Story_{Guid.NewGuid().ToString().Replace("-", "")}"; storyNode.Attributes["Self"].Value = newId; storyDocs.Add(doc); } // 收集并重命名Spreads string spreadsDir = Path.Combine(extractDir, "Spreads"); foreach (string spreadFile in Directory.GetFiles(spreadsDir, "*.xml")) { XmlDocument doc = new XmlDocument(); doc.Load(spreadFile); XmlNode spreadNode = doc.SelectSingleNode("//Spread"); string newId = $"Spread_{Guid.NewGuid().ToString().Replace("-", "")}"; spreadNode.Attributes["Self"].Value = newId; spreadDocs.Add(doc); } // 收集去重的Resources资源 string resourcesDir = Path.Combine(extractDir, "Resources"); if (Directory.Exists(resourcesDir)) { foreach (string resFile in Directory.GetFiles(resourcesDir, "*.*", SearchOption.AllDirectories)) { string relativePath = resFile.Substring(extractDir.Length + 1); if (!resourceFiles.ContainsKey(relativePath)) { resourceFiles.Add(relativePath, resFile); } } } } // 创建输出目录结构 string outputDir = Path.Combine(tempRoot, "output"); Directory.CreateDirectory(Path.Combine(outputDir, "Stories")); Directory.CreateDirectory(Path.Combine(outputDir, "Spreads")); Directory.CreateDirectory(Path.Combine(outputDir, "Resources")); // 写入合并后的Stories int storyIndex = 1; foreach (XmlDocument storyDoc in storyDocs) { string storyPath = Path.Combine(outputDir, "Stories", $"Story_{storyIndex}.xml"); storyDoc.Save(storyPath); storyIndex++; } // 写入合并后的Spreads int spreadIndex = 1; foreach (XmlDocument spreadDoc in spreadDocs) { string spreadPath = Path.Combine(outputDir, "Spreads", $"Spread_{spreadIndex}.xml"); spreadDoc.Save(spreadPath); spreadIndex++; } // 复制资源文件 foreach (var kvp in resourceFiles) { string destPath = Path.Combine(outputDir, kvp.Key); Directory.CreateDirectory(Path.GetDirectoryName(destPath)); File.Copy(kvp.Value, destPath, true); } // 生成新的DesignMap.xml GenerateDesignMap(outputDir, storyDocs.Count, spreadDocs.Count); // 打包为IDML ZipFile.CreateFromDirectory(outputDir, outputPath); // 清理临时文件 Directory.Delete(tempRoot, true); } static void GenerateDesignMap(string outputDir, int storyCount, int spreadCount) { XmlDocument designMap = new XmlDocument(); XmlDeclaration dec = designMap.CreateXmlDeclaration("1.0", "UTF-8", null); designMap.AppendChild(dec); XmlElement root = designMap.CreateElement("Document"); root.SetAttribute("DOMVersion", "17.0"); designMap.AppendChild(root); // 添加Stories引用 XmlElement storiesElem = designMap.CreateElement("Stories"); for (int i = 1; i <= storyCount; i++) { XmlElement storyRef = designMap.CreateElement("Story"); storyRef.SetAttribute("Self", $"Story_{i}"); storyRef.SetAttribute("Src", $"Stories/Story_{i}.xml"); storiesElem.AppendChild(storyRef); } root.AppendChild(storiesElem); // 添加Spreads引用 XmlElement spreadsElem = designMap.CreateElement("Spreads"); for (int i = 1; i <= spreadCount; i++) { XmlElement spreadRef = designMap.CreateElement("Spread"); spreadRef.SetAttribute("Self", $"Spread_{i}"); spreadRef.SetAttribute("Src", $"Spreads/Spread_{i}.xml"); spreadsElem.AppendChild(spreadRef); } root.AppendChild(spreadsElem); string mapPath = Path.Combine(outputDir, "DesignMap.xml"); designMap.Save(mapPath); } }
关键注意点
- 通过GUID重命名Story和Spread的ID,避免合并时ID冲突
- 资源文件只保留一份重复项,减少文件体积
DesignMap.xml是IDML的核心索引文件,必须正确关联所有故事和页面
Python 实现方案
核心思路
利用Python的zipfile模块处理IDML的压缩/解压,xml.etree.ElementTree解析和修改XML,流程和C#一致:解压→合并内容→更新索引→重新打包。
具体代码实现
import zipfile import os import uuid import shutil from xml.etree import ElementTree as ET def merge_idml_files(idml_paths, output_path): temp_root = os.path.join(os.gettempdir(), str(uuid.uuid4())) os.makedirs(temp_root, exist_ok=True) story_elements = [] spread_elements = [] resource_files = {} for idml_path in idml_paths: extract_dir = os.path.join(temp_root, os.path.splitext(os.path.basename(idml_path))[0]) with zipfile.ZipFile(idml_path, 'r') as zip_ref: zip_ref.extractall(extract_dir) # 处理Stories stories_dir = os.path.join(extract_dir, 'Stories') for story_file in os.listdir(stories_dir): if story_file.endswith('.xml'): tree = ET.parse(os.path.join(stories_dir, story_file)) root = tree.getroot() new_id = f"Story_{uuid.uuid4().hex}" root.attrib['Self'] = new_id story_elements.append(root) # 处理Spreads spreads_dir = os.path.join(extract_dir, 'Spreads') for spread_file in os.listdir(spreads_dir): if spread_file.endswith('.xml'): tree = ET.parse(os.path.join(spreads_dir, spread_file)) root = tree.getroot() new_id = f"Spread_{uuid.uuid4().hex}" root.attrib['Self'] = new_id spread_elements.append(root) # 处理Resources resources_dir = os.path.join(extract_dir, 'Resources') if os.path.exists(resources_dir): for root_dir, _, files in os.walk(resources_dir): for file in files: src_path = os.path.join(root_dir, file) rel_path = os.path.relpath(src_path, extract_dir) if rel_path not in resource_files: resource_files[rel_path] = src_path # 创建输出目录结构 output_dir = os.path.join(temp_root, 'output') os.makedirs(os.path.join(output_dir, 'Stories'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'Spreads'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'Resources'), exist_ok=True) # 写入合并后的Stories for idx, story in enumerate(story_elements, 1): story_path = os.path.join(output_dir, 'Stories', f'Story_{idx}.xml') tree = ET.ElementTree(story) tree.write(story_path, encoding='UTF-8', xml_declaration=True) # 写入合并后的Spreads for idx, spread in enumerate(spread_elements, 1): spread_path = os.path.join(output_dir, 'Spreads', f'Spread_{idx}.xml') tree = ET.ElementTree(spread) tree.write(spread_path, encoding='UTF-8', xml_declaration=True) # 复制资源文件 for rel_path, src_path in resource_files.items(): dest_path = os.path.join(output_dir, rel_path) os.makedirs(os.path.dirname(dest_path), exist_ok=True) shutil.copy(src_path, dest_path) # 生成DesignMap.xml generate_design_map(output_dir, len(story_elements), len(spread_elements)) # 打包为IDML with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zip_ref: for root_dir, _, files in os.walk(output_dir): for file in files: file_path = os.path.join(root_dir, file) rel_path = os.path.relpath(file_path, output_dir) zip_ref.write(file_path, rel_path) # 清理临时文件 shutil.rmtree(temp_root) def generate_design_map(output_dir, story_count, spread_count): root = ET.Element('Document', {'DOMVersion': '17.0'}) # 添加Stories节点 stories_elem = ET.SubElement(root, 'Stories') for idx in range(1, story_count+1): ET.SubElement(stories_elem, 'Story', { 'Self': f'Story_{idx}', 'Src': f'Stories/Story_{idx}.xml' }) # 添加Spreads节点 spreads_elem = ET.SubElement(root, 'Spreads') for idx in range(1, spread_count+1): ET.SubElement(spreads_elem, 'Spread', { 'Self': f'Spread_{idx}', 'Src': f'Spreads/Spread_{idx}.xml' }) tree = ET.ElementTree(root) map_path = os.path.join(output_dir, 'DesignMap.xml') tree.write(map_path, encoding='UTF-8', xml_declaration=True) # 示例调用 if __name__ == '__main__': idml_files = ['file1.idml', 'file2.idml'] merge_idml_files(idml_files, 'merged_output.idml')
关键注意点
- 使用
uuid生成唯一ID避免冲突 - XML写入时指定UTF-8编码,确保IDML兼容性
- 资源文件去重,避免重复打包相同资源
内容的提问来源于stack exchange,提问作者Satoko EntertainMent
相关产品推荐
相关产品推荐

