├── .github
└── workflows
│ └── dotnet.yml
├── LICENSE
├── README.md
├── pdf
└── TestPdf.pdf
└── src
├── .editorconfig
├── .gitattributes
├── .gitignore
├── DtronixPdf.ImageSharp
├── DtronixPdf.ImageSharp.csproj
└── PdfBitmapExtensions.cs
├── DtronixPdf.Tests
├── DtronixPdf.Tests.csproj
├── PdfDocumentTests.cs
├── PdfPageRenderTests.cs
├── PdfPageRendererTests
│ └── pixel_test_1.png
└── PdfPageTests.cs
├── DtronixPdf.props
├── DtronixPdf.sln
├── DtronixPdf
├── DtronixPdf.csproj
├── PDFiumManager.cs
├── PdfActionSynchronizer.cs
├── PdfBitmap.cs
├── PdfDocument.cs
├── PdfFileWriteCopyStream.cs
├── PdfPage.Edit.cs
├── PdfPage.cs
├── PdfPageRenderConfig.cs
├── PdfiumConfig.cs
└── VectorHelpers.cs
└── DtronixPdfBenchmark
├── DtronixPdfBenchmark.csproj
└── Program.cs
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: Build, Pack & Publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | tags:
8 | - 'v*'
9 | pull_request:
10 | branches:
11 | - '*'
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 | submodules: true
23 |
24 | - name: Install .NET
25 | uses: actions/setup-dotnet@v4
26 | with:
27 | dotnet-version: 8.0.*
28 | source-url: https://api.nuget.org/v3/index.json
29 | env:
30 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
31 |
32 | - name: Build
33 | run: dotnet build src -c Release
34 |
35 | - name: Pack DtronixPdf
36 | run: dotnet pack src/DtronixPdf -c Release -o ./artifacts
37 |
38 | - name: Pack DtronixPdf.ImageSharp
39 | run: dotnet pack src/DtronixPdf.ImageSharp -c Release -o ./artifacts
40 |
41 | - name: Unit tests
42 | run: dotnet test src/DtronixPdf.Tests -c Release
43 |
44 | - name: Export artifacts
45 | uses: actions/upload-artifact@v4
46 | with:
47 | path: |
48 | artifacts/*.nupkg
49 | artifacts/*.snupkg
50 |
51 | - name: Get tag name
52 | if: startsWith(github.ref, 'refs/tags/')
53 | uses: olegtarasov/get-tag@v2.1
54 | id: tagName
55 |
56 | - name: Create release
57 | uses: softprops/action-gh-release@v1
58 | if: startsWith(github.ref, 'refs/tags/')
59 | with:
60 | name: "DtronixPdf ${{ steps.tagName.outputs.tag }} Released"
61 | files: |
62 | artifacts/*.nupkg
63 | artifacts/*.snupkg
64 |
65 | - name: Push Nuget packages
66 | if: startsWith(github.ref, 'refs/tags/')
67 | run: dotnet nuget push artifacts/*.nupkg --api-key ${{ secrets.ORG_NUGET_AUTH_TOKEN }} --skip-duplicate
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dtronix
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DtronixPdf [](https://www.nuget.org/packages/DtronixPdf) [](https://github.com/Dtronix/DtronixPdf/actions)
2 |
3 | DtronixPdf is a .NET 8.0 library to handle interactions with PDFs via the PDFium library which is inherently not thread safe. This library will serialize all calls which are made to the PDFium backend and execute them all on a single thread via a dispatcher. Results are then returned through Tasks to the calling site.
4 |
5 | Supports Linux-x64, OSX-x64, Win-x64, Win-x86.
6 |
7 | [Project Roadmap](https://github.com/orgs/Dtronix/projects/1)
8 |
9 | ### Usage
10 |
11 | - [Nuget Package](https://www.nuget.org/packages/DtronixPdf).
12 | - Manual building. `dotnet build -c Release`
13 |
14 | ### Build Requirements
15 | - .NET 8.0
16 |
17 | ### References
18 |
19 | - https://github.com/Dtronix/PDFiumCore
20 | - https://pdfium.googlesource.com/pdfium/
21 | - https://github.com/bblanchon/pdfium-binaries
22 | - https://github.com/mono/CppSharp
23 |
24 | ### License
25 | [MIT](LICENSE) License
--------------------------------------------------------------------------------
/pdf/TestPdf.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dtronix/DtronixPdf/4751c6f1ea928dacb9c96969057f968f070405bd/pdf/TestPdf.pdf
--------------------------------------------------------------------------------
/src/.editorconfig:
--------------------------------------------------------------------------------
1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs
2 | ###############################
3 | # Core EditorConfig Options #
4 | ###############################
5 | root = true
6 | # All files
7 | [*]
8 | indent_style = space
9 |
10 | # XML project files
11 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
12 | indent_size = 2
13 |
14 | # XML config files
15 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
16 | indent_size = 2
17 |
18 | # Code files
19 | [*.{cs,csx,vb,vbx}]
20 | indent_size = 4
21 | insert_final_newline = true
22 | charset = utf-8-bom
23 | ###############################
24 | # .NET Coding Conventions #
25 | ###############################
26 | [*.{cs,vb}]
27 | # Organize usings
28 | dotnet_sort_system_directives_first = true
29 | # this. preferences
30 | dotnet_style_qualification_for_field = false:silent
31 | dotnet_style_qualification_for_property = false:silent
32 | dotnet_style_qualification_for_method = false:silent
33 | dotnet_style_qualification_for_event = false:silent
34 | # Language keywords vs BCL types preferences
35 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
36 | dotnet_style_predefined_type_for_member_access = true:silent
37 | # Parentheses preferences
38 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
40 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
41 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
42 | # Modifier preferences
43 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
44 | dotnet_style_readonly_field = true:suggestion
45 | # Expression-level preferences
46 | dotnet_style_object_initializer = true:suggestion
47 | dotnet_style_collection_initializer = true:suggestion
48 | dotnet_style_explicit_tuple_names = true:suggestion
49 | dotnet_style_null_propagation = true:suggestion
50 | dotnet_style_coalesce_expression = true:suggestion
51 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
52 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
53 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
54 | dotnet_style_prefer_auto_properties = true:silent
55 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
56 | dotnet_style_prefer_conditional_expression_over_return = true:silent
57 | ###############################
58 | # Naming Conventions #
59 | ###############################
60 | # Style Definitions
61 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
62 | # Use PascalCase for constant fields
63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
65 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
66 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
67 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
68 | dotnet_naming_symbols.constant_fields.required_modifiers = const
69 | # Private Fields
70 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
71 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
72 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion
73 | dotnet_naming_symbols.private_fields.applicable_kinds = field
74 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private
75 | dotnet_naming_symbols.private_fields.required_modifiers = readonly
76 |
77 | dotnet_naming_style.prefix_underscore.capitalization = camel_case
78 | dotnet_naming_style.prefix_underscore.required_prefix = _
79 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
80 | tab_width = 4
81 | end_of_line = crlf
82 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
83 | ###############################
84 | # C# Coding Conventions #
85 | ###############################
86 | [*.cs]
87 | # var preferences
88 | csharp_style_var_for_built_in_types = true:silent
89 | csharp_style_var_when_type_is_apparent = true:silent
90 | csharp_style_var_elsewhere = true:silent
91 | # Expression-bodied members
92 | csharp_style_expression_bodied_methods = false:silent
93 | csharp_style_expression_bodied_constructors = false:silent
94 | csharp_style_expression_bodied_operators = false:silent
95 | csharp_style_expression_bodied_properties = true:silent
96 | csharp_style_expression_bodied_indexers = true:silent
97 | csharp_style_expression_bodied_accessors = true:silent
98 | # Pattern matching preferences
99 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
100 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
101 | # Null-checking preferences
102 | csharp_style_throw_expression = true:suggestion
103 | csharp_style_conditional_delegate_call = true:suggestion
104 | # Modifier preferences
105 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
106 | # Expression-level preferences
107 | csharp_prefer_braces = true:silent
108 | csharp_style_deconstructed_variable_declaration = true:suggestion
109 | csharp_prefer_simple_default_expression = true:suggestion
110 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
111 | csharp_style_inlined_variable_declaration = true:suggestion
112 | ###############################
113 | # C# Formatting Rules #
114 | ###############################
115 | # New line preferences
116 | csharp_new_line_before_open_brace = all
117 | csharp_new_line_before_else = true
118 | csharp_new_line_before_catch = true
119 | csharp_new_line_before_finally = true
120 | csharp_new_line_before_members_in_object_initializers = true
121 | csharp_new_line_before_members_in_anonymous_types = true
122 | csharp_new_line_between_query_expression_clauses = true
123 | # Indentation preferences
124 | csharp_indent_case_contents = true
125 | csharp_indent_switch_labels = true
126 | csharp_indent_labels = flush_left
127 | # Space preferences
128 | csharp_space_after_cast = false
129 | csharp_space_after_keywords_in_control_flow_statements = true
130 | csharp_space_between_method_call_parameter_list_parentheses = false
131 | csharp_space_between_method_declaration_parameter_list_parentheses = false
132 | csharp_space_between_parentheses = false
133 | csharp_space_before_colon_in_inheritance_clause = true
134 | csharp_space_after_colon_in_inheritance_clause = true
135 | csharp_space_around_binary_operators = before_and_after
136 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
137 | csharp_space_between_method_call_name_and_opening_parenthesis = false
138 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
139 | # Wrapping preferences
140 | csharp_preserve_single_line_statements = true
141 | csharp_preserve_single_line_blocks = true
142 | csharp_using_directive_placement = outside_namespace:silent
143 | csharp_prefer_simple_using_statement = true:suggestion
144 | csharp_style_namespace_declarations = block_scoped:silent
145 | csharp_style_prefer_method_group_conversion = true:silent
146 | csharp_style_prefer_top_level_statements = true:silent
147 | csharp_style_expression_bodied_lambdas = true:silent
148 | csharp_style_expression_bodied_local_functions = false:silent
--------------------------------------------------------------------------------
/src/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # NCrunch
114 | _NCrunch_*
115 | .*crunch*.local.xml
116 | nCrunchTemp_*
117 |
118 | # MightyMoose
119 | *.mm.*
120 | AutoTest.Net/
121 |
122 | # Web workbench (sass)
123 | .sass-cache/
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.[Pp]ublish.xml
143 | *.azurePubxml
144 | # TODO: Comment the next line if you want to checkin your web deploy settings
145 | # but database connection strings (with potential passwords) will be unencrypted
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
150 | # checkin your Azure Web App publish settings, but sensitive information contained
151 | # in these scripts will be unencrypted
152 | PublishScripts/
153 |
154 | # NuGet Packages
155 | *.nupkg
156 | # The packages folder can be ignored because of Package Restore
157 | **/packages/*
158 | # except build/, which is used as an MSBuild target.
159 | !**/packages/build/
160 | # Uncomment if necessary however generally it will be regenerated when needed
161 | #!**/packages/repositories.config
162 | # NuGet v3's project.json files produces more ignoreable files
163 | *.nuget.props
164 | *.nuget.targets
165 |
166 | # Microsoft Azure Build Output
167 | csx/
168 | *.build.csdef
169 |
170 | # Microsoft Azure Emulator
171 | ecf/
172 | rcf/
173 |
174 | # Windows Store app package directories and files
175 | AppPackages/
176 | BundleArtifacts/
177 | Package.StoreAssociation.xml
178 | _pkginfo.txt
179 |
180 | # Visual Studio cache files
181 | # files ending in .cache can be ignored
182 | *.[Cc]ache
183 | # but keep track of directories ending in .cache
184 | !*.[Cc]ache/
185 |
186 | # Others
187 | ClientBin/
188 | ~$*
189 | *~
190 | *.dbmdl
191 | *.dbproj.schemaview
192 | *.jfm
193 | *.pfx
194 | *.publishsettings
195 | node_modules/
196 | orleans.codegen.cs
197 |
198 | # Since there are multiple workflows, uncomment next line to ignore bower_components
199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
200 | #bower_components/
201 |
202 | # RIA/Silverlight projects
203 | Generated_Code/
204 |
205 | # Backup & report files from converting an old project file
206 | # to a newer Visual Studio version. Backup files are not needed,
207 | # because we have git ;-)
208 | _UpgradeReport_Files/
209 | UpgradeLog*.XML
210 | UpgradeLog*.htm
211 |
212 | # SQL Server files
213 | *.mdf
214 | *.ldf
215 |
216 | # Business Intelligence projects
217 | *.rdl.data
218 | *.bim.layout
219 | *.bim_*.settings
220 |
221 | # Microsoft Fakes
222 | FakesAssemblies/
223 |
224 | # GhostDoc plugin setting file
225 | *.GhostDoc.xml
226 |
227 | # Node.js Tools for Visual Studio
228 | .ntvs_analysis.dat
229 |
230 | # Visual Studio 6 build log
231 | *.plg
232 |
233 | # Visual Studio 6 workspace options file
234 | *.opt
235 |
236 | # Visual Studio LightSwitch build output
237 | **/*.HTMLClient/GeneratedArtifacts
238 | **/*.DesktopClient/GeneratedArtifacts
239 | **/*.DesktopClient/ModelManifest.xml
240 | **/*.Server/GeneratedArtifacts
241 | **/*.Server/ModelManifest.xml
242 | _Pvt_Extensions
243 |
244 | # Paket dependency manager
245 | .paket/paket.exe
246 | paket-files/
247 |
248 | # FAKE - F# Make
249 | .fake/
250 |
251 | # JetBrains Rider
252 | .idea/
253 | *.sln.iml
254 |
255 | # CodeRush
256 | .cr/
257 |
258 | # Python Tools for Visual Studio (PTVS)
259 | __pycache__/
260 | *.pyc
--------------------------------------------------------------------------------
/src/DtronixPdf.ImageSharp/DtronixPdf.ImageSharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ImageSharp bindings for DtronixPdf
5 | 1.2.0.0
6 |
7 |
8 | net8.0
9 | enable
10 | enable
11 | true
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/DtronixPdf.ImageSharp/PdfBitmapExtensions.cs:
--------------------------------------------------------------------------------
1 | using SixLabors.ImageSharp.PixelFormats;
2 | using SixLabors.ImageSharp;
3 |
4 | namespace DtronixPdf.ImageSharp
5 | {
6 | public static class PdfBitmapExtensions
7 | {
8 | public static unsafe Image GetImage(this PdfBitmap bitmap)
9 | {
10 | return Image.WrapMemory(
11 | bitmap.Pointer.ToPointer(),
12 | (int)(bitmap.Viewport[2] - bitmap.Viewport[0]),
13 | (int)(bitmap.Viewport[3] - bitmap.Viewport[1]));
14 | }
15 |
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/DtronixPdf.Tests/DtronixPdf.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | false
6 |
7 |
8 |
9 |
10 | PreserveNewest
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | PreserveNewest
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/DtronixPdf.Tests/PdfDocumentTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using NUnit.Framework;
4 |
5 | namespace DtronixPdf.Tests
6 | {
7 | public class PdfDocumentTests
8 | {
9 | [Test]
10 | public void LoadsDocument()
11 | {
12 | using var document = PdfDocument.Load("TestPdf.pdf", null);
13 | Assert.AreEqual(1, document.Pages);
14 | }
15 |
16 | [Test]
17 | public void LoadsMemoryDocument()
18 | {
19 | using var stream = File.OpenRead("TestPdf.pdf");
20 | using var document = PdfDocument.Load(stream, null);
21 | Assert.AreEqual(1, document.Pages);
22 | }
23 |
24 | [Test]
25 | public void SavesDocument()
26 | {
27 | using var document = PdfDocument.Load("TestPdf.pdf", null);
28 | using var sw = new MemoryStream();
29 | document.Save(sw);
30 |
31 | Assert.Greater(sw.Length, 10000);
32 | }
33 |
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/DtronixPdf.Tests/PdfPageRenderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using System.IO;
3 | using System.Net.Mime;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using DtronixPdf.ImageSharp;
7 | using NUnit.Framework;
8 | using PDFiumCore;
9 | using SixLabors.ImageSharp.Formats.Png;
10 | using SixLabors.ImageSharp.PixelFormats;
11 |
12 | namespace DtronixPdf.Tests
13 | {
14 | public class PdfPageRenderTests
15 | {
16 | [Test]
17 | public void RendererCreatesImageSize()
18 | {
19 | using var document = PdfDocument.Load("TestPdf.pdf", null);
20 | using var page = document.GetPage(0);
21 | var renderPage = page.Render(1);
22 | var image = renderPage.GetImage();
23 |
24 | Assert.AreEqual(page.Width, image.Width);
25 | Assert.AreEqual(page.Height, image.Height);
26 | }
27 |
28 | [Test]
29 | public void RendererSavesImage()
30 | {
31 | using var document = PdfDocument.Load("TestPdf.pdf", null);
32 | using var page = document.GetPage(0);
33 | var renderPage = page.Render(1);
34 |
35 | using var writer = File.OpenWrite("test.png");
36 | renderPage.GetImage().Save(writer, new PngEncoder());
37 | }
38 |
39 | /*
40 | public void PixelTest_1()
41 | {
42 | using var document = PdfDocument.Load("TestPdf.pdf", null);
43 | using var page = document.GetPage(0);
44 | using var expectedImageStream = File.OpenRead("PdfPageRendererTests/pixel_test_1.png");
45 | using var expectedImage = MediaTypeNames.Image.Load(expectedImageStream, new PngDecoder());
46 |
47 | var renderPage = page.Render(
48 | 1,
49 | (uint)Color.White.ToArgb(),
50 | new BoundaryF(522, 477, 3, 3));
51 |
52 | var renderPage2 = page.Render(
53 | 1,
54 | (uint)Color.White.ToArgb(),
55 | new BoundaryF(522, 477, 30, 30));
56 |
57 | renderPage2.Image.SaveAsPng("png1Crop.png");
58 |
59 | var renderPage3 = page.Render(1, Color.White);
60 |
61 | renderPage3.Image.SaveAsPng("png1Full.png");
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | var pageBitmap = page.Render(1, Color.White);
74 |
75 | for (int x = 0; x < 3; x++)
76 | {
77 | for (int y = 0; y < 3; y++)
78 | {
79 | Assert.AreEqual(expectedImage[x, y], pageBitmap.Image[x, y]);
80 | }
81 | }
82 | }*/
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/DtronixPdf.Tests/PdfPageRendererTests/pixel_test_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dtronix/DtronixPdf/4751c6f1ea928dacb9c96969057f968f070405bd/src/DtronixPdf.Tests/PdfPageRendererTests/pixel_test_1.png
--------------------------------------------------------------------------------
/src/DtronixPdf.Tests/PdfPageTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using NUnit.Framework;
4 | using SixLabors.ImageSharp;
5 |
6 | namespace DtronixPdf.Tests
7 | {
8 | public class PdfPageTests
9 | {
10 | [Test]
11 | public void LoadsPage()
12 | {
13 | using var document = PdfDocument.Load("TestPdf.pdf", null);
14 | using var page = document.GetPage(0);
15 | }
16 |
17 | [Test]
18 | public void PageSizeIsReturned()
19 | {
20 | using var document = PdfDocument.Load("TestPdf.pdf", null);
21 | using var page = document.GetPage(0);
22 | Assert.AreEqual(new SizeF(792, 612), new SizeF(page.Width, page.Height));
23 | }
24 |
25 | [Test]
26 | public void InitalIndexIsReturned()
27 | {
28 | using var document = PdfDocument.Load("TestPdf.pdf", null);
29 | using var page = document.GetPage(0);
30 | Assert.AreEqual(0, page.InitialIndex);
31 | }
32 |
33 | [Test]
34 | public void GetRotationReturnsCorrectValue()
35 | {
36 | using var document = PdfDocument.Load("TestPdf.pdf", null);
37 | using var page = document.GetPage(0);
38 | Assert.AreEqual(3, page.GetRotation());
39 | }
40 |
41 | [Test]
42 | public void SetRotationSetsValue()
43 | {
44 | using var document = PdfDocument.Load("TestPdf.pdf", null);
45 | using var page = document.GetPage(0);
46 | page.SetRotation(1);
47 | Assert.AreEqual(1, page.GetRotation());
48 | }
49 |
50 | [Test]
51 | public void GetTextReturnsText()
52 | {
53 | using var document = PdfDocument.Load("TestPdf.pdf", null);
54 | using var page = document.GetPage(0);
55 |
56 | string actual = page.GetText(0, 150, page.Width, 500);
57 | Assert.AreEqual("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
58 | actual);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/DtronixPdf.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | true
5 | true
6 | Dtronix
7 | Dtronix PDF
8 | Copyright © Dtronix 2025
9 | DJGosnell
10 | https://github.com/Dtronix/DtronixPdf
11 | https://github.com/Dtronix/DtronixPdf
12 | MIT
13 | true
14 | pdf editor modify viewer export
15 | true
16 | snupkg
17 | true
18 |
19 |
--------------------------------------------------------------------------------
/src/DtronixPdf.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32014.148
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtronixPdf", "DtronixPdf\DtronixPdf.csproj", "{23F0BD01-C9B9-4DC8-A444-978A4A91F5EE}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtronixPdf.Tests", "DtronixPdf.Tests\DtronixPdf.Tests.csproj", "{1FE9E658-7FBB-4C89-8173-05DC58A68DE1}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtronixPdfBenchmark", "DtronixPdfBenchmark\DtronixPdfBenchmark.csproj", "{2639F2B5-D7A0-4566-B00A-BBFF0181C27F}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtronixPdf.ImageSharp", "DtronixPdf.ImageSharp\DtronixPdf.ImageSharp.csproj", "{59A2B9DB-514C-4463-8AB8-3779B62FD99E}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E77BD9A3-165B-4B7B-A011-C79F7AB476D2}"
15 | ProjectSection(SolutionItems) = preProject
16 | .editorconfig = .editorconfig
17 | DtronixPdf.props = DtronixPdf.props
18 | EndProjectSection
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {23F0BD01-C9B9-4DC8-A444-978A4A91F5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {23F0BD01-C9B9-4DC8-A444-978A4A91F5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {23F0BD01-C9B9-4DC8-A444-978A4A91F5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {23F0BD01-C9B9-4DC8-A444-978A4A91F5EE}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {1FE9E658-7FBB-4C89-8173-05DC58A68DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {1FE9E658-7FBB-4C89-8173-05DC58A68DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {1FE9E658-7FBB-4C89-8173-05DC58A68DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {1FE9E658-7FBB-4C89-8173-05DC58A68DE1}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {2639F2B5-D7A0-4566-B00A-BBFF0181C27F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {2639F2B5-D7A0-4566-B00A-BBFF0181C27F}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {2639F2B5-D7A0-4566-B00A-BBFF0181C27F}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {2639F2B5-D7A0-4566-B00A-BBFF0181C27F}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {59A2B9DB-514C-4463-8AB8-3779B62FD99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {59A2B9DB-514C-4463-8AB8-3779B62FD99E}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {59A2B9DB-514C-4463-8AB8-3779B62FD99E}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {59A2B9DB-514C-4463-8AB8-3779B62FD99E}.Release|Any CPU.Build.0 = Release|Any CPU
42 | EndGlobalSection
43 | GlobalSection(SolutionProperties) = preSolution
44 | HideSolutionNode = FALSE
45 | EndGlobalSection
46 | GlobalSection(ExtensibilityGlobals) = postSolution
47 | SolutionGuid = {7762582E-0386-40D3-8B62-690FA06D560E}
48 | EndGlobalSection
49 | EndGlobal
50 |
--------------------------------------------------------------------------------
/src/DtronixPdf/DtronixPdf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tool to view and perform common modifications to PDFs. Based on PDFium.
5 | 1.3.1.0
6 |
7 |
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 | <_Parameter1>DtronixPdf.Tests
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PDFiumManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using PDFiumCore;
5 |
6 | namespace DtronixPdf
7 | {
8 |
9 | public class PdfiumManager
10 | {
11 | private static bool _isInitialized;
12 |
13 | private static PdfiumManager _managerDefaultInstance;
14 | public static PdfiumManager Default => _managerDefaultInstance ??= new PdfiumManager();
15 |
16 | ///
17 | /// Gets the instance used to synchronize actions in a PDF document.
18 | ///
19 | public PdfActionSynchronizer Synchronizer { get; }
20 |
21 | private readonly ConcurrentDictionary _loadedDocuments = new ();
22 |
23 | private static readonly ConcurrentBag _loadedManagers = new ();
24 |
25 | private PdfiumManager()
26 | {
27 | _loadedManagers.Add(this);
28 |
29 | Synchronizer = new PdfActionSynchronizer();
30 | }
31 |
32 | ///
33 | /// Initialized the PDFiumCore library.
34 | ///
35 | ///
36 | internal static void Initialize()
37 | {
38 | if (_isInitialized)
39 | return;
40 |
41 | _isInitialized = true;
42 | // Initialize the library.
43 | Default.Synchronizer.SyncExec(fpdfview.FPDF_InitLibrary);
44 | }
45 |
46 | public static void Unload()
47 | {
48 | if (!_isInitialized)
49 | return;
50 |
51 | foreach (var pdfiumCoreManager in _loadedManagers)
52 | {
53 | if (pdfiumCoreManager._loadedDocuments.Count > 0)
54 | throw new InvalidOperationException("Can't destroy loaded library since it is still in use by PdfDocument(s)");
55 | }
56 |
57 | _isInitialized = false;
58 |
59 | Default.Synchronizer.SyncExec(fpdfview.FPDF_DestroyLibrary);
60 | }
61 |
62 | internal void AddDocument(PdfDocument document)
63 | {
64 | if (document == null)
65 | throw new ArgumentNullException(nameof(document));
66 |
67 | lock (_loadedDocuments)
68 | {
69 | _loadedDocuments.TryAdd(document, document);
70 | }
71 | }
72 |
73 | internal void RemoveDocument(PdfDocument document)
74 | {
75 | if (document == null)
76 | throw new ArgumentNullException(nameof(document));
77 |
78 | lock (_loadedDocuments)
79 | {
80 | _loadedDocuments.Remove(document, out _);
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfActionSynchronizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace DtronixPdf
5 | {
6 | public class PdfActionSynchronizer
7 | {
8 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
9 | private const int Timeout = 60_000;
10 |
11 | public void SyncExec(Action action)
12 | {
13 | bool acquired = false;
14 | try
15 | {
16 | if (!_semaphore.Wait(Timeout))
17 | throw new TimeoutException("Timed out acquiring exclusive lock...");
18 | acquired = true;
19 | action();
20 | _semaphore.Release();
21 | }
22 | catch
23 | {
24 | if(acquired)
25 | _semaphore.Release();
26 | throw;
27 | }
28 | }
29 |
30 | public T SyncExec(Func function)
31 | {
32 | bool acquired = false;
33 | try
34 | {
35 | if (!_semaphore.Wait(Timeout))
36 | throw new TimeoutException("Timed out acquiring exclusive lock...");
37 | acquired = true;
38 | var result = function();
39 | _semaphore.Release();
40 | return result;
41 | }
42 | catch
43 | {
44 | if (acquired)
45 | _semaphore.Release();
46 | throw;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfBitmap.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Intrinsics;
3 | using PDFiumCore;
4 |
5 | namespace DtronixPdf
6 | {
7 | public class PdfBitmap : IDisposable
8 | {
9 | private readonly FpdfBitmapT _pdfBitmap;
10 |
11 | public float Scale { get; }
12 |
13 | ///
14 | /// MinX, MinY, MaxX, MaxY
15 | ///
16 | public Vector128 Viewport { get; }
17 |
18 | public IntPtr Pointer { get; }
19 |
20 | public int Stride { get; }
21 |
22 | public int Width { get; }
23 |
24 | public int Height { get; }
25 |
26 | public bool IsDisposed { get; private set; }
27 |
28 | ///
29 | /// Only call within the synchronizer since dll calls are made.
30 | ///
31 | ///
32 | ///
33 | /// MinX, MinY, MaxX, MaxY
34 | internal PdfBitmap(
35 | FpdfBitmapT pdfBitmap,
36 | float scale,
37 | Vector128 viewport)
38 | {
39 | _pdfBitmap = pdfBitmap;
40 | Stride = fpdfview.FPDFBitmapGetStride(_pdfBitmap);
41 | Width = (int)viewport.GetWidth();
42 | Height = (int)viewport.GetHeight();
43 | Pointer = fpdfview.FPDFBitmapGetBuffer(_pdfBitmap);
44 | Scale = scale;
45 | Viewport = viewport;
46 | }
47 |
48 | public void Dispose()
49 | {
50 | if (IsDisposed)
51 | return;
52 |
53 | IsDisposed = true;
54 |
55 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDFBitmapDestroy(_pdfBitmap));
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfDocument.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.InteropServices;
4 | using PDFiumCore;
5 |
6 |
7 | namespace DtronixPdf
8 | {
9 | public class PdfDocument : IDisposable
10 | {
11 | internal readonly FpdfDocumentT Instance;
12 |
13 | private bool _isDisposed = false;
14 | private IntPtr? _documentPointer;
15 |
16 | //private static PdfActionSynchronizer _synchronizer = new PdfActionSynchronizer();
17 |
18 | public int Pages { get; private set; }
19 |
20 | private PdfDocument(FpdfDocumentT instance)
21 | {
22 | Instance = instance;
23 | PdfiumManager.Default.AddDocument(this);
24 | }
25 |
26 | public static PdfDocument Load(string path, string password)
27 | {
28 | PdfiumManager.Initialize();
29 |
30 | var document = PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDF_LoadDocument(path, password));
31 | var pages = PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDF_GetPageCount(document));
32 |
33 | if (document == null)
34 | return null;
35 |
36 | var pdfDocument = new PdfDocument(document) { Pages = pages, };
37 |
38 | return pdfDocument;
39 |
40 | }
41 |
42 | public static unsafe PdfDocument Load(Stream stream, string password)
43 | {
44 | //var synchronizer = new PdfActionSynchronizer();
45 |
46 | var length = (int)stream.Length;
47 |
48 | var ptr = NativeMemory.Alloc((nuint)length);
49 |
50 | Span ptrSpan = new Span(ptr, length);
51 | var pointer = new IntPtr(ptr);
52 | var readLength = 0;
53 |
54 | // Copy the data to the memory.
55 | while ((readLength = stream.Read(ptrSpan)) > 0)
56 | ptrSpan = ptrSpan.Slice(readLength);
57 |
58 | PdfiumManager.Initialize();
59 |
60 | int pages = -1;
61 | var result = PdfiumManager.Default.Synchronizer.SyncExec(() =>
62 | {
63 | var document = fpdfview.FPDF_LoadMemDocument(pointer, length, password);
64 | pages = fpdfview.FPDF_GetPageCount(document);
65 | return document;
66 | });
67 |
68 | if (result == null)
69 | return null;
70 |
71 | var pdfDocument = new PdfDocument(result)
72 | {
73 | Pages = pages,
74 | _documentPointer = pointer
75 | };
76 |
77 | return pdfDocument;
78 | }
79 |
80 | public static PdfDocument Create()
81 | {
82 | //var synchronizer = new PdfActionSynchronizer();
83 |
84 | var result = PdfiumManager.Default.Synchronizer.SyncExec(fpdf_edit.FPDF_CreateNewDocument);
85 |
86 | if (result == null)
87 | return null;
88 |
89 | return new PdfDocument(result);
90 | }
91 |
92 | public PdfPage GetPage(int pageIndex)
93 | {
94 | return PdfPage.Create(this, pageIndex);
95 | }
96 |
97 | ///
98 | /// Imports pages from another PDF document.
99 | ///
100 | ///
101 | ///
102 | /// Pages are 1 based. Pages are separated by commas. Such as "1,3,5-7".
103 | /// If null, all pages are imported.
104 | /// Insertion index is 0 based.
105 | /// True on success, false on failure.
106 | public bool ImportPages(PdfDocument document, string pageRange, int insertIndex)
107 | {
108 | return PdfiumManager.Default.Synchronizer.SyncExec(() =>
109 | fpdf_ppo.FPDF_ImportPages(Instance, document.Instance, pageRange, insertIndex) == 1);
110 | }
111 |
112 | ///
113 | /// Extracts the specified page range into a new PdfDocument.
114 | ///
115 | /// Pages are 1 based. Pages are separated by commas. Such as "1,3,5-7".
116 | /// New document with the specified pages.
117 | public PdfDocument ExtractPages(string pageRange)
118 | {
119 | var newDocument = Create();
120 | newDocument.ImportPages(this, pageRange, 0);
121 |
122 | return newDocument;
123 | }
124 |
125 | ///
126 | /// Deletes the specified page from the document.
127 | ///
128 | /// 0 based index.
129 | /// True on success, false on failure.
130 | public void DeletePage(int pageIndex)
131 | {
132 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdf_edit.FPDFPageDelete(Instance, pageIndex));
133 | }
134 |
135 |
136 | ///
137 | /// Saves the current document to the specified file path.
138 | ///
139 | /// Path to save the PdfDocument.
140 | /// True on success, false on failure.
141 | public bool Save(string path)
142 | {
143 | using var fs = new FileStream(path, FileMode.Create);
144 | return Save(fs);
145 | }
146 |
147 | ///
148 | /// Saves the current document to the passed stream.
149 | ///
150 | /// Destination stream to write the PdfDocument.
151 | /// True on success, false on failure.
152 | public bool Save(Stream stream)
153 | {
154 | var writer = new PdfFileWriteCopyStream(stream);
155 | /*
156 | Flags
157 | #define FPDF_INCREMENTAL 1
158 | #define FPDF_NO_INCREMENTAL 2
159 | #define FPDF_REMOVE_SECURITY 3
160 | */
161 |
162 | var result = PdfiumManager.Default.Synchronizer.SyncExec(() => fpdf_save.FPDF_SaveAsCopy(Instance, writer, 1));
163 |
164 | return result == 1;
165 | }
166 |
167 | public unsafe void Dispose()
168 | {
169 | if (_isDisposed)
170 | return;
171 |
172 | _isDisposed = true;
173 |
174 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDF_CloseDocument(Instance));
175 |
176 | PdfiumManager.Default.RemoveDocument(this);
177 |
178 | // Free the native memory.
179 | if (_documentPointer != null)
180 | {
181 | NativeMemory.Free(_documentPointer.Value.ToPointer());
182 | _documentPointer = null;
183 | }
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfFileWriteCopyStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using PDFiumCore;
4 |
5 | namespace DtronixPdf
6 | {
7 | class PdfFileWriteCopyStream : FPDF_FILEWRITE_
8 | {
9 | public Stream WriteStream { get; }
10 |
11 | public PdfFileWriteCopyStream(Stream writeStream)
12 | {
13 | WriteStream = writeStream;
14 | WriteBlock = CopyToStream;
15 | }
16 |
17 | private unsafe int CopyToStream(IntPtr pthis, IntPtr pdata, uint size)
18 | {
19 | var reader = new UnmanagedMemoryStream((byte*) pdata, size);
20 | reader.CopyTo(WriteStream);
21 | return 1;
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfPage.Edit.cs:
--------------------------------------------------------------------------------
1 | using PDFiumCore;
2 |
3 | namespace DtronixPdf
4 | {
5 | public partial class PdfPage
6 | {
7 |
8 | ///
9 | /// Rotates the page
10 | ///
11 | ///
12 | /// 0 - No rotation.
13 | /// 1 - Rotated 90 degrees clockwise.
14 | /// 2 - Rotated 180 degrees clockwise.
15 | /// 3 - Rotated 270 degrees clockwise.
16 | ///
17 | ///
18 | public void SetRotation(int rotation)
19 | {
20 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdf_edit.FPDFPageSetRotation(PageInstance, rotation));
21 | }
22 |
23 | ///
24 | /// Rotates the page
25 | ///
26 | ///
27 | /// 0 - No rotation.
28 | /// 1 - Rotated 90 degrees clockwise.
29 | /// 2 - Rotated 180 degrees clockwise.
30 | /// 3 - Rotated 270 degrees clockwise.
31 | ///
32 | public int GetRotation()
33 | {
34 | return PdfiumManager.Default.Synchronizer.SyncExec(() => fpdf_edit.FPDFPageGetRotation(PageInstance));
35 | }
36 |
37 |
38 | ///
39 | /// Deletes the page.
40 | ///
41 | public void Delete()
42 | {
43 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdf_edit.FPDFPageDelete(Document.Instance, InitialIndex));
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfPage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 | using System.Runtime.Intrinsics;
5 | using System.Text;
6 | using System.Threading;
7 | using PDFiumCore;
8 |
9 | namespace DtronixPdf
10 | {
11 | public partial class PdfPage : IDisposable
12 | {
13 | internal readonly PdfDocument Document;
14 | private readonly FpdfPageT _pageInstance;
15 | private bool _isDisposed = false;
16 |
17 | public float Width { get; private set; }
18 | public float Height { get; private set; }
19 |
20 | public int InitialIndex { get; private set; }
21 |
22 | internal FpdfPageT PageInstance => _pageInstance;
23 |
24 | private PdfPage(PdfDocument document, FpdfPageT pageInstance)
25 | {
26 | Document = document;
27 | _pageInstance = pageInstance;
28 | }
29 |
30 | internal static PdfPage Create(
31 | PdfDocument document,
32 | int pageIndex)
33 | {
34 | var loadPageResult = PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDF_LoadPage(document.Instance, pageIndex));
35 | if (loadPageResult == null)
36 | throw new Exception($"Failed to open page for page index {pageIndex}.");
37 |
38 | var page = new PdfPage(document, loadPageResult)
39 | {
40 | InitialIndex = pageIndex
41 | };
42 |
43 | var getPageSizeResult = PdfiumManager.Default.Synchronizer.SyncExec(() =>
44 | {
45 | var size = new FS_SIZEF_();
46 |
47 | var result = fpdfview.FPDF_GetPageSizeByIndexF(document.Instance, pageIndex, size);
48 |
49 | return result == 0 ? null : size;
50 | });
51 |
52 | if (getPageSizeResult == null)
53 | throw new Exception($"Could not retrieve page size for page index {pageIndex}.");
54 | page.Width = getPageSizeResult.Width;
55 | page.Height = getPageSizeResult.Height;
56 |
57 | return page;
58 | }
59 |
60 | public PdfBitmap Render(float scale, CancellationToken cancellationToken = default)
61 | {
62 | if (_isDisposed)
63 | throw new ObjectDisposedException(nameof(PdfPage));
64 |
65 | var config = new PdfPageRenderConfig()
66 | {
67 | Scale = scale,
68 | Viewport = Vector128.Create(0, 0, Width * scale, Height * scale),
69 | CancellationToken = cancellationToken,
70 | };
71 |
72 | return Render(config);
73 | }
74 |
75 | public PdfBitmap Render(PdfPageRenderConfig config)
76 | {
77 | if (_isDisposed)
78 | throw new ObjectDisposedException(nameof(PdfPage));
79 |
80 | FpdfBitmapT bitmap = null;
81 |
82 | var viewportHeight = config.Viewport.GetHeight();
83 | var viewportHeightInt = (int)viewportHeight;
84 | var viewportWidth = config.Viewport.GetWidth();
85 | var viewportWidthInt = (int)viewportWidth;
86 |
87 |
88 |
89 | try
90 | {
91 | config.CancellationToken.ThrowIfCancellationRequested();
92 |
93 | bitmap = PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDFBitmapCreateEx(
94 | viewportWidthInt,
95 | viewportHeightInt,
96 | (int)FPDFBitmapFormat.BGRA,
97 | IntPtr.Zero,
98 | 0));
99 |
100 | if (bitmap == null)
101 | throw new Exception("failed to create a bitmap object");
102 |
103 | config.CancellationToken.ThrowIfCancellationRequested();
104 |
105 | if (config.BackgroundColor.HasValue)
106 | {
107 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDFBitmapFillRect(
108 | bitmap,
109 | 0,
110 | 0,
111 | viewportWidthInt,
112 | viewportHeightInt,
113 | config.BackgroundColor.Value));
114 |
115 | config.CancellationToken.ThrowIfCancellationRequested();
116 | }
117 |
118 | using var clipping = new FS_RECTF_
119 | {
120 | Left = 0,
121 | Right = viewportWidth,
122 | Bottom = 0,
123 | Top = viewportHeight
124 | };
125 |
126 | // | | a b 0 |
127 | // | matrix = | c d 0 |
128 | // | | e f 1 |
129 | using var matrix = new FS_MATRIX_
130 | {
131 | A = config.Scale,
132 | B = 0,
133 | C = 0,
134 | D = config.Scale,
135 | E = config.OffsetX,
136 | F = config.OffsetY
137 | };
138 |
139 | PdfiumManager.Default.Synchronizer.SyncExec(() =>
140 | fpdfview.FPDF_RenderPageBitmapWithMatrix(bitmap, _pageInstance, matrix, clipping,
141 | (int)config.Flags));
142 |
143 | config.CancellationToken.ThrowIfCancellationRequested();
144 |
145 | return new PdfBitmap(bitmap, config.Scale, config.Viewport);
146 | }
147 | catch (OperationCanceledException)
148 | {
149 | if (bitmap != null)
150 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDFBitmapDestroy(bitmap));
151 | throw;
152 | }
153 | catch (Exception ex)
154 | {
155 | if (bitmap != null)
156 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDFBitmapDestroy(bitmap));
157 |
158 | throw new Exception("Error rendering page. Check inner exception.", ex);
159 | }
160 | finally
161 | {
162 |
163 | }
164 | }
165 |
166 | public string GetText(double x, double y, double width, double height)
167 | {
168 | if (_isDisposed)
169 | throw new ObjectDisposedException(nameof(PdfPage));
170 |
171 | return PdfiumManager.Default.Synchronizer.SyncExec(() =>
172 | {
173 | var textPage = fpdf_text.FPDFTextLoadPage(_pageInstance);
174 | if (textPage.__Instance.ToInt64() == IntPtr.Zero)
175 | throw new InvalidOperationException("Failed to load text page.");
176 |
177 | try
178 | {
179 | double x2 = x + width;
180 | double y2 = y + height;
181 |
182 | // First call to get the required buffer length
183 | int length = fpdf_text.FPDFTextGetBoundedText(textPage, x, y, x2, y2, ref Unsafe.NullRef(), 0);
184 | if (length <= 0)
185 | return string.Empty;
186 |
187 | var buffer = GC.AllocateArray(length, true);
188 |
189 | // Extract the text into the buffer
190 | fpdf_text.FPDFTextGetBoundedText(textPage, x, y, x2, y2, ref buffer[0], length);
191 |
192 | return Encoding.Unicode.GetString(MemoryMarshal.AsBytes(buffer.AsSpan()));
193 | }
194 | finally
195 | {
196 | fpdf_text.FPDFTextClosePage(textPage);
197 | }
198 | });
199 | }
200 |
201 | public void Dispose()
202 | {
203 | if (_isDisposed)
204 | return;
205 |
206 | _isDisposed = true;
207 |
208 | PdfiumManager.Default.Synchronizer.SyncExec(() => fpdfview.FPDF_ClosePage(PageInstance));
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfPageRenderConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Intrinsics;
2 | using System.Threading;
3 | using PDFiumCore;
4 |
5 | namespace DtronixPdf
6 | {
7 | public record class PdfPageRenderConfig
8 | {
9 | public float Scale { get; init; }
10 |
11 | ///
12 | /// Viewport must be setup with MinX, MinY, MaxX, MaxY formatting.
13 | ///
14 | public Vector128 Viewport { get; init; }
15 |
16 | public uint? BackgroundColor { get; init; }
17 |
18 | public float OffsetX { get; init; }
19 |
20 | public float OffsetY { get; init; }
21 |
22 | public RenderFlags Flags { get; init; } = RenderFlags.RenderAnnotations;
23 |
24 | public CancellationToken CancellationToken { get; init; }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/DtronixPdf/PdfiumConfig.cs:
--------------------------------------------------------------------------------
1 | namespace DtronixPdf
2 | {
3 | public class PdfiumConfig {
4 | public bool AutoUnload { get; init; }
5 | }
6 | }
--------------------------------------------------------------------------------
/src/DtronixPdf/VectorHelpers.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.Intrinsics;
3 |
4 | namespace DtronixPdf;
5 |
6 | internal static class VectorHelpers
7 | {
8 | ///
9 | /// Gets the width of the vector.
10 | ///
11 | /// Order needs to be MinX, MinY, MaxX, MaxY
12 | ///
13 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
14 | public static float GetWidth(this in Vector128 vector)
15 | {
16 | return vector[2] - vector[0];
17 | }
18 |
19 | ///
20 | /// Gets the height of the vector.
21 | ///
22 | /// Order needs to be MinX, MinY, MaxX, MaxY
23 | ///
24 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
25 | public static float GetHeight(this in Vector128 vector)
26 | {
27 | return vector[3] - vector[1];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/DtronixPdfBenchmark/DtronixPdfBenchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
10 | PreserveNewest
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | PreserveNewest
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/DtronixPdfBenchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.Intrinsics;
5 | using System.Threading.Tasks;
6 | using DtronixPdf;
7 | using DtronixPdf.ImageSharp;
8 | using SixLabors.ImageSharp;
9 |
10 | namespace DtronixPdfBenchmark
11 | {
12 | class Program
13 | {
14 | static Stopwatch sw = new Stopwatch();
15 | private const string TestPdf = "TestPdf.pdf";
16 | static async Task Main(string[] args)
17 | {
18 | if (!Directory.Exists("output"))
19 | Directory.CreateDirectory("output");
20 |
21 | RenderViewportScaling();
22 | OpenAndCloseBenchmark();
23 |
24 |
25 | Console.ReadLine();
26 | }
27 |
28 | static async Task RenderViewportScaling()
29 | {
30 | Console.WriteLine($"RenderViewport Benchmark {TestPdf}");
31 | var document = PdfDocument.Load(TestPdf, null);
32 |
33 | sw.Restart();
34 | var iterations = 25;
35 |
36 | for (int i = 1; i < iterations; i++)
37 | {
38 | using var page = document.GetPage(0);
39 |
40 | float scale = i * 0.25f;
41 | Point center = new Point(0, 0);
42 | Size size = new Size(1920, 1080);
43 |
44 | var viewport = Vector128.Create(
45 | 0f,
46 | 0f,
47 | 1920f,
48 | 1080f);
49 |
50 | using var result = page.Render(new PdfPageRenderConfig()
51 | {
52 | Viewport = viewport,
53 | Scale = scale,
54 | BackgroundColor = uint.MaxValue
55 | });
56 | result.GetImage().SaveAsPng($"output/{TestPdf}-{i}.png");
57 | Console.WriteLine($"{sw.ElapsedMilliseconds:##,###} Milliseconds");
58 | sw.Restart();
59 | }
60 |
61 | sw.Stop();
62 | document.Dispose();
63 |
64 | Console.WriteLine($"Rendering {TestPdf} Complete");
65 | }
66 |
67 | static async Task OpenAndCloseBenchmark()
68 | {
69 | Console.WriteLine($"Open and Close {TestPdf}");
70 | sw.Restart();
71 | var iterations = 100;
72 |
73 | for (int i = 1; i < iterations; i++)
74 | {
75 | using var document = PdfDocument.Load(TestPdf, null);
76 | using var page = document.GetPage(0);
77 |
78 | Console.WriteLine($"{sw.ElapsedMilliseconds:##,###} Milliseconds");
79 | sw.Restart();
80 | }
81 |
82 | sw.Stop();
83 |
84 | Console.WriteLine($"Open and Close {TestPdf} Complete");
85 | }
86 |
87 | static async Task ImportTests()
88 | {
89 | var drawing = PdfDocument.Load("drawing.pdf", null);
90 | var testDocument = PdfDocument.Load("testdoc1.pdf", null);
91 |
92 | var newDocument = PdfDocument.Create();
93 |
94 | newDocument.ImportPages(drawing, "1", 0);
95 | newDocument.ImportPages(testDocument, null, 1);
96 | newDocument.ImportPages(drawing, "1", 2);
97 |
98 | var page = newDocument.GetPage(1);
99 |
100 | newDocument.Save("output/importtests.pdf");
101 |
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------