如何避免在文档的页面之间拆分表

问题描述 投票:0回答:1

我有一个Google文档模板,可以在其中克隆并生成报告。我正在向这些报告中填充内容的表格添加表格。有时,所有表都可以放在一个页面中,但在某些情况下,它们会溢出到第二页中,从而使一个表在两个页面之间拆分。我想通过将此表推到第二页来避免该表在页面之间分裂。

GAS API不提供禁用此功能或提供信息以检索当前页面或页码的方法。是否有人有解决方法来避免上述情况?下面是我在做什么的示例代码。

function TestFunction() {
  var oFolder = DriveApp.getFolderById(Z__FOLDER_ID);
  var oReport = DriveApp.getFileById(Z_TEMPLATE_ID).makeCopy("TEST", oFolder);
  var oDoc = DocumentApp.openById(oReport.getId());

  var arrTables = oDoc.getBody().getTables();
  var copiedTable = arrTables[2].copy();
  arrTables[2].removeFromParent();

  var iTableCount = 0;

  for(var iHdrIdx = 0; iHdrIdx < 7; iHdrIdx++) {        
    var oCompTable = copiedTable.copy();

    oCompTable.replaceText("<PLCHLDER_1>", "TEST_1");
    oCompTable.replaceText("<PLCHLDER_2>", "TEST_2");

    iTableCount = iTableCount + 1;
    oDoc.getBody().insertTable(13 + iTableCount, oCompTable);
  }

  oDoc.saveAndClose();
}
google-apps-script google-docs
1个回答
0
投票

文档树

[如果您仔细查看Google文档的structure,您会注意到它是树数据结构,而不是广义上的文档(尽管可能会认为章节/节/段落也是树)样的结构)。

[我怀疑以上是API缺乏页面相关方法的原因-尽管将来可能会添加它们]]

由于文档是树,因此可以将确定何时发生分页的问题简化为计算子高度之和

使页面高度溢出时的点。

问题细分

为了正确获得发生分裂的位置(并跟踪此类元素),我们需要解决几个子问题:

  1. 获取页面的高度,宽度边距
  2. 横向元素,跟踪总高度。在每一步:
    1. 计算元素的高度[[full
    2. 将高度加到总计中,检查是否发生溢出。
  3. 如果总
  4. overflows
  5. 页面高度,则保证最后一个最外层(最接近根)元素被分割。将元素添加到列表中,缓存溢出并重置总数(新页面)。
观察

    当遇到PageBreak时,可以重置总计数器,因为下一个元素将位于顶部(由溢出偏移)。请注意,由于PageBreak不是独立的(包装为ParagraphListItem,所以可以随时遇到。)>
  1. TableCell中最高的TableRow计入总高度。

  • 某些元素继承自ContainerElement,这意味着它们的高度等于
  • 其子代高度的总和
  • +上下边界。
    助手功能

    我们可以定义几个有用的函数(有关详细信息,请参见JSDoc注释:]

    /** * @summary checks if element is a container * @param {GoogleAppsScript.Document.Element} elem * @param {GoogleAppsScript.Document.ElementType} type * @returns {boolean} */ const isContainer = (elem, type) => { const Types = DocumentApp.ElementType; const containerTypes = [ Types.BODY_SECTION, Types.EQUATION, Types.EQUATION_FUNCTION, Types.FOOTER_SECTION, Types.HEADER_SECTION, Types.LIST_ITEM, Types.PARAGRAPH, Types.TABLE, Types.TABLE_CELL, Types.TABLE_ROW, Types.TABLE_OF_CONTENTS ]; return containerTypes.includes(type || elem.getType()); }; /** * @summary gets aspect ratio of a font * @param {string} fontFamily * @returns {number} * @default .52 */ const getAspectRatio = (fontFamily) => { const aspects = { Arial: .52, Calibri: .47, Courier: .43, Garamond: .38, Georgia: .48, Helvetica: .52, Times: .45, Verdana: .58 }; return aspects[fontFamily] || .618; }; /** * @summary checks if Element is direct child of Body * @param {GoogleAppsScript.Document.Element} elem * @returns {boolean} */ const isTopLevel = (elem) => { const { ElementType } = DocumentApp; return elem.getParent().getType() === ElementType.BODY_SECTION; }; /** * @summary copies non-object array values as is * @param {any[]} arr * @returns {any[]} */ const shallowCopy = (arr) => { return arr.map(el => el); };

    状态跟踪

    由于我们还必须跟踪溢出状态,处理的元素等,因此我选择添加一个Tracker“类”来处理状态管理。最值得注意的是:

    processResults方法:

      确保计算嵌套元素的高度后,确保元素边界(页面大小)得以恢复(setDimensionssetMarginsresetDimensionsresetMargins和私有inits方法允许我们操纵边界)。
    1. 修改特定元素类型的已处理高度:
      1. Body的高度设置为0(否则它将复制子高度)。
      2. [TableRow的高度设置为最高的TableCell
      3. 其他类型的身高加上孩子的身高。
  • handleOverflow方法:
    1. 防止将嵌套元素添加到拆分列表中(可以安全删除)。>>
    2. 将总高度重置为最新的溢出偏移量(元素拆分的一部分的高度)。
  • totalHeight设置器:
  • 每次重新计算都会查找高度溢出,并在需要时调用溢出处理程序。

    /** * @typedef {object} Tracker * @property {Map.<GoogleAppsScript.Document.ElementType, function>} callbacks map of height processers * @property {?GoogleAppsScript.Document.Element} currElement current elemenet processed * @property {number[]} dimensions exposes dimensions of a page * @property {function(): void} handleOverflow handles page height overflow * @property {function(): boolean} isOverflow checks if height overflew page height * @property {number[]} margins exposes margins of a page * @property {number} overflow getter for overflow status * @property {function(boolean, ...number): number} processResults process callback results * @property {function(): Tracker} resetDimensions restores old dimensions * @property {function(): Tracker} resetMargins restores old margins * @property {function(): void} resetOverflow resets most resent overflow * @property {function(): void} resetTotalHeight resets accumulated height * @property {function(...number): void} setDimensions reinits containing dimensions * @property {function(...number): void} setMargins reinits containing margins * @property {function(string, ...any): void} setStore abstract property store setter * @property {number} significantWidth exposes significant page width * @property {number} significantHeight exposes significant page height * @property {GoogleAppsScript.Document.Element[]} splits list of elements split over page * @property {number} totalHeight total height * * @summary factory for element trackers * @param {Tracker#callbacks} callbacks * @param {Bounds} bounds * @param {Tracker#splits} [splits] * @returns {Tracker} */ function makeTracker(callbacks, bounds, splits = []) { const inits = { dimensions: shallowCopy(bounds.dimensions), margins: shallowCopy(bounds.margins) }; const privates = { bounds, current: null, currentType: null, currOverflow: 0, needsReset: 0, totalHeight: 0 }; const { ElementType } = DocumentApp; const ResultProcessors = new Map() .set(ElementType.BODY_SECTION, () => 0) .set(ElementType.TABLE_ROW, (results) => { return results.reduce((result, acc) => result > acc ? result : acc, 0); }) .set("default", (results) => { return results.reduce((result, acc) => result + acc, 0); }); return ({ callbacks, splits, get currElement() { return privates.current; }, set currElement(element) { privates.current = element; privates.currentType = element.getType(); }, get dimensions() { const { bounds } = privates; return bounds.dimensions; }, get margins() { const { bounds } = privates; return bounds.margins; }, get overflow() { const { bounds, totalHeight } = privates; return totalHeight - bounds.significantHeight; }, get significantHeight() { const { bounds } = privates; return bounds.significantHeight; }, get significantWidth() { const { bounds } = privates; return bounds.significantWidth; }, get totalHeight() { return privates.totalHeight; }, /** * @summary total height setter * @description intercepts & recalcs overflow * @param {number} height */ set totalHeight(height) { privates.totalHeight = height; if (this.isOverflow()) { privates.currOverflow = this.overflow; this.handleOverflow(); } }, isOverflow() { return this.overflow > 0; }, handleOverflow() { const { currElement, splits } = this; const type = privates.currentType; const ignore = [ ElementType.TEXT, ElementType.TABLE_ROW ]; if (!ignore.includes(type)) { splits.push(currElement); } this.resetTotalHeight(); }, processResults(...results) { this.resetMargins().resetDimensions(); const { currentType } = privates; const processed = ( ResultProcessors.get(currentType) || ResultProcessors.get("default") )(results); return processed; }, resetDimensions() { const { bounds } = privates; const { dimensions } = bounds; dimensions.length = 0; dimensions.push(...inits.dimensions); return this; }, resetMargins() { const { bounds } = privates; const { margins } = bounds; margins.length = 0; margins.push(...inits.margins); return this; }, resetOverflow() { privates.currOverflow = 0; }, resetTotalHeight() { const { currOverflow } = privates; this.totalHeight = currOverflow; this.resetOverflow(); }, setDimensions(...newDimensions) { return this.setStore("dimensions", ...newDimensions); }, setMargins(...newMargins) { return this.setStore("margins", ...newMargins); }, setStore(property, ...values) { const { bounds } = privates; const initStore = inits[property]; const temp = values.map((val, idx) => { return val === null ? initStore[idx] : val; }); const store = bounds[property]; store.length = 0; store.push(...temp); } }); };


    I。获取页面边界

    第一个子问题很容易解决(示例可能很复杂,但是便于传递状态)。这里需要注意的是significantWidthsignificantHeight吸气剂,它们返回元素可以占用的宽度和高度(即,没有边距)。

    [如果您想知道,为什么将54添加到顶部和底部页边距,它是一个等于1.5默认垂直页边距(36点)的“魔术数字”,以确保正确的页面溢出(我花了数小时来弄清楚为什么会有大约appx的空间。尽管HeaderSectionFooterSection默认为null,但此大小会添加到首页和底页页边距中,但似乎没有)。

    /** * @typedef {object} Bounds * @property {number} bottom bottom page margin * @property {number[]} dimensions page constraints * @property {number} left left page margin * @property {number[]} margins page margins * @property {number} right right page margin * @property {number} top top page margin * @property {number} xMargins horizontal page margins * @property {number} yMargins vertical page margins * * @summary gets dimensions of pages in body * @param {Body} body * @returns {Bounds} */ function getDimensions(body) { const margins = [ body.getMarginTop() + 54, body.getMarginRight(), body.getMarginBottom() + 54, body.getMarginLeft() ]; const dimensions = [ body.getPageHeight(), body.getPageWidth() ]; return ({ margins, dimensions, get top() { return this.margins[0]; }, get right() { return this.margins[1]; }, get bottom() { return this.margins[2]; }, get left() { return this.margins[3]; }, get xMargins() { return this.left + this.right; }, get yMargins() { return this.top + this.bottom; }, get height() { return this.dimensions[0]; }, get width() { return this.dimensions[1]; }, get significantWidth() { return this.width - this.xMargins; }, get significantHeight() { return this.height - this.yMargins; } }); }


    II。遍历元素

    我们需要从根(Body)开始逐步遍历所有子代,直到到达一片叶子(没有子代的元素),获取它们的外部高度和子代的高度(如果有的话),同时始终跟踪PageBreaks和积累的高度。作为Element直系子代的每个Body都将被拆分。

    注意,PageBreak将重置总高度计数器:

    /** * @summary executes a callback for element and its children * @param {GoogleAppsScript.Document.Element} root * @param {Tracker} tracker * @param {boolean} [inCell] * @returns {number} */ function walkElements(root, tracker, inCell = false) { const { ElementType } = DocumentApp; const type = root.getType(); if (type === ElementType.PAGE_BREAK) { tracker.resetTotalHeight(); return 0; } const { callbacks } = tracker; const callback = callbacks.get(type); const elemResult = callback(root, tracker); const isCell = type === ElementType.TABLE_CELL; const cellBound = inCell || isCell; const childResults = []; if (isCell || isContainer(root, type)) { const numChildren = root.getNumChildren(); for (let i = 0; i < numChildren; i++) { const child = root.getChild(i); const result = walkElements(child, tracker, cellBound); childResults.push(result); } } tracker.currElement = root; const processed = tracker.processResults(elemResult, ...childResults); isTopLevel(root) && (tracker.totalHeight += processed); return processed; }


    III。计算元素高度

    通常,

    full

    元素的高度是顶部,底部边距(或填充或边框)+ base
    高度。此外,由于某些元素是容器,因此它们的基本高度等于其子代的总高度之和。因此,我们可以将第三个子问题细分为:
      原始类型的高度(无子代)
  • 容器类型的高度
  • 原始类型

  • 文字高度

    [Text]元素由海图字符组成,因此要计算基本高度必须:

      获取父母的身分
  • 获取字符高度和宽度(为简单起见,它取决于字体的长宽比)
  • 从有用的页面宽度减去缩进量(=行宽)
  • 对于每个字符,增加行宽直到溢出并增加行数,然后
  • 1
  • 文本高度将通过字符高度等于行数并应用行间距修饰符
  • 1

  • 此处不需要遍历char,但是如果需要更高的精度,则可以映射char宽度修饰符,引入字距调整等。/** * @summary calculates Text element height * @param {GoogleAppsScript.Document.Text} elem * @param {Tracker} tracker * @returns {number} */ function getTextHeight(elem, tracker) { const { significantWidth } = tracker; const fontFamily = elem.getFontFamily(); const charHeight = elem.getFontSize() || 11; const charWidth = charHeight * getAspectRatio(fontFamily); /** @type {GoogleAppsScript.Document.ListItem|GoogleAppsScript.Document.Paragraph} */ const parent = elem.getParent(); const lineSpacing = parent.getLineSpacing(); const startIndent = parent.getIndentStart(); const endIndent = parent.getIndentEnd(); const lineWidth = significantWidth - (startIndent + endIndent); const text = elem.getText(); let adjustedWidth = 0, numLines = 1; for (const char of text) { adjustedWidth += charWidth; const diff = adjustedWidth - lineWidth; if (diff > 0) { adjustedWidth = diff; numLines++; } } return numLines * charHeight * lineSpacing; }

    容器类型

    幸运的是,我们的walker递归处理子元素,因此我们只需要处理每种容器类型的细节(跟踪器的processResults方法将连接子高度)。

    段落

    Paragraph有两个属性集,这些属性增加了其全部高度:

    边距

    (其中我们只需要顶部和底部-通过getAttributes()可访问)和spacing/** * @summary calcs par height * @param {GoogleAppsScript.Document.Paragraph} par * @returns {number} */ function getParagraphHeight(par) { const attrEnum = DocumentApp.Attribute; const attributes = par.getAttributes(); const before = par.getSpacingBefore(); const after = par.getSpacingAfter(); const spacing = before + after; const marginTop = attributes[attrEnum.MARGIN_TOP] || 0; const marginBottom = attributes[attrEnum.MARGIN_BOTTOM] || 0; let placeholderHeight = 0; if (par.getNumChildren() === 0) { const text = par.asText(); placeholderHeight = (text.getFontSize() || 11) * (par.getLineSpacing() || 1.15); } return marginTop + marginBottom + spacing + placeholderHeight; }
    注意placeholderHeight部分-因为当您附加Table时,必须插入一个空的Paragraph(无Text),它等于1行默认文本。

    表格单元格

    TableCell元素是一个容器,用作其子元素的主体,因此,例如,计算单元格内部的Text的高度,尺寸和边距(填充

    在此上下文中>是相同的边界的边界临时设置为该单元格的边界(高度可以保留不变):

    /** * @summary calcs TableCell height * @param {GoogleAppsScript.Document.TableCell} elem * @param {Tracker} tracker * @returns {number} */ function getTableCellHeight(elem, tracker) { const top = elem.getPaddingTop(); const bottom = elem.getPaddingBottom(); const left = elem.getPaddingLeft(); const right = elem.getPaddingRight(); const width = elem.getWidth(); tracker.setDimensions(null, width); tracker.setMargins(top, right, bottom, left); return top + bottom; }

    表格行

    [TableRow没有任何特定属性可计入全高度(并且我们的跟踪器处理TableCell高度):]]

    /** * @summary calcs TableRow height * @param {GoogleAppsScript.Document.TableRow} row * @returns {number} */ function getTableRowHeight(row) { return 0; }

    表格

    Table仅包含行,仅将水平边框宽度添加到总数中(只有顶部[或底部]行具有2个边界而不会发生碰撞,因此仅

    行数+ 1

    个边界计数):/** * @summary calcs Table height * @param {GoogleAppsScript.Document.Table} elem * @returns {number} */ function getTableHeight(elem) { const border = elem.getBorderWidth(); const rows = elem.getNumRows(); return border * (rows + 1); }

    IV。确定溢出

    第四个子问题仅连接前面的部分:

    /** * @summary finds elements spl it by pages * @param {GoogleAppsScript.Document.Document} doc * @returns {GoogleAppsScript.Document.Element[]} */ function findSplitElements(doc) { const body = doc.getBody(); const bounds = getDimensions(body); const TypeEnum = DocumentApp.ElementType; const heightMap = new Map() .set(TypeEnum.BODY_SECTION, () => 0) .set(TypeEnum.PARAGRAPH, getParagraphHeight) .set(TypeEnum.TABLE, getTableHeight) .set(TypeEnum.TABLE_ROW, getTableRowHeight) .set(TypeEnum.TABLE_CELL, getTableCellHeight) .set(TypeEnum.TEXT, getTextHeight); const tracker = makeTracker(heightMap, bounds); walkElements(body, tracker); return tracker.splits; };

    驱动程序功能

    为了测试整个解决方案是否有效,我使用了该驱动程序:

    function doNTimes(n, callback, ...args) { for (let i = 0; i < n; i++) { callback(...args); } } function prepareDoc() { const doc = getTestDoc(); //gets Document somehow const body = doc.getBody(); doNTimes(30, () => body.appendParagraph("Redrum Redrum Redrum Redrum".repeat(8))); const cells = [ [1, 2, 0, "A", "test"], [3, 4, 0, "B", "test"], [5, 6, 0, "C", "test"], [7, 8, 0, "D", "test"], [9, 10, 0, "E", "test"], [11, 12, 0, "F", "test"] ]; body.appendTable(cells); doNTimes(8, (c) => body.appendTable(c), cells); body.appendPageBreak(); doNTimes(5, (c) => body.appendTable(c), cells); const splits = findSplitElements(doc); for (const split of splits) { split.setAttributes({ [DocumentApp.Attribute.BACKGROUND_COLOR]: "#fd9014" }); } return doc.getUrl(); }

    驱动程序功能将使用背景颜色标记每个拆分元素(您可能希望在每个元素之前添加PageBreak

    Element split sample

    注意

      答案很可能会忽略某些内容(即,如果Table的一整行都适合上一页,则它不会以某种方式算作溢出),并且可以加以改进(+将扩展为其他类,例如[C0 ]以后),因此,如果有人知道对问题的任何部分有更好的解决方案,让我们进行讨论(或辞退并直接做出贡献)。
    1. 参考

        [ListItemContainerElement
      1. [docs枚举ElementType

    2. [specParagraph
    3. [docsTableCell

  • docs的结构
  • 最新问题
    © www.soinside.com 2019 - 2024. All rights reserved.