Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added algorithm for pagination to PDF export, allowing to export consent forms with more than 1 page (#49) #52

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ec18eab
Introduced algorithm to split exported consent document across multip…
RealLast Jun 22, 2024
4ee81b1
Removed leftover continuation from older version of the code.
RealLast Jun 22, 2024
457b273
Resolved swiftlint issues.
RealLast Jun 26, 2024
42c2443
Removed unused variables, further cleaned up code.
RealLast Jun 27, 2024
ee2f2f4
Changed PDF export to use TPPDF for PDF generation, instead of creati…
RealLast Jul 11, 2024
23e5333
Changed personName -> signature.
RealLast Jul 11, 2024
a947d7d
Reverted changes.
RealLast Jul 11, 2024
5fbc2a7
Update iPad Testing Identifier.
RealLast Jul 12, 2024
8ec56a2
Update Tests and try Beta 4
PSchmiedmayer Aug 3, 2024
5238a7b
Update Tests
PSchmiedmayer Aug 3, 2024
a417fa9
Update GitHub Action
PSchmiedmayer Aug 3, 2024
f2e767c
Merging in changes from main.
RealLast Aug 14, 2024
a11df71
Separated PDF export functionality from ConsentDocument. Added type C…
RealLast Aug 14, 2024
2d793db
Added known good PDF files for iOS, macOS and visionOS, which is used…
RealLast Aug 14, 2024
c7cc518
Added known-good PDF documents to test against in testPDFExport.
RealLast Aug 14, 2024
0d287e5
Merge remote-tracking branch 'upstream/main' into PDFPagination
RealLast Aug 24, 2024
e6080f6
Merged in changes from #52.
RealLast Aug 24, 2024
d15f5e3
Resolved errors on macOS.
RealLast Aug 24, 2024
4e919db
Expanded unit test for PDF export to include documents with two pages.
RealLast Aug 24, 2024
c747606
Fixed swiftlint issues. Added license information.
RealLast Aug 24, 2024
d42fa7d
Added missing files.
RealLast Aug 24, 2024
eb1bcea
Added missing comments.
RealLast Aug 24, 2024
b716ada
Merge branch 'main' into PDFPagination
PSchmiedmayer Aug 30, 2024
bd5d975
Try LFS Support
PSchmiedmayer Aug 30, 2024
b813492
Update build-and-test.yml
PSchmiedmayer Aug 30, 2024
aa84929
Update build-and-test.yml
PSchmiedmayer Aug 30, 2024
1e0c539
Added new pdf documents for the tests with better spacing.
RealLast Sep 4, 2024
448e0e4
Resolved swiftlint issues.
RealLast Sep 4, 2024
af181aa
Removed leftover code.
RealLast Sep 4, 2024
2697855
Made PDF export throwing if PDF generation fails. A possible exceptio…
RealLast Sep 4, 2024
9194b05
Resolved swiftlint issue.
RealLast Sep 4, 2024
7abf6cc
Introduced ExportConfiguration.FontSettings to enable more precise co…
RealLast Oct 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,6 @@ jobs:
buildandtestuitests_ios:
name: Build and Test UI Tests iOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
resultBundle: TestApp-iOS.xcresult
artifactname: TestApp-iOS.xcresult
- buildConfig: Release
resultBundle: TestApp-iOS-Release.xcresult
artifactname: TestApp-iOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
Expand Down Expand Up @@ -125,23 +116,13 @@ jobs:
buildandtestuitests_visionos:
name: Build and Test UI Tests visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
resultBundle: TestApp-visionOS.xcresult
artifactname: TestApp-visionOS.xcresult
- buildConfig: Release
resultBundle: TestApp-visionOS-Release.xcresult
artifactname: TestApp-visionOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
scheme: TestApp
destination: 'platform=visionOS Simulator,name=Apple Vision Pro'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: TestApp-visionOS.xcresult
artifactname: TestApp-visionOS.xcresult
uploadcoveragereport:
name: Upload Coverage Report
needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos]
Expand Down
13 changes: 9 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

//
// This source file is part of the Stanford Spezi open-source project
//
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
//
// SPDX-License-Identifier: MIT
//

Expand Down Expand Up @@ -33,7 +33,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
.package(url: "https://github.com/techprimate/TPPDF", from: "2.6.0")
] + swiftLintPackage(),
targets: [
.target(
Expand All @@ -42,7 +43,8 @@ let package = Package(
.product(name: "Spezi", package: "Spezi"),
.product(name: "SpeziViews", package: "SpeziViews"),
.product(name: "SpeziPersonalInfo", package: "SpeziViews"),
.product(name: "OrderedCollections", package: "swift-collections")
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "TPPDF", package: "TPPDF")
],
swiftSettings: [
swiftConcurrency
Expand All @@ -54,6 +56,9 @@ let package = Package(
dependencies: [
.target(name: "SpeziOnboarding")
],
resources: [
.process("Resources/")
],
swiftSettings: [
swiftConcurrency
],
Expand Down
106 changes: 13 additions & 93 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift
RealLast marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import PDFKit
import PencilKit
import SwiftUI

import TPPDF

/// Extension of `ConsentDocument` enabling the export of the signed consent page.
extension ConsentDocument {
Expand All @@ -18,7 +18,7 @@ extension ConsentDocument {
/// force the ink used in the `UIImage` of the `PKDrawing` to always be black by adjusting the signature ink according to the color scheme.
@MainActor private var blackInkSignatureImage: UIImage {
var updatedDrawing = PKDrawing()

for stroke in signature.strokes {
let blackStroke = PKStroke(
ink: PKInk(stroke.ink.inkType, color: colorScheme == .light ? .black : .white),
Expand All @@ -43,101 +43,21 @@ extension ConsentDocument {
}
#endif


/// Creates a representation of the consent form that is ready to be exported via the SwiftUI `ImageRenderer`.
///
/// - Parameters:
/// - markdown: The markdown consent content as an `AttributedString`.
///
/// - Returns: A SwiftUI `View` representation of the consent content and signature.
///
/// - Note: This function avoids the use of asynchronous operations.
/// Asynchronous tasks are incompatible with SwiftUI's `ImageRenderer`,
/// which expects all rendering processes to be synchronous.
@MainActor
private func exportBody(markdown: AttributedString) -> some View {
VStack {
if exportConfiguration.includingTimestamp {
HStack {
Spacer()

Text("EXPORTED_TAG", bundle: .module)
+ Text(verbatim: ": \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))")
}
.font(.caption)
.padding()
}

OnboardingTitleView(title: exportConfiguration.consentTitle)

Text(markdown)
.padding()

Spacer()

ZStack(alignment: .bottomLeading) {
SignatureViewBackground(name: name, backgroundColor: .clear)

#if !os(macOS)
Image(uiImage: blackInkSignatureImage)
#else
Text(signature)
.padding(.bottom, 32)
.padding(.leading, 46)
.font(.custom("Snell Roundhand", size: 24))
#endif
}
#if !os(macOS)
.frame(width: signatureSize.width, height: signatureSize.height)
#else
.padding(.horizontal, 100)
#endif
}
}

/// Exports the signed consent form as a `PDFDocument` via the SwiftUI `ImageRenderer`.
///
/// Exports the signed consent form as a `PDFKit.PDFDocument`.
/// The PDF generated by TPPDF and then converted to a TPDFKit.PDFDocument.
/// Renders the `PDFDocument` according to the specified ``ConsentDocument/ExportConfiguration``.
///
/// - Returns: The exported consent form in PDF format as a PDFKit `PDFDocument`
@MainActor
func export() async -> PDFDocument? {
let markdown = await asyncMarkdown()

let markdownString = (try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)) ?? AttributedString(String(localized: "MARKDOWN_LOADING_ERROR", bundle: .module))

let renderer = ImageRenderer(content: exportBody(markdown: markdownString))
let paperSize = CGSize(
width: exportConfiguration.paperSize.dimensions.width,
height: exportConfiguration.paperSize.dimensions.height
func export() async -> PDFKit.PDFDocument? {
RealLast marked this conversation as resolved.
Show resolved Hide resolved
let personName = name.formatted(.name(style: .long))

#if !os(macOS)
return await viewModel.export(
personName: personName, signatureImage: blackInkSignatureImage
)
renderer.proposedSize = .init(paperSize)

return await withCheckedContinuation { continuation in
renderer.render { _, context in
var box = CGRect(origin: .zero, size: paperSize)

/// Create in-memory `CGContext` that stores the PDF
guard let mutableData = CFDataCreateMutable(kCFAllocatorDefault, 0),
let consumer = CGDataConsumer(data: mutableData),
let pdf = CGContext(consumer: consumer, mediaBox: &box, nil) else {
continuation.resume(returning: nil)
return
}

pdf.beginPDFPage(nil)
pdf.translateBy(x: 0, y: 0)

context(pdf)

pdf.endPDFPage()
pdf.closePDF()

continuation.resume(returning: PDFDocument(data: mutableData as Data))
}
}
#else
return await viewModel.export(personName: personName, signature: signature)
#endif
}
}
13 changes: 8 additions & 5 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ public struct ConsentDocument: View {
/// The maximum width such that the drawing canvas fits onto the PDF.
static let maxWidthDrawing: CGFloat = 550

let asyncMarkdown: () async -> Data
private let givenNameTitle: LocalizedStringResource
private let givenNamePlaceholder: LocalizedStringResource
private let familyNameTitle: LocalizedStringResource
private let familyNamePlaceholder: LocalizedStringResource
let exportConfiguration: ExportConfiguration

let viewModel: ConsentDocumentModel

@Environment(\.colorScheme) var colorScheme
@State var name = PersonNameComponents()
Expand Down Expand Up @@ -134,7 +134,7 @@ public struct ConsentDocument: View {

public var body: some View {
VStack {
MarkdownView(asyncMarkdown: asyncMarkdown, state: $viewState.base)
MarkdownView(asyncMarkdown: viewModel.asyncMarkdown, state: $viewState.base)
Spacer()
Group {
nameView
Expand Down Expand Up @@ -203,13 +203,16 @@ public struct ConsentDocument: View {
familyNamePlaceholder: LocalizedStringResource = LocalizationDefaults.familyNamePlaceholder,
exportConfiguration: ExportConfiguration = .init()
) {
self.asyncMarkdown = markdown
self._viewState = viewState
self.givenNameTitle = givenNameTitle
self.givenNamePlaceholder = givenNamePlaceholder
self.familyNameTitle = familyNameTitle
self.familyNamePlaceholder = familyNamePlaceholder
self.exportConfiguration = exportConfiguration

self.viewModel = ConsentDocumentModel(
markdown: markdown,
exportConfiguration: exportConfiguration
)
}
}

Expand Down
Loading
Loading