diff --git a/.travis.yml b/.travis.yml index 0322d53b..15aa4c4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,7 @@ jobs: - bundle exec pod install --project-directory="${ROOT_FOLDER}" - cd "${ROOT_FOLDER}" script: - - set -o pipefail + - set -o pipefail - xcodebuild -workspace "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk iphonesimulator @@ -122,7 +122,7 @@ jobs: -clonedSourcePackagesDirPath . -derivedDataPath ${TRAVIS_BUILD_DIR}/derived_data -configuration Debug | xcpretty - - set -o pipefail + - set -o pipefail - xcodebuild -project ${PROJECT} -scheme ${EXAMPLE_SCHEME} -clonedSourcePackagesDirPath . @@ -146,7 +146,7 @@ jobs: -clonedSourcePackagesDirPath . -derivedDataPath ${TRAVIS_BUILD_DIR}/derived_data -configuration Debug | xcpretty - - set -o pipefail + - set -o pipefail - xcodebuild -project ${PROJECT} -scheme ${EXAMPLE_SCHEME} -clonedSourcePackagesDirPath . diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b74dfc..d3be1fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,21 @@ **Implemented enhancements:** +- Added optional table cell splicing disabling + **Fixed bugs:** **Closed issues:** +- Issue #205 +- Issue #222 +- Issue #243 +- Issue #249 + **Merged pull requests:** +- PR #223 + ## [2.3.1](https://github.com/techprimate/TPPDF/tree/2.3.1) (2020-09-23) [Full Changelog](https://github.com/techprimate/TPPDF/compare/2.3.0...2.3.1) diff --git a/Shared/Examples/ExperimentFactory.swift b/Shared/Examples/ExperimentFactory.swift index 82799a05..58234932 100644 --- a/Shared/Examples/ExperimentFactory.swift +++ b/Shared/Examples/ExperimentFactory.swift @@ -19,6 +19,7 @@ class ExperimentFactory: ExampleFactory { table.margin = 10 table.padding = 10 table.showHeadersOnEveryPage = false + table.shouldSplitCellsOnPageBreak = false table.style.columnHeaderCount = 3 for row in 0.. PDFTableCalculatedCell { - var frame: PDFTableCalculatedCell = ( + var frame = PDFTableCalculatedCell( cell: cell, type: type, style: style, @@ -335,8 +330,18 @@ internal class PDFTableObject: PDFRenderObject { minOffset += headerHeight } - var onPageCells: [PDFTableCalculatedCell] - (onPageCells, nextPageCells) = filterCellsOnPage(for: generator, items: nextPageCells, minOffset: minOffset, maxOffset: maxOffset) + let filterResult = filterCellsOnPage(for: generator, + items: nextPageCells, + minOffset: minOffset, + maxOffset: maxOffset, + shouldSplitCellsOnPageBeak: table.shouldSplitCellsOnPageBreak) + let onPageCells = filterResult.cells + nextPageCells = filterResult.remainder + // If none of the cells fit on the current page, the algorithm will try again on the next page and if it occurs again, an error should be thrown + if onPageCells.isEmpty && !firstPage, let firstInvalidCell = nextPageCells.first { + throw PDFError.tableCellTooBig(cell: firstInvalidCell.cell) + } + for (idx, item) in onPageCells.enumerated() { let cellFrame = item.frames.cell @@ -392,29 +397,60 @@ internal class PDFTableObject: PDFRenderObject { return (objects: result, offset: pageEnd.y) } - internal typealias FilteredCells = (cells: [PDFTableCalculatedCell], rest: [PDFTableCalculatedCell]) + /// Holds two lists of cells, used during table calculations + internal struct FilteredCells { + /// List of calculated cells on the active page + var cells: [PDFTableCalculatedCell] + /// List of remaining cells on further pages + var remainder: [PDFTableCalculatedCell] + } + - internal func filterCellsOnPage(for generator: PDFGenerator, items: [PDFTableCalculatedCell], minOffset: CGFloat, maxOffset: CGFloat) -> FilteredCells { + /// Filters the given list of cells into the ones that fit on the current page, and all remainding cells, repositioned for the next page. + /// + /// - Parameters: + /// - generator: Active instance of `PDFGenerator` + /// - items: List of cells to filter + /// - minOffset: Minimum `y`-position on the page + /// - maxOffset: Maximum `y`-position on the page + /// - shouldSplitCellsOnPageBreak: If `true`, cells won't be sliced and shown on both pages, instead moved entirely to the next page + /// - Returns: Two lists of cells, see `FilteredCells` + internal func filterCellsOnPage(for generator: PDFGenerator, items: [PDFTableCalculatedCell], minOffset: CGFloat, maxOffset: CGFloat, shouldSplitCellsOnPageBeak: Bool) -> FilteredCells { + // Maximum height available let contentHeight = maxOffset - minOffset + var result = FilteredCells(cells: [], remainder: []) - var cells: [PDFTableCalculatedCell] = [] - var rest: [PDFTableCalculatedCell] = [] + var offsetFix: CGFloat! - for item in items { + // Iterate each cell and decide if it fits on current page or if it needs to be moved to the further pages + for item in items { let cellFrame = item.frames.cell - if cellFrame.maxY < maxOffset { - cells.append(item) + + // Cells needs to fit the current available space entirely + if cellFrame.maxY < maxOffset { // TODO: is the row padding relevant here? + result.cells.append(item) } else { - if cellFrame.minY < maxOffset { - cells.append(item) + // If cells should be split and cell is partially on current page, add it to the cells, the cell will be sliced afterwards + if shouldSplitCellsOnPageBeak && cellFrame.minY < maxOffset { + result.cells.append(item) } + // In any case, if the cell does not fit on the active page entirely, it must be repositioned for further pages var nextPageCell = item - nextPageCell.frames.cell.origin.y -= contentHeight - nextPageCell.frames.content.origin.y -= contentHeight - rest.append(nextPageCell) + if shouldSplitCellsOnPageBeak { + nextPageCell.frames.cell.origin.y -= contentHeight + nextPageCell.frames.content.origin.y -= contentHeight + } else { + let cellContentOffset = nextPageCell.frames.content.minY - nextPageCell.frames.cell.minY + if offsetFix == nil { + offsetFix = nextPageCell.frames.cell.minY - minOffset + } + nextPageCell.frames.cell.origin.y -= offsetFix + nextPageCell.frames.content.origin.y = nextPageCell.frames.cell.minY + cellContentOffset + } + result.remainder.append(nextPageCell) } } - return (cells: cells, rest: rest) + return result } internal func createSliceObject(frame: CGRect, elements: [PDFRenderObject], minOffset: CGFloat, maxOffset: CGFloat) -> PDFSlicedObject { @@ -430,9 +466,12 @@ internal class PDFTableObject: PDFRenderObject { return sliceObject } - /** - Creates a render object for the cell background - */ + /// Creates a render object for the cell background + /// + /// - Parameters: + /// - style: Style of table cell + /// - frame: Frame of cell + /// - Returns: Calculated `PDFRectangleObject` internal func createCellBackgroundObject(style: PDFTableCellStyle, frame: CGRect) -> PDFRenderObject { let object = PDFRectangleObject(lineStyle: .none, size: frame.size, fillColor: style.colors.fill) object.frame = frame @@ -500,14 +539,11 @@ internal class PDFTableObject: PDFRenderObject { return .content } - /** - Returns the style of a given cell, depending on the type. - - - parameters tableStyle: Style configuration of table - - parameters type: Type of cell - - - returns: Style of cell - */ + /// Returns the style of a given cell, depending on the type. + /// - Parameters: + /// - tableStyle: Style configuration of table + /// - type: Type of cell + /// - Returns: Style of cell internal func getStyle(tableStyle: PDFTableStyle, type: CellType) -> PDFTableCellStyle { switch type { case .header: @@ -525,14 +561,11 @@ internal class PDFTableObject: PDFRenderObject { } } - /** - Creates four outline line objects around a given frame using the given style. - - - parameter borders: Style of each border direction - - parameter frame: Frame of rectangle - - - returns: Array of `PDFLineObject` - */ + /// Creates four outline line objects around a given frame using the given style. + /// - Parameters: + /// - borders: Style of each border edge + /// - frame: Frame of rectangle + /// - Returns: Array of `PDFLineObject` internal func createCellOutlineObjects(borders: PDFTableCellBorders, frame: CGRect) -> [PDFLineObject] { [ PDFLineObject(style: borders.top, @@ -550,9 +583,7 @@ internal class PDFTableObject: PDFRenderObject { ] } - /** - Creates a new `PDFTableObject` with the same properties - */ + /// Creates a new `PDFTableObject` with the same properties override internal var copy: PDFRenderObject { PDFTableObject(table: table.copy) } diff --git a/TPPDF.xcodeproj/project.pbxproj b/TPPDF.xcodeproj/project.pbxproj index f3369aa7..ab96d17f 100644 --- a/TPPDF.xcodeproj/project.pbxproj +++ b/TPPDF.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ D4803D3A24703E5300DDA039 /* PDFAttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4803C9F24703E5300DDA039 /* PDFAttributedText.swift */; }; D4803D3B24703E5300DDA039 /* PDFText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4803CA024703E5300DDA039 /* PDFText.swift */; }; D4803D3C24703E5300DDA039 /* PDFTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4803CA124703E5300DDA039 /* PDFTextStyle.swift */; }; + D4839C39253706950005BB87 /* PDFTableCalculatedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4839C38253706950005BB87 /* PDFTableCalculatedCell.swift */; }; D4EE2F9724A34C990004E3B9 /* PDFContextGraphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EE2F9624A34C990004E3B9 /* PDFContextGraphics.swift */; }; D4EE2F9924A34CBF0004E3B9 /* CrossPlattformGraphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EE2F9824A34CBF0004E3B9 /* CrossPlattformGraphics.swift */; }; OBJ_419 /* AdapterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_331 /* AdapterProtocols.swift */; }; @@ -621,6 +622,7 @@ D4803C9F24703E5300DDA039 /* PDFAttributedText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFAttributedText.swift; sourceTree = ""; }; D4803CA024703E5300DDA039 /* PDFText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFText.swift; sourceTree = ""; }; D4803CA124703E5300DDA039 /* PDFTextStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTextStyle.swift; sourceTree = ""; }; + D4839C38253706950005BB87 /* PDFTableCalculatedCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTableCalculatedCell.swift; sourceTree = ""; }; D4EE2F9624A34C990004E3B9 /* PDFContextGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFContextGraphics.swift; sourceTree = ""; }; D4EE2F9824A34CBF0004E3B9 /* CrossPlattformGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossPlattformGraphics.swift; sourceTree = ""; }; "Nimble::Nimble::Product" /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1054,6 +1056,7 @@ D4803C3724703E5300DDA039 /* Table */ = { isa = PBXGroup; children = ( + D4839C38253706950005BB87 /* PDFTableCalculatedCell.swift */, D4803C3824703E5300DDA039 /* PDFTableMergeUtil.swift */, D4803C3924703E5300DDA039 /* PDFTableNode.swift */, D4803C3A24703E5300DDA039 /* PDFTableObject.swift */, @@ -2265,6 +2268,7 @@ D4803D1124703E5300DDA039 /* PDFBezierPath.swift in Sources */, D4803D3A24703E5300DDA039 /* PDFAttributedText.swift in Sources */, D4803D2224703E5300DDA039 /* PDFTable+RowSubscripts.swift in Sources */, + D4839C39253706950005BB87 /* PDFTableCalculatedCell.swift in Sources */, D4803CF924703E5300DDA039 /* PDFSection.swift in Sources */, D4803CB924703E5300DDA039 /* PDFPaginationStyle+Equatable.swift in Sources */, D4803CD124703E5300DDA039 /* PDFRectangleObject.swift in Sources */, diff --git a/Tests/TPPDFTests/Internal/Table/PDFTableObjectSpec.swift b/Tests/TPPDFTests/Internal/Table/PDFTableObjectSpec.swift new file mode 100644 index 00000000..32fb5a7b --- /dev/null +++ b/Tests/TPPDFTests/Internal/Table/PDFTableObjectSpec.swift @@ -0,0 +1,181 @@ +import CoreGraphics +import Quick +import Nimble +@testable import TPPDF + +class PDFTableObjectSpec: QuickSpec { + + override func spec() { + // Let's not test on macOS, as small changes in the font messes up all values + #if os(iOS) + describe("PDFTableObject") { + describe("calculation result frames") { + context("unmerged cells on multiple pages without splicing and no table headers on every page") { + let container = PDFContainer.contentLeft + + let rows = 40 + let columns = 4 + let count = rows * columns + let table = PDFTable(rows: rows, columns: columns) + table.widths = [0.1, 0.3, 0.3, 0.3] + table.margin = 10 + table.padding = 10 + table.showHeadersOnEveryPage = false + table.shouldSplitCellsOnPageBeak = false + table.style.columnHeaderCount = 3 + + for row in 0.. [CGRect] in + (0..<4).map { col -> CGRect in + CGRect(x: [70, 117.5, 260, 402.5][col], + y: 85 + CGFloat(row) * 47, + width: [27.5, 122.5, 122.5, 122.5][col], + height: row >= 10 ? 48 : 37) + } + }) + let frames_11_13: [[CGRect]] = (11..<14) + .map ({ row -> [CGRect] in + (0..<4).map { col -> CGRect in + CGRect(x: [70, 117.5, 260, 402.5][col], + y: 613 + CGFloat(row - 11) * 58, + width: [27.5, 122.5, 122.5, 122.5][col], + height: 48) + } + }) + let frames_13_25: [[CGRect]] = (14..<26) + .map ({ row -> [CGRect] in + (0..<4).map { col -> CGRect in + CGRect(x: [70, 117.5, 260, 402.5][col], + y: 60 + CGFloat(row - 14) * 58, + width: [27.5, 122.5, 122.5, 122.5][col], + height: 48) + } + }) + let frames_25_37: [[CGRect]] = (26..<39) + .map ({ row -> [CGRect] in + (0..<4).map { col -> CGRect in + CGRect(x: [70, 117.5, 260, 402.5][col], + y: 60 + CGFloat(row - 26) * 58, + width: [27.5, 122.5, 122.5, 122.5][col], + height: 48) + } + }) + let frames_37_40: [[CGRect]] = (38..<40) + .map ({ row -> [CGRect] in + (0..<4).map { col -> CGRect in + CGRect(x: [70, 117.5, 260, 402.5][col], + y: 60 + CGFloat(row - 38) * 58, + width: [27.5, 122.5, 122.5, 122.5][col], + height: 48) + } + }) + + // test cells on first page + for row in 0..<14 { + for column in 0..<4 { + context("cell \(row) \(column)") { + let locatedCell = result[row * columns + column] + + it("should be in the correct container") { + expect(locatedCell.0) == container + } + + it("should have correct frame") { + let expectedFrames = frames_0_10 + frames_11_13 + expect(locatedCell.1.frame) == expectedFrames[row][column] + } + } + } + } + + // test cells on second page + for row in 14..<26 { + for column in 0..<4 { + context("cell \(row) \(column)") { + let locatedCell = result[1 + (row * columns + column)] + + it("should be in the correct container") { + expect(locatedCell.0) == container + } + + it("should have correct frame") { + expect(locatedCell.1.frame) == frames_13_25[row - 14][column] + } + } + } + } + + // test cells on third page + for row in 26..<38 { + for column in 0..<4 { + context("cell \(row) \(column)") { + let locatedCell = result[2 + (row * columns + column)] + + it("should be in the correct container") { + expect(locatedCell.0) == container + } + + it("should have correct frame") { + expect(locatedCell.1.frame) == frames_25_37[row - 26][column] + } + } + } + } + + // test cells on fourth page + for row in 38..<40 { + for column in 0..<4 { + context("cell \(row) \(column)") { + let locatedCell = result[3 + row * columns + column] + + it("should be in the correct container") { + expect(locatedCell.0) == container + } + + it("should have correct frame") { + expect(locatedCell.1.frame) == frames_37_40[row - 38][column] + } + } + } + } + } + } + } + #endif + } +}