Apps Script如何遍历Google Docs所有页面并为每页添加独立标题?
实现核心前提
Google Docs的Apps Script原生没有提供直接按页遍历的API,因为分页属于前端渲染层动态计算的结果,和文档存储的结构化元素没有固定映射关系,没法直接调用类似getPages()的方法拿到分页位置,需要通过排版参数模拟计算分页点,同时适配跨页元素不拆分的需求。
具体实现思路
- 第一步先获取文档基础排版参数
调用Document服务拿到Body的页面高度、上下边距数值,计算出单页可容纳的有效内容高度,所有计算统一用磅(pt)作为单位,和Docs原生排版单位对齐,同时预留2pt左右的容错值抵消行高、空行带来的渲染误差。 - 第二步逐元素遍历累计高度判断分页点
从文档Body的第一个子元素开始逐个遍历(覆盖段落、表格、图片、列表所有块级元素),根据元素类型计算单个元素的渲染高度:普通段落按字体大小、行高、段前段后间距算总高度,表格、图片直接取内嵌的高度属性即可。遍历过程中维护一个累计高度变量,每处理一个元素就累加对应高度:- 累加后高度未超过单页有效高度:元素属于当前页,继续遍历
- 遇到手动分页符元素:直接在分页符后插入对应序号的标题,重置累计高度为0,优先尊重手动分页逻辑
- 累加前高度未超限、累加后超限:说明当前元素是跨页元素,不要拆分元素,直接在该元素的起始位置插入下一页标题,将该元素整体归到下一页,避免同一段落被标题截断
- 第三步标题插入的防重复处理
你预存的titles数组按页码顺序取值即可,每插入一个标题就将数组索引+1;可以给插入的标题设置专属样式或者隐藏标记,避免后续重复运行脚本时重复插入标题。 - 精度优化选项
如果需要100%匹配前端渲染的分页位置,可以临时将文档导出为PDF,解析PDF的分页偏移量反向映射回Docs的元素位置,不过实现成本较高,普通办公场景用高度累计的方案误差不会超过1行,完全够用。
核心逻辑参考代码
function addPageTopTitles() { const doc = DocumentApp.getActiveDocument(); const body = doc.getBody(); // 按页码顺序存储的每页标题数组 const titles = ["第一页标题", "第二页标题", "第三页标题", "第四页标题"]; // 计算单页有效内容高度 单位pt const pageHeight = body.getPageHeight(); const marginTop = body.getMarginTop(); const marginBottom = body.getMarginBottom(); const effectivePageHeight = pageHeight - marginTop - marginBottom; const errorTolerance = 2; // 渲染误差容错值 let currentAccumHeight = 0; let titleIndex = 0; let totalChildCount = body.getNumChildren(); // 第一页顶部插入第一个标题 if (titles.length > 0) { const firstTitle = body.insertParagraph(0, titles[titleIndex]); firstTitle.setHeading(DocumentApp.ParagraphHeading.HEADING1); // 累加标题本身的高度 currentAccumHeight += firstTitle.getFontSize() * 1.5; titleIndex += 1; totalChildCount = body.getNumChildren(); } // 遍历所有正文元素 for (let i = 1; i < totalChildCount; i++) { const currentChild = body.getChild(i); let childHeight = 0; // 按元素类型计算高度 switch(currentChild.getType()) { case DocumentApp.ElementType.PARAGRAPH: const para = currentChild.asParagraph(); // 检测到手动分页符,直接在后面插标题 if (para.findElement(DocumentApp.ElementType.PAGE_BREAK) && titleIndex < titles.length) { const insertPos = i + 1; const newTitle = body.insertParagraph(insertPos, titles[titleIndex]); newTitle.setHeading(DocumentApp.ParagraphHeading.HEADING1); titleIndex += 1; currentAccumHeight = 0; totalChildCount = body.getNumChildren(); i += 1; // 跳过刚插入的标题元素 continue; } // 计算段落高度 const fontSize = para.getFontSize() || 12; const lineSpacing = para.getLineSpacing() ? para.getLineSpacing()/100 : 1.15; const spaceBefore = para.getSpacingBefore() || 0; const spaceAfter = para.getSpacingAfter() || 0; childHeight = fontSize * lineSpacing + spaceBefore + spaceAfter; break; case DocumentApp.ElementType.TABLE: const table = currentChild.asTable(); childHeight = table.getRowHeight() * table.getNumRows(); break; case DocumentApp.ElementType.INLINE_IMAGE: childHeight = currentChild.asInlineImage().getHeight(); break; case DocumentApp.ElementType.LIST_ITEM: const listItem = currentChild.asListItem(); const listFontSize = listItem.getFontSize() || 12; childHeight = listFontSize * 1.2 + (listItem.getSpacingBefore()||0) + (listItem.getSpacingAfter()||0); break; default: childHeight = 12; } // 判断是否到达分页点 if (currentAccumHeight + childHeight > effectivePageHeight - errorTolerance && titleIndex < titles.length) { // 跨页元素不拆分,在元素前插入下一页标题 const newTitle = body.insertParagraph(i, titles[titleIndex]); newTitle.setHeading(DocumentApp.ParagraphHeading.HEADING1); titleIndex += 1; // 重置累计高度为当前跨页元素+新标题的高度 currentAccumHeight = childHeight + newTitle.getFontSize()*1.5; totalChildCount = body.getNumChildren(); i += 1; // 跳过刚插入的标题 } else { currentAccumHeight += childHeight; } } }
注意:脚本运行前请先复制原文档做备份,上述代码中的元素高度计算是通用简化逻辑,如果你的文档有复杂混排、特殊字体、自定义段间距,可以对应调整高度计算规则提升精度。
内容的提问来源于stack exchange,提问作者Marco Maggioni
相关产品推荐
相关产品推荐

