├── .gitignore ├── Documentation ├── Changelog.md ├── Documentation.md ├── Layout Manifesto.md ├── Roadmap.md ├── example-columns.jpg ├── example-columns.md ├── example-columns.pdf ├── example-custom.jpg ├── example-custom.md ├── example-custom.pdf ├── example-gradient.jpg ├── example-gradient.md ├── example-gradient.pdf ├── example-report.jpg ├── example-report.md ├── example-report.pdf ├── example-stacks.jpg ├── example-stacks.md ├── example-stacks.pdf ├── example-vector.jpg ├── example-vector.md ├── example-vector.pdf ├── logo.png └── xcode-preview.png ├── Examples ├── Example+AttributedString.swift ├── Example+Clipped.swift ├── Example+Columns.swift ├── Example+ColumnsInVStack.swift ├── Example+Effects.swift ├── Example+FontWeight.swift ├── Example+FontWidth.swift ├── Example+ForEach.swift ├── Example+Frame.swift ├── Example+Gradient.swift ├── Example+HStack.swift ├── Example+Line.swift ├── Example+Logo.swift ├── Example+PageNumberReader.swift ├── Example+ProportionalFrame.swift ├── Example+Rotation.swift ├── Example+Shapes.swift ├── Example+Table.swift ├── Example+TableGrouped.swift ├── Example+Text.swift ├── Example+TextAlignment.swift ├── Example+TextStroke.swift ├── Example+VGrid+Wrap.swift ├── Example+VGrid.swift ├── Example+~Testing.swift └── ExampleSupport.swift ├── LICENSE ├── Package.swift ├── Readme.md ├── Sources └── PDFBlocks │ ├── API │ ├── Blocks │ │ ├── AnyBlock.swift │ │ ├── Color+Block.swift │ │ ├── Columns.swift │ │ ├── Divider.swift │ │ ├── EitherBlock.swift │ │ ├── EmptyBlock.swift │ │ ├── ForEach.swift │ │ ├── Group.swift │ │ ├── HStack.swift │ │ ├── Image.swift │ │ ├── Line.swift │ │ ├── LinearGradient+Block.swift │ │ ├── Never+Block.swift │ │ ├── Optional+Block.swift │ │ ├── Page.swift │ │ ├── PageNumberReader.swift │ │ ├── RadialGradient+Block.swift │ │ ├── Repeat.swift │ │ ├── Spacer.swift │ │ ├── Table.swift │ │ ├── TableColumnTitles.swift │ │ ├── TableRow.swift │ │ ├── Text.swift │ │ ├── TupleBlock.swift │ │ ├── VGrid.swift │ │ ├── VStack.swift │ │ └── ZStack.swift │ ├── Builders │ │ ├── BlockBuilder.swift │ │ ├── TableColumnBuilder.swift │ │ └── TableGroupBuilder.swift │ ├── Core │ │ ├── Block.swift │ │ ├── BlockModifier.swift │ │ ├── Environment.swift │ │ ├── EnvironmentValues.swift │ │ ├── PlatformColor.swift │ │ ├── PlatformFont.swift │ │ ├── PlatformImage.swift │ │ ├── Shape.swift │ │ ├── ShapeStyle.swift │ │ └── StrokeStyle.swift │ ├── Modifiers │ │ ├── Block+AspectRatio.swift │ │ ├── Block+Background.swift │ │ ├── Block+Bold.swift │ │ ├── Block+BoldFontName.swift │ │ ├── Block+Border.swift │ │ ├── Block+Clipped.swift │ │ ├── Block+Environment.swift │ │ ├── Block+Fill.swift │ │ ├── Block+Font.swift │ │ ├── Block+ForegroundColor.swift │ │ ├── Block+ForegroundStyle.swift │ │ ├── Block+Frame.swift │ │ ├── Block+Italic.swift │ │ ├── Block+Kerning.swift │ │ ├── Block+MultilineTextAlignment.swift │ │ ├── Block+Offset.swift │ │ ├── Block+OnRender.swift │ │ ├── Block+Opacity.swift │ │ ├── Block+Overlay.swift │ │ ├── Block+Padding.swift │ │ ├── Block+ProportionalFrame.swift │ │ ├── Block+RotationEffect.swift │ │ ├── Block+ScaleEffect.swift │ │ ├── Block+Stroke.swift │ │ ├── Block+Tag.swift │ │ ├── Block+TextFill.swift │ │ ├── Block+TextStroke.swift │ │ └── Block+TruncationMode.swift │ ├── Platform │ │ ├── AppKit.swift │ │ └── UIKit.swift │ ├── ShapeStyles │ │ ├── Color+ShapeStyle.swift │ │ ├── LinearGradient.swift │ │ └── RadialGradient.swift │ ├── Shapes │ │ ├── Capsule.swift │ │ ├── Circle.swift │ │ ├── Ellipse.swift │ │ ├── Rectangle.swift │ │ ├── RoundedRectangle.swift │ │ └── Square.swift │ └── Types │ │ ├── Alignment.swift │ │ ├── Angle.swift │ │ ├── Color.swift │ │ ├── ContentMode.swift │ │ ├── Dimension.swift │ │ ├── Edge.swift │ │ ├── EdgeInsets.swift │ │ ├── Font.swift │ │ ├── FontName.swift │ │ ├── Gradient.swift │ │ ├── PageNumberProxy.swift │ │ ├── PageSize.swift │ │ ├── Path.swift │ │ ├── Size.swift │ │ ├── StackSpacing.swift │ │ ├── TextAlignment.swift │ │ ├── TextTruncationMode.swift │ │ └── UnitPoint.swift │ └── Internal │ ├── Blocks │ ├── AnyBlock+Renderable.swift │ ├── ArrayBlock.swift │ ├── AspectRatio.swift │ ├── Background.swift │ ├── Border.swift │ ├── ClipRegion.swift │ ├── Columns+Renderable.swift │ ├── Divider+Renderable.swift │ ├── EitherBlock+Renderable.swift │ ├── EmptyBlock+Renderable.swift │ ├── EnvironmentBlock.swift │ ├── ForEach+GroupBlock.swift │ ├── Frame.swift │ ├── Group+GroupBlock.swift │ ├── HStack+Renderable.swift │ ├── Image+Renderable.swift │ ├── Line+Renderable.swift │ ├── ModifiedContent.swift │ ├── Offset.swift │ ├── OnRender.swift │ ├── Opacity.swift │ ├── Optional+Renderable.swift │ ├── Overlay.swift │ ├── Padding.swift │ ├── Page+Renderable.swift │ ├── PageNumberReader+Renderable.swift │ ├── ProportionalFrame.swift │ ├── RenderableShape.swift │ ├── Repeat+GroupBlock.swift │ ├── RotationEffect.swift │ ├── ScaleEffect.swift │ ├── Spacer+Renderable.swift │ ├── Table+Renderable.swift │ ├── TableContentSpacer+Renderable.swift │ ├── TableContentSpacer.swift │ ├── Text+Renderable.swift │ ├── TupleBlock+GroupBlock.swift │ ├── VGrid+Renderable.swift │ ├── VStack+Renderable.swift │ └── ZStack+Renderable.swift │ ├── Core │ ├── Block+AppendToArray.swift │ ├── Context.swift │ ├── EnvironmentProperty.swift │ ├── GroupBlock.swift │ ├── Renderable.swift │ ├── Renderer.swift │ └── Trait.swift │ ├── EnvironmentValues │ ├── ColumnsLayout.swift │ ├── LayoutAxis.swift │ ├── RenderMode.swift │ └── TableColumns.swift │ ├── Platform │ └── CGRenderer.swift │ └── Types │ ├── Alignment+Additions.swift │ ├── BlockSize.swift │ ├── FloatingPoint+Operators.swift │ ├── FoundationExtensions.swift │ ├── PageInfo.swift │ ├── Proposal.swift │ ├── StackSpacing+Additions.swift │ ├── TextStroke.swift │ ├── ValueWrapper.swift │ └── WrapMode.swift └── Tests └── PDFBlocksTests └── PDFBlocksTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build/ 3 | .swiftpm/ 4 | .vscode/ 5 | .devcontainer/ 6 | /Packages 7 | xcuserdata/ 8 | Package.resolved 9 | -------------------------------------------------------------------------------- /Documentation/Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.5.2 4 | * Breaking Change: In VStack, VGrid, and Columns the wrapping: parameter is renamed to wrap:. 5 | * Added .none and .wrap to truncationMode(:). 6 | * Changed Text: will not truncate unless truncationMode .tail is set. 7 | * Changed Text: will not wrap unless truncationMode .wrap is set. 8 | * Changed Table: removed page number from pageHeader and pageFooter blocks 9 | * Enhanced `PageNumberReader` with ability to report total pages 10 | * Fixed Text wrap in VStack 11 | * Fixes various layout issues 12 | 13 | ## v0.2.5.1 14 | * Performance improvment for Text 15 | * Breaking Change: In VStack, VGrid, and Columns the pageWrap: parameter is renamed to wrapContents:. 16 | * Various fixes 17 | * Moved Examples out of Sources directory 18 | 19 | ## v0.2.5 20 | * Added gradient fill support for Text 21 | * Added Repeat block 22 | * Added AttributedString support for Text 23 | * Changed .font(size:) to .fontSize(:) 24 | * Changed Columns to adjust its height so that column lengths are even. 25 | 26 | ## v0.2.4 27 | * Added StrokeStyle 28 | * Added Line 29 | * Added .fontWeight, .fontWidth, and .fontDesign 30 | * Added .kerning 31 | * Added fixedLength option to Spacer 32 | * Added .opacity modifier to Color 33 | * Renamed HGrid to VGrid 34 | * Fixed if/then, switch functionality in @BlockBuilder 35 | 36 | ## v0.2.3 37 | * Added .scaleEffect modifier 38 | * Early look at Columns layout. Not complete 39 | * Early look at page or column wrapable Text 40 | * Changed default layout to center instead of top leading 41 | * Changed name of Size to Dimension and allow points to be expressed as literals 42 | * Change Text to use CoreText rendering 43 | 44 | ## v0.2.2 45 | * Added .rotationEffect modifier 46 | * Added .offset modifier 47 | * Added more Shape drawing functions 48 | 49 | ## v0.2.1 50 | * Added Shapes 51 | * Added Gradient fill 52 | * Added ShapeStyle 53 | * Added .textStroke modifier 54 | * Added .textFill modifier 55 | -------------------------------------------------------------------------------- /Documentation/Layout Manifesto.md: -------------------------------------------------------------------------------- 1 | # Layout Manifesto 2 | 3 | ### Columns 4 | Columns automatically balances the length of its columns by this heuristic: 5 | ... 6 | 7 | ### Ideal Size 8 | PDFBlocks has not implemented Ideal Size yet. 9 | 10 | ### Layout Priority 11 | Not implemented 12 | -------------------------------------------------------------------------------- /Documentation/Roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## Fixes 4 | * The entire library needs to be poked and prodded to find layout problems, especially within nested layout structures. 5 | 6 | ## Roadmap 7 | The following is a list of items that could be added. 8 | 9 | * URL links. 10 | * Clickable regions within documents. 11 | * Grids: grid layout is limited at present. I would welcome feedback as to what layout support would be useful. 12 | * Justified `Text`. This could be done poorly easily. Using TextKit and hyphenation would give a better implementation. It seems that NSAttributedString has some support for hyphenation. Need to exlore this as it would be an easier implementation than TextKit. 13 | * Barcodes. This is probably beyond the scope of this project and should be done with another library and used in PDFBlocks as an image. 14 | * Charts. This is probably beyond the scope of this project and should be done with another library and used in PDFBlocks as an image. 15 | 16 | ## Longterm Goals 17 | I would highly welcome collaboration in the following areas. 18 | * Unit tests. 19 | * Right to Left language support. 20 | * This project has been designed with the possiblity of a future cross platform implementation. The iOS/macOS implementation relies upon CoreGraphics and CoreText. I believe that Skia could provide the required functionality for a true cross platform implementation. 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Documentation/example-columns.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-columns.jpg -------------------------------------------------------------------------------- /Documentation/example-columns.md: -------------------------------------------------------------------------------- 1 | ## Column Layout 2 | 3 | The `Columns` component allows you to layout your content in columns that will auto-size themselves for even column lengths. 4 | ### PDF 5 | [example-columns.pdf](example-columns.pdf) 6 | ### Code 7 | 8 | ```swift 9 | struct ExampleColumns: Block { 10 | let speech = "..." 11 | 12 | var body: some Block { 13 | VStack(pageWrap: true) { 14 | Text("I Have a Dream") 15 | .italic() 16 | .fontSize(36) 17 | Text("Martin Luther King, Jr.") 18 | .fontSize(18) 19 | .padding(.bottom, 24) 20 | Columns(count: 3, spacing: 18, wrap: true) { 21 | Text(speech) 22 | .truncationMode(.wrap) 23 | .fontSize(10) 24 | .kerning(-0.25) 25 | } 26 | } 27 | .fontDesign(.serif) 28 | } 29 | } 30 | ``` 31 | 32 | 33 | The `wrap: true` parameter within `Columns(count: 3, spacing: 18, wrap: true)` indicates that `Columns` should start a new page when its contents overflow the space provided. The modifier `.truncationMode(.wrap)` indicates that `Text(speech)` should wrap its contents to a new column when necessary. 34 | -------------------------------------------------------------------------------- /Documentation/example-columns.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-columns.pdf -------------------------------------------------------------------------------- /Documentation/example-custom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-custom.jpg -------------------------------------------------------------------------------- /Documentation/example-custom.md: -------------------------------------------------------------------------------- 1 | ## Custom Component 2 | As with a `View` in SwiftUI, you can write a reusable custom `Block` in PDFBlocks that is a composite of built-in primitive blocks. This example, which generates the PDFBlocks logo, uses a custom reusable component called `LetterBlock`. 3 | 4 | ### PDF 5 | [example-custom.pdf](example-custom.pdf) 6 | ### Code 7 | 8 | ```swift 9 | struct ExampleCustom: Block { 10 | var body: some Block { 11 | VStack(alignment: .leading, spacing: 2) { 12 | HStack(spacing: 2) { 13 | ForEach(["P", "D", "F"]) { item in 14 | LetterBlock(letter: item, color: .red) 15 | } 16 | } 17 | HStack(spacing: 2) { 18 | ForEach(["B", "L", "O", "C", "K", "S"]) { item in 19 | LetterBlock(letter: item, color: .cyan) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | struct LetterBlock: Block { 27 | let letter: String 28 | let color: Color 29 | var body: some Block { 30 | Text(letter) 31 | .foregroundColor(.white) 32 | .frame(width: 84, height: 84, alignment: .center) 33 | .background(color) 34 | .font(.init(.init(name: "American Typewriter", size: 68))) 35 | .bold() 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /Documentation/example-custom.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-custom.pdf -------------------------------------------------------------------------------- /Documentation/example-gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-gradient.jpg -------------------------------------------------------------------------------- /Documentation/example-gradient.md: -------------------------------------------------------------------------------- 1 | ## Gradient Fill 2 | A `Shape` or `Text` can be filled with either a `Color`, a `LinearGradient`, or a `RadialGradient`. 3 | 4 | ### PDF 5 | [example-gradient.pdf](example-gradient.pdf) 6 | ### Code 7 | 8 | ```swift 9 | 10 | 11 | struct ExampleGradient: Block { 12 | let linearGradient = LinearGradient(stops: [.init(color: .yellow, location: 0), 13 | .init(color: .orange, location: 0.75), 14 | .init(color: .red, location: 0.95)], 15 | startPoint: .top, 16 | endPoint: .bottom) 17 | 18 | let radialGradient = RadialGradient(colors: [.red, .orange, .yellow], 19 | center: .center, 20 | startRadius: .in(0), 21 | endRadius: .in(3)) 22 | 23 | var body: some Block { 24 | Page(size: .letter, margins: .in(1)) { 25 | Text("Hotter\nthan the\nMidday Sun") 26 | .foregroundStyle(linearGradient) 27 | .fontWeight(.black) 28 | .fontWidth(.expanded) 29 | .fontSize(64) 30 | .multilineTextAlignment(.center) 31 | Spacer(fixedLength: .in(1)) 32 | Circle() 33 | .fill(radialGradient) 34 | .frame(height: .in(4)) 35 | } 36 | } 37 | } 38 | 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /Documentation/example-gradient.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-gradient.pdf -------------------------------------------------------------------------------- /Documentation/example-report.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-report.jpg -------------------------------------------------------------------------------- /Documentation/example-report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-report.pdf -------------------------------------------------------------------------------- /Documentation/example-stacks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-stacks.jpg -------------------------------------------------------------------------------- /Documentation/example-stacks.md: -------------------------------------------------------------------------------- 1 | ## Stack Layout 2 | The primary layout tools for PDFBlocks are `VStack` (vertical stack), `HStack` (horizontal stack), and `ZStack` (zed stack--not demonstrated here). With a `spacing: .flex` parameter, stacks will lay out their contents with even spacing. 3 | ### PDF 4 | [example-stacks.pdf](example-stacks.pdf) 5 | ### Code 6 | 7 | ```swift 8 | struct ExampleStacks: Block { 9 | let poem = "That time of year thou mayest in me behold, when yellow leaves or none or few do hang upon these boughs which shake against the cold, bare ruined choirs where late the sweet birds sang." 10 | 11 | var body: some Block { 12 | Page(size: .letter, margins: .in(1)) { 13 | VStack(spacing: .flex) { 14 | Columns(count: 2, spacing: 36) { 15 | Text(poem) 16 | .truncationMode(.wrap) 17 | .fontSize(30) 18 | } 19 | HStack(spacing: .flex) { 20 | Repeat(count: 18) { 21 | Image(.init(systemName: "rhombus.fill")) 22 | .frame(width: 6) 23 | } 24 | } 25 | Columns(count: 3, spacing: 18) { 26 | Text(poem) 27 | .truncationMode(.wrap) 28 | .fontSize(24) 29 | .opacity(0.80) 30 | } 31 | HStack(spacing: .flex) { 32 | Repeat(count: 18) { 33 | Image(.init(systemName: "rhombus.fill")) 34 | .frame(width: 6) 35 | } 36 | } 37 | Columns(count: 4, spacing: 12) { 38 | Text(poem) 39 | .truncationMode(.wrap) 40 | .fontSize(18) 41 | .opacity(0.60) 42 | } 43 | } 44 | } 45 | .italic() 46 | .fontDesign(.serif) 47 | .border(Color.black, width: 8) 48 | } 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /Documentation/example-stacks.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-stacks.pdf -------------------------------------------------------------------------------- /Documentation/example-vector.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-vector.jpg -------------------------------------------------------------------------------- /Documentation/example-vector.md: -------------------------------------------------------------------------------- 1 | ## Vector Drawing 2 | PDFBlocks has a built-in `Line` component and shapes for `Rectangle`, `Ellipse`, `Circle`, `Square`, `RoundedRectangle`, and `Capsule`. Custom vector drawing can be accomplished by creating a struct that conforms to the `Shape` protocol as with the `Arc` shape in this example. Code for SwiftUI custom shapes found on GitHub or blogs can usually be copied straight into PDFBlocks. 3 | 4 | ### PDF 5 | [example-vector.pdf](example-vector.pdf) 6 | ### Code 7 | 8 | ```swift 9 | struct ExampleVectorDrawing: Block { 10 | var body: some Block { 11 | Page(size: .letter, margins: .in(1)) { 12 | HStack(spacing: 32) { 13 | Arc(startAngle: .degrees(35), endAngle: .degrees(325), clockwise: false) 14 | .fill(.yellow) 15 | .stroke(.black, lineWidth: 3) 16 | .frame(height: 150) 17 | Line(start: .leading, end: .trailing) 18 | .stroke(.white, style: StrokeStyle(lineWidth: .pt(15), lineCap: .round, dash: [0, 80])) 19 | 20 | } 21 | } 22 | .background(.black) 23 | .border(.blue, width: 10) 24 | } 25 | } 26 | 27 | struct Arc: Shape { 28 | var startAngle: Angle 29 | var endAngle: Angle 30 | var clockwise: Bool 31 | 32 | func path(in rect: CGRect) -> Path { 33 | Path { path in 34 | let radius = min(rect.width, rect.height) / 2 35 | path.addLines([CGPoint(x: rect.midX, y: rect.midY)]) 36 | path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 37 | path.closeSubpath() 38 | } 39 | } 40 | 41 | public func sizeThatFits(_ proposal: CGSize) -> CGSize { 42 | let minLength = min(proposal.width, proposal.height) 43 | return .init(width: minLength, height: minLength) 44 | } 45 | } 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /Documentation/example-vector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/example-vector.pdf -------------------------------------------------------------------------------- /Documentation/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/logo.png -------------------------------------------------------------------------------- /Documentation/xcode-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyowell/PDFBlocks/c1bc9a47e9ce37eb36a563bbaa27131e397ec0a7/Documentation/xcode-preview.png -------------------------------------------------------------------------------- /Examples/Example+AttributedString.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | let text: AttributedString = { 13 | var result: AttributedString = "One fish.\nTwo fish.\nRed fish.\nBlue fish." 14 | if let range = result.range(of: "Blue") { 15 | result[range].foregroundColor = .systemBlue 16 | } 17 | if let range = result.range(of: "Red") { 18 | result[range].foregroundColor = .systemRed 19 | } 20 | return result 21 | }() 22 | 23 | var body: some Block { 24 | Text(text) 25 | .fontSize(64) 26 | .fontDesign(.serif) 27 | .fontWeight(.semibold) 28 | } 29 | } 30 | 31 | #Preview { 32 | previewForDocument(Document()) 33 | } 34 | -------------------------------------------------------------------------------- /Examples/Example+Clipped.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | VStack(spacing: 72) { 14 | HStack(spacing: 72) { 15 | VStack(alignment: .leading, spacing: 12) { 16 | Text("1. Fill") 17 | Square() 18 | .fill(.red) 19 | } 20 | VStack(alignment: .leading, spacing: 12) { 21 | Text("2. Rotate") 22 | .offset(x: -24, y: 0) 23 | Square() 24 | .fill(.red) 25 | .rotationEffect(.degrees(45)) 26 | } 27 | } 28 | HStack(spacing: 72) { 29 | VStack(alignment: .leading, spacing: 12) { 30 | Text("3. Clip") 31 | Square() 32 | .fill(.red) 33 | .rotationEffect(.degrees(45)) 34 | .clipped() 35 | } 36 | VStack(alignment: .leading, spacing: 12) { 37 | Text("4. Overlay") 38 | .offset(x: -24, y: 0) 39 | Square() 40 | .fill(.red) 41 | .rotationEffect(.degrees(45)) 42 | .clipped() 43 | .overlay { 44 | Text("STOP") 45 | .foregroundStyle(.white) 46 | .font(.system(size: 60)) 47 | .fontWeight(.heavy) 48 | } 49 | } 50 | } 51 | } 52 | .fontSize(24) 53 | } 54 | } 55 | 56 | #Preview { 57 | previewForDocument(Document()) 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Example+Columns.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | public struct ExampleColumns2: Block { 12 | let poem = "That time of year thou mayest in me behold, when yellow leaves or none or few do hang upon these boughs which shake against the cold, bare ruined choirs where late the sweet birds sang." 13 | public init() {} 14 | 15 | public var body: some Block { 16 | Page(size: .letter, margins: .in(1)) { 17 | VStack(spacing: .flex) { 18 | Columns(count: 2, spacing: 36) { 19 | Text(poem) 20 | .truncationMode(.wrap) 21 | .fontSize(30) 22 | } 23 | HStack(spacing: .flex) { 24 | Repeat(count: 18) { 25 | Image(.init(systemName: "rhombus.fill")) 26 | .frame(width: 6) 27 | } 28 | } 29 | Columns(count: 3, spacing: 18) { 30 | Text(poem) 31 | .truncationMode(.wrap) 32 | .fontSize(24) 33 | .opacity(0.80) 34 | } 35 | HStack(spacing: .flex) { 36 | Repeat(count: 18) { 37 | Image(.init(systemName: "rhombus.fill")) 38 | .frame(width: 6) 39 | } 40 | } 41 | Columns(count: 4, spacing: 12) { 42 | Text(poem) 43 | .truncationMode(.wrap) 44 | .fontSize(18) 45 | .opacity(0.60) 46 | } 47 | } 48 | } 49 | .italic() 50 | .fontDesign(.serif) 51 | .border(Color.black, width: 8) 52 | } 53 | } 54 | 55 | #Preview { 56 | previewForDocument(ExampleColumns2()) 57 | } 58 | -------------------------------------------------------------------------------- /Examples/Example+ColumnsInVStack.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | struct ExampleColumns: Block { 12 | var body: some Block { 13 | VStack(wrap: true) { 14 | Text("I Have a Dream") 15 | .italic() 16 | .fontSize(36) 17 | Text("Martin Luther King, Jr.") 18 | .fontSize(18) 19 | .padding(.bottom, 24) 20 | Columns(count: 3, spacing: 18, wrap: true) { 21 | Text(speech) 22 | .kerning(-0.25) 23 | .truncationMode(.wrap) 24 | } 25 | } 26 | .fontDesign(.serif) 27 | .fontSize(10) 28 | } 29 | } 30 | 31 | #Preview { 32 | previewForDocument(ExampleColumns()) 33 | } 34 | -------------------------------------------------------------------------------- /Examples/Example+Effects.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | // RotationEffect, ScaleEffect, and Offset change the appearance of the elements, but do not 12 | // change their true size as concerns layout within a stack. 13 | 14 | private struct Document: Block { 15 | var body: some Block { 16 | VStack(spacing: 20) { 17 | Text("Red") 18 | .foregroundStyle(.red) 19 | .scaleEffect(1.5) 20 | // This border shows the true size and position. 21 | .border(.black) 22 | Text("Fish") 23 | Text("Blue") 24 | .foregroundStyle(.blue) 25 | // This border is scaled along with the text. 26 | .border(.black) 27 | .scaleEffect(1.5) 28 | Text("Fish") 29 | Text("Red") 30 | .foregroundStyle(.red) 31 | .rotationEffect(.degrees(-45)) 32 | // This border shows the true size and position. 33 | .border(.black) 34 | Text("Fish") 35 | Text("Blue") 36 | .foregroundStyle(.blue) 37 | // This border is rotated along with the text. 38 | .border(.black) 39 | .rotationEffect(.degrees(-45)) 40 | Text("Fish") 41 | } 42 | .font(.init(.init(name: "American Typewriter", size: 48))) 43 | } 44 | } 45 | 46 | #Preview { 47 | previewForDocument(Document()) 48 | } 49 | -------------------------------------------------------------------------------- /Examples/Example+FontWeight.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Page(size: .init(width: .in(4), height: .in(6)), margins: .in(0.5)) { 14 | Group { 15 | Text("Ultralight") 16 | .fontWeight(.ultraLight) 17 | Text("Thin") 18 | .fontWeight(.thin) 19 | Text("Light") 20 | .fontWeight(.light) 21 | Text("Regular") 22 | .fontWeight(.regular) 23 | Text("Medium") 24 | .fontWeight(.medium) 25 | Text("Semibold") 26 | .fontWeight(.semibold) 27 | Text("Bold") 28 | .fontWeight(.bold) 29 | Text("Heavy") 30 | .fontWeight(.heavy) 31 | Text("Black") 32 | .fontWeight(.black) 33 | } 34 | .font(Font(.init(name: "Helvetica Neue", size: 24))) 35 | } 36 | .border(.black, width: 4) 37 | } 38 | } 39 | 40 | #Preview { 41 | previewForDocument(Document()) 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Example+FontWidth.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | VStack(spacing: 24) { 14 | Text("Where no man has gone before") 15 | .fontWidth(.compressed) 16 | Text("Where no man has gone before") 17 | .fontWidth(.condensed) 18 | Text("Where no man has gone before") 19 | .fontWidth(.standard) 20 | Text("Where no man has gone before") 21 | .fontWidth(.expanded) 22 | } 23 | .fontSize(24) 24 | } 25 | } 26 | 27 | #Preview { 28 | previewForDocument(Document()) 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Example+ForEach.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | let weights: [Font.Weight] = [.ultraLight, .thin, .light, .regular, .medium, .semibold, .heavy, .black] 13 | let designs: [Font.Design] = [.default, .rounded, .serif, .monospaced] 14 | 15 | var body: some Block { 16 | VGrid(columnCount: 2, columnSpacing: 12, rowSpacing: 12) { 17 | ForEach(designs) { design in 18 | VStack { 19 | ForEach(weights) { weight in 20 | Text("One Fish") 21 | .fontWeight(weight) 22 | } 23 | } 24 | .fontDesign(design) 25 | } 26 | } 27 | .fontSize(36) 28 | } 29 | } 30 | 31 | #Preview { 32 | previewForDocument(Document()) 33 | } 34 | -------------------------------------------------------------------------------- /Examples/Example+Frame.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | // Text is truncated when constrained 14 | Text("The quick brown fox jumped over the lazy dog.") 15 | .fontSize(42) 16 | .fontDesign(.rounded) 17 | .fontWeight(.bold) 18 | // .frame is used to constrain element 19 | .frame(width: .in(3), height: .in(3)) 20 | .border(.orange) 21 | .padding(12) 22 | // .frame is used to align element 23 | .frame(width: .max, height: .max, alignment: .bottomTrailing) 24 | .border(.cyan) 25 | } 26 | } 27 | 28 | #Preview { 29 | previewForDocument(Document()) 30 | } 31 | -------------------------------------------------------------------------------- /Examples/Example+Gradient.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | let linearGradient = LinearGradient(stops: [.init(color: .yellow, location: 0), 12 | .init(color: .orange, location: 0.75), 13 | .init(color: .red, location: 0.95)], 14 | startPoint: .top, 15 | endPoint: .bottom) 16 | 17 | let radialGradient = RadialGradient(colors: [.red, .orange, .yellow], 18 | center: .center, 19 | startRadius: .in(0), 20 | endRadius: .in(3)) 21 | 22 | private struct ExampleGradient: Block { 23 | var body: some Block { 24 | Page(size: .letter, margins: .in(1)) { 25 | Text("Hotter\nthan the\nMidday Sun") 26 | .foregroundStyle(linearGradient) 27 | .fontWeight(.black) 28 | .fontWidth(.expanded) 29 | .fontSize(64) 30 | .multilineTextAlignment(.center) 31 | Spacer(fixedLength: .in(1)) 32 | Circle() 33 | .fill(radialGradient) 34 | .frame(height: .in(4)) 35 | } 36 | .border(linearGradient, width: 12) 37 | } 38 | } 39 | 40 | #Preview { 41 | previewForDocument(ExampleGradient()) 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Example+HStack.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | 13 | var body: some Block { 14 | HStack(alignment: .center, spacing: 10) { 15 | Rectangle().fill(.pink) 16 | Text("They're Pinky and the Brain.\nThey're Pinky and the Brain.\nOne is a genius.\nThe other's insane.") 17 | Rectangle().fill(.cyan) 18 | Text("They're Pinky and the Brain.\nThey're Pinky and the Brain.\nOne is a genius.\nThe other's insane.") 19 | Rectangle().fill(.pink) 20 | } 21 | .truncationMode(.none) 22 | .fontWidth(.compressed) 23 | .fontSize(18) 24 | } 25 | } 26 | 27 | #Preview { 28 | previewForDocument(Document()) 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Example+Line.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Page(size: .letter, margins: .in(1)) { 14 | Line(start: .init(x: 0.25, y: 0.25), end: .init(x: 0.75, y: 0.75)) 15 | .stroke(.black, style: StrokeStyle(lineWidth: 2)) 16 | } 17 | Page(size: .letter, margins: .in(1)) { 18 | ZStack { 19 | Line(start: .topLeading, end: .bottomTrailing) 20 | Line(start: .bottomLeading, end: .topTrailing) 21 | Line(start: .top, end: .bottom) 22 | Line(start: .leading, end: .trailing) 23 | } 24 | } 25 | .stroke(.purple, style: .init(lineWidth: 20, lineCap: .round)) 26 | Page(size: .letter, margins: .in(1)) { 27 | VStack { 28 | Line(start: .topLeading, end: .bottomTrailing) 29 | .stroke(.green, style: .init(lineWidth: 20, lineCap: .round)) 30 | Line(start: .bottomLeading, end: .topTrailing) 31 | .stroke(.orange, style: .init(lineWidth: 20, lineCap: .round)) 32 | Line(start: .top, end: .bottom) 33 | .stroke(.blue, style: .init(lineWidth: 20, lineCap: .round)) 34 | Line(start: .leading, end: .trailing) 35 | .stroke(.cyan, style: .init(lineWidth: 20, lineCap: .round)) 36 | } 37 | } 38 | } 39 | } 40 | 41 | 42 | #Preview { 43 | previewForDocument(Document()) 44 | } 45 | -------------------------------------------------------------------------------- /Examples/Example+Logo.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | public struct ExampleLogo: Block { 12 | public init() {} 13 | 14 | public var body: some Block { 15 | VStack(alignment: .leading, spacing: 2) { 16 | HStack(spacing: 2) { 17 | ForEach(["P", "D", "F"]) { item in 18 | LetterBlock(letter: item, color: .red) 19 | } 20 | } 21 | HStack(spacing: 2) { 22 | ForEach(["B", "L", "O", "C", "K", "S"]) { item in 23 | LetterBlock(letter: item, color: .cyan) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | private struct LetterBlock: Block { 31 | let letter: String 32 | let color: Color 33 | var body: some Block { 34 | Text(letter) 35 | .foregroundColor(.white) 36 | .frame(width: 84, height: 84, alignment: .center) 37 | .background(color) 38 | .font(.init(.init(name: "American Typewriter", size: 68))) 39 | .bold() 40 | } 41 | } 42 | 43 | #Preview { 44 | previewForDocument(ExampleLogo()) 45 | } 46 | -------------------------------------------------------------------------------- /Examples/Example+PageNumberReader.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | public struct ExamplePageNumberReader: Block { 12 | public init() {} 13 | public var body: some Block { 14 | VStack { 15 | // The first wrapping block encountered takes as much of the page space 16 | // as it can for its wrapping region. This is illustrated by turning 17 | // its background gray. 18 | VStack(wrap: true) { 19 | Text("I Have a Dream") 20 | .italic() 21 | .fontSize(36) 22 | Text("Martin Luther King, Jr.") 23 | .fontSize(18) 24 | .padding(.bottom, 24) 25 | Columns(count: 3, spacing: 18, wrap: true) { 26 | Text(speech) 27 | .fontSize(10) 28 | .kerning(-0.25) 29 | .truncationMode(.wrap) 30 | } 31 | } 32 | .background(.gray.opacity(0.15)) 33 | // Because this block is outside of the first wrapping block, it will be 34 | // repeated on every page. 35 | PageNumberReader(computePageCount: true) { proxy in 36 | Text("\(proxy.pageNo) / \(proxy.pageCount)") 37 | .fontSize(10) 38 | } 39 | .padding(.top, 12) 40 | } 41 | .fontDesign(.serif) 42 | } 43 | } 44 | 45 | #Preview { 46 | previewForDocument(ExamplePageNumberReader()) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Examples/Example+ProportionalFrame.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | VStack(spacing: 12) { 14 | Text("Equal Proportions") 15 | HStack(spacing: 4) { 16 | Text("x 1") 17 | .proportionalFrame(width: 1, alignment: .center) 18 | .background { Color.red.opacity(0.5) } 19 | Text("x 1") 20 | .proportionalFrame(width: 1, alignment: .center) 21 | .background { Color.green.opacity(0.5) } 22 | Text("x 1") 23 | .proportionalFrame(width: 1, alignment: .center) 24 | .background { Color.blue.opacity(0.5) } 25 | } 26 | .padding(.bottom, 12) 27 | Text("Center Twice as Wide as Ends") 28 | HStack(spacing: 4) { 29 | Text("x 1") 30 | .proportionalFrame(width: 1, alignment: .center) 31 | .background { Color.red.opacity(0.5) } 32 | Text("x 2") 33 | .proportionalFrame(width: 2, alignment: .center) 34 | .background { Color.green.opacity(0.5) } 35 | Text("x 1") 36 | .proportionalFrame(width: 1, alignment: .center) 37 | .background { Color.blue.opacity(0.5) } 38 | } 39 | .padding(.bottom, 12) 40 | Text("Block 1 Width + Block 2 Width = Block 3 Width") 41 | HStack(spacing: 4) { 42 | Text("x 1") 43 | .proportionalFrame(width: 1, alignment: .center) 44 | .background { Color.red.opacity(0.5) } 45 | Text("x 2") 46 | .proportionalFrame(width: 2, alignment: .center) 47 | .background { Color.green.opacity(0.5) } 48 | Text("x 3") 49 | .proportionalFrame(width: 3, alignment: .center) 50 | .background { Color.blue.opacity(0.5) } 51 | } 52 | .padding(.bottom, 12) 53 | Text("Block 1 = 35%, Block 2 = 20%, Block 3 = 55%") 54 | HStack(spacing: 4) { 55 | Text("35%") 56 | .proportionalFrame(width: 35, alignment: .center) 57 | .background { Color.red.opacity(0.5) } 58 | Text("20%") 59 | .proportionalFrame(width: 20, alignment: .center) 60 | .background { Color.green.opacity(0.5) } 61 | Text("55%") 62 | .proportionalFrame(width: 55, alignment: .center) 63 | .background { Color.blue.opacity(0.5) } 64 | } 65 | } 66 | .fontSize(18) 67 | } 68 | } 69 | 70 | #Preview { 71 | previewForDocument(Document()) 72 | } 73 | -------------------------------------------------------------------------------- /Examples/Example+Rotation.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Page(size: .init(width: .in(8), height: .in(8)), margins: .in(1)) { 14 | VGrid(columnCount: 3, columnSpacing: 24, rowSpacing: 24) { 15 | Repeat(count: 9) { 16 | VGrid(columnCount: 3, columnSpacing: 4, rowSpacing: 4) { 17 | Group { 18 | Color.red 19 | Color.blue 20 | Color.purple 21 | Color.purple 22 | Color.red 23 | Color.blue 24 | Color.blue 25 | Color.purple 26 | Color.red 27 | } 28 | .rotationEffect(.degrees(5)) 29 | .aspectRatio(1) 30 | } 31 | } 32 | } 33 | } 34 | .opacity(0.5) 35 | } 36 | } 37 | 38 | #Preview { 39 | previewForDocument(Document()) 40 | } 41 | -------------------------------------------------------------------------------- /Examples/Example+Shapes.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | struct ExampleShapes: Block { 12 | var body: some Block { 13 | Page(size: .letter, margins: .in(1)) { 14 | HStack(spacing: 32) { 15 | Arc(startAngle: .degrees(35), endAngle: .degrees(325), clockwise: false) 16 | .fill(.yellow) 17 | .stroke(.black, lineWidth: 3) 18 | .frame(height: 150) 19 | Line(start: .leading, end: .trailing) 20 | .stroke(.white, style: StrokeStyle(lineWidth: .pt(15), lineCap: .round, dash: [0, 80])) 21 | } 22 | } 23 | .background(.black) 24 | .border(.blue, width: 10) 25 | } 26 | } 27 | 28 | struct Arc: Shape { 29 | var startAngle: Angle 30 | var endAngle: Angle 31 | var clockwise: Bool 32 | 33 | func path(in rect: CGRect) -> Path { 34 | Path { path in 35 | let radius = min(rect.width, rect.height) / 2 36 | path.addLines([CGPoint(x: rect.midX, y: rect.midY)]) 37 | path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 38 | path.closeSubpath() 39 | } 40 | } 41 | 42 | public func sizeThatFits(_ proposal: CGSize) -> CGSize { 43 | let minLength = min(proposal.width, proposal.height) 44 | return .init(width: minLength, height: minLength) 45 | } 46 | } 47 | 48 | #Preview { 49 | previewForDocument(ExampleShapes()) 50 | } 51 | -------------------------------------------------------------------------------- /Examples/Example+Table.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | public struct ExampleTable: Block { 12 | let data: [CustomerData] = loadData(CustomerData.self, from: customerData) 13 | 14 | public init() {} 15 | 16 | public var body: some Block { 17 | Table(data) { 18 | TableColumn("Last Name", value: \.lastName, width: 20) 19 | TableColumn("First Name", value: \.firstName, width: 20) 20 | TableColumn("Address", value: \.address, width: 35) 21 | TableColumn("City", value: \.city, width: 25) 22 | TableColumn("State", value: \.state, width: 10, alignment: .trailing) 23 | } 24 | } 25 | } 26 | 27 | #Preview { 28 | previewForDocument(ExampleTable()) 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Example+Text.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | VStack(spacing: 10) { 14 | Text("This is a sample text block that cannot be truncated.") 15 | Text("This is a sample text block that allows truncation.") 16 | .truncationMode(.tail) 17 | .frame(height: 120) 18 | } 19 | .font(.system(size: 44)) 20 | } 21 | } 22 | 23 | #Preview { 24 | previewForDocument(Document()) 25 | } 26 | -------------------------------------------------------------------------------- /Examples/Example+TextAlignment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | let text = "The quick brown fox jumped over the lazy cow. The quick brown fox jumped over the lazy cow. The quick brown fox jumped over the lazy cow. The quick brown fox jumped over the lazy cow. " 13 | var body: some Block { 14 | VStack(spacing: .flex) { 15 | ForEach([TextAlignment.leading, .center, .trailing]) { alignment in 16 | Text(text) 17 | .multilineTextAlignment(alignment) 18 | } 19 | } 20 | .fontSize(20) 21 | .padding(.vertical, 72) 22 | } 23 | } 24 | 25 | #Preview { 26 | previewForDocument(Document()) 27 | } 28 | -------------------------------------------------------------------------------- /Examples/Example+TextStroke.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Text("Orange\nCrush") 14 | .fontSize(144) 15 | .textStroke(color: .blue, lineWidth: 3) 16 | .textFill(.orange) 17 | .font(.init(.init(name: "Courier", size: 164))) 18 | .italic() 19 | .bold() 20 | .kerning(-13) 21 | .multilineTextAlignment(.center) 22 | } 23 | } 24 | 25 | #Preview { 26 | previewForDocument(Document()) 27 | } 28 | -------------------------------------------------------------------------------- /Examples/Example+VGrid+Wrap.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Page(size: .init(width: .in(6), height: .in(6)), margins: .in(1)) { 14 | VGrid(columnCount: 3, columnSpacing: 0, rowSpacing: 0, wrap: true) { 15 | Text("A") 16 | Text("B") 17 | Text("C") 18 | Text("D") 19 | Text("E") 20 | Text("F") 21 | Text("G") 22 | Text("H") 23 | Text("I") 24 | Text("J") 25 | Text("K") 26 | Text("L") 27 | Text("M") 28 | Text("N") 29 | Text("O") 30 | Text("P") 31 | Text("Q") 32 | Text("R") 33 | Text("S") 34 | Text("T") 35 | Text("U") 36 | Text("V") 37 | Text("W") 38 | Text("X") 39 | Text("Y") 40 | Text("Z") 41 | } 42 | .padding(24) 43 | .border(.blue, width: 12) 44 | .rotationEffect(.degrees(10)) 45 | } 46 | .background(.orange) 47 | .border(Color.black, width: 12) 48 | .font(.init(.init(name: "American Typewriter", size: 32))) 49 | } 50 | } 51 | 52 | #Preview { 53 | previewForDocument(Document()) 54 | } 55 | -------------------------------------------------------------------------------- /Examples/Example+VGrid.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | var body: some Block { 13 | Page(size: .init(width: .in(6), height: .in(6)), margins: .in(1)) { 14 | VGrid(columnCount: 3, columnSpacing: 0, rowSpacing: 0) { 15 | Text("A") 16 | Text("B") 17 | Text("C") 18 | Text("D") 19 | Text("E") 20 | Text("F") 21 | Text("G") 22 | Text("H") 23 | Text("I") 24 | Text("J") 25 | Text("K") 26 | Text("L") 27 | Text("M") 28 | Text("N") 29 | Text("O") 30 | Text("P") 31 | Text("Q") 32 | Text("R") 33 | Text("S") 34 | Text("T") 35 | Text("U") 36 | Text("V") 37 | Text("W") 38 | Text("X") 39 | Text("Y") 40 | Text("Z") 41 | } 42 | .padding(24) 43 | .rotationEffect(.degrees(10)) 44 | } 45 | .background(.orange) 46 | .border(Color.black, width: 12) 47 | .font(.system(size: 42, design: .serif)) 48 | } 49 | } 50 | 51 | #Preview { 52 | previewForDocument(Document()) 53 | } 54 | -------------------------------------------------------------------------------- /Examples/Example+~Testing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import PDFBlocks 9 | import PDFKit 10 | 11 | private struct Document: Block { 12 | let text: AttributedString = { 13 | var result: AttributedString = "One fish\nTwo fish\nRed fish\nBlue fish" 14 | if let range = result.range(of: "Blue") { 15 | result[range].foregroundColor = .systemBlue 16 | } 17 | if let range = result.range(of: "Red") { 18 | result[range].foregroundColor = .systemRed 19 | } 20 | return result 21 | }() 22 | 23 | var body: some Block { 24 | VStack(wrap: true) { 25 | Repeat(count: 100) { 26 | Text(text) 27 | .truncationMode(.wrap) 28 | } 29 | } 30 | .fontSize(48) 31 | .fontDesign(.serif) 32 | .fontWeight(.semibold) 33 | } 34 | } 35 | 36 | #Preview { 37 | previewForDocument(Document()) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Yowell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PDFBlocks", 8 | platforms: [.macOS(.v14), .iOS(.v17)], 9 | products: [ 10 | .library( 11 | name: "PDFBlocksExamples", 12 | targets: ["PDFBlocksExamples"]), 13 | .library( 14 | name: "PDFBlocks", 15 | targets: ["PDFBlocks"]), 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/krzyzanowskim/CoreTextSwift.git", .upToNextMajor(from: "0.0.0")), 19 | .package(url: "https://github.com/apple/swift-algorithms.git", .upToNextMajor(from: "1.2.0")), 20 | ], 21 | targets: [ 22 | .target(name: "PDFBlocks", dependencies: [.product(name: "Algorithms", package: "swift-algorithms"), 23 | .product(name: "CoreTextSwift", package: "CoreTextSwift")]), 24 | .target(name: "PDFBlocksExamples", dependencies: ["PDFBlocks"], path: "Examples"), 25 | .testTarget(name: "PDFBlocksTests", dependencies: ["PDFBlocks"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/AnyBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type-erased block. 10 | /// 11 | /// An AnyBlock can allow you to use data driven 12 | /// content in your documents. Consider the following example 13 | /// in which a Response can be displayed as one of three 14 | /// types of Blocks (Image, RotationEffect, or Text). 'any Block' 15 | /// cannot be used within a BlockBuilder, but AnyBlock can. 16 | /// 17 | /// enum Response { 18 | /// case likes 19 | /// case dislikes 20 | /// case custom(String) 21 | /// 22 | /// var block: any Block { 23 | /// switch self { 24 | /// case .likes: 25 | /// Image(.init(systemName: "hand.thumbsup")!) 26 | /// case .dislikes: 27 | /// Image(.init(systemName: "hand.thumbsup")!) 28 | /// .rotationEffect(.degrees(180)) 29 | /// case .custom(let string): 30 | /// Text(string) 31 | /// } 32 | /// } 33 | /// 34 | /// struct Document: Block { 35 | /// let responses: [Response] 36 | /// 37 | /// var body: some Block { 38 | /// ForEach(responses) { response in 39 | /// AnyBlock(response.block) 40 | /// } 41 | /// } 42 | /// } 43 | /// 44 | public struct AnyBlock: Block { 45 | let content: any Block 46 | 47 | /// Creates an instance with the given parameters.. 48 | /// 49 | /// - Parameters: 50 | /// - content: The block to be erased. 51 | public init(_ content: some Block) { 52 | self.content = content 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Color+Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Color: Block { 10 | public var body: some Block { 11 | Rectangle() 12 | .fill(self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Columns.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that arranges its contents into equal length columns. 10 | /// 11 | /// NOTE: Elements within `Columns` with unconstrained heights 12 | /// can give unexpected results with respect to balanced column heights. 13 | public struct Columns: Block where Content: Block { 14 | let count: Int 15 | let spacing: Dimension 16 | let wrap: Bool 17 | let content: Content 18 | 19 | /// Creates an instance with the given parameters.. 20 | /// 21 | /// - Parameters: 22 | /// - count: The number of columns. 23 | /// - spacing: The horizontal spacing between columns. 24 | /// - wrapping: Start a new page or column when the content overflows its space. 25 | /// - content: A block builder that creates the Columns content. 26 | public init(count: Int, spacing: Dimension, wrap: Bool = false, @BlockBuilder content: () -> Content) { 27 | self.count = max(1, count) 28 | self.spacing = spacing 29 | self.wrap = wrap 30 | self.content = content() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Divider.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A padded line, drawn vertically in an `HStack` 10 | /// and horizontally in a `VStack`. 11 | public struct Divider: Block { 12 | let thickness: Dimension 13 | let padding: Dimension 14 | 15 | @Environment(\.layoutAxis) var layoutAxis 16 | 17 | /// Creates an instance with the given parameters.. 18 | /// 19 | /// - Parameters: 20 | /// - thickness: The thickness of the line. 21 | /// - padding: The padding on either side of the line. 22 | public init(thickness: Dimension = .pt(0.75), padding: Dimension = .pt(1)) { 23 | self.thickness = thickness 24 | self.padding = padding 25 | } 26 | 27 | public var body: some Block { 28 | switch layoutAxis { 29 | case .horizontal: 30 | Line(start: .top, end: .bottom) 31 | .frame(width: thickness) 32 | .padding(.horizontal, padding) 33 | .stroke(.black) 34 | case .vertical, .undefined: 35 | Line(start: .leading, end: .trailing) 36 | .frame(height: thickness) 37 | .padding(.vertical, padding) 38 | .stroke(.black) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/EitherBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EitherBlock.swift 3 | // 4 | // 5 | // Created by David Yowell on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A special block used by `@BlockBuilder` for constructing 11 | /// blocks with if/else and switch statements. `EitherBlock` 12 | /// cannot be constructed directly. 13 | public struct EitherBlock: Block where TrueContent: Block, FalseContent: Block { 14 | let value: Value 15 | 16 | enum Value { 17 | case trueContent(TrueContent) 18 | case falseContent(FalseContent) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/EmptyBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that occupies no space and renders no content. 10 | /// 11 | /// NOTE: EmptyView in SwiftUI is a special view that really does 12 | /// indicate the absence of a view. For instance, .onAppear {} 13 | /// will ever be executed on an EmptyView. EmptyBlock on the 14 | /// other hand has no special semantics. It is simply a view that 15 | /// takes no space and renders nothing. 16 | public struct EmptyBlock: Block { 17 | /// Creates an instance.. 18 | public init() {} 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/ForEach.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that computes a group of blocks on demand from an underlying 10 | /// array of data. 11 | /// 12 | /// Example: 13 | /// 14 | /// VStack { 15 | /// ForEach(["Quick", "Brown", "Fox"]) { item in 16 | /// Text(item) 17 | /// } 18 | /// } 19 | public struct ForEach: Block where Content: Block { 20 | let content: (T) -> Content 21 | let data: [T] 22 | 23 | /// Creates an instance with the given parameters.. 24 | /// 25 | /// - Parameters: 26 | /// - data: The data used to create blocks dynamically. 27 | /// - content: The block builder that creates blocks dynamically. 28 | public init(_ data: [T], @BlockBuilder content: @escaping (T) -> Content) { 29 | self.data = data 30 | self.content = content 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Group.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that collects multiple subblocks into a single unit. 10 | /// 11 | /// A modifier can be applied to multiple blocks at once in 12 | /// this manner: 13 | /// 14 | /// Group { 15 | /// Text("Quick") 16 | /// Text("Brown") 17 | /// Text("Fox") 18 | /// } 19 | /// .fontSize(18) 20 | /// 21 | public struct Group: Block where Content: Block { 22 | let content: Content 23 | 24 | /// Creates an instance with the given parameters.. 25 | /// 26 | /// - Parameters: 27 | /// - content: A block builder that creates the `Group` content. 28 | public init(@BlockBuilder content: () -> Content) { 29 | self.content = content() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/HStack.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A container block that arranges its contents in a horizontal line. 10 | /// 11 | /// This example shows a simple horizontal stack of three text blocks: 12 | /// 13 | /// var body: some Block { 14 | /// HStack(spacing: .pt(12) { 15 | /// Text("Quick") 16 | /// Text("Brown") 17 | /// Text("Fox") 18 | /// } 19 | /// } 20 | public struct HStack: Block { 21 | let alignment: VerticalAlignment 22 | let spacing: StackSpacing 23 | let content: Content 24 | let cacheId = UUID() 25 | 26 | /// Creates a horizontal stack with the given spacing and vertical alignment. 27 | /// 28 | /// - Parameters: 29 | /// - alignment: The vertical alignment for the contents of the stack. 30 | /// - spacing: The distance between elements of the stack. 31 | /// - content: A block builder that creates the content of this stack. 32 | public init(alignment: VerticalAlignment = .center, spacing: StackSpacing = .none, @BlockBuilder content: () -> Content) { 33 | self.alignment = alignment 34 | self.spacing = spacing 35 | self.content = content() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Image.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that prints an image. 10 | /// 11 | /// An `Image` is always resizable and will always 12 | /// retain its native aspectRatio. 13 | public struct Image: Block { 14 | let image: PlatformImage 15 | 16 | /// Creates an Image from an instance of a `PlatformImage` 17 | /// - Parameter image: A platform defined image type. 18 | public init(_ image: PlatformImage) { 19 | self.image = image 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Line.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that draws a line. 10 | /// 11 | /// Like a `Shape`, a `Line` will take all of the space that it is offered, 12 | /// 13 | /// The following line takes its full offered width, with a height of 2, drawing 14 | /// a lineWidth of 2. 15 | /// 16 | /// Line(start: .leading, end: .trailing) 17 | /// .stroke(.black, style: StrokeStyle(lineWidth: 2)) 18 | /// .frame(height: 2) 19 | /// 20 | /// This example draws a diagonal line, starting 25% of the width away from the leading edge and 21 | /// 25% of the height away from the top. The line ends 75% away from the 22 | /// leading edge and 75% away from the top. 23 | /// 24 | /// Line(start: .init(x: 0.25, y: 0.25), end: .init(x: 0.75, y: 0.75)) 25 | /// 26 | /// 27 | public struct Line: Block { 28 | let start: UnitPoint 29 | let end: UnitPoint 30 | 31 | /// Creates an instance with the given parameters.. 32 | /// 33 | /// - Parameters: 34 | /// - start: A unit point within the block that serves as the line origin.. 35 | /// - end: A unit point within the block that serves as the line end. 36 | public init(start: UnitPoint, end: UnitPoint) { 37 | self.start = start 38 | self.end = end 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/LinearGradient+Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension LinearGradient: Block { 10 | public var body: some Block { 11 | Rectangle() 12 | .fill(self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Never+Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Never: Block { 10 | public var body: Never { 11 | fatalError() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Optional+Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Optional: Block where Wrapped: Block {} 10 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Page.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that defines an entire PDF page. 10 | /// 11 | /// A page should be the outermost container within a document, 12 | /// although most modifiers can be applied to `Page`. 13 | public struct Page: Block where Content: Block { 14 | let pageInfo: PageInfo 15 | let content: Content 16 | 17 | /// Creates an instance with the given parameters. 18 | /// 19 | /// - Parameters: 20 | /// - size: The size of the page. 21 | /// - margins: The margins of the page. 22 | /// - precomputePageCount: Pre-flight run of document to compute total pages for "Page x of n" style reporting. 23 | /// - content: A block builder that creates the content of this page. 24 | public init(size: PageSize, 25 | margins: EdgeInsets, 26 | @BlockBuilder content: @escaping () -> Content) 27 | { 28 | pageInfo = .init(size: size, margins: margins) 29 | self.content = content() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/PageNumberReader.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A container block that dynamically creates its content, 10 | /// providing the current page number and total page count 11 | /// as an input parameter to the block builder function. 12 | /// 13 | public struct PageNumberReader: Block where Content: Block { 14 | let computePageCount: Bool 15 | let content: (PageNumberProxy) -> Content 16 | 17 | /// Creates an instance with the given parameters.. 18 | /// 19 | /// - Parameters: 20 | /// 21 | /// - computePageCount: PageNumberReader will return the total page count at the expense of pdf generation speed. 22 | /// - content: A block builder that creates a block dynamically. 23 | public init(computePageCount: Bool, @BlockBuilder content: @escaping (PageNumberProxy) -> Content) { 24 | self.computePageCount = computePageCount 25 | self.content = content 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/RadialGradient+Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension RadialGradient: Block { 10 | public var body: some Block { 11 | Rectangle() 12 | .fill(self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Repeat.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that repeats its contents n times. 10 | /// 11 | /// Example: 12 | /// 13 | /// VStack { 14 | /// Repeat(count: 20) { 15 | /// Text("ABC") 16 | /// } 17 | /// } 18 | public struct Repeat: Block where Content: Block { 19 | let count: Int 20 | let content: Content 21 | 22 | /// Creates an instance with the given parameters.. 23 | /// 24 | /// - Parameters: 25 | /// - count: The number of times to repeat the content. 26 | /// - content: A block builder that creates the content. 27 | public init(count: Int, @BlockBuilder content: () -> Content) { 28 | self.count = count 29 | self.content = content() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Spacer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// An invisible spacer for use within a layout block. 10 | /// 11 | /// Spacer will adapt its orientation according to the layout axis 12 | /// of the layout block within which it is contained. 13 | /// 14 | /// Almost anything that can be done with a fixed length spacer can also 15 | /// be accomplished with a `padding` modifier. There is one exception: 16 | /// When used with in a `VStack` that allows page wraps, a spacer will be 17 | /// ignored if it is the first item within the stack on a new page. 18 | /// 19 | /// AT PRESENT SPACER FUNCTIONALITY DOES NOT 20 | /// MATCH SWIFTUI. IN SWIFTUI, SPACER HAS A 21 | /// LAYOUTPRIORITY OF LESS THAN 0. LAYOUT PRIORITY 22 | /// SUPPORT HAS NOT BEEN ADDED. 23 | /// 24 | public struct Spacer: Block { 25 | enum SpacerValue { 26 | case fixed(Dimension) 27 | case min(Dimension) 28 | } 29 | 30 | let value: SpacerValue 31 | 32 | /// Creates a flexible spacer with no minimum length.. 33 | /// 34 | public init() { 35 | value = .min(0) 36 | } 37 | 38 | /// Creates a flexible spacer. 39 | /// 40 | /// - Parameters: 41 | /// - minLength: The spacer will be at least this length. 42 | public init(minLength: Dimension) { 43 | value = .min(minLength) 44 | } 45 | 46 | /// Creates a fixed length spacer. 47 | /// 48 | /// - Parameters: 49 | /// - fixedLength: The length of a spacer. 50 | public init(fixedLength: Dimension) { 51 | value = .fixed(fixedLength) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/TableColumnTitles.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // TODO: DOCS 10 | /// A block that prints the table column titles. 11 | /// 12 | /// This block is used to render the column titles of a table. It will usually 13 | /// be used within a table header, a group header, or a page header: 14 | /// 15 | /// Table(data: [data]) { 16 | /// TableColumn("Col 1", \.col1, 33) 17 | /// TableColumn("Col 2", \.col2, 33) 18 | /// TableColumn("Col 3", \.col3, 33) 19 | /// } groups { 20 | /// TableGroup(on: \.col1) { rows, value in 21 | /// Text("Group header for \(value)") 22 | /// TableColumnTitles() 23 | /// } footer: { rows, value in 24 | /// } 25 | /// } 26 | public struct TableColumnTitles: Block { 27 | @Environment(\.tableColumns) private var columns 28 | 29 | /// Creates an instance. 30 | public init() {} 31 | 32 | public var body: some Block { 33 | VStack { 34 | HStack(alignment: .top, spacing: .pt(2)) { 35 | ForEach(columns.filter(\.visible)) { column in 36 | Text(column.title) 37 | .fontWeight(.medium) 38 | .proportionalFrame(width: column.width, alignment: column.alignment) 39 | } 40 | } 41 | Divider() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/TableRow.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block for printing a row within a `Table`. 10 | public struct TableRow: Block { 11 | private let record: Value 12 | @Environment(\.tableColumns) private var columns 13 | 14 | /// Creates an instance. 15 | /// - Parameters: 16 | /// - record: The table record to be displayed. 17 | public init(record: Value) { 18 | self.record = record 19 | } 20 | 21 | public var body: some Block { 22 | HStack(alignment: .top, spacing: .pt(2)) { 23 | ForEach(columns.filter(\.visible).compactMap { $0 as? any TableColumnContent }) { column in 24 | AnyBlock(column.cellContent(record: record)) 25 | .proportionalFrame(width: column.width, alignment: column.alignment) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/Text.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block for printing text. 10 | /// 11 | /// Text can render a string as in this example. 12 | /// 13 | /// Text("What's taters, precious?") 14 | /// 15 | /// It can also render other types when passed a FormatStyle. 16 | /// 17 | /// Text(Date(), format: .dateTime) 18 | /// 19 | /// Use .truncationMode(:) to specify how to lay out 20 | /// `Text` when it is larger than its frame. The 21 | /// default truncation mode is .none, which means 22 | /// the entire text value will be printed, even if it 23 | /// overflows its container. Use .tail to truncate 24 | /// the text string with an ellipsis so that it fits 25 | /// its frame. Use .wrap to wrap the text into the 26 | /// next column or page when its container allows 27 | /// for it. 28 | /// 29 | public struct Text: Block { 30 | let value: NSAttributedString 31 | 32 | /// Creates a text block that displays a string value. 33 | /// 34 | /// - Parameters: 35 | /// - value: A string value to print. 36 | public init(_ value: String) { 37 | self.value = NSAttributedString(string: value) 38 | } 39 | 40 | public init(_ value: AttributedString) { 41 | self.value = NSAttributedString(value) 42 | } 43 | 44 | public init(_ value: NSAttributedString) { 45 | self.value = value 46 | } 47 | 48 | /// Creates a text view that prints the formatted representation 49 | /// of a nonstring type supported by a corresponding format style. 50 | /// 51 | /// - Parameters: 52 | /// - input: The underlying value to print. 53 | /// - format: A format style of type `F` to convert the underlying value 54 | /// of type `F.FormatInput` to a string representation. 55 | public init(_ input: F.FormatInput, format: F) where F: FormatStyle, F.FormatInput: Equatable, F.FormatOutput == String { 56 | value = NSAttributedString(string: format.format(input)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/TupleBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block returned by `@BlockBuilder` when a block has 10 | /// has multiple elements for its contents. 11 | /// 12 | /// Users should not need to use `TupleBlock` explicitly. 13 | public struct TupleBlock: Block { 14 | let _blocks: [any Block] 15 | 16 | public init(_ value: V) where repeat each T: Block, V == (repeat each T) { 17 | // In Swift 5.9, one of the only things that can be done with a type pack is to call a 18 | // method on each element. That is done here with an inout paramater to load an array. 19 | var array: [any Block] = [] 20 | (repeat (each value).appendToArray(&array)) 21 | _blocks = array 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/VGrid.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A container block that arranges its child blocks in a grid that 10 | /// grows vertically. 11 | /// 12 | /// A `VGrid` will fill the width of its container with equal width cells. 13 | public struct VGrid: Block where Content: Block { 14 | let columnCount: Int 15 | let columnSpacing: Dimension 16 | let rowSpacing: Dimension 17 | let wrap: Bool 18 | let content: Content 19 | 20 | /// Creates a grid that grows vertically. 21 | /// 22 | /// - Parameters: 23 | /// - columnCount: The number of columns within the grid. 24 | /// - columnSpacing: The horizontal distance between cells. 25 | /// - rowSpacing: The vertical distance between cells. 26 | /// - pageWrap: Start a new page or column when the content overflows its space. 27 | /// - content: A block builder that creates the content of this stack. 28 | public init(columnCount: Int, columnSpacing: Dimension, rowSpacing: Dimension, wrap: Bool = false, @BlockBuilder content: () -> Content) { 29 | self.columnCount = max(1, columnCount) 30 | self.columnSpacing = columnSpacing 31 | self.rowSpacing = rowSpacing 32 | self.wrap = wrap 33 | self.content = content() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/VStack.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A container block that arranges its contents in a vertical line. 10 | /// 11 | /// This example shows a simple vertical stack of three text blocks: 12 | /// 13 | /// var body: some Block { 14 | /// VStack(spacing: .pt(12) { 15 | /// Text("Quick") 16 | /// Text("Brown") 17 | /// Text("Fox") 18 | /// } 19 | /// } 20 | public struct VStack: Block where Content: Block { 21 | let alignment: HorizontalAlignment 22 | let spacing: StackSpacing 23 | let wrap: Bool 24 | let content: Content 25 | let cacheId = UUID() 26 | 27 | /// Creates a vertical stack with the given spacing and vertical alignment. 28 | /// 29 | /// - Parameters: 30 | /// - alignment: The horizontal alignment for the contents of the stack. 31 | /// - spacing: The distance between elements of the stack. 32 | /// - wrap: Start a new page or column when the content overflows its space. 33 | /// - content: A block builder that creates the content of this stack. 34 | public init(alignment: HorizontalAlignment = .center, 35 | spacing: StackSpacing = .none, 36 | wrap: Bool = false, 37 | @BlockBuilder content: () -> Content) 38 | { 39 | self.alignment = alignment 40 | self.spacing = spacing 41 | self.wrap = wrap 42 | self.content = content() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Blocks/ZStack.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block that arranges its contents with one element overlaid on 10 | /// top of another. 11 | /// 12 | /// This example shows a simple z stack of three color blocks: 13 | /// 14 | /// var body: some Block { 15 | /// ZStack(alignment: .center) { 16 | /// Color.red 17 | /// .frame(width: 3, height: 3) 18 | /// Color.blue 19 | /// .frame(width: 2, height: 2) 20 | /// Color.green 21 | /// .frame(width: 1, height: 1) 22 | /// } 23 | /// } 24 | public struct ZStack: Block { 25 | let alignment: Alignment 26 | let content: Content 27 | 28 | /// Creates a z stack with the given alignment. 29 | /// 30 | /// - Parameters: 31 | /// - alignment: The horizontal and vertical alignment for the contents of the stack. 32 | /// - content: A block builder that creates the content of this stack. 33 | public init(alignment: Alignment = .center, @BlockBuilder content: () -> Content) { 34 | self.alignment = alignment 35 | self.content = content() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Builders/BlockBuilder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A custom parameter attribute that constructs blocks from closures. 10 | @resultBuilder 11 | public struct BlockBuilder { 12 | /// Produces an empty block from a block containing no statements. 13 | public static func buildBlock() -> EmptyBlock { 14 | EmptyBlock() 15 | } 16 | 17 | /// Passes a single block written as a child block through unmodified. 18 | public static func buildBlock(_ content: Content) -> Content where Content: Block { 19 | content 20 | } 21 | 22 | /// Produces a tuple block for multi block content. 23 | public static func buildBlock(_ content: repeat each Content) -> TupleBlock < (repeat each Content)> where repeat each Content: Block { 24 | TupleBlock((repeat each content)) 25 | } 26 | 27 | /// Produces an optional block for conditional statements in multi-statement 28 | /// closures that's only visible when the condition evaluates to true. 29 | public static func buildOptional(_ content: Content?) -> Content? where Content: Block { 30 | // Note: this requires a conformance of Optional to Block 31 | content 32 | } 33 | 34 | /// Produces content for a conditional statement in a multi-statement closure 35 | /// when the condition is true. 36 | public static func buildEither(first: T) -> EitherBlock where T: Block, F: Block { 37 | EitherBlock(value: .trueContent(first)) 38 | } 39 | 40 | /// Produces content for a conditional statement in a multi-statement closure 41 | /// when the condition is false. 42 | public static func buildEither(second: F) -> EitherBlock where T: Block, F: Block { 43 | EitherBlock(value: .falseContent(second)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Builders/TableColumnBuilder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A custom parameter attribute that constructs table columns from closures 10 | @resultBuilder 11 | public struct TableColumnBuilder { 12 | public typealias T = any TableColumnContent 13 | 14 | public static func buildExpression(_ content: T) -> T { 15 | content 16 | } 17 | 18 | public static func buildBlock() -> [T] { 19 | [] 20 | } 21 | 22 | public static func buildBlock(_ content: T...) -> [T] { 23 | content 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Builders/TableGroupBuilder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A custom parameter attribute that constructs table groups from closures 10 | @resultBuilder 11 | public struct TableGroupBuilder { 12 | public typealias T = any TableGroupContent 13 | 14 | public static func buildExpression(_ content: T) -> T { 15 | content 16 | } 17 | 18 | public static func buildBlock() -> [T] { 19 | [] 20 | } 21 | 22 | public static func buildBlock(_ content: T...) -> [T] { 23 | var result: [T] = content 24 | // Chain groups. 25 | for index in result.indices.dropLast() { 26 | result[index].nextGroup = result[result.index(after: index)] 27 | } 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/Block.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type that represents part of a PDF document. 10 | /// 11 | /// You create custom blocks by declaring types that conform to the `Block` 12 | /// protocol. Implement the required ``Block/body-swift.property`` computed 13 | /// property to provide the content for your custom block. 14 | /// 15 | /// struct MyBlock: Block { 16 | /// var body: some Block { 17 | /// Text("Hello, World!") 18 | /// } 19 | /// } 20 | /// 21 | /// Assemble the block's body by combining one or more of the built-in blocks 22 | /// provided by PDF Blocks, like the ``Text`` instance in the example above, plus 23 | /// other custom blocks that you define, into a hierarchy of blocks. 24 | /// 25 | /// The `Block` protocol provides a set of modifiers — protocol 26 | /// methods with default implementations — that you use to configure 27 | /// blocks in the layout of your app. Modifiers work by wrapping the 28 | /// block instance on which you call them in another block with the specified 29 | /// characteristics. For example, adding the ``Block/opacity(_:)`` modifier to a 30 | /// text block returns a new block with some amount of transparency: 31 | /// 32 | /// Text("Hello, World!") 33 | /// .opacity(0.5) // Display partially transparent text. 34 | /// 35 | 36 | public protocol Block { 37 | /// The type of block representing the body of this view. 38 | /// 39 | /// When you create a custom block, Swift infers this type from your 40 | /// implementation of the required body property. 41 | associatedtype Body: Block 42 | /// The content and behavior of the block. 43 | /// 44 | /// When you implement a custom view, you must implement a computed 45 | /// `body` property to provide the content for your block. Return a block 46 | /// that's composed of built-in blocks that PDFBlocks provides, plus other 47 | /// composite blocks that you've already defined: 48 | /// 49 | /// struct MyBlock: Block { 50 | /// var body: some Block { 51 | /// Text("Hello, World!") 52 | /// } 53 | /// } 54 | /// 55 | @BlockBuilder var body: Body { get } 56 | } 57 | 58 | public extension Block { 59 | func renderPDF(size: PageSize = .letter, margins: EdgeInsets = .in(1)) async throws -> Data? { 60 | try await Context().render(size: size, margins: margins, content: self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/BlockModifier.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public protocol BlockModifier { 10 | typealias Content = _BlockModifier_Content 11 | 12 | /// The type of view representing the body. 13 | associatedtype Body: Block 14 | /// Gets the current body of the caller. 15 | /// 16 | /// `content` is a proxy for the view that will have the modifier 17 | /// represented by `Self` applied to it. 18 | @BlockBuilder func body(content: Content) -> Self.Body 19 | /// The content view type passed to `body()`. 20 | /// 21 | } 22 | 23 | public struct _BlockModifier_Content: Block { 24 | public init(modifier: Modifier, block: AnyBlock) { 25 | self.modifier = modifier 26 | self.block = block 27 | } 28 | 29 | let modifier: Modifier 30 | let block: AnyBlock 31 | 32 | public init(modifier: Modifier, block: some Block) { 33 | self.modifier = modifier 34 | self.block = AnyBlock(block) 35 | } 36 | 37 | public var body: some Block { 38 | block 39 | } 40 | } 41 | 42 | public struct ModifiedContent { 43 | public init(content: Content, modifier: Modifier) { 44 | self.content = content 45 | self.modifier = modifier 46 | } 47 | 48 | var content: Content 49 | var modifier: Modifier 50 | } 51 | 52 | extension ModifiedContent: Block where Content: Block, Modifier: BlockModifier { 53 | public var body: Never { 54 | fatalError() 55 | } 56 | } 57 | 58 | public extension Block { 59 | func modifier(_ modifier: Modifier) -> ModifiedContent { 60 | ModifiedContent(content: self, modifier: modifier) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/Environment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A property wrapper that reads a value from a block's environment. 10 | @propertyWrapper public struct Environment { 11 | public init(_ keyPath: KeyPath) { 12 | self.keyPath = keyPath 13 | } 14 | 15 | private let keyPath: KeyPath 16 | @MutableValue var environment: EnvironmentValues? 17 | 18 | public var wrappedValue: Value { 19 | guard let environment else { 20 | fatalError("EnvironmentValues not present.") 21 | } 22 | return environment[keyPath: keyPath] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/EnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct EnvironmentValues { 10 | var values: [String: Any] = [:] 11 | 12 | public subscript(key: K.Type) -> K.Value where K: EnvironmentKey { 13 | get { 14 | let keyString = String(describing: key) 15 | return (values[keyString] as? K.Value) ?? K.defaultValue 16 | } 17 | set { 18 | let keyString = String(describing: key) 19 | values[keyString] = newValue 20 | } 21 | } 22 | } 23 | 24 | public protocol EnvironmentKey { 25 | associatedtype Value 26 | static var defaultValue: Self.Value { get } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/PlatformColor.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A protocol that allows for platform specific color types. 10 | public protocol PlatformColor { 11 | func opacity(value: CGFloat) -> PlatformColor 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/PlatformFont.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A protocol that allows for platform specific font types. 10 | public protocol PlatformFont {} 11 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/PlatformImage.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A protocol that allows for platform specific image types. 10 | public protocol PlatformImage { 11 | var size: CGSize { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/Shape.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A 2D shape that you can use when drawing a block. 10 | /// 11 | /// Shapes without an explicit fill or stroke get a default fill based on the 12 | /// foreground color. 13 | public protocol Shape: Block { 14 | func path(in rect: CGRect) -> Path 15 | func sizeThatFits(_ proposal: CGSize) -> CGSize 16 | } 17 | 18 | public extension Shape { 19 | var body: some Block { 20 | RenderableShape(shape: self) 21 | } 22 | } 23 | 24 | public extension Shape { 25 | func sizeThatFits(_ proposal: CGSize) -> CGSize { 26 | proposal 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/ShapeStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A color or pattern to use when rendering a shape. 10 | public protocol ShapeStyle {} 11 | 12 | public extension ShapeStyle where Self == Color { 13 | static var clear: Color { 14 | Color.clear 15 | } 16 | 17 | static var blue: Color { 18 | Color.blue 19 | } 20 | 21 | static var red: Color { 22 | Color.red 23 | } 24 | 25 | static var green: Color { 26 | Color.green 27 | } 28 | 29 | static var yellow: Color { 30 | Color.yellow 31 | } 32 | 33 | static var orange: Color { 34 | Color.orange 35 | } 36 | 37 | static var purple: Color { 38 | Color.purple 39 | } 40 | 41 | static var pink: Color { 42 | Color.pink 43 | } 44 | 45 | static var cyan: Color { 46 | Color.cyan 47 | } 48 | 49 | static var gray: Color { 50 | Color.gray 51 | } 52 | 53 | static var white: Color { 54 | Color.white 55 | } 56 | 57 | static var black: Color { 58 | Color.black 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Core/StrokeStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | #if os(iOS) 9 | import UIKit 10 | #endif 11 | #if os(macOS) 12 | import AppKit 13 | #endif 14 | 15 | /// The characteristics of a stroke that traces a path. 16 | public struct StrokeStyle { 17 | /// The width of the stroked path. 18 | public var lineWidth: Dimension 19 | 20 | /// The endpoint style of a line. 21 | public var lineCap: CGLineCap 22 | 23 | /// The join type of a line. 24 | public var lineJoin: CGLineJoin 25 | 26 | /// A threshold used to determine whether to use a bevel instead of a 27 | /// miter at a join. 28 | public var miterLimit: CGFloat 29 | 30 | /// The lengths of painted and unpainted segments used to make a dashed line. 31 | public var dash: [CGFloat] 32 | 33 | /// How far into the dash pattern the line starts. 34 | public var dashPhase: CGFloat 35 | 36 | /// Creates a new stroke style from the given components. 37 | /// 38 | /// - Parameters: 39 | /// - lineWidth: The width of the segment. 40 | /// - lineCap: The endpoint style of a segment. 41 | /// - lineJoin: The join type of a segment. 42 | /// - miterLimit: The threshold used to determine whether to use a bevel 43 | /// instead of a miter at a join. 44 | /// - dash: The lengths of painted and unpainted segments used to make a 45 | /// dashed line. 46 | /// - dashPhase: How far into the dash pattern the line starts. 47 | public init(lineWidth: Dimension = .pt(1), lineCap: CGLineCap = .butt, lineJoin: CGLineJoin = .miter, miterLimit: CGFloat = 10, 48 | dash: [CGFloat] = [CGFloat](), dashPhase: CGFloat = 0) 49 | { 50 | self.lineWidth = lineWidth 51 | self.lineCap = lineCap 52 | self.lineJoin = lineJoin 53 | self.miterLimit = miterLimit 54 | self.dash = dash 55 | self.dashPhase = dashPhase 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+AspectRatio.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Constrains a block to a specified aspect ratio. 11 | /// 12 | /// In this example, the second color block will be only as tall as 13 | /// it is wide. 14 | /// 15 | /// HStack { 16 | /// Color(.systemRed) 17 | /// Color(.systemGreen) 18 | /// .aspectRatio(1) 19 | /// Color(.systemBlue) 20 | /// } 21 | /// 22 | /// - Parameter value: The aspect ratio (width/height) to apply. 23 | /// - Returns: A block that applies an aspect ratio to this block. 24 | func aspectRatio(_ value: CGFloat, contentMode _: ContentMode = .fit) -> some Block { 25 | modifier(AspectRatioModifier(value: value)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Background.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Adds a background to this block with the specified content and alignment. 11 | /// 12 | /// - Parameter alignment: The alignment of the background content. 13 | /// - Parameter content: The background content. 14 | /// - Returns: A block that adds a background to this block. 15 | func background(alignment: Alignment = .center, @BlockBuilder _ content: () -> some Block) -> some Block { 16 | modifier(BackgroundModifier(background: content(), alignment: alignment)) 17 | } 18 | 19 | /// Sets the block's background to a fill style. 20 | /// 21 | /// Use this modifier to place a type that conforms to the ``ShapeStyle`` 22 | /// protocol --- like a ``Color`` or ``Gradient`` --- behind a view. 23 | /// For example, you can add a ``Color`` behind a ``Text``: 24 | /// 25 | /// Text("The Quick Brown Fox") 26 | /// .padding() 27 | /// .background(.cyan) 28 | /// 29 | /// - Parameter style: An instance of a type that conforms to ``ShapeStyle`` that 30 | /// will be drawn behind this block. 31 | /// - Returns: A block with the specified style drawn behind it. 32 | func background(_ style: some ShapeStyle) -> some Block { 33 | background { 34 | Rectangle() 35 | .fill(style) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Bold.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets emphasized value of block. 11 | func bold(_ value: Bool = true) -> some Block { 12 | environment(\.bold, value) 13 | } 14 | } 15 | 16 | struct BoldKey: EnvironmentKey { 17 | static let defaultValue = false 18 | } 19 | 20 | extension EnvironmentValues { 21 | var bold: Bool { 22 | get { self[BoldKey.self] } 23 | set { self[BoldKey.self] = newValue } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+BoldFontName.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets text to bold. 11 | func boldFontName(_ value: FontName) -> some Block { 12 | environment(\.boldFontName, value) 13 | } 14 | } 15 | 16 | struct BoldFontNameKey: EnvironmentKey { 17 | static let defaultValue: FontName? = nil 18 | } 19 | 20 | extension EnvironmentValues { 21 | var boldFontName: FontName? { 22 | get { self[BoldFontNameKey.self] } 23 | set { self[BoldFontNameKey.self] = newValue } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Border.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Adds a border to this block with the specified color and width. 11 | /// 12 | /// - Parameters: 13 | /// - color: The color of the border. 14 | /// - width: The thickness of the border. 15 | /// 16 | /// - Returns: A block that adds a border with the specified style and width 17 | /// to this block. 18 | func border(_ content: some ShapeStyle, width: Dimension = .pt(1)) -> some Block { 19 | modifier(BorderModifier(shapeStyle: content, width: width)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Clipped.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func clipped() -> some Block { 11 | modifier(ClipRegionModifier()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Environment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the environment value of the specified key path to the given value. 11 | /// 12 | /// Use this modifier to set one of the writable properties of the 13 | /// ``EnvironmentValues`` structure, including custom values that you 14 | /// create. For example, you can set the value associated with the 15 | /// ``EnvironmentValues/fontSize`` key: 16 | /// 17 | /// MyBlock() 18 | /// .environment(\.fontSize, 12) 19 | /// 20 | /// You then read the value inside `MyBlock` or one of its descendants 21 | /// using the ``Environment`` property wrapper: 22 | /// 23 | /// struct MyBlock: Block { 24 | /// @Environment(\.fontSize) var fontSize: CGFloat 25 | /// 26 | /// var body: some Block { ... } 27 | /// } 28 | /// 29 | /// PDFBuilder provides dedicated block modifiers for setting most 30 | /// environment values, like the ``Block/font(size:)`` 31 | /// modifier which sets the ``EnvironmentValues/fontSize`` value: 32 | /// 33 | /// MyBlock() 34 | /// .font(size: 12) 35 | /// 36 | /// Prefer the dedicated modifier when available, and offer your own when 37 | /// defining custom environment values, as described in 38 | /// ``EnvironmentKey``. 39 | /// 40 | /// This modifier affects the given block, 41 | /// as well as that block's descendant blocks. It has no effect 42 | /// outside the block hierarchy on which you call it. 43 | /// 44 | /// - Parameters: 45 | /// - keyPath: A key path that indicates the property of the 46 | /// ``EnvironmentValues`` structure to update. 47 | /// - value: The new value to set for the item specified by `keyPath`. 48 | /// 49 | /// - Returns: A block that has the given value set in its environment. 50 | func environment(_ keyPath: WritableKeyPath, _ value: V) -> some Block { 51 | modifier(EnvironmentModifier(keyPath: keyPath, value: value)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Fill.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func fill(_ value: some ShapeStyle) -> some Block { 11 | environment(\.fill, value) 12 | } 13 | } 14 | 15 | struct FillKey: EnvironmentKey { 16 | static let defaultValue: ShapeStyle? = nil 17 | } 18 | 19 | extension EnvironmentValues { 20 | var fill: ShapeStyle? { 21 | get { self[FillKey.self] } 22 | set { self[FillKey.self] = newValue } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Font.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func font(_ font: Font) -> some Block { 11 | environment(\.font, font) 12 | } 13 | 14 | func fontSize(_ size: CGFloat) -> some Block { 15 | environment(\.fontSize, size) 16 | } 17 | 18 | func fontWeight(_ weight: Font.Weight) -> some Block { 19 | environment(\.fontWeight, weight) 20 | } 21 | 22 | func fontDesign(_ design: Font.Design) -> some Block { 23 | environment(\.fontDesign, design) 24 | } 25 | 26 | func fontWidth(_ width: Font.Width) -> some Block { 27 | environment(\.fontWidth, width) 28 | } 29 | } 30 | 31 | struct FontKey: EnvironmentKey { 32 | static let defaultValue: Font = .init(kitFont: .systemFont(ofSize: 12), weight: .regular) 33 | } 34 | 35 | extension EnvironmentValues { 36 | var font: Font { 37 | get { self[FontKey.self] } 38 | set { 39 | self[FontKey.self].kitFont = newValue.kitFont 40 | if let weight = newValue.weight { 41 | self[FontKey.self].weight = weight 42 | } 43 | if let width = newValue.width { 44 | self[FontKey.self].width = width 45 | } 46 | if let design = newValue.design { 47 | self[FontKey.self].design = design 48 | } 49 | } 50 | } 51 | } 52 | 53 | extension EnvironmentValues { 54 | var fontSize: CGFloat { 55 | get { 56 | self[FontKey.self].kitFont.pointSize 57 | } 58 | set { 59 | let font = self[FontKey.self] 60 | let newFont = font.kitFont.withSize(newValue) 61 | self[FontKey.self] = .init(kitFont: newFont, weight: font.weight, design: font.design, width: font.width) 62 | } 63 | } 64 | } 65 | 66 | extension EnvironmentValues { 67 | var fontWeight: KitFont.Weight? { 68 | get { 69 | self[FontKey.self].weight 70 | } 71 | set { 72 | self[FontKey.self].weight = newValue 73 | } 74 | } 75 | } 76 | 77 | extension EnvironmentValues { 78 | var fontWidth: KitFont.Width? { 79 | get { 80 | self[FontKey.self].width 81 | } 82 | set { 83 | self[FontKey.self].width = newValue 84 | } 85 | } 86 | } 87 | 88 | extension EnvironmentValues { 89 | var fontDesign: KitFontDescriptor.SystemDesign? { 90 | get { 91 | self[FontKey.self].design 92 | } 93 | set { 94 | self[FontKey.self].design = newValue 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+ForegroundColor.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the foreground color for a block. 11 | func foregroundColor(_ value: Color) -> some Block { 12 | environment(\.foregroundStyle, value) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+ForegroundStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the foreground color for a block. 11 | func foregroundStyle(_ value: some ShapeStyle) -> some Block { 12 | environment(\.foregroundStyle, value) 13 | } 14 | } 15 | 16 | struct ForegroundStyleKey: EnvironmentKey { 17 | static let defaultValue: ShapeStyle = Color.black 18 | } 19 | 20 | extension EnvironmentValues { 21 | var foregroundStyle: ShapeStyle { 22 | get { self[ForegroundStyleKey.self] } 23 | set { self[ForegroundStyleKey.self] = newValue } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Frame.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Positions this block within an invisible frame with the specified size and 11 | /// content alignment. 12 | /// 13 | /// - Parameters: 14 | /// - width: A fixed width for the resulting block. If `width` is `nil`, 15 | /// the resulting block assumes this block's sizing behavior. 16 | /// - height: A fixed height for the resulting block. If `height` is `nil`, 17 | /// the resulting block assumes this block's sizing behavior. 18 | /// - alignment: The alignment of this block inside the resulting frame. 19 | /// 20 | /// - Returns: A block with fixed dimensions of `width` and `height`, for the 21 | /// parameters that are non-`nil`. 22 | func frame(width: Dimension? = nil, height: Dimension? = nil, alignment: Alignment = .center) -> some Block { 23 | modifier(FrameModifier(width: width, height: height, alignment: alignment)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Italic.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets italic value of block. 11 | func italic(_ value: Bool = true) -> some Block { 12 | environment(\.italic, value) 13 | } 14 | } 15 | 16 | struct ItalicKey: EnvironmentKey { 17 | static let defaultValue = false 18 | } 19 | 20 | extension EnvironmentValues { 21 | var italic: Bool { 22 | get { self[ItalicKey.self] } 23 | set { self[ItalicKey.self] = newValue } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Kerning.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func kerning(_ kerning: CGFloat) -> some Block { 11 | environment(\.kerning, kerning) 12 | } 13 | } 14 | 15 | struct KerningKey: EnvironmentKey { 16 | static let defaultValue: CGFloat = 0 17 | } 18 | 19 | extension EnvironmentValues { 20 | var kerning: CGFloat { 21 | get { 22 | self[KerningKey.self] 23 | } 24 | set { 25 | self[KerningKey.self] = newValue 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+MultilineTextAlignment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the alignment of a text block that contains multiple lines of text. 11 | /// 12 | /// Use this modifier to set an alignment for a multiline block of text. 13 | /// For example, the modifier centers the contents of the following 14 | /// ``Text`` block: 15 | /// 16 | /// Text("This is a block of text that shows up in a text element as multiple lines.") 17 | /// .frame(width: .in(2)) 18 | /// .multilineTextAlignment(.center) 19 | /// 20 | /// - Parameter alignment: The alignment value to apply. 21 | /// - Returns: A block for which the given opacity has been set. 22 | func multilineTextAlignment(_ alignment: TextAlignment) -> some Block { 23 | environment(\.multilineTextAlignment, alignment) 24 | } 25 | } 26 | 27 | struct MultilineTextAlignmentKey: EnvironmentKey { 28 | static let defaultValue: TextAlignment = .leading 29 | } 30 | 31 | extension EnvironmentValues { 32 | var multilineTextAlignment: TextAlignment { 33 | get { self[MultilineTextAlignmentKey.self] } 34 | set { self[MultilineTextAlignmentKey.self] = newValue } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Offset.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func offset(x: Dimension, y: Dimension) -> some Block { 11 | modifier(OffsetModifier(x: x, y: y)) 12 | } 13 | 14 | func offset(_ offset: Size) -> some Block { 15 | modifier(OffsetModifier(x: offset.width, y: offset.height)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+OnRender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Adds an action to perform after this block is rendered. 11 | /// 12 | /// - Parameter action: The action to perform. 13 | /// 14 | /// - Returns: A block that triggers `action` after it is rendered. 15 | func onRender(perform action: @escaping () -> Void) -> some Block { 16 | modifier(OnRenderModifier(onRender: action)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Opacity.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the transparency of this block. 11 | /// 12 | /// When applying the `opacity(_:)` modifier to a block that has already had 13 | /// its opacity transformed, the modifier multiplies the effect of the 14 | /// underlying opacity transformation. 15 | /// 16 | /// - Parameter opacity: A value between 0 (fully transparent) and 1 (fully opaque). 17 | /// - Returns: A block that sets the transparency of this block. 18 | func opacity(_ opacity: CGFloat) -> some Block { 19 | modifier(OpacityModifier(opacity: opacity)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Overlay.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Adds a overlay to this block with the specified content and alignment. 11 | /// 12 | /// - Parameter alignment: The alignment of the overlay content. 13 | /// - Parameter content: The overlay content. 14 | /// - Returns: A block that adds an overlay to this block. 15 | func overlay(alignment: Alignment = .center, @BlockBuilder _ content: () -> some Block) -> some Block { 16 | modifier(OverlayModifier(overlay: content(), alignment: alignment)) 17 | } 18 | 19 | /// Sets the block's overlay to a fill style. 20 | /// 21 | /// Use this modifier to place a type that conforms to the ``ShapeStyle`` 22 | /// protocol --- like a ``Color`` or ``Gradient`` --- behind a view. 23 | /// For example, you can add a ``Color`` behind a ``Text``: 24 | /// 25 | /// Text("The Quick Brown Fox") 26 | /// .padding() 27 | /// .background(.cyan) 28 | /// 29 | /// - Parameter style: An instance of a type that conforms to ``ShapeStyle`` that 30 | /// will be drawn on top of this block. 31 | /// - Returns: A block with the specified style drawn on top of it. 32 | func overlay(_ style: some ShapeStyle) -> some Block { 33 | overlay { 34 | Rectangle() 35 | .fill(style) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Padding.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func padding(_ edges: Set, _ length: Dimension) -> some Block { 11 | let top = edges.contains(.top) ? length : .pt(0) 12 | let leading = edges.contains(.leading) ? length : .pt(0) 13 | let bottom = edges.contains(.bottom) ? length : .pt(0) 14 | let trailing = edges.contains(.trailing) ? length : .pt(0) 15 | return modifier(PaddingModifier(padding: .init(top: top, leading: leading, bottom: bottom, trailing: trailing))) 16 | } 17 | 18 | func padding(_ length: Dimension) -> some Block { 19 | padding(.all, length) 20 | } 21 | 22 | func padding(top: Dimension = .pt(0), leading: Dimension = .pt(0), bottom: Dimension = .pt(0), trailing: Dimension = .pt(0)) -> some Block { 23 | modifier(PaddingModifier(padding: .init(top: top, leading: leading, bottom: bottom, trailing: trailing))) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+ProportionalFrame.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the proportional width of a block within an `HStack`. 11 | /// 12 | /// The width parameter is a proportion of the sum of all the 13 | /// proportional widths. In the following example, the green block 14 | /// takes twice the width of the other blocks. The red block takes 15 | /// 25% of the width (1/4), the green block takes 50% of the width 16 | /// (2/4), and the blue block takes 25% of the width (1/4). 17 | /// 18 | /// HStack() { 19 | /// Color.red 20 | /// .proportionalFrame(width: 1) 21 | /// Color.green 22 | /// .proportionalFrame(width: 2) 23 | /// Color.blue 24 | /// .proportionalFrame(width: 1) 25 | /// } 26 | /// 27 | /// In this example, the red block takes 35% of the width, 28 | /// the green block takes 20%, and the blue 29 | /// block takes 55%. 30 | /// 31 | /// HStack() { 32 | /// Color.red 33 | /// .proportionalFrame(width: 35) 34 | /// Color.green 35 | /// .proportionalFrame(width: 20) 36 | /// Color.blue 37 | /// .proportionalFrame(width: 55) 38 | /// } 39 | /// 40 | /// - Parameter width: The proportional width to apply. 41 | /// - Parameter alignment: The horizontal alignment of the block within its frame. 42 | /// - Returns: A block that sets the proportional width of this block. 43 | func proportionalFrame(width: CGFloat = 1, alignment: HorizontalAlignment = .center) -> some Block { 44 | ProporionalFrame(width: width, horizontalAlignment: alignment, content: self) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+RotationEffect.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some Block { 11 | modifier(RotationModifier(angle: angle, anchor: anchor)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+ScaleEffect.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some Block { 11 | modifier(ScaleModifier(scale: scale, anchor: anchor)) 12 | } 13 | 14 | func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some Block { 15 | modifier(ScaleModifier(scale: .init(width: s, height: s), anchor: anchor)) 16 | } 17 | 18 | func scaleEffect(x: CGFloat = 1, y: CGFloat = 1, anchor: UnitPoint = .center) -> some Block { 19 | modifier(ScaleModifier(scale: .init(width: x, height: y), anchor: anchor)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Stroke.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func stroke(_ content: some ShapeStyle, lineWidth: Dimension = .pt(1)) -> some Block { 11 | environment(\.strokeContent, content) 12 | .environment(\.strokeStyle, StrokeStyle(lineWidth: lineWidth)) 13 | } 14 | 15 | func stroke(_ content: some ShapeStyle, style: StrokeStyle) -> some Block { 16 | environment(\.strokeContent, content) 17 | .environment(\.strokeStyle, style) 18 | } 19 | } 20 | 21 | struct StokeContentKey: EnvironmentKey { 22 | static let defaultValue: ShapeStyle? = nil 23 | } 24 | 25 | extension EnvironmentValues { 26 | var strokeContent: ShapeStyle? { 27 | get { self[StokeContentKey.self] } 28 | set { self[StokeContentKey.self] = newValue } 29 | } 30 | } 31 | 32 | struct StrokeStyleKey: EnvironmentKey { 33 | static let defaultValue = StrokeStyle() 34 | } 35 | 36 | extension EnvironmentValues { 37 | var strokeStyle: StrokeStyle { 38 | get { self[StrokeStyleKey.self] } 39 | set { self[StrokeStyleKey.self] = newValue } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+Tag.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func tag(_ value: String) -> some Block { 11 | environment(\.tag, value) 12 | } 13 | } 14 | 15 | // Used for debugging. 16 | struct TagKey: EnvironmentKey { 17 | static let defaultValue = "" 18 | } 19 | 20 | extension EnvironmentValues { 21 | var tag: String { 22 | get { self[TagKey.self] } 23 | set { self[TagKey.self] = newValue } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+TextFill.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func textStroke(color: Color = Color.black, lineWidth: CGFloat = 3.0) -> some Block { 11 | environment(\.textStroke, TextStroke(color: color, lineWidth: lineWidth)) 12 | } 13 | } 14 | 15 | struct TextStrokeKey: EnvironmentKey { 16 | static let defaultValue: TextStroke? = nil 17 | } 18 | 19 | extension EnvironmentValues { 20 | var textStroke: TextStroke? { 21 | get { self[TextStrokeKey.self] } 22 | set { self[TextStrokeKey.self] = newValue } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+TextStroke.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | func textFill(_ color: Color) -> some Block { 11 | environment(\.textFill, color) 12 | } 13 | } 14 | 15 | struct TextFillKey: EnvironmentKey { 16 | static let defaultValue: Color? = nil 17 | } 18 | 19 | extension EnvironmentValues { 20 | var textFill: Color? { 21 | get { self[TextFillKey.self] } 22 | set { self[TextFillKey.self] = newValue } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Modifiers/Block+TruncationMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Block { 10 | /// Sets the truncation mode for lines of text that are too long to fit in 11 | /// the available width. 12 | /// 13 | /// Use the `truncationMode(_:)` modifier to determine whether text in a 14 | /// long line is truncated at the beginning, middle, or end. Truncation is 15 | /// indicated by adding an ellipsis (…) to the line when removing text to 16 | /// indicate to readers that text is missing. 17 | /// 18 | /// In the example below, the bounds of text block constrains the amount of 19 | /// text that the block displays and the `truncationMode(_:)` specifies from 20 | /// which direction and where to display the truncation indicator: 21 | /// 22 | /// Text("This is a block of text that shows up in a text element as multiple lines.") 23 | /// .frame(width: .in(1), height: .in(1)) 24 | /// .truncationMode(.tail) 25 | /// 26 | /// - Parameter mode: The truncation mode that specifies where to truncate 27 | /// the text within the text block, if needed. You can truncate at the 28 | /// beginning, middle, or end of the text block. 29 | /// 30 | /// - Returns: A block that truncates text at different points in a line 31 | /// depending on the mode you select. 32 | func truncationMode(_ mode: TextTruncationMode) -> some Block { 33 | environment(\.truncationMode, mode) 34 | } 35 | } 36 | 37 | struct TruncationModeKey: EnvironmentKey { 38 | static let defaultValue: TextTruncationMode = .none 39 | } 40 | 41 | extension EnvironmentValues { 42 | var truncationMode: TextTruncationMode { 43 | get { self[TruncationModeKey.self] } 44 | set { self[TruncationModeKey.self] = newValue } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Platform/AppKit.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | #if os(macOS) 10 | import AppKit 11 | 12 | extension NSColor: PlatformColor { 13 | public func opacity(value: CGFloat) -> PlatformColor { 14 | withAlphaComponent(value * cgColor.alpha) 15 | } 16 | } 17 | 18 | public extension Color { 19 | init(_ color: NSColor) { 20 | platformColor = color 21 | } 22 | } 23 | 24 | public extension Color { 25 | static var clear: Color { 26 | .init(NSColor.clear) 27 | } 28 | 29 | static var blue: Color { 30 | .init(NSColor.systemBlue) 31 | } 32 | 33 | static var red: Color { 34 | .init(NSColor.systemRed) 35 | } 36 | 37 | static var green: Color { 38 | .init(NSColor.systemGreen) 39 | } 40 | 41 | static var yellow: Color { 42 | .init(NSColor.systemYellow) 43 | } 44 | 45 | static var orange: Color { 46 | .init(NSColor.systemOrange) 47 | } 48 | 49 | static var purple: Color { 50 | .init(NSColor.systemPurple) 51 | } 52 | 53 | static var pink: Color { 54 | .init(NSColor.systemPink) 55 | } 56 | 57 | static var cyan: Color { 58 | .init(NSColor.systemCyan) 59 | } 60 | 61 | static var gray: Color { 62 | .init(NSColor.gray) 63 | } 64 | 65 | static var white: Color { 66 | .init(NSColor.white) 67 | } 68 | 69 | static var black: Color { 70 | .init(NSColor.black) 71 | } 72 | } 73 | 74 | extension NSImage: PlatformImage {} 75 | 76 | public extension Image { 77 | init(_ image: NSImage?) { 78 | self.image = image ?? KitImage() 79 | } 80 | 81 | init(path: String) { 82 | image = NSImage(contentsOfFile: path) ?? KitImage() 83 | } 84 | } 85 | 86 | public extension NSImage { 87 | convenience init?(systemName: String) { 88 | self.init(systemSymbolName: systemName, accessibilityDescription: nil) 89 | } 90 | } 91 | #endif 92 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Platform/UIKit.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | extension UIColor: PlatformColor { 13 | public func opacity(value: CGFloat) -> PlatformColor { 14 | var red: CGFloat = 0 15 | var green: CGFloat = 0 16 | var blue: CGFloat = 0 17 | var alpha: CGFloat = 0 18 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 19 | return withAlphaComponent(value * alpha) 20 | } 21 | } 22 | 23 | public extension Color { 24 | init(_ color: UIColor) { 25 | platformColor = color 26 | } 27 | } 28 | 29 | public extension Color { 30 | static var clear: Color { 31 | .init(UIColor.clear) 32 | } 33 | 34 | static var blue: Color { 35 | .init(UIColor.systemBlue) 36 | } 37 | 38 | static var red: Color { 39 | .init(UIColor.systemRed) 40 | } 41 | 42 | static var green: Color { 43 | .init(UIColor.systemGreen) 44 | } 45 | 46 | static var yellow: Color { 47 | .init(UIColor.systemYellow) 48 | } 49 | 50 | static var orange: Color { 51 | .init(UIColor.systemOrange) 52 | } 53 | 54 | static var purple: Color { 55 | .init(UIColor.systemPurple) 56 | } 57 | 58 | static var pink: Color { 59 | .init(UIColor.systemPink) 60 | } 61 | 62 | static var cyan: Color { 63 | .init(UIColor.systemCyan) 64 | } 65 | 66 | static var gray: Color { 67 | .init(UIColor.gray) 68 | } 69 | 70 | static var white: Color { 71 | .init(UIColor.white) 72 | } 73 | 74 | static var black: Color { 75 | .init(UIColor.black) 76 | } 77 | } 78 | 79 | extension UIImage: PlatformImage {} 80 | 81 | public extension Image { 82 | init(_ image: UIImage?) { 83 | self.image = image ?? UIImage() 84 | } 85 | 86 | init(path: String) { 87 | image = UIImage(contentsOfFile: path) ?? UIImage() 88 | } 89 | } 90 | 91 | #endif 92 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/ShapeStyles/Color+ShapeStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Color: ShapeStyle {} 10 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/ShapeStyles/LinearGradient.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct RadialGradient: ShapeStyle { 10 | let gradient: Gradient 11 | let center: UnitPoint 12 | let startRadius: Dimension 13 | let endRadius: Dimension 14 | 15 | /// Creates a radial gradient from a base gradient. 16 | public init(gradient: Gradient, center: UnitPoint, startRadius: Dimension, endRadius: Dimension) { 17 | self.gradient = gradient 18 | self.center = center 19 | self.startRadius = startRadius 20 | self.endRadius = endRadius 21 | } 22 | 23 | /// Creates a radial gradient from a collection of colors. 24 | public init(colors: [Color], center: UnitPoint, startRadius: Dimension, endRadius: Dimension) { 25 | gradient = Gradient(colors: colors) 26 | self.center = center 27 | self.startRadius = startRadius 28 | self.endRadius = endRadius 29 | } 30 | 31 | /// Creates a radial gradient from a collection of color stops. 32 | public init(stops: [Gradient.Stop], center: UnitPoint, startRadius: Dimension, endRadius: Dimension) { 33 | gradient = Gradient(stops: stops) 34 | self.center = center 35 | self.startRadius = startRadius 36 | self.endRadius = endRadius 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/ShapeStyles/RadialGradient.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct LinearGradient: ShapeStyle { 10 | let gradient: Gradient 11 | let startPoint: UnitPoint 12 | let endPoint: UnitPoint 13 | 14 | /// Creates a linear gradient from a base gradient. 15 | public init(gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) { 16 | self.gradient = gradient 17 | self.startPoint = startPoint 18 | self.endPoint = endPoint 19 | } 20 | 21 | /// Creates a linear gradient from a collection of colors. 22 | public init(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) { 23 | gradient = .init(colors: colors) 24 | self.startPoint = startPoint 25 | self.endPoint = endPoint 26 | } 27 | 28 | /// Creates a linear gradient from a collection of color stops. 29 | public init(stops: [Gradient.Stop], startPoint: UnitPoint, endPoint: UnitPoint) { 30 | gradient = .init(stops: stops) 31 | self.startPoint = startPoint 32 | self.endPoint = endPoint 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/Capsule.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Capsule: Shape { 10 | public init() {} 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | let radius = min(rect.width, rect.height) / 2 14 | return Path(roundedRect: rect, cornerRadius: radius) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/Circle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Circle: Shape { 10 | public init() {} 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | let length = min(rect.width, rect.height) 14 | let size = CGSize(width: length, height: length) 15 | let origin = CGPoint(x: (rect.width - length) / 2, y: (rect.height - length) / 2) 16 | return Path(ellipseIn: CGRect(origin: origin, size: size)) 17 | } 18 | 19 | public func sizeThatFits(_ proposal: CGSize) -> CGSize { 20 | let minLength = min(proposal.width, proposal.height) 21 | return .init(width: minLength, height: minLength) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/Ellipse.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Ellipse: Shape { 10 | public init() {} 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | Path(ellipseIn: rect) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/Rectangle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Rectangle: Shape { 10 | public init() {} 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | Path(rect) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/RoundedRectangle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct RoundedRectangle: Shape { 10 | let cornerRadius: Dimension 11 | 12 | public init(cornerRadius: Dimension) { 13 | self.cornerRadius = cornerRadius 14 | } 15 | 16 | public func path(in rect: CGRect) -> Path { 17 | Path(roundedRect: rect, cornerRadius: cornerRadius.points) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Shapes/Square.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Square: Shape { 10 | public init() {} 11 | 12 | public func path(in rect: CGRect) -> Path { 13 | let length = min(rect.width, rect.height) 14 | let size = CGSize(width: length, height: length) 15 | let origin = CGPoint(x: (rect.width - length) / 2, y: (rect.height - length) / 2) 16 | return Path(CGRect(origin: origin, size: size)) 17 | } 18 | 19 | public func sizeThatFits(_ proposal: CGSize) -> CGSize { 20 | let minLength = min(proposal.width, proposal.height) 21 | return .init(width: minLength, height: minLength) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Alignment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing horizontal and vertical alignment. 10 | public enum Alignment { 11 | case top 12 | case center 13 | case bottom 14 | case leading 15 | case trailing 16 | case topLeading 17 | case topTrailing 18 | case bottomLeading 19 | case bottomTrailing 20 | } 21 | 22 | /// A type for expressing horizontal alignment. 23 | public enum HorizontalAlignment { 24 | case leading 25 | case center 26 | case trailing 27 | } 28 | 29 | /// A type for expressing vertical alignment. 30 | public enum VerticalAlignment { 31 | case top 32 | case center 33 | case bottom 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Angle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Angle { 10 | public var radians: Double 11 | 12 | public var degrees: Double { 13 | radians * 180 / .pi 14 | } 15 | 16 | public init() { 17 | radians = 0 18 | } 19 | 20 | public init(radians: Double) { 21 | self.radians = radians 22 | } 23 | 24 | public init(degrees: Double) { 25 | radians = degrees * .pi / 180 26 | } 27 | 28 | public static func radians(_ radians: Double) -> Angle { 29 | Angle(radians: radians) 30 | } 31 | 32 | public static func degrees(_ degrees: Double) -> Angle { 33 | Angle(degrees: degrees) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Color.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | import Foundation 10 | 11 | /// Color is both a representation for color generally and a block for rendering 12 | /// a rectangular region of color. 13 | /// 14 | /// To set the foregroundColor for a block: 15 | /// 16 | /// Text("Hello, world.") 17 | /// .foregroundColor(Color.red) 18 | /// 19 | /// To draw a blue square: 20 | /// 21 | /// Color(UIColor.blue) 22 | /// .frame(width: .in(1), height: .in(1)) 23 | /// 24 | /// Each platform (iOS/macOS/Linux) uses its own color type that conforms 25 | /// to PlatformColor. 26 | public struct Color { 27 | let platformColor: any PlatformColor 28 | } 29 | 30 | public extension Color { 31 | /// Multiplies the opacity of the color by the given amount. 32 | /// 33 | /// - Parameter opacity: The amount by which to multiply the opacity of the 34 | /// color. 35 | /// - Returns: A color with modified opacity. 36 | func opacity(_ opacity: Double) -> Color { 37 | Color(platformColor: platformColor.opacity(value: opacity)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/ContentMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public enum ContentMode { 10 | /// An option that resizes the content so it's all within the available space, 11 | /// both vertically and horizontally. 12 | /// 13 | /// This mode preserves the content's aspect ratio. 14 | /// If the content doesn't have the same aspect ratio as the available 15 | /// space, the content becomes the same size as the available space on 16 | /// one axis and leaves empty space on the other. 17 | case fit 18 | 19 | /// An option that resizes the content so it occupies all available space, 20 | /// both vertically and horizontally. 21 | /// 22 | /// This mode preserves the content's aspect ratio. 23 | /// If the content doesn't have the same aspect ratio as the available 24 | /// space, the content becomes the same size as the available space on 25 | /// one axis, and larger on the other axis. 26 | // case fill 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Dimension.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing width and height dimmensions or spacing. 10 | /// 11 | /// `Dimmension` can be expressed in points (72 ppi), inches, milimeters. 12 | /// 13 | /// The following paddings values are all equivalent. 14 | /// 15 | /// Text("Hello, world.") 16 | /// .padding(.mm(25.4)) 17 | /// Text("Hello, world.") 18 | /// .padding(.in(1)) 19 | /// Text("Hello, world.") 20 | /// .padding(.pt(72)) 21 | /// 22 | /// 23 | /// `Dimmension` can also be given a pseudo-value of `max` that indicates the maximum 24 | /// availible width, height, or spacing. 25 | /// 26 | /// Text("Hello, world.") 27 | /// .padding(horizontal: .max) 28 | /// 29 | public struct Dimension: Equatable { 30 | public let points: CGFloat 31 | public let max: Bool 32 | 33 | public static var max: Dimension { 34 | .init(points: 0, max: true) 35 | } 36 | 37 | public static func pt(_ value: CGFloat) -> Dimension { 38 | .init(points: value, max: false) 39 | } 40 | 41 | public static func `in`(_ value: CGFloat) -> Dimension { 42 | .init(points: value * 72, max: false) 43 | } 44 | 45 | public static func mm(_ value: CGFloat) -> Dimension { 46 | .init(points: value * 72 / 25.4, max: false) 47 | } 48 | } 49 | 50 | extension Dimension: ExpressibleByIntegerLiteral { 51 | public init(integerLiteral value: IntegerLiteralType) { 52 | points = CGFloat(value) 53 | max = false 54 | } 55 | } 56 | 57 | extension Dimension: ExpressibleByFloatLiteral { 58 | public init(floatLiteral value: FloatLiteralType) { 59 | points = value 60 | max = false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Edge.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public enum Edge { 10 | case top 11 | case leading 12 | case bottom 13 | case trailing 14 | } 15 | 16 | public extension Set { 17 | /// Horizontal and vertical edges. 18 | static var all: Self { [.top, .leading, .bottom, .trailing] } 19 | 20 | /// Top and bottom edges. 21 | static var vertical: Self { [.top, .bottom] } 22 | 23 | /// Leading and trailing edges. 24 | static var horizontal: Self { [.leading, .trailing] } 25 | 26 | /// Top edge. 27 | static var top: Self { [.top] } 28 | 29 | /// Bottom edge. 30 | static var bottom: Self { [.bottom] } 31 | 32 | /// Leading edge. 33 | static var leading: Self { [.leading] } 34 | 35 | /// Trailing edge. 36 | static var trailing: Self { [.trailing] } 37 | 38 | /// Bottom and trailing edges. 39 | static var bottomTrailing: Self { [.bottom, .trailing] } 40 | 41 | /// Bottom and leading edges. 42 | static var bottomLeading: Self { [.bottom, .leading] } 43 | 44 | /// Top and trailing edges. 45 | static var topTrailing: Self { [.top, .trailing] } 46 | 47 | /// Top and leading edges. 48 | static var topLeading: Self { [.top, .leading] } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/EdgeInsets.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing edge insets. 10 | /// 11 | /// Used for page margins and block padding. 12 | public struct EdgeInsets: Equatable { 13 | public var top: Dimension 14 | public var leading: Dimension 15 | public var bottom: Dimension 16 | public var trailing: Dimension 17 | 18 | public init(top: Dimension = .pt(0), leading: Dimension = .pt(0), bottom: Dimension = .pt(0), trailing: Dimension = .pt(0)) { 19 | self.top = top 20 | self.leading = leading 21 | self.bottom = bottom 22 | self.trailing = trailing 23 | } 24 | 25 | public init(top: Dimension) { 26 | self.top = top 27 | leading = .pt(0) 28 | bottom = .pt(0) 29 | trailing = .pt(0) 30 | } 31 | 32 | public init(leading: Dimension) { 33 | top = .pt(0) 34 | self.leading = leading 35 | bottom = .pt(0) 36 | trailing = .pt(0) 37 | } 38 | 39 | public init(bottom: Dimension) { 40 | top = .pt(0) 41 | leading = .pt(0) 42 | self.bottom = bottom 43 | trailing = .pt(0) 44 | } 45 | 46 | public init(trailing: Dimension) { 47 | top = .pt(0) 48 | leading = .pt(0) 49 | bottom = .pt(0) 50 | self.trailing = trailing 51 | } 52 | 53 | public init(_ value: Dimension) { 54 | top = value 55 | leading = value 56 | bottom = value 57 | trailing = value 58 | } 59 | 60 | public static func pt(_ value: CGFloat) -> EdgeInsets { 61 | .init(top: .pt(value), leading: .pt(value), bottom: .pt(value), trailing: .pt(value)) 62 | } 63 | 64 | public static func `in`(_ value: CGFloat) -> EdgeInsets { 65 | .init(top: .in(value), leading: .in(value), bottom: .in(value), trailing: .in(value)) 66 | } 67 | 68 | public static func mm(_ value: CGFloat) -> EdgeInsets { 69 | .init(top: .mm(value), leading: .mm(value), bottom: .mm(value), trailing: .mm(value)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/FontName.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing a font name. 10 | /// 11 | /// FontName allows for users to define extensions that can be 12 | /// used for code completion without littering the String namespace: 13 | /// 14 | /// extension FontName { 15 | /// static var helveitcaRegular: FontName {"Helvetica"} 16 | /// static var helveticaLight: FontName {"Helvetica-Light"} 17 | /// } 18 | /// 19 | /// struct MyBlock: Block { 20 | /// var body: some Block { 21 | /// Text("Hello") 22 | /// .font(name: .helveticaRegular) 23 | /// } 24 | /// } 25 | /// 26 | public struct FontName: ExpressibleByStringLiteral { 27 | public let value: String 28 | public init(stringLiteral: String) { 29 | value = stringLiteral 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Gradient.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct Gradient { 10 | public struct Stop { 11 | /// The color for the stop. 12 | public var color: Color 13 | 14 | /// The parametric location of the stop. 15 | /// 16 | /// This value must be in the range `[0, 1]`. 17 | public var location: CGFloat 18 | 19 | /// Creates a color stop with a color and location. 20 | public init(color: Color, location: CGFloat) { 21 | self.color = color 22 | self.location = location 23 | } 24 | } 25 | 26 | public var stops: [Gradient.Stop] 27 | 28 | public init(colors: [Color]) { 29 | let denominator = max(1, colors.count - 1) 30 | stops = colors.enumerated() 31 | .map { Stop(color: $0.element, location: Double($0.offset) / Double(denominator)) } 32 | } 33 | 34 | public init(stops: [Gradient.Stop]) { 35 | self.stops = stops 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/PageNumberProxy.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | public struct PageNumberProxy { 10 | public let pageNo: Int 11 | public let pageCount: Int 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/PageSize.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing page size. 10 | public struct PageSize: Equatable { 11 | public let width: Dimension 12 | public let height: Dimension 13 | 14 | public init(width: Dimension, height: Dimension) { 15 | self.width = width 16 | self.height = height 17 | } 18 | } 19 | 20 | public extension PageSize { 21 | static var letter: Self { 22 | .init(width: .in(8.5), height: .in(11)) 23 | } 24 | 25 | static var legal: Self { 26 | .init(width: .in(8.5), height: .in(14)) 27 | } 28 | 29 | static var tabloid: Self { 30 | .init(width: .in(11), height: .in(17)) 31 | } 32 | 33 | static var a4: Self { 34 | .init(width: .mm(210), height: .mm(297)) 35 | } 36 | 37 | static var a5: Self { 38 | .init(width: .mm(148), height: .mm(210)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/Size.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing 2D size. 10 | public struct Size { 11 | public let width: Dimension 12 | public let height: Dimension 13 | 14 | public init(width: Dimension, height: Dimension) { 15 | self.width = width 16 | self.height = height 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/StackSpacing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing the spacing between elements 10 | /// in a stack. 11 | public enum StackSpacing { 12 | case flex(min: Dimension) 13 | case fixed(Dimension) 14 | } 15 | 16 | public extension StackSpacing { 17 | static var none: StackSpacing { 18 | .fixed(.pt(0)) 19 | } 20 | 21 | static var flex: StackSpacing { 22 | .flex(min: .pt(0)) 23 | } 24 | 25 | static func pt(_ value: CGFloat) -> StackSpacing { 26 | .fixed(.pt(value)) 27 | } 28 | 29 | static func `in`(_ value: CGFloat) -> StackSpacing { 30 | .fixed(.in(value)) 31 | } 32 | 33 | static func mm(_ value: CGFloat) -> StackSpacing { 34 | .fixed(.mm(value)) 35 | } 36 | } 37 | 38 | extension StackSpacing { 39 | var isFlexible: Bool { 40 | if case .flex = self { 41 | true 42 | } else { 43 | false 44 | } 45 | } 46 | } 47 | 48 | extension StackSpacing: ExpressibleByIntegerLiteral { 49 | public init(integerLiteral value: IntegerLiteralType) { 50 | self = .fixed(.pt(CGFloat(value))) 51 | } 52 | } 53 | 54 | extension StackSpacing: ExpressibleByFloatLiteral { 55 | public init(floatLiteral value: FloatLiteralType) { 56 | self = .fixed(.pt(value)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/TextAlignment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A type for expressing text alignment. 10 | public enum TextAlignment { 11 | case leading 12 | case center 13 | case trailing 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/API/Types/TextTruncationMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// The type of truncation to apply to a line of text when it's too long to 10 | /// fit in the available width. 11 | /// 12 | /// NOTE: AT PRESENT, ONLY TAIL TRUNCATION IS SUPPORTED. 13 | /// 14 | /// When a text block contains more text than it's able to display, the block 15 | /// might truncate the text and place an ellipsis (...) at the truncation 16 | /// point. Use the ``Block/truncationMode(_:)`` modifier with one of the 17 | /// `TruncationMode` values to indicate which part of the text to 18 | /// truncate, either at the beginning, in the middle, or at the end. 19 | public enum TextTruncationMode: Sendable { 20 | /// Truncate at the beginning of the line. 21 | /// 22 | /// Use this kind of truncation to omit characters from the beginning of 23 | /// the string. For example, you could truncate the English alphabet as 24 | /// "...wxyz". 25 | // case head 26 | 27 | /// Truncate at the end of the line. 28 | /// 29 | /// Use this kind of truncation to omit characters from the end of the 30 | /// string. For example, you could truncate the English alphabet as 31 | /// "abcd...". 32 | case none 33 | case tail 34 | case wrap 35 | 36 | /// Truncate in the middle of the line. 37 | /// 38 | /// Use this kind of truncation to omit characters from the middle of 39 | /// the string. For example, you could truncate the English alphabet as 40 | /// "ab...yz". 41 | // case middle 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/AnyBlock+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension AnyBlock: Renderable { 10 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 11 | content.getRenderable(environment: environment) 12 | .getTrait(context: context, environment: environment, keypath: keypath) 13 | } 14 | 15 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 16 | content.getRenderable(environment: environment) 17 | .remainder(context: context, environment: environment, size: size) 18 | } 19 | 20 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 21 | let block = content.getRenderable(environment: environment) 22 | return block.sizeFor(context: context, environment: environment, proposal: proposal) 23 | } 24 | 25 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 26 | let block = content.getRenderable(environment: environment) 27 | if let remainder = block.render(context: context, environment: environment, rect: rect) { 28 | return AnyBlock(remainder) 29 | } else { 30 | return nil 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ArrayBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct ArrayBlock: Block { 10 | let blocks: [any Block] 11 | } 12 | 13 | extension ArrayBlock: GroupBlock {} 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/AspectRatio.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct AspectRatio: Block where Content: Block { 10 | let value: CGFloat 11 | let content: Content 12 | } 13 | 14 | extension AspectRatio: Renderable { 15 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 16 | content.getRenderable(environment: environment) 17 | .getTrait(context: context, environment: environment, keypath: keypath) 18 | } 19 | 20 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 21 | content.getRenderable(environment: environment) 22 | .remainder(context: context, environment: environment, size: size) 23 | } 24 | 25 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 26 | let block = content.getRenderable(environment: environment) 27 | let size = block.sizeFor(context: context, environment: environment, proposal: proposal) 28 | if size.min == size.max { 29 | return size 30 | } else { 31 | let constrainedSize = if proposal.width < proposal.height { 32 | CGSize(width: proposal.width, height: proposal.width / value) 33 | } else { 34 | CGSize(width: proposal.height * value, height: proposal.height) 35 | } 36 | return BlockSize(min: size.min, max: constrainedSize) 37 | } 38 | } 39 | 40 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 41 | let remainder = content.getRenderable(environment: environment) 42 | .render(context: context, environment: environment, rect: rect) 43 | if let content = remainder as? AnyBlock { 44 | return AspectRatio(value: value, content: content) 45 | } else { 46 | return nil 47 | } 48 | } 49 | } 50 | 51 | struct AspectRatioModifier: BlockModifier { 52 | let value: CGFloat 53 | 54 | func body(content: Content) -> some Block { 55 | AspectRatio(value: value, content: content) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Border.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct Border: Block where Content: Block { 10 | let shapeStyle: ShapeStyle 11 | let width: Dimension 12 | let content: Content 13 | } 14 | 15 | extension Border: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | content.getRenderable(environment: environment) 18 | .getTrait(context: context, environment: environment, keypath: keypath) 19 | } 20 | 21 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 22 | content.getRenderable(environment: environment) 23 | .remainder(context: context, environment: environment, size: size) 24 | } 25 | 26 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 27 | content.getRenderable(environment: environment) 28 | .sizeFor(context: context, environment: environment, proposal: proposal) 29 | } 30 | 31 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 32 | let block = content.getRenderable(environment: environment) 33 | let remainder = block.render(context: context, environment: environment, rect: rect) 34 | context.renderer.renderBorder(environment: environment, rect: rect, shapeStyle: shapeStyle, width: width.points) 35 | if let content = remainder as? AnyBlock { 36 | return Border(shapeStyle: shapeStyle, width: width, content: content) 37 | } else { 38 | return nil 39 | } 40 | } 41 | } 42 | 43 | struct BorderModifier: BlockModifier { 44 | let shapeStyle: ShapeStyle 45 | let width: Dimension 46 | 47 | func body(content: Content) -> some Block { 48 | Border(shapeStyle: shapeStyle, width: width, content: content) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ClipRegion.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct ClipRegion: Block where Content: Block { 10 | let content: Content 11 | } 12 | 13 | extension ClipRegion: Renderable { 14 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 15 | content.getRenderable(environment: environment) 16 | .getTrait(context: context, environment: environment, keypath: keypath) 17 | } 18 | 19 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 20 | content.getRenderable(environment: environment) 21 | .remainder(context: context, environment: environment, size: size) 22 | } 23 | 24 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 25 | content.getRenderable(environment: environment) 26 | .sizeFor(context: context, environment: environment, proposal: proposal) 27 | } 28 | 29 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 30 | context.renderer.starClipRegion(rect: rect) 31 | let remainder = content.getRenderable(environment: environment) 32 | .render(context: context, environment: environment, rect: rect) 33 | context.renderer.endClipRegion() 34 | if let remainder = remainder as? AnyBlock { 35 | return ClipRegion(content: remainder) 36 | } else { 37 | return nil 38 | } 39 | } 40 | } 41 | 42 | struct ClipRegionModifier: BlockModifier { 43 | func body(content: Content) -> some Block { 44 | ClipRegion(content: content) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Divider+Renderable.swift: -------------------------------------------------------------------------------- 1 | /// ** 2 | // * PDF Blocks 3 | // * Copyright (c) David Yowell 2024 4 | // * MIT license, see LICENSE file for details 5 | // */ 6 | // 7 | // import Foundation 8 | // 9 | // extension Divider: Renderable { 10 | // func sizeFor(context _: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 11 | // if environment.layoutAxis == .horizontal { 12 | // BlockSize(width: thickness.points + padding.points * 2, height: proposal.height) 13 | // } else { 14 | // BlockSize(width: proposal.width, height: thickness.points + padding.points * 2) 15 | // } 16 | // } 17 | // 18 | // func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 19 | // if environment.layoutAxis == .horizontal { 20 | // let rect = CGRect(x: rect.minX + padding.points, y: rect.minY, width: rect.width - padding.points * 2, height: rect.height) 21 | // context.renderer.renderLine(dash: [], environment: environment, rect: rect) 22 | // } else { 23 | // let rect = CGRect(x: rect.minX, y: rect.minY + padding.points, width: rect.width, height: rect.height - padding.points * 2) 24 | // context.renderer.renderLine(dash: [], environment: environment, rect: rect) 25 | // } 26 | // return nil 27 | // } 28 | // } 29 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/EitherBlock+Renderable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EitherBlock+Renderable.swift 3 | // 4 | // 5 | // Created by David Yowell on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension EitherBlock: Renderable { 11 | // TODO: Trait, Remainder, etc. 12 | 13 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 14 | switch value { 15 | case let .trueContent(content): 16 | let node = content.getRenderable(environment: environment) 17 | return node.sizeFor(context: context, environment: environment, proposal: proposal) 18 | case let .falseContent(content): 19 | let node = content.getRenderable(environment: environment) 20 | return node.sizeFor(context: context, environment: environment, proposal: proposal) 21 | } 22 | } 23 | 24 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 25 | switch value { 26 | case let .trueContent(content): 27 | let node = content.getRenderable(environment: environment) 28 | return node.render(context: context, environment: environment, rect: rect) 29 | case let .falseContent(content): 30 | let node = content.getRenderable(environment: environment) 31 | return node.render(context: context, environment: environment, rect: rect) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/EmptyBlock+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension EmptyBlock: Renderable { 10 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal _: Proposal) -> BlockSize { 11 | .init(min: .zero, max: .zero) 12 | } 13 | 14 | func render(context _: Context, environment _: EnvironmentValues, rect _: CGRect) -> (any Renderable)? { 15 | nil 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/EnvironmentBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct EnvironmentBlock: Block where Content: Block { 10 | let keyPath: WritableKeyPath 11 | let value: V 12 | let content: Content 13 | } 14 | 15 | extension EnvironmentBlock: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> X { 17 | var environment = environment 18 | environment[keyPath: keyPath] = value 19 | let block = content.getRenderable(environment: environment) 20 | return block.getTrait(context: context, environment: environment, keypath: keypath) 21 | } 22 | 23 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 24 | var environment = environment 25 | environment[keyPath: keyPath] = value 26 | let block = content.getRenderable(environment: environment) 27 | if let remainder = block.remainder(context: context, environment: environment, size: size) { 28 | return EnvironmentBlock(keyPath: keyPath, value: value, content: AnyBlock(remainder)) 29 | } else { 30 | return nil 31 | } 32 | } 33 | 34 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 35 | var environment = environment 36 | environment[keyPath: keyPath] = value 37 | let block = content.getRenderable(environment: environment) 38 | return block.sizeFor(context: context, environment: environment, proposal: proposal) 39 | } 40 | 41 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 42 | var environment = environment 43 | environment[keyPath: keyPath] = value 44 | let block = content.getRenderable(environment: environment) 45 | let remainder = block.render(context: context, environment: environment, rect: rect) 46 | if let content = remainder as? AnyBlock { 47 | return EnvironmentBlock(keyPath: keyPath, value: value, content: content) 48 | } else { 49 | return nil 50 | } 51 | } 52 | } 53 | 54 | struct EnvironmentModifier: BlockModifier { 55 | let keyPath: WritableKeyPath 56 | let value: V 57 | 58 | func body(content: Content) -> some Block { 59 | EnvironmentBlock(keyPath: keyPath, value: value, content: content) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ForEach+GroupBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension ForEach: GroupBlock { 10 | var blocks: [any Block] { 11 | data.map { content($0) } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Group+GroupBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Group: GroupBlock { 10 | var blocks: [any Block] { 11 | if let cast = content as? GroupBlock { 12 | cast.blocks 13 | } else { 14 | [content] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Image+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import AVFoundation 8 | import Foundation 9 | 10 | extension Image: Renderable { 11 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal: Proposal) -> BlockSize { 12 | let rect = AVMakeRect(aspectRatio: image.size, insideRect: .init(origin: .zero, size: proposal)) 13 | return .init(min: .zero, max: rect.size) 14 | } 15 | 16 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 17 | let rect = AVMakeRect(aspectRatio: image.size, insideRect: .init(origin: rect.origin, size: rect.size)) 18 | context.renderer.renderImage(image, environment: environment, rect: rect) 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Line+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | #if os(macOS) 9 | import AppKit 10 | #endif 11 | #if os(iOS) 12 | import UIKit 13 | #endif 14 | 15 | extension Line: Renderable { 16 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal: Proposal) -> BlockSize { 17 | BlockSize(min: .zero, max: proposal) 18 | } 19 | 20 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 21 | let path = CGMutablePath() 22 | let startPoint = CGPoint(x: rect.minX + rect.width * start.x, 23 | y: rect.minY + rect.height * start.y) 24 | let endPoint = CGPoint(x: rect.minX + rect.width * end.x, 25 | y: rect.minY + rect.height * end.y) 26 | path.addLines(between: [startPoint, endPoint]) 27 | context.renderer.renderLine(environment: environment, path: path) 28 | return nil 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ModifiedContent.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension ModifiedContent: Renderable where Content: Block, Modifier: BlockModifier { 10 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 11 | let nmc = _BlockModifier_Content(modifier: modifier, block: content) 12 | let modifiedContent = modifier.body(content: nmc) 13 | let block = modifiedContent.getRenderable(environment: environment) 14 | return block.getTrait(context: context, environment: environment, keypath: keypath) 15 | } 16 | 17 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 18 | let nmc = _BlockModifier_Content(modifier: modifier, block: content) 19 | let modifiedContent = modifier.body(content: nmc) 20 | let block = modifiedContent.getRenderable(environment: environment) 21 | if let remainder = block.remainder(context: context, environment: environment, size: size) { 22 | return ModifiedContent(content: AnyBlock(remainder), modifier: modifier) 23 | } else { 24 | return nil 25 | } 26 | } 27 | 28 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 29 | let nmc = _BlockModifier_Content(modifier: modifier, block: content) 30 | let modifiedContent = modifier.body(content: nmc) 31 | let block = modifiedContent.getRenderable(environment: environment) 32 | return block.sizeFor(context: context, environment: environment, proposal: proposal) 33 | } 34 | 35 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 36 | let nmc = _BlockModifier_Content(modifier: modifier, block: content) 37 | let modifiedContent = modifier.body(content: nmc) 38 | let block = modifiedContent.getRenderable(environment: environment) 39 | return block.render(context: context, environment: environment, rect: rect) 40 | } 41 | } 42 | 43 | extension ModifiedContent: GroupBlock where Content: GroupBlock, Modifier: BlockModifier { 44 | var blocks: [any Block] { 45 | content.flattenedBlocks().map { AnyBlock($0).modifier(modifier) } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Offset.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct Offset: Block where Content: Block { 10 | let x: Dimension 11 | let y: Dimension 12 | let content: Content 13 | } 14 | 15 | extension Offset: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | content.getRenderable(environment: environment) 18 | .getTrait(context: context, environment: environment, keypath: keypath) 19 | } 20 | 21 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 22 | content.getRenderable(environment: environment) 23 | .remainder(context: context, environment: environment, size: size) 24 | } 25 | 26 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 27 | content.getRenderable(environment: environment) 28 | .sizeFor(context: context, environment: environment, proposal: proposal) 29 | } 30 | 31 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 32 | let block = content.getRenderable(environment: environment) 33 | context.renderer.startOffset(x: x.points, y: y.points) 34 | let remainder = block.render(context: context, environment: environment, rect: rect) 35 | context.renderer.restoreState() 36 | if let content = remainder as? AnyBlock { 37 | return Offset(x: x, y: y, content: content) 38 | } else { 39 | return nil 40 | } 41 | } 42 | } 43 | 44 | struct OffsetModifier: BlockModifier { 45 | let x: Dimension 46 | let y: Dimension 47 | 48 | func body(content: Content) -> some Block { 49 | Offset(x: x, y: y, content: content) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/OnRender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct OnRender: Block where Content: Block { 10 | let onRender: () -> Void 11 | let content: Content 12 | } 13 | 14 | extension OnRender: Renderable { 15 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 16 | content.getRenderable(environment: environment) 17 | .getTrait(context: context, environment: environment, keypath: keypath) 18 | } 19 | 20 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 21 | content.getRenderable(environment: environment) 22 | .remainder(context: context, environment: environment, size: size) 23 | } 24 | 25 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 26 | let block = content.getRenderable(environment: environment) 27 | return block.sizeFor(context: context, environment: environment, proposal: proposal) 28 | } 29 | 30 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 31 | let block = content.getRenderable(environment: environment) 32 | let remainder = block.render(context: context, environment: environment, rect: rect) 33 | onRender() 34 | if let content = remainder as? AnyBlock { 35 | return OnRender(onRender: onRender, content: content) 36 | } else { 37 | return nil 38 | } 39 | } 40 | } 41 | 42 | struct OnRenderModifier: BlockModifier { 43 | let onRender: () -> Void 44 | 45 | func body(content: Content) -> some Block { 46 | OnRender(onRender: onRender, content: content) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Opacity.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct Opacity: Block where Content: Block { 10 | let opacity: CGFloat 11 | let content: Content 12 | } 13 | 14 | extension Opacity: Renderable { 15 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 16 | content.getRenderable(environment: environment) 17 | .getTrait(context: context, environment: environment, keypath: keypath) 18 | } 19 | 20 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 21 | content.getRenderable(environment: environment) 22 | .remainder(context: context, environment: environment, size: size) 23 | } 24 | 25 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 26 | content.getRenderable(environment: environment) 27 | .sizeFor(context: context, environment: environment, proposal: proposal) 28 | } 29 | 30 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 31 | let block = content.getRenderable(environment: environment) 32 | context.renderer.startOpacity(opacity: opacity) 33 | let remainder = block.render(context: context, environment: environment, rect: rect) 34 | context.renderer.restoreOpacity() 35 | if let remainder = remainder as? AnyBlock { 36 | return Opacity(opacity: opacity, content: remainder) 37 | } else { 38 | return nil 39 | } 40 | } 41 | } 42 | 43 | struct OpacityModifier: BlockModifier { 44 | let opacity: CGFloat 45 | 46 | func body(content: Content) -> some Block { 47 | Opacity(opacity: opacity, content: content) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Optional+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Optional: Renderable where Wrapped: Block { 10 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 11 | if let self { 12 | self.getRenderable(environment: environment) 13 | .getTrait(context: context, environment: environment, keypath: keypath) 14 | } else { 15 | Trait()[keyPath: keypath] 16 | } 17 | } 18 | 19 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 20 | if let self { 21 | let block = self.getRenderable(environment: environment) 22 | return block.sizeFor(context: context, environment: environment, proposal: proposal) 23 | } else { 24 | return .init(min: .zero, max: .zero) 25 | } 26 | } 27 | 28 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 29 | if let self { 30 | let block = self.getRenderable(environment: environment) 31 | return block.render(context: context, environment: environment, rect: rect) 32 | } else { 33 | return nil 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Page+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // The rendering context wraps Page with RealPage. Page provides rendering context witha PageInfo and it applies 10 | // margins for its content. 11 | // 12 | // By design: 13 | // .background or .overlay on page will be full bleed and not within the margins 14 | // .padding on a page will increase the pages margins 15 | extension Page: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | if keypath == \.computePageCount { 18 | let blocks = content.getRenderables(environment: environment) 19 | let result = blocks.reduce(false) { $0 || $1.computePageCount(context: context, environment: environment) } 20 | return Trait(computePageCount: result)[keyPath: keypath] 21 | } else { 22 | return Trait(pageInfo: pageInfo)[keyPath: keypath] 23 | } 24 | } 25 | 26 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal: Proposal) -> BlockSize { 27 | BlockSize(proposal) 28 | } 29 | 30 | @discardableResult func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 31 | let marginRect = CGRect(x: rect.minX + pageInfo.margins.leading.points, 32 | y: rect.minY + pageInfo.margins.top.points, 33 | width: rect.width - pageInfo.margins.leading.points - pageInfo.margins.trailing.points, 34 | height: rect.height - pageInfo.margins.top.points - pageInfo.margins.bottom.points) 35 | let block = content.getRenderable(environment: environment) 36 | let size = block.sizeFor(context: context, environment: environment, proposal: marginRect.size).max 37 | let dx: CGFloat = (marginRect.width - size.width) / 2.0 38 | let dy: CGFloat = (marginRect.height - size.height) / 2.0 39 | let renderRect = CGRect(origin: marginRect.origin.offset(dx: dx, dy: dy), size: size) 40 | block.render(context: context, environment: environment, rect: renderRect) 41 | return nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/PageNumberReader+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension PageNumberReader: Renderable { 10 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 11 | if keypath == \Trait.computePageCount { 12 | Trait(computePageCount: computePageCount)[keyPath: keypath] 13 | } else { 14 | content(context.pageNumberProxy).getRenderable(environment: environment) 15 | .getTrait(context: context, environment: environment, keypath: keypath) 16 | } 17 | } 18 | 19 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 20 | content(context.pageNumberProxy) 21 | .getRenderable(environment: environment) 22 | .sizeFor(context: context, environment: environment, proposal: proposal) 23 | } 24 | 25 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 26 | let remainder = content(context.pageNumberProxy) 27 | .getRenderable(environment: environment) 28 | .render(context: context, environment: environment, rect: rect) 29 | if let content = remainder as? Content { 30 | let function: (PageNumberProxy) -> Content = { _ in 31 | content 32 | } 33 | return PageNumberReader(computePageCount: computePageCount, content: function) 34 | } else { 35 | return nil 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ProportionalFrame.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct ProporionalFrame: Block where Content: Block { 10 | var width: Double 11 | let horizontalAlignment: HorizontalAlignment 12 | let content: Content 13 | } 14 | 15 | extension ProporionalFrame: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | if keypath == \.proprtionalWidth { 18 | Trait(proprtionalWidth: width)[keyPath: keypath] 19 | } else { 20 | content.getRenderable(environment: environment) 21 | .getTrait(context: context, environment: environment, keypath: keypath) 22 | } 23 | } 24 | 25 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 26 | content.getRenderable(environment: environment) 27 | .remainder(context: context, environment: environment, size: size) 28 | } 29 | 30 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 31 | content.getRenderable(environment: environment) 32 | .sizeFor(context: context, environment: environment, proposal: proposal) 33 | } 34 | 35 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 36 | // TODO: Review .isSeoncaryPageWrapBlock. Review remainder 37 | let renderable = content.getRenderable(environment: environment) 38 | if renderable.isSecondaryPageWrapBlock(context: context, environment: environment) { 39 | renderable.render(context: context, environment: environment, rect: rect) 40 | } else { 41 | let size = renderable.sizeFor(context: context, environment: environment, proposal: rect.size).max 42 | let dx: CGFloat = switch horizontalAlignment { 43 | case .leading: 44 | 0 45 | case .center: 46 | (rect.width - size.width) / 2 47 | case .trailing: 48 | rect.width - size.width 49 | } 50 | let renderRect = CGRect(x: rect.minX + dx, y: rect.minY, width: size.width, height: rect.height) 51 | renderable.render(context: context, environment: environment, rect: renderRect) 52 | } 53 | return nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/RenderableShape.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct RenderableShape: Renderable { 10 | let shape: any Shape 11 | 12 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal: Proposal) -> BlockSize { 13 | BlockSize(min: .zero, max: shape.sizeThatFits(proposal), maxWidth: true, maxHeight: true) 14 | } 15 | 16 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 17 | let zeroOrignRect = CGRect(origin: .zero, size: rect.size) 18 | let path = shape.path(in: zeroOrignRect).cgPath 19 | var transform = CGAffineTransform(translationX: rect.minX, y: rect.minY) 20 | if let offsetPath = path.copy(using: &transform) { 21 | context.renderer.renderPath(environment: environment, path: offsetPath) 22 | } 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Repeat+GroupBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Repeat: GroupBlock { 10 | var blocks: [any Block] { 11 | Array(repeating: content, count: count) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/RotationEffect.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct RotationEffect: Block where Content: Block { 10 | let angle: Angle 11 | let anchor: UnitPoint 12 | let content: Content 13 | } 14 | 15 | extension RotationEffect: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | content.getRenderable(environment: environment) 18 | .getTrait(context: context, environment: environment, keypath: keypath) 19 | } 20 | 21 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 22 | content.getRenderable(environment: environment) 23 | .remainder(context: context, environment: environment, size: size) 24 | } 25 | 26 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 27 | content.getRenderable(environment: environment) 28 | .sizeFor(context: context, environment: environment, proposal: proposal) 29 | } 30 | 31 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 32 | let block = content.getRenderable(environment: environment) 33 | context.renderer.startRotation(angle: angle.radians, anchor: anchor, rect: rect) 34 | let remainder = block.render(context: context, environment: environment, rect: rect) 35 | context.renderer.restoreState() 36 | if let content = remainder as? AnyBlock { 37 | return RotationEffect(angle: angle, anchor: anchor, content: content) 38 | } else { 39 | return nil 40 | } 41 | } 42 | } 43 | 44 | struct RotationModifier: BlockModifier { 45 | let angle: Angle 46 | let anchor: UnitPoint 47 | 48 | func body(content: Content) -> some Block { 49 | RotationEffect(angle: angle, anchor: anchor, content: content) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ScaleEffect.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct ScaleEffect: Block where Content: Block { 10 | let scale: CGSize 11 | let anchor: UnitPoint 12 | let content: Content 13 | } 14 | 15 | extension ScaleEffect: Renderable { 16 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 17 | content.getRenderable(environment: environment) 18 | .getTrait(context: context, environment: environment, keypath: keypath) 19 | } 20 | 21 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 22 | content.getRenderable(environment: environment) 23 | .remainder(context: context, environment: environment, size: size) 24 | } 25 | 26 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 27 | content.getRenderable(environment: environment) 28 | .sizeFor(context: context, environment: environment, proposal: proposal) 29 | } 30 | 31 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 32 | let block = content.getRenderable(environment: environment) 33 | context.renderer.startScale(scale: scale, anchor: anchor, rect: rect) 34 | let remainder = block.render(context: context, environment: environment, rect: rect) 35 | context.renderer.restoreState() 36 | if let content = remainder as? AnyBlock { 37 | return ScaleEffect(scale: scale, anchor: anchor, content: content) 38 | } else { 39 | return nil 40 | } 41 | } 42 | } 43 | 44 | struct ScaleModifier: BlockModifier { 45 | let scale: CGSize 46 | let anchor: UnitPoint 47 | 48 | func body(content: Content) -> some Block { 49 | ScaleEffect(scale: scale, anchor: anchor, content: content) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Spacer+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // TODO: Add layout priority support. In SwiftUI, spacer gets a lower priority than Color. 10 | extension Spacer: Renderable { 11 | func sizeFor(context _: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 12 | switch environment.layoutAxis { 13 | case .vertical: 14 | switch value { 15 | case let .fixed(length): 16 | BlockSize(min: .init(width: 0, height: length.points), 17 | max: .init(width: 0, height: length.points)) 18 | case let .min(length): 19 | BlockSize(min: .init(width: 0, height: length.points), 20 | max: .init(width: 0, height: proposal.height), maxWidth: false, maxHeight: true) 21 | } 22 | case .horizontal: 23 | switch value { 24 | case let .fixed(length): 25 | BlockSize(min: .init(width: length.points, height: 0), 26 | max: .init(width: length.points, height: 0)) 27 | case let .min(length): 28 | BlockSize(min: .init(width: length.points, height: 0), 29 | max: .init(width: proposal.width, height: 0), maxWidth: true, maxHeight: false) 30 | } 31 | case .undefined: 32 | BlockSize(width: 0, height: 0) 33 | } 34 | } 35 | 36 | func render(context _: Context, environment _: EnvironmentValues, rect _: CGRect) -> (any Renderable)? { 37 | nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/TableContentSpacer+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension TableContentSpacer: Renderable { 10 | func sizeFor(context _: Context, environment _: EnvironmentValues, proposal: Proposal) -> BlockSize { 11 | BlockSize(proposal) 12 | } 13 | 14 | func render(context: Context, environment _: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 15 | context.setPageWrapRect(rect) 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/TableContentSpacer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A block for use within a Table pageFrame that will reserve space 10 | /// for the Table content. 11 | struct TableContentSpacer: Block { 12 | public init() {} 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/Text+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Text: Renderable { 10 | func remainder(context: Context, environment: EnvironmentValues, size: CGSize) -> (any Renderable)? { 11 | let remainder = context.renderer.textRemainder(value, environment: environment, rect: .init(origin: .zero, size: size)) 12 | if remainder.length > 0, environment.truncationMode == .wrap { 13 | return Text(remainder) 14 | } else { 15 | return nil 16 | } 17 | } 18 | 19 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 20 | if rect.width == 0 { 21 | return nil 22 | } 23 | let remainder = context.renderer.renderText(value, environment: environment, rect: rect) 24 | if remainder.length > 0, environment.truncationMode == .wrap { 25 | return Text(remainder) 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 32 | if proposal.width == 0 { 33 | BlockSize(CGSize(width: 0, height: proposal.height)) 34 | } else { 35 | BlockSize(context.renderer.sizeForText(value, environment: environment, proposal: proposal)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/TupleBlock+GroupBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension TupleBlock: GroupBlock { 10 | var blocks: [any Block] { 11 | _blocks 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Blocks/ZStack+Renderable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension ZStack: Renderable { 10 | func getTrait(context: Context, environment: EnvironmentValues, keypath: KeyPath) -> Value { 11 | if keypath == \.computePageCount { 12 | let blocks = content.getRenderables(environment: environment) 13 | let result = blocks.reduce(false) { $0 || $1.computePageCount(context: context, environment: environment) } 14 | return Trait(computePageCount: result)[keyPath: keypath] 15 | } else { 16 | return Trait()[keyPath: keypath] 17 | } 18 | } 19 | 20 | // TODO: Review minSize 21 | func sizeFor(context: Context, environment: EnvironmentValues, proposal: Proposal) -> BlockSize { 22 | let blocks = content.getRenderables(environment: environment) 23 | let sizes = blocks.map { $0.sizeFor(context: context, environment: environment, proposal: proposal) } 24 | let minWidth = sizes.map(\.min.width).reduce(0, max) 25 | let minHeight = sizes.map(\.min.height).reduce(0, max) 26 | let maxWidth = sizes.map(\.max.width).reduce(0, max) 27 | let maxHeight = sizes.map(\.max.height).reduce(0, max) 28 | return .init(min: .init(width: minWidth, height: minHeight), 29 | max: .init(width: maxWidth, height: maxHeight)) 30 | } 31 | 32 | func render(context: Context, environment: EnvironmentValues, rect: CGRect) -> (any Renderable)? { 33 | for block in content.getRenderables(environment: environment) { 34 | let size = block.sizeFor(context: context, environment: environment, proposal: rect.size) 35 | let dx: CGFloat = 36 | switch alignment.horizontalAlignment { 37 | case .leading: 38 | 0 39 | case .center: 40 | (rect.width - size.max.width) / 2.0 41 | case .trailing: 42 | rect.width - size.max.width 43 | } 44 | let dy: CGFloat = 45 | switch alignment.verticalAlignment { 46 | case .top: 47 | 0 48 | case .center: 49 | (rect.height - size.max.height) / 2.0 50 | case .bottom: 51 | rect.height - size.max.height 52 | } 53 | let renderRect = CGRect(origin: rect.origin.offset(dx: dx, dy: dy), size: size.max) 54 | block.render(context: context, environment: environment, rect: renderRect) 55 | } 56 | return nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Core/Block+AppendToArray.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Block { 10 | // Used by TupleBlock to extract the contents of a variadic generic parameter to an array. 11 | func appendToArray(_ array: inout [any Block]) { 12 | array.append(self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Core/EnvironmentProperty.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // This protocol is the mechanism by which a block's @Environment property is 10 | // updated. With the use of Mirror, a block's properties are scanned to see 11 | // which conform to EnvironmentProperty and .update(:) is called on the 12 | // conforming properties. 13 | protocol EnvironmentProperty { 14 | func update(_ value: EnvironmentValues) 15 | } 16 | 17 | extension Environment: EnvironmentProperty { 18 | func update(_ value: EnvironmentValues) { 19 | environment = value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Core/GroupBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // GroupBlocks are a special class of blocks that do not render themselves, but instead supply 10 | // their content as an array of blocks for other blocks such as VStack or HStack to layout and 11 | // render. 12 | 13 | protocol GroupBlock { 14 | var blocks: [any Block] { get } 15 | } 16 | 17 | extension GroupBlock { 18 | public var body: Never { 19 | fatalError() 20 | } 21 | } 22 | 23 | extension GroupBlock { 24 | // Returns a recursively flattened array of a GroupBlock's blocks. Blocks #1 and #2 25 | // are senamtically the same. 26 | 27 | // #1 28 | // Group { 29 | // Text("1") 30 | // Group { 31 | // Text("2") 32 | // Text("3") 33 | // } 34 | // } 35 | 36 | // #2 37 | // Group { 38 | // Text("1") 39 | // Text("2") 40 | // Text("3") 41 | // } 42 | func flattenedBlocks() -> [any Block] { 43 | blocks.reduce([]) { partialResult, block in 44 | if let cast = block as? GroupBlock { 45 | partialResult + cast.flattenedBlocks() 46 | } else { 47 | partialResult + [block] 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Core/Renderer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import CoreGraphics 8 | import Foundation 9 | 10 | // A protocol that allows for different PDF rendering libraries to be used. 11 | protocol Renderer { 12 | func setLayer(_ value: Int) 13 | func setLayerFilter(_ value: Int) 14 | func starClipRegion(rect: CGRect) 15 | func endClipRegion() 16 | func render(renderingCallback: () -> Void) throws -> Data? 17 | func startNewPage(pageSize: CGSize) 18 | func endPage() 19 | func startOffset(x: CGFloat, y: CGFloat) 20 | func startScale(scale: CGSize, anchor: UnitPoint, rect: CGRect) 21 | func startRotation(angle: CGFloat, anchor: UnitPoint, rect: CGRect) 22 | func startOpacity(opacity: CGFloat) 23 | func restoreOpacity() 24 | func restoreState() 25 | func renderBorder(environment: EnvironmentValues, rect: CGRect, shapeStyle: ShapeStyle, width: CGFloat) 26 | func renderLine(environment: EnvironmentValues, path: CGPath) 27 | func renderPath(environment: EnvironmentValues, path: CGPath) 28 | func renderImage(_ image: PlatformImage, environment: EnvironmentValues, rect: CGRect) 29 | func textRemainder(_ text: NSAttributedString, environment: EnvironmentValues, rect: CGRect) -> NSAttributedString 30 | func renderText(_ text: NSAttributedString, environment: EnvironmentValues, rect: CGRect) -> NSAttributedString 31 | func sizeForText(_ text: NSAttributedString, environment: EnvironmentValues, proposal: CGSize) -> CGSize 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Core/Trait.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | struct Trait { 8 | var proprtionalWidth: Double? 9 | var layoutPriority: Int = 0 10 | var wrapContents: Bool = false 11 | var pageInfo: PageInfo? 12 | var computePageCount: Bool = false 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/EnvironmentValues/ColumnsLayout.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct ColumnsLayoutKey: EnvironmentKey { 10 | static let defaultValue = false 11 | } 12 | 13 | extension EnvironmentValues { 14 | var columnsLayout: Bool { 15 | get { self[ColumnsLayoutKey.self] } 16 | set { self[ColumnsLayoutKey.self] = newValue } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/EnvironmentValues/LayoutAxis.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // LayoutAxis is stored in the environment by blocks like VStack and HStack. It is read by blocks such as Divider and 10 | // Spacer so they can know which direction to orient themselves. 11 | enum LayoutAxis { 12 | case horizontal 13 | case vertical 14 | case undefined 15 | } 16 | 17 | struct LayoutAxisKey: EnvironmentKey { 18 | static let defaultValue = LayoutAxis.undefined 19 | } 20 | 21 | extension EnvironmentValues { 22 | var layoutAxis: LayoutAxis { 23 | get { self[LayoutAxisKey.self] } 24 | set { self[LayoutAxisKey.self] = newValue } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/EnvironmentValues/RenderMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct RenderModeKey: EnvironmentKey { 10 | static let defaultValue = RenderMode.measured 11 | } 12 | 13 | enum RenderMode { 14 | case measured 15 | case wrapping 16 | } 17 | 18 | extension EnvironmentValues { 19 | var renderMode: RenderMode { 20 | get { self[RenderModeKey.self] } 21 | set { self[RenderModeKey.self] = newValue } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/EnvironmentValues/TableColumns.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // A Table's columns are stored in the environment. They are read from the environment by TableColumnTitles and 10 | // TableRow. 11 | private struct TableColumnsKey: EnvironmentKey { 12 | static let defaultValue: [any TableColumnContent] = [] 13 | } 14 | 15 | extension EnvironmentValues { 16 | var tableColumns: [any TableColumnContent] { 17 | get { self[TableColumnsKey.self] } 18 | set { self[TableColumnsKey.self] = newValue } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/Alignment+Additions.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Alignment { 10 | // The horizontal portion of a 2D Alignment. 11 | var horizontalAlignment: HorizontalAlignment { 12 | switch self { 13 | case .leading, .topLeading, .bottomLeading: 14 | .leading 15 | case .center, .top, .bottom: 16 | .center 17 | case .trailing, .topTrailing, .bottomTrailing: 18 | .trailing 19 | } 20 | } 21 | 22 | // The vertical portion of a 2D Alignment. 23 | var verticalAlignment: VerticalAlignment { 24 | switch self { 25 | case .top, .topLeading, .topTrailing: 26 | .top 27 | case .center, .leading, .trailing: 28 | .center 29 | case .bottom, .bottomLeading, .bottomTrailing: 30 | .bottom 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/BlockSize.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // A type used in the Renderable protocol for block sizing. This allows for 10 | // representation of the minimum and maximum size of blocks that can have 11 | // flexible sizes. 12 | struct BlockSize { 13 | let min: CGSize 14 | let max: CGSize 15 | 16 | init(min: CGSize, max: CGSize, maxWidth _: Bool = false, maxHeight _: Bool = false) { 17 | self.min = min 18 | self.max = max 19 | } 20 | 21 | init(_ size: CGSize) { 22 | self.min = size 23 | self.max = size 24 | } 25 | 26 | init(width: CGFloat, height: CGFloat) { 27 | self.min = .init(width: width, height: height) 28 | self.max = .init(width: width, height: height) 29 | } 30 | } 31 | 32 | extension BlockSize { 33 | // Is a block flexibile horizontally? 34 | var hFlexible: Bool { 35 | max.width != min.width 36 | } 37 | 38 | // Is a block flexibile horizontally? 39 | var vFlexible: Bool { 40 | max.height != min.height 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/FloatingPoint+Operators.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | import Foundation 7 | 8 | private extension FloatingPoint { 9 | var kround: Self { 10 | (self * 1000).rounded(.awayFromZero) 11 | } 12 | } 13 | 14 | infix operator ~<=: ComparisonPrecedence 15 | 16 | public func ~<= (left: F, right: F) -> Bool where F: FloatingPoint { 17 | left.kround <= right.kround 18 | } 19 | 20 | infix operator ~>=: ComparisonPrecedence 21 | 22 | public func ~>= (left: F, right: F) -> Bool where F: FloatingPoint { 23 | left.kround >= right.kround 24 | } 25 | 26 | infix operator ~<: ComparisonPrecedence 27 | 28 | public func ~< (left: F, right: F) -> Bool where F: FloatingPoint { 29 | left.kround < right.kround 30 | } 31 | 32 | infix operator ~>: ComparisonPrecedence 33 | 34 | public func ~> (left: F, right: F) -> Bool where F: FloatingPoint { 35 | left.kround > right.kround 36 | } 37 | 38 | infix operator ~==: ComparisonPrecedence 39 | 40 | public func ~== (left: F, right: F) -> Bool where F: FloatingPoint { 41 | left.kround == right.kround 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension CGSize { 10 | func scaled(by factor: CGFloat) -> CGSize { 11 | .init(width: width * factor, height: height * factor) 12 | } 13 | } 14 | 15 | extension CGPoint { 16 | func offset(dx: CGFloat, dy: CGFloat) -> CGPoint { 17 | .init(x: x + dx, y: y + dy) 18 | } 19 | 20 | func offset(dx: CGFloat) -> CGPoint { 21 | .init(x: x + dx, y: y) 22 | } 23 | 24 | func offset(dy: CGFloat) -> CGPoint { 25 | .init(x: x, y: y + dy) 26 | } 27 | } 28 | 29 | extension CGRect { 30 | func rectInCenter(size: CGSize) -> CGRect { 31 | .init(origin: .init(x: minX + (width - size.width) / 2, y: minY + (height - size.height) / 2), 32 | size: size) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/PageInfo.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct PageInfo { 10 | let size: PageSize 11 | let margins: EdgeInsets 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/Proposal.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // A type used in the Renderable protocol for block sizing. At present, it is simply 10 | // an alias for CGSize. 11 | typealias Proposal = CGSize 12 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/StackSpacing+Additions.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | extension StackSpacing { 10 | // A function used by Stacks to determine the width of their spacing. 11 | var fixedPoints: CGFloat { 12 | switch self { 13 | case let .flex(value): 14 | // The min spacing is a flex spacing is counted as fixedPoints 15 | value.points 16 | case let .fixed(value): 17 | value.points 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/TextStroke.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct TextStroke { 10 | let color: Color 11 | let lineWidth: CGFloat 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/ValueWrapper.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | // A property wrapper purposed for use in value types in order to allow them to have the 10 | // ergonomics of having mutable value type properties. This is Swift "syntactic sugar" 11 | // that is accomplished by wrapping a value type within a reference type. 12 | @propertyWrapper class MutableValue { 13 | var wrappedValue: T 14 | 15 | init(wrappedValue: T) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PDFBlocks/Internal/Types/WrapMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF Blocks 3 | * Copyright (c) David Yowell 2024 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | enum WrapMode { 10 | case atomic 11 | case primary 12 | case secondary 13 | } 14 | -------------------------------------------------------------------------------- /Tests/PDFBlocksTests/PDFBlocksTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PDFBlocks 3 | 4 | final class PDFBlocksTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------