diff --git a/LICENSE b/LICENSE index 7300e256..70d0bf3c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2001-2024 empira Software GmbH, Troisdorf (Cologne Area), Germany +Copyright (c) 2001-2025 empira Software GmbH, Troisdorf (Cologne Area), Germany http://docs.pdfsharp.net diff --git a/PdfSharp.sln b/PdfSharp.sln index 8795a23f..3c82ce5b 100644 --- a/PdfSharp.sln +++ b/PdfSharp.sln @@ -104,6 +104,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{31FE507C-F342-4DA4-9BE5-B3D2ED93E9CC}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + gitversion-6.x.yml = gitversion-6.x.yml gitversion.yml = gitversion.yml LICENSE = LICENSE README.md = README.md diff --git a/PdfSharp.sln.DotSettings b/PdfSharp.sln.DotSettings index a8032f59..1415191e 100644 --- a/PdfSharp.sln.DotSettings +++ b/PdfSharp.sln.DotSettings @@ -14,6 +14,13 @@ TSA URI URL + None + True + BUG + Warning + True + TODO + Edit True ID True @@ -40,6 +47,7 @@ True True True + True True True True @@ -50,6 +58,7 @@ True True True + True True True True diff --git a/README.md b/README.md index 69aaeea8..dd680deb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # PDFsharp & MigraDoc 6 -Version **6.2.0 Preview 2** -Published **2024-12-10** +Version **6.2.0 Preview 3** +Published **2025-02-06** This is a preview version of the **PDFsharp** project, the main project of PDFsharp & MigraDoc 6 with updates for C# 12 and .NET 6. -PDFsharp: Copyright (c) 2005-2024 empira Software GmbH, Troisdorf (Cologne Area), Germany -MigraDoc: Copyright (c) 2001-2024 empira Software GmbH, Troisdorf (Cologne Area), Germany +PDFsharp: Copyright (c) 2005-2025 empira Software GmbH, Troisdorf (Cologne Area), Germany +MigraDoc: Copyright (c) 2001-2025 empira Software GmbH, Troisdorf (Cologne Area), Germany Published Open Source under the [MIT License](https://docs.pdfsharp.net/LICENSE.html) For more information see [docs.pdfsharp.net](https://docs.pdfsharp.net/) diff --git a/dev/build-local-nuget-packages-release.ps1 b/dev/build-local-nuget-packages-release.ps1 index d6eeba5b..14be2328 100644 --- a/dev/build-local-nuget-packages-release.ps1 +++ b/dev/build-local-nuget-packages-release.ps1 @@ -19,18 +19,18 @@ try { Push-Location .. try { - Write-Host "Invoking ’dotnet build’" + Write-Host "Invoking ‘dotnet build’" dotnet build -c release $build = $LASTEXITCODE - Write-Host "’dotnet build’ has finished" + Write-Host "‘dotnet build’ has finished" } finally { Pop-Location } if ($build -gt 0) { - Write-Host "’dotnet build’ failed with code " $build - throw "’dotnet build’ failed with code " + $build + Write-Host "‘dotnet build’ failed with code " $build + throw "‘dotnet build’ failed with code " + $build } .\update-local-nuget-packages-release.ps1 diff --git a/dev/set-en.ps1 b/dev/set-en.ps1 new file mode 100644 index 00000000..c5077ee6 --- /dev/null +++ b/dev/set-en.ps1 @@ -0,0 +1,16 @@ +# Sets UI language for dotnet build to en-US. +#Requires -Version 7 +#Requires -PSEdition Core + +# I want to see English messages on my German Windows dev machine +# when I run e.g. dotnet build. + +# What works: +$env:VSLANG = 1033 +Write-Output "Compiler output language set to en-US." + +# What does not work: +#$env:DOTNET_CLI_UI_LANG = 'en-US' +#$env:PreferredUILang = 'en-US' +#powershell -ExecutionPolicy Bypass -NoExit -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'; [System.Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US';" +#powershell -ExecutionPolicy Bypass -NoExit -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'; [System.Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US';" diff --git a/docs/MigraDoc/changes/v6.2.0-log.md b/docs/MigraDoc/changes/v6.2.0-log.md index 2bb645ad..059a9ccf 100644 --- a/docs/MigraDoc/changes/v6.2.0-log.md +++ b/docs/MigraDoc/changes/v6.2.0-log.md @@ -4,7 +4,7 @@ MigraDoc changes come here. -TODO / JUST A COPY OF PDFSHARP CHANTE LOG +TODO / JUST A COPY OF PDFSHARP CHANGE LOG ### PDFsharp 6.2.0 Preview 1 @@ -37,7 +37,7 @@ Some assemblies had a subfolder `de` containing German messages. We removed all `.restext` resources. All messages are now available in US English only. **PDFsharp.Shared** -Code that grants friendly access to all PDFsharp Foundation assemblies lives in this new assembly. +Code that grants friendly access to all PDFsharp assemblies lives in this new assembly. **Bug in parsing object referenced** Lexer now supports white-space within object references. diff --git a/docs/_READ-THIS-FIRST.md b/docs/_READ-THIS-FIRST.md index eb240f7e..0094a1e9 100644 --- a/docs/_READ-THIS-FIRST.md +++ b/docs/_READ-THIS-FIRST.md @@ -1,6 +1,6 @@ # Read this first -This folder is **not* the documentation of PDFsharp. +This folder is ***not*** the documentation of PDFsharp. It contains internal technical information for/from the PDFsharp team. You find the documentation of PDFsharp [here](https://docs.pdfsharp.net/). \ No newline at end of file diff --git a/docs/change-log/v6.2.0-log.md b/docs/change-log/v6.2.0-log.md index 885e2271..4796d252 100644 --- a/docs/change-log/v6.2.0-log.md +++ b/docs/change-log/v6.2.0-log.md @@ -4,10 +4,48 @@ This text is copied to `docs.pdfsharp.net`. ## Version 6.2 -### PDFsharp 6.2.0 Preview 3 +### PDFsharp 6.2.0 (final release) *not yet released* +### PDFsharp 6.2.0 Preview 3 + +#### General issues + +#### PDFsharp features + +**Improved access to CropBox, ArtBox, BleedBox, TrimBox** +PdfPage now has new properties that make access to those boxes easier. +These are `HasBleedBox`, `BleedBoxReadOnly`, and `EffectiveBleedBoxReadOnly`. +Same applies to ArtBox, CropBox and TrimBox. + +**Loading images: Improved access to buffer of MemoryStream** +LoadImage from MemoryStream now works with a buffer that is not publicly visible. +For better performance, set 'publiclyVisible' to true when creating the MemoryStream. + +#### PDFsharp issues + +**Lexer.ScanNumber and CLexer.ScanNumber** +Based on a bug that crashes PDFsharp if a number in a PDFfile has to much leading zeros, we revised the code of **Lexer.ScanNumber** and **CLexer.ScanNumber**. + +**No more commas allowed in XUnit** +An old hack allows **XUnit** to be assigned from a string that uses a comma as decimal separator. +This was a introduced for languages like German, that use a comma instead of a point as decimal separator. +Now you get an exception. Applies also to **XUnitPt**. + +#### MigraDoc features + +**MigraDoc Preview user control restored for GDI build** +The MigraDoc Preview user control is again available in the GDI+ build. +MigraDoc Preview samples for GDI and WPF have been added to the samples repository. + +**Color.Parse no longer causes exceptions when invoked with a number value** +If Color.Parse is invoked with a number value, as in `var color = Color.Parse("#f00");`, +it no longer throws a handled exception internally. + +#### MigraDoc issues + + --- ### PDFsharp 6.2.0 Preview 2 @@ -140,7 +178,7 @@ We added .NET 8 (`net8.0`) to ``. PDF documents can now be PDF/A conforming. We are still working on this feature, so currently there are some limitations. **PDFsharp.Shared** -Code that grants friendly access to all PDFsharp Foundation assemblies lives in this new assembly. +Code that grants friendly access to all PDFsharp Project assemblies lives in this new assembly. #### Bug fixes diff --git a/docs/publishing/BoilerplateText.md b/docs/publishing/BoilerplateText.md index 81d1e871..87ab7c65 100644 --- a/docs/publishing/BoilerplateText.md +++ b/docs/publishing/BoilerplateText.md @@ -4,9 +4,9 @@ ## PDFsharp -PDFsharp project - PDFsharp -PDFsharp Foundation - PDFsharp library -MigraDoc Foundation - MigraDoc library +PDFsharp Project - PDFsharp +PDFsharp Library - PDFsharp +MigraDoc Library - Library Source @@ -66,17 +66,17 @@ MigraDoc is a .NET library that allows developers to create documents such as PD Note: Do not replicate information from the metadata (like "6.0.0" or "prerelease"). PDFsharp: -This is a version of PDFsharp using .NET 6. -The package ’PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-gdi’ and ’PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. +This is a version of PDFsharp compatible with .NET 6 and higher. +The package ‘PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-gdi’ and ‘PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. We also publish packages that contain PDFsharp plus MigraDoc. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html PDFsharp & MigraDoc: -This is a version of PDFsharp and MigraDoc Foundation using .NET 6. -The package ’PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-MigraDoc-GDI’ and ’PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. +This is a version of PDFsharp and MigraDoc compatible with .NET 6 and higher. +The package ‘PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-MigraDoc-GDI’ and ‘PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/gitversion-6.x.yml b/gitversion-6.x.yml new file mode 100644 index 00000000..1fc45b27 --- /dev/null +++ b/gitversion-6.x.yml @@ -0,0 +1,50 @@ +# Last update: 25-01-30 +assembly-versioning-scheme: MajorMinorPatch +# PDFSHARP_BUILD_VERSION are the days since 2005-01-01. #CHECK_BEFORE_RELEASE +# Command Window: ? (DateTime.Now - new DateTime(2005, 1, 1)).Days; +# C# Interactive: Console.WriteLine((DateTime.Now - new DateTime(2005, 1, 1)).Days); +assembly-file-versioning-format: '{Major}.{Minor}.{Patch}.{env:PDFSHARP_BUILD_VERSION ?? 7334}' +strategies: [Mainline] +assembly-informational-format: '{InformationalVersion}' +branches: + develop: # Current development branch + # branch: develop -- is always 'develop' + # version: {6.0.0}-develop.123 -- taken from git tag plus 'develop' plus commit count + regex: ^develop$ + mode: ContinuousDeployment + increment: None + label: develop + # Investigate this currently undocumented breaking change when switching to v6.x. + # The statement may not be needed. + # Not in v6.x: prevent-increment-of-merged-branch-version: false + # Not working: prevent-increment.of-merged-branch: false + # Not working: prevent-increment.when-branch-merged: false + track-merge-target: true + source-branches: ['feature', 'release'] + release: # Release and preview versions. + # branch: release/6.0.0-preview-3 -- must be same as git tag without leading 'v'. + # version: {6-0-0-preview-3} -- taken from git tag only + regex: ^(release[/-]|master) + # Must not have mode set to be mainline? + increment: None + label: '' + # Not in v6.x: prevent-increment-of-merged-branch-version: true + track-merge-target: true + is-release-branch: true + is-main-branch: true + source-branches: ['develop', 'release', 'feature'] + feature: # Features and bug fixes. + # branch: feature/my-new-feature -- arbitrary name, e.g. a new preview + # version: {6.0.0}-dev-{my-new-feature}.123 -- taken from git tag plus 'dev-' plus branch name plus commit count + regex: ^(user|feature|fix)[/-] + mode: ContinuousDeployment + increment: None + label: 'dev-{BranchName}' + # Not in v6.x: prevent-increment-of-merged-branch-version: true + track-merge-target: true + source-branches: ['develop', 'release', 'feature'] + pull-request: + regex: ^(pull|pull\-requests|pr)[/-] + label: PullRequest + mode: ContinuousDeployment +merge-message-formats: {} diff --git a/gitversion.yml b/gitversion.yml index 7d1cc407..117f187f 100644 --- a/gitversion.yml +++ b/gitversion.yml @@ -1,9 +1,9 @@ -# Last update: 24-11-27 +# Last update: 25-02-06 assembly-versioning-scheme: MajorMinorPatch # PDFSHARP_BUILD_VERSION are the days since 2005-01-01. #CHECK_BEFORE_RELEASE # Command Window: ? (DateTime.Now - new DateTime(2005, 1, 1)).Days; # C# Interactive: Console.WriteLine((DateTime.Now - new DateTime(2005, 1, 1)).Days); -assembly-file-versioning-format: '{Major}.{Minor}.{Patch}.{env:PDFSHARP_BUILD_VERSION ?? 7283}' +assembly-file-versioning-format: '{Major}.{Minor}.{Patch}.{env:PDFSHARP_BUILD_VERSION ?? 7341}' mode: Mainline assembly-informational-format: '{NuGetVersion}' branches: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5368dc95..ec393bfd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,6 +6,7 @@ enable NU1507 + false @@ -26,7 +27,7 @@ PDFsharp empira Software - © 2024 empira + © 2025 empira PDFsharp Team empira Software GmbH @@ -45,4 +46,4 @@ all - \ No newline at end of file + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index a87c9b4d..3e0c2628 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,7 @@ - + $(DefineConstants);USE_LONG_SIZE;TEST_CODE_xxx \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index cecab9c1..b3272e12 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -15,23 +15,25 @@ - + - - + + - + - - + + + + \ No newline at end of file diff --git a/src/foundation/nuget/src/MigraDoc.NuGet-gdi/ReleaseNotes.txt b/src/foundation/nuget/src/MigraDoc.NuGet-gdi/ReleaseNotes.txt index 9cd08e6a..46a1582f 100644 --- a/src/foundation/nuget/src/MigraDoc.NuGet-gdi/ReleaseNotes.txt +++ b/src/foundation/nuget/src/MigraDoc.NuGet-gdi/ReleaseNotes.txt @@ -1,5 +1,5 @@ -This is a version of PDFsharp and MigraDoc Foundation using .NET 6. -The package ’PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-MigraDoc-GDI’ and ’PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. +This is a version of PDFsharp and MigraDoc compatible with .NET 6 and higher. +The package ‘PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-MigraDoc-GDI’ and ‘PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/MigraDoc.NuGet-wpf/ReleaseNotes.txt b/src/foundation/nuget/src/MigraDoc.NuGet-wpf/ReleaseNotes.txt index 9cd08e6a..46a1582f 100644 --- a/src/foundation/nuget/src/MigraDoc.NuGet-wpf/ReleaseNotes.txt +++ b/src/foundation/nuget/src/MigraDoc.NuGet-wpf/ReleaseNotes.txt @@ -1,5 +1,5 @@ -This is a version of PDFsharp and MigraDoc Foundation using .NET 6. -The package ’PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-MigraDoc-GDI’ and ’PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. +This is a version of PDFsharp and MigraDoc compatible with .NET 6 and higher. +The package ‘PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-MigraDoc-GDI’ and ‘PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/MigraDoc.NuGet/ReleaseNotes.txt b/src/foundation/nuget/src/MigraDoc.NuGet/ReleaseNotes.txt index 9cd08e6a..46a1582f 100644 --- a/src/foundation/nuget/src/MigraDoc.NuGet/ReleaseNotes.txt +++ b/src/foundation/nuget/src/MigraDoc.NuGet/ReleaseNotes.txt @@ -1,5 +1,5 @@ -This is a version of PDFsharp and MigraDoc Foundation using .NET 6. -The package ’PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-MigraDoc-GDI’ and ’PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. +This is a version of PDFsharp and MigraDoc compatible with .NET 6 and higher. +The package ‘PDFsharp-MigraDoc’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-MigraDoc-GDI’ and ‘PDFsharp-MigraDoc-WPF’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/PDFsharp.NuGet-gdi/ReleaseNotes.txt b/src/foundation/nuget/src/PDFsharp.NuGet-gdi/ReleaseNotes.txt index 829ff6ee..f5df80ae 100644 --- a/src/foundation/nuget/src/PDFsharp.NuGet-gdi/ReleaseNotes.txt +++ b/src/foundation/nuget/src/PDFsharp.NuGet-gdi/ReleaseNotes.txt @@ -1,6 +1,6 @@ -This is a version of PDFsharp using .NET 6. -The package ’PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-gdi’ and ’PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. -We also publish packages that contain MigraDoc and reference the corresponding PDFsharp package. +This is a version of PDFsharp compatible with .NET 6 and higher. +The package ‘PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-gdi’ and ‘PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. +We also publish packages that contain PDFsharp plus MigraDoc. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/PDFsharp.NuGet-wpf/ReleaseNotes.txt b/src/foundation/nuget/src/PDFsharp.NuGet-wpf/ReleaseNotes.txt index 829ff6ee..f5df80ae 100644 --- a/src/foundation/nuget/src/PDFsharp.NuGet-wpf/ReleaseNotes.txt +++ b/src/foundation/nuget/src/PDFsharp.NuGet-wpf/ReleaseNotes.txt @@ -1,6 +1,6 @@ -This is a version of PDFsharp using .NET 6. -The package ’PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-gdi’ and ’PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. -We also publish packages that contain MigraDoc and reference the corresponding PDFsharp package. +This is a version of PDFsharp compatible with .NET 6 and higher. +The package ‘PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-gdi’ and ‘PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. +We also publish packages that contain PDFsharp plus MigraDoc. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/PDFsharp.NuGet/ReleaseNotes.txt b/src/foundation/nuget/src/PDFsharp.NuGet/ReleaseNotes.txt index 829ff6ee..f5df80ae 100644 --- a/src/foundation/nuget/src/PDFsharp.NuGet/ReleaseNotes.txt +++ b/src/foundation/nuget/src/PDFsharp.NuGet/ReleaseNotes.txt @@ -1,6 +1,6 @@ -This is a version of PDFsharp using .NET 6. -The package ’PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ’net6.0’ and ’netstandard2.0’. -The packages ’PDFsharp-gdi’ and ’PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ’net6.0-windows’ and ’net462’. -We also publish packages that contain MigraDoc and reference the corresponding PDFsharp package. +This is a version of PDFsharp compatible with .NET 6 and higher. +The package ‘PdfSharp’ can be used on any platform including Windows and Linux. The Target Frameworks are ‘net6.0’, ‘net8.0’ and ‘netstandard2.0’. +The packages ‘PDFsharp-gdi’ and ‘PDFsharp-wpf’ can be used under Windows only. The Target Frameworks are ‘net6.0-windows’, ‘net8.0-windows’ and ‘net462’. +We also publish packages that contain PDFsharp plus MigraDoc. See the project docs site for further information: https://docs.pdfsharp.net/link/readme-v6.2.html diff --git a/src/foundation/nuget/src/README.md b/src/foundation/nuget/src/README.md index 3093f708..0d977820 100644 --- a/src/foundation/nuget/src/README.md +++ b/src/foundation/nuget/src/README.md @@ -41,10 +41,10 @@ See Project Information for details. ### MigraDoc -MigraDoc Foundation - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF. +MigraDoc - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF. Text from MigraDoc 1.51: -MigraDoc Foundation - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF. +MigraDoc - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF. #### MigraDoc Core diff --git a/src/foundation/src/MigraDoc/features/MigraDoc.Features/MigraDoc.Features.csproj b/src/foundation/src/MigraDoc/features/MigraDoc.Features/MigraDoc.Features.csproj index 41f1d5ad..74954604 100644 --- a/src/foundation/src/MigraDoc/features/MigraDoc.Features/MigraDoc.Features.csproj +++ b/src/foundation/src/MigraDoc/features/MigraDoc.Features/MigraDoc.Features.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/DateField.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/DateField.cs index 368b1aec..d841d538 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/DateField.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/DateField.cs @@ -48,7 +48,7 @@ internal override void Serialize(Serializer serializer) if (!String.IsNullOrEmpty(Values.Format)) str += "[Format = \"" + Format + "\"]"; else - str += "[]"; //Has to be appended to avoid confusion with '[' in immediately following text. + str += "[]"; // Has to be appended to avoid confusion with '[' in immediately following text. serializer.Write(str); } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/PageField.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/PageField.cs index 2859532f..d50c2194 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/PageField.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/PageField.cs @@ -40,7 +40,7 @@ internal override void Serialize(Serializer serializer) if (!String.IsNullOrEmpty(Values.Format)) str += "[Format = \"" + Format + "\"]"; else - str += "[]"; //Has to be appended to avoid confusion with '[' in immediately following text. + str += "[]"; // Has to be appended to avoid confusion with '[' in immediately following text. serializer.Write(str); } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/SectionField.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/SectionField.cs index 2b6b410d..45c4cdc9 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/SectionField.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Fields/SectionField.cs @@ -40,7 +40,7 @@ internal override void Serialize(Serializer serializer) if (!String.IsNullOrEmpty(Values.Format)) str += "[Format = \"" + Format + "\"]"; else - str += "[]"; //Has to be appended to avoid confusion with '[' in directly following text. + str += "[]"; // Has to be appended to avoid confusion with '[' in directly following text. serializer.Write(str); } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlParser.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlParser.cs index ab837e78..00a983a7 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlParser.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlParser.cs @@ -728,7 +728,7 @@ void ParseFontSize(FormattedText formattedText, int nestingLevel) EnsureSymbol(Symbol.ParenLeft); ReadCode(); - //NYI: Check token for correct Unit format + // NYI: Check token for correct Unit format formattedText.Font.Size = Token; ReadCode(); EnsureSymbol(Symbol.ParenRight); @@ -960,7 +960,7 @@ void ParseHyperlink(ParagraphElements elements, int nestingLevel) ReadCode(); Hyperlink hyperlink = elements.AddHyperlink(""); - //NYI: Without name and type the hyperlink is senseless, so attributes need to be checked + // NYI: Without name and type the hyperlink is senseless, so attributes need to be checked if (Symbol == Symbol.BracketLeft) ParseAttributes(hyperlink); @@ -1255,8 +1255,8 @@ void ParseImage(Image image, bool paragraphContent) // Future syntax by example // \image("Name") // \image("Name")[...] - // \image{base64...} //NYI - // \image[...]{base64...} //NYI + // \image{base64...} // NYI + // \image[...]{base64...} // NYI Debug.Assert(image != null); try @@ -1903,7 +1903,7 @@ void ParseAttributeStatement(DocumentObject? doc) switch (Symbol) { case Symbol.Assign: - //DomValueDescriptor is needed from assignment routine. + // DomValueDescriptor is needed from assignment routine. var pvd = doc.Meta[valueName]; EnsureCondition(pvd != null, () => MdDomMsgs.InvalidValueName(valueName)); ParseAssign(doc, pvd); @@ -2609,7 +2609,7 @@ void AdjustToNextStatement() switch (Symbol) { case Symbol.Assign: - //read one more symbol + // Read one more symbol. ReadCode(); break; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs index e87f9f2c..d038f718 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs @@ -94,7 +94,7 @@ internal Symbol ReadCode() Symbol = Symbol.StringLiteral; TokenType = TokenType.StringLiteral; } - //NYI: else if (IsNumber()) + // NYI: else if (IsNumber()) // symbol = ScanNumber(false); else if (IsDigit(_currChar) || _currChar is '-' or '+' && IsDigit(_nextChar)) @@ -606,7 +606,7 @@ internal bool MoveToNextParagraphContentLine(bool rootLevel) return false; } - //TODO_OLD NiSc NYI + // TODO_OLD NiSc NYI //Check.NotImplemented("empty line at non-root level"); } break; @@ -816,7 +816,7 @@ internal char ScanNextChar() break; case Chars.LF: - //NYI: Unix uses LF only + // NYI: Unix uses LF only _idxLine++; _idxLinePos = 0; break; @@ -1410,7 +1410,7 @@ string ScanStringLiteral() } break; - //NYI: octal numbers + // NYI: octal numbers //case '0': //{ // ScanNextChar(); diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Meta.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Meta.cs index 5a19415d..6f736716 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Meta.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Meta.cs @@ -79,8 +79,8 @@ public void SetValue(DocumentObject dom, string name, object? val) if (trail != null) { - //REVIEW DaSt: dom.GetValue(name) and call SetValue recursively, - // or dom.GetValue(name.BisVorletzteElement) and then call SetValue? + // REVIEW DaSt: dom.GetValue(name) and call SetValue recursively, + // or dom.GetValue(name.BisVorletzteElement) and then call SetValue? var doc = (DocumentObject?)dom.GetValue(name); if (doc == null) throw new InvalidOperationException($"No value named '{name}' exists."); diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Color.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Color.cs index ed647ca2..4d1a106f 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Color.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Color.cs @@ -306,7 +306,7 @@ public static Color Parse(string color) // because the two operations are not always equivalent. // // While comparing a string to "" may seem like it’s checking for an empty string, it’s actually checking - // for a specific value of an empty string.This can be a problem if the empty string is represented by + // for a specific value of an empty string. This can be a problem if the empty string is represented by // something other than "" in the code, such as null or whitespace. // // On the other hand, comparing the length of a string to 0 is always checking for an empty string, @@ -314,7 +314,7 @@ public static Color Parse(string color) // a string to "", because it requires calculating the length of the string before doing the comparison. // // Therefore, the decision of whether to use a string comparison with "" or a length comparison to 0 should - // depend on the specific use case and the expected behavior of the code.In general, if you want to check if + // depend on the specific use case and the expected behavior of the code. In general, if you want to check if // a string is empty regardless of how it’s represented, a length comparison to 0 is a safer choice. // But if you specifically want to check for an empty string represented by "", then a string comparison with "" // may be more appropriate. @@ -328,16 +328,20 @@ public static Color Parse(string color) try { uint clr; - // Must use Enum.Parse because Enum.IsDefined is case-sensitive - try + // Do not parse strings that do not start with a letter, thus cannot be enum values. + if (!String.IsNullOrEmpty(color) && Char.IsLetter(color[0])) { - var obj = Enum.Parse(typeof(ColorName), color, true); - clr = (uint)obj; - return new(clr); - } - catch - { - // Ignore exception because it’s not a ColorName. + // Must use Enum.Parse because Enum.IsDefined is case-sensitive. + try + { + var obj = Enum.Parse(typeof(ColorName), color, true); + clr = (uint)obj; + return new(clr); + } + catch + { + // Ignore exception because it’s not a ColorName. + } } var numberStyle = NumberStyles.Integer; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs index 6ba7cad4..f417f857 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs @@ -224,21 +224,6 @@ public bool Subscript // .Kerning = 0 // .Animation = wdAnimationNone - ///// - ///// Gets a value indicating whether the specified font exists. - ///// - //[Obsolete("This function is removed from DocumentObjectModel and always returns false.")] - //public static bool Exists(string fontName) - //{ - // //System.Drawing.FontFamily[] families = System.Drawing.FontFamily.Families; - // //foreach (System.Drawing.FontFamily family in families) - // //{ - // // if (String.Compare(family.Name, fontName, true) == 0) - // // return true; - // //} - // return false; - //} - /// /// Get a bitmask of all non-null properties. /// diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Footnote.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Footnote.cs index 9cd92099..c8185b95 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Footnote.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Footnote.cs @@ -18,7 +18,7 @@ public class Footnote : DocumentObject, IVisitable public Footnote() { BaseValues = new FootnoteValues(this); - //NYI: Nested footnote check! + // NYI: Nested footnote check! } /// diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Hyperlink.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Hyperlink.cs index 85d3052e..2f5e97b7 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Hyperlink.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Hyperlink.cs @@ -514,7 +514,7 @@ public string Filename /// /// Gets or sets the target bookmark name of the Hyperlink. - ///Used for HyperlinkTypes ExternalBookmark and Bookmark. + /// Used for HyperlinkTypes ExternalBookmark and Bookmark. /// public string BookmarkName { diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs index de562a8b..a45c9b3f 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs @@ -232,7 +232,7 @@ public StyleType Type if (Values.BaseStyle == "") throw new ArgumentException("User-defined Style defined without a BaseStyle"); - //REVIEW KlPo4StLa Special treatment for DefaultParagraphFont faulty (DefaultParagraphFont not returned via styles["name"]). + // REVIEW KlPo4StLa Special treatment for DefaultParagraphFont faulty (DefaultParagraphFont not returned via styles["name"]). if (Values.BaseStyle == DefaultParagraphFontName) return styles[0]; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Styles.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Styles.cs index e7107148..10a69930 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Styles.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Styles.cs @@ -428,7 +428,7 @@ internal override void Serialize(Serializer serializer) fSerialized[0] = true; // consider DefaultParagraphFont as serialized bool[] fSerializePending = new bool[count]; // currently serializing bool newLine = false; // gets true if at least one style was written - //Start from 1 and do not serialize DefaultParagraphFont + // Start from 1 and do not serialize DefaultParagraphFont for (int index = 1; index < count; index++) { if (!fSerialized[index]) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Properties/MigraDocProductVersionInformation.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Properties/MigraDocProductVersionInformation.cs index 0ad14211..cddf77a9 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Properties/MigraDocProductVersionInformation.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Properties/MigraDocProductVersionInformation.cs @@ -1,7 +1,7 @@ // MigraDoc - Creating Documents on the Fly // See the LICENSE file in the solution root for more information. -#pragma warning disable 0436 +using PdfSharp.Internal; namespace MigraDoc { @@ -27,38 +27,38 @@ public static class MigraDocProductVersionInformation /// /// The major version number of the product. /// - public static readonly string VersionMajor = GitVersionInformation.Major; + public static readonly string VersionMajor = PdfSharpGitVersionInformation.Major; /// /// The minor version number of the product. /// - public static readonly string VersionMinor = GitVersionInformation.Minor; + public static readonly string VersionMinor = PdfSharpGitVersionInformation.Minor; /// /// The patch number of the product. /// - public static readonly string VersionPatch = GitVersionInformation.Patch; + public static readonly string VersionPatch = PdfSharpGitVersionInformation.Patch; /// /// The Version pre-release string for NuGet. /// - public static readonly string VersionPreRelease = GitVersionInformation.NuGetPreReleaseTagV2; + public static readonly string VersionPreRelease = PdfSharpGitVersionInformation.PreReleaseLabel; /// /// The PDF creator application information string. /// The PDF producer (created by) is PDFsharp anyway. /// - public static readonly string Creator = $"{Title} {GitVersionInformation.NuGetVersion} ({Url})"; + public static readonly string Creator = $"{Title} {PdfSharpGitVersionInformation.InformationalVersion} ({Url})"; /// /// The full version number. /// - public static readonly string Version = GitVersionInformation.MajorMinorPatch; + public static readonly string Version = PdfSharpGitVersionInformation.MajorMinorPatch; /// /// The full semantic version number created by GitVersion. /// - public static readonly string SemanticVersion = GitVersionInformation.SemVer; + public static readonly string SemanticVersion = PdfSharpGitVersionInformation.SemVer; /// /// The home page of this product. @@ -83,7 +83,7 @@ public static class MigraDocProductVersionInformation /// /// The copyright information. /// - public const string Copyright = "Copyright © 2001-2024 empira Software GmbH."; // Also used as NuGet Copyright. + public const string Copyright = "Copyright © 2001-2025 empira Software GmbH."; // Also used as NuGet Copyright. /// /// The trademark of the product. @@ -103,7 +103,7 @@ public static class MigraDocProductVersionInformation // ReSharper restore RedundantNameQualifier #endif -#if Not_used_anymore +#if Not_used_anymore // #DELETE 25-12-31 /// /// E.g. "2005-01-01", for use in NuGet Script @@ -137,7 +137,7 @@ public static class MigraDocProductVersionInformation /// /// Nuspec Doc: A long description of the package. This shows up in the right pane of the Add Package Dialog as well as in the Package Manager Console when listing packages using the Get-Package command. /// - public const string NuGetDescription = "MigraDoc Foundation - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF."; + public const string NuGetDescription = "MigraDoc - the Open Source .NET library that easily creates documents based on an object model with paragraphs, tables, styles, etc. and renders them into PDF or RTF."; /// /// Nuspec Doc: A description of the changes made in each release of the package. This field only shows up when the _Updates_ tab is selected and the package is an update to a previously installed package. diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.cs new file mode 100644 index 00000000..7356ef0e --- /dev/null +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.cs @@ -0,0 +1,542 @@ +#region MigraDoc - Creating Documents on the Fly +// +// Authors: +// Stefan Lange +// +// Copyright (c) 2001-2019 empira Software GmbH, Cologne Area (Germany) +// +// http://www.pdfsharp.com +// http://www.migradoc.com +// http://sourceforge.net/projects/pdfsharp +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; +using PdfSharp.Drawing; + +namespace MigraDoc.Rendering.Forms +{ + /// + /// Event handler for the PagePreview event. + /// + public delegate void PagePreviewEventHandler(object sender, EventArgs e); + + /// + /// Represents a Windows control to display a MigraDoc document. + /// + public class DocumentPreview : UserControl + { + private PdfSharp.Forms.PagePreview _preview; + private readonly Container _components = null!; + + /// + /// Initializes a new instance of the class. + /// + public DocumentPreview() + { + InitializeComponent(); + _preview!.ZoomChanged += PreviewZoomChanged; + _preview.SetRenderFunction(RenderPage); + } + + /// + /// Clean up any resources being used. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_components != null!) + _components.Dispose(); + } + base.Dispose(disposing); + } + + Zoom GetNewZoomFactor(int currentZoom, bool larger) + { + int[] values = new int[] + { + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 120, 140, 160, 180, 200, + 250, 300, 350, 400, 450, 500, 600, 700, 800 + }; + + if (currentZoom <= 10 && !larger) + return Zoom.Percent10; + if (currentZoom >= 800 && larger) + return Zoom.Percent800; + + if (larger) + { + for (int i = 0; i < values.Length; i++) + { + if (currentZoom < values[i]) + return (Zoom)values[i]; + } + } + else + { + for (int i = values.Length - 1; i >= 0; i--) + { + if (currentZoom > values[i]) + return (Zoom)values[i]; + } + } + return Zoom.Percent100; + } + + /// + /// Gets or sets the border style of the tree view control. + /// + /// + /// + /// One of the values. The default is . + /// + /// + /// The assigned value is not one of the values. + /// + /// + /// + /// + [DefaultValue((int)BorderStyle.Fixed3D), Description("Determines the style of the border."), Category("Preview Properties")] + public new BorderStyle BorderStyle + { + get { return _preview.BorderStyle; } + set { _preview.BorderStyle = value; } + } + + // #PFC + //w/// + ///// Gets or sets the private fonts of the document. If used, must be set before Ddl is set! + ///// + //public XPrivateFontCollection PrivateFonts + //{ + // get { return _privateFonts; } + // set { _privateFonts = value; } + //} + //internal XPrivateFontCollection _privateFonts; + + /// + /// Gets or sets a DDL string or file. + /// + public string Ddl + { + get { return _ddl; } + set + { + _ddl = value; + DdlUpdated(); + } + } + string _ddl = null!; + + /// + /// Gets or sets the current page. + /// + public int Page + { + get { return _page; } + set + { + try + { + if (_preview != null!) + { + if (_page != value) + { + _page = value; + PageInfo pageInfo = _renderer.FormattedDocument.GetPageInfo(_page); + if (pageInfo.Orientation == PdfSharp.PageOrientation.Portrait) + _preview.PageSize = new Size((int)pageInfo.Width, (int)pageInfo.Height); + else + _preview.PageSize = new Size((int)pageInfo.Height, (int)pageInfo.Width); + + _preview.Invalidate(); + OnPageChanged(EventArgs.Empty); + } + } + else + _page = -1; + } + // ReSharper disable once EmptyGeneralCatchClause + catch { } + } + } + int _page; + + /// + /// Gets the number of pages. + /// + public int PageCount + { + get + { + if (_renderer != null!) + return _renderer.FormattedDocument.PageCount; + return 0; + } + } + + /// + /// Goes to the first page. + /// + public void FirstPage() + { + if (_renderer != null!) + { + Page = 1; + _preview.Invalidate(); + OnPageChanged(EventArgs.Empty); + } + } + + /// + /// Goes to the next page. + /// + public void NextPage() + { + if (_renderer != null! && _page < PageCount) + { + Page++; + _preview.Invalidate(); + OnPageChanged(EventArgs.Empty); + } + } + + /// + /// Goes to the previous page. + /// + public void PrevPage() + { + if (_renderer != null! && _page > 1) + { + Page--; + } + } + + /// + /// Goes to the last page. + /// + public void LastPage() + { + if (_renderer != null!) + { + Page = PageCount; + _preview.Invalidate(); + OnPageChanged(EventArgs.Empty); + } + } + + ///// + ///// Gets or sets the working directory. + ///// + //public string WorkingDirectory + //{ + // get + // { + // return this.workingDirectory; + // } + // set + // { + // this.workingDirectory = value; + // } + //} + //string workingDirectory = ""; + + /// + /// Called when the Ddl property has changed. + /// + void DdlUpdated() + { + if (_ddl != null!) + { + _document = DocumentObjectModel.IO.DdlReader.DocumentFromString(_ddl); + _renderer = new DocumentRenderer(_document); + //_renderer.PrivateFonts = _privateFonts; + _renderer.PrepareDocument(); + Page = 1; + _preview.Invalidate(); + } + // if (this.job != null) + // this.job.Dispose(); + // + // if (this.ddl == null || this.ddl == "") + // return; + // + // this.job = new PrintJob(); + // this.job.Type = JobType.Standard; + // this.job.Ddl = this.ddl; + // this.job.WorkingDirectory = this.workingDirectory; + // this.job.InitDocument(); + // this.preview = this.job.GetPreview(this.Handle); + // this.previewHandle = this.preview.Hwnd; + // + // if (this.preview != null) + // this.preview.Page = 1; + } + + /// + /// Gets or sets the MigraDoc document that is previewed in this control. + /// + public MigraDoc.DocumentObjectModel.Document Document + { + get { return _document; } + set + { + if (value != null!) + { + _document = value; + _renderer = new DocumentRenderer(value); + _renderer.PrepareDocument(); + Page = 1; + _preview.Invalidate(); + } + else + { + _document = null!; + _renderer = null!; + _preview.Invalidate(); + } + } + } + DocumentObjectModel.Document _document = null!; + + /// + /// Gets the underlying DocumentRenderer of the document currently in preview, or null, if no renderer exists. + /// You can use this renderer for printing or creating PDF file. This evades the necessity to format the + /// document a second time when you want to print it or convert it into PDF. + /// + public DocumentRenderer Renderer + { + get { return _renderer; } + } + + void RenderPage(XGraphics gfx) + { + if (_renderer == null!) + return; + + if (_renderer != null!) + { + try + { + _renderer.RenderPage(gfx, _page); + } + // ReSharper disable once EmptyGeneralCatchClause + catch { }; + } + } + DocumentRenderer _renderer = null!; + + /// + /// Gets or sets a predefined zoom factor. + /// + [DefaultValue((int)Zoom.FullPage), Description("Determines the zoom of the page."), Category("Preview Properties")] + public Zoom Zoom + { + get { return (Zoom)_preview.Zoom; } + set + { + if (_preview.Zoom != (PdfSharp.Forms.Zoom)value) + { + _preview.Zoom = (PdfSharp.Forms.Zoom)value; + OnZoomChanged(EventArgs.Empty); + } + } + } + + /// + /// Gets or sets an arbitrary zoom factor. The range is from 10 to 800. + /// + [DefaultValue((int)Zoom.FullPage), Description("Determines the zoom of the page."), Category("Preview Properties")] + public int ZoomPercent + { + get { return _preview.ZoomPercent; } + set + { + if (_preview.ZoomPercent != value) + { + _preview.ZoomPercent = value; + OnZoomChanged(EventArgs.Empty); + } + } + } + //internal int _zoomPercent = 100; + + /// + /// Makes zoom factor smaller. + /// + public void MakeSmaller() + { + ZoomPercent = (int)GetNewZoomFactor(ZoomPercent, false); + } + + /// + /// Makes zoom factor larger. + /// + public void MakeLarger() + { + ZoomPercent = (int)GetNewZoomFactor(ZoomPercent, true); + } + + /// + /// Gets or sets the color of the page. + /// + /// The color of the page. + [Description("The background color of the page."), Category("Preview Properties")] + public Color PageColor + { + get { return _preview.PageColor; } + set { _preview.PageColor = value; } + } + Color _pageColor = Color.GhostWhite; + + /// + /// Gets or sets the color of the desktop. + /// + /// The color of the desktop. + [Description("The color of the desktop."), Category("Preview Properties")] + public Color DesktopColor + { + get { return _preview.DesktopColor; } + set { _preview.DesktopColor = value; } + } + internal Color desktopColor = SystemColors.ControlDark; + + /// + /// Gets or sets a value indicating whether to show scrollbars. + /// + /// true if [show scrollbars]; otherwise, false. + [DefaultValue(true), Description("Determines whether the scrollbars are visible."), Category("Preview Properties")] + public bool ShowScrollbars + { + get { return _preview.ShowScrollbars; } + set { _preview.ShowScrollbars = value; } + } + internal bool showScrollbars = true; + + /// + /// Gets or sets a value indicating whether to show the page. + /// + /// true if [show page]; otherwise, false. + [DefaultValue(true), Description("Determines whether the page visible."), Category("Preview Properties")] + public bool ShowPage + { + get { return _preview.ShowPage; } + set { _preview.ShowPage = value; } + } + internal bool showPage = true; + + /// + /// Gets or sets the page size in point. + /// + [Description("Determines the size (in points) of the page."), Category("Preview Properties")] + public Size PageSize + { + get { return new Size((int)_preview.PageSize.Width, (int)_preview.PageSize.Height); } + set { _preview.PageSize = value; } + } + + /// + /// Raises the ZoomChanged event when the zoom factor changed. + /// + protected virtual void OnZoomChanged(EventArgs e) + { + if (ZoomChanged != null!) + ZoomChanged(this, e); + } + + /// + /// Occurs when the zoom factor changed. + /// + [Description("Occurs when the zoom factor changed."), Category("Preview Properties")] + public event PagePreviewEventHandler ZoomChanged = null!; + + /// + /// Raises the ZoomChanged event when the current page changed. + /// + protected virtual void OnPageChanged(EventArgs e) + { + if (PageChanged != null!) + PageChanged(this, e); + } + + /// + /// Occurs when the current page changed. + /// + [Description("Occurs when the current page changed."), Category("Preview Properties")] + public event PagePreviewEventHandler PageChanged = null!; + + + #region Component Designer generated code + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + _preview = new PdfSharp.Forms.PagePreview(); + SuspendLayout(); + // + // preview + // + _preview.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; + _preview.DesktopColor = System.Drawing.SystemColors.ControlDark; + _preview.Dock = System.Windows.Forms.DockStyle.Fill; + _preview.Location = new System.Drawing.Point(0, 0); + _preview.Name = "_preview"; + _preview.PageColor = System.Drawing.Color.GhostWhite; + _preview.PageSize = new System.Drawing.Size(595, 842); + _preview.Size = new System.Drawing.Size(200, 200); + _preview.TabIndex = 0; + _preview.Zoom = PdfSharp.Forms.Zoom.FullPage; + _preview.ZoomPercent = 15; + // + // PagePreview + // + this.Controls.Add(_preview); + Name = "PagePreview"; + Size = new System.Drawing.Size(200, 200); + this.ResumeLayout(false); + + } + #endregion + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected override void OnMouseWheel(MouseEventArgs e) + { + int delta = e.Delta; + if (delta > 0) + PrevPage(); + else if (delta < 0) + NextPage(); + } + + private void PreviewZoomChanged(object? sender, EventArgs e) + { + OnZoomChanged(e); + } + } +} diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsg.de.resx b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.resx similarity index 73% rename from src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsg.de.resx rename to src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.resx index 1b7707d9..19dc0dd8 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsg.de.resx +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering-gdi/Rendering.Forms/DocumentPreview.resx @@ -1,4 +1,4 @@ - + - - - diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfDocumentRenderer.cs b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfDocumentRenderer.cs index aafafea6..ab92aabb 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfDocumentRenderer.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfDocumentRenderer.cs @@ -35,25 +35,6 @@ internal override void Render() /// public void Render(Document doc, string file, string workingDirectory) { - // #DELETE - //#if NET6_0_OR_GREATER - // var ansiEncoding = CodePagesEncodingProvider.Instance.GetEncoding(1252)!; - //#else - // Encoding? ansiEncoding; - // try - // { - // // Try to get ANSI encoding. - // ansiEncoding = Encoding.GetEncoding(1252); - // } - // catch (NotSupportedException) - // { - //#if NET6_0_OR_GREATER - // // Register provider if ANSI encoding is not available. - // Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - //#endif - // ansiEncoding = Encoding.GetEncoding(1252); - // } - //#endif var ansiEncoding = PdfEncoders.WinAnsiEncoding; StreamWriter? streamWriter = null; diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/Helper/TestHelper.cs b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/Helper/TestHelper.cs index 2cef5e9b..11be8c9b 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/Helper/TestHelper.cs +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/Helper/TestHelper.cs @@ -10,21 +10,6 @@ namespace MigraDoc.DocumentObjectModel.Tests.Helper { public class TestHelper { - //[Obsolete("Not needed anymore")] - //public static void InitializeFontResolver() - //{ - //} - - //[Obsolete("Not needed anymore")] - //public static void InitializeFontResolverWithSegoeWpAsDefault(Document doc) - //{ - // InitializeFontResolver(); - - // var style = doc.Styles[StyleNames.Normal]; - // style.Should().NotBeNull(); - // //style!.Font.Name = "segoe wp"; - //} - public static void RemoveStyles(Document doc) { for (var i = doc.Styles.Count - 1; i >= 0; i--) diff --git a/src/foundation/src/PDFsharp/docs/AboutFonts.md b/src/foundation/src/PDFsharp/docs/AboutFonts.md index 664e05b4..8934b30e 100644 --- a/src/foundation/src/PDFsharp/docs/AboutFonts.md +++ b/src/foundation/src/PDFsharp/docs/AboutFonts.md @@ -65,7 +65,7 @@ The **XFont** exists independently of a PDF docuemnt. You can think of an XFont as a moniker to a specific font face plus a specific font size. It is a fascade to XGlyphTypeface. Holds a reference to **OpenTypeDescriptor** to get all information about a the font face. -This is possible because once loaded font faces persists in memory. +This is possible because once loaded font faces persist in memory. Holds a reference to **XGlyphTypeface**. That keeps the reference to the font face. In contrast to an **XGlyphTypeface** a font holds the following additional information. * The font em-size used for rendering the font. @@ -152,7 +152,7 @@ Holds a reference ... The class **PdfCIDFont** represents character encoded PDF fonts. It is used together with **PdfType0Font** to represent text in a content by a sequence of the -glyph ids of the Unicode code points of the original UTF-32 encoded text. +glyph IDs of the Unicode code points of the original UTF-32 encoded text. ### PdfTrueTypeFont : PdfFont @@ -165,7 +165,7 @@ but the character set is limited to (roughly) ANSI characters (codepage 1252). The class **PdfType0Font** represents a PDF font dictionary derived from **PdfCIDFont**. Together with **PdfCIDFont** t is used to represent text in a content stream by a sequende of -glyph ids of the Unicode code points of the original text. This class hold the reference to the +glyph IDs of the Unicode code points of the original text. This class hold the reference to the ToUnicode map. ### sealed PdfFontDescriptor : PdfDictionary diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/DeviceInfos.cs b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/DeviceInfos.cs new file mode 100644 index 00000000..88e89fdb --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/DeviceInfos.cs @@ -0,0 +1,102 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Security; +//using System.Security.Permissions; + +#if GDI +namespace PdfSharp.Forms +{ + /// + /// Contains information about a physical device like a display or a printer. + /// + public struct DeviceInfos + { + /// + /// Width, in millimeters, of the physical screen or device. + /// + public int HorizontalSize; + + /// + /// Height, in millimeters, of the physical screen or device. + /// + public int VerticalSize; + + /// + /// Width, in pixels, of the screen or device. + /// + public int HorizontalResolution; + + /// + /// Height, in pixels, of the screen or device. + /// + public int VerticalResolution; + + /// + /// Number of pixels per logical inch along the screen or device width. + /// + public int LogicalDpiX; + + /// + /// Number of pixels per logical inch along the screen or device height. + /// + public int LogicalDpiY; + + /// + /// Number of pixels per physical inch along the screen or device width. + /// + public float PhysicalDpiX; + + /// + /// Number of pixels per physical inch along the screen or device height. + /// + public float PhysicalDpiY; + + /// + /// The ratio of LogicalDpiX and PhysicalDpiX. + /// + public float ScaleX; + + /// + /// The ratio of LogicalDpiY and PhysicalDpiY. + /// + public float ScaleY; + + /// + /// Gets a DeviceInfo for the specifed device context. + /// + [SuppressUnmanagedCodeSecurity] + public static DeviceInfos GetInfos(IntPtr hdc) + { + DeviceInfos devInfo; + + devInfo.HorizontalSize = GetDeviceCaps(hdc, HORZSIZE); + devInfo.VerticalSize = GetDeviceCaps(hdc, VERTSIZE); + devInfo.HorizontalResolution = GetDeviceCaps(hdc, HORZRES); + devInfo.VerticalResolution = GetDeviceCaps(hdc, VERTRES); + devInfo.LogicalDpiX = GetDeviceCaps(hdc, LOGPIXELSX); + devInfo.LogicalDpiY = GetDeviceCaps(hdc, LOGPIXELSY); + devInfo.PhysicalDpiX = devInfo.HorizontalResolution * 25.4f / devInfo.HorizontalSize; + devInfo.PhysicalDpiY = devInfo.VerticalResolution * 25.4f / devInfo.VerticalSize; + devInfo.ScaleX = devInfo.LogicalDpiX / devInfo.PhysicalDpiX; + devInfo.ScaleY = devInfo.LogicalDpiY / devInfo.PhysicalDpiY; + + return devInfo; + } + + [DllImport("gdi32.dll")] + static extern int GetDeviceCaps(IntPtr hdc, int capability); + // ReSharper disable InconsistentNaming + const int HORZSIZE = 4; + const int VERTSIZE = 6; + const int HORZRES = 8; + const int VERTRES = 10; + const int LOGPIXELSX = 88; + const int LOGPIXELSY = 90; + // ReSharper restore InconsistentNaming + } +} +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.cs b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.cs new file mode 100644 index 00000000..a162bca9 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.cs @@ -0,0 +1,1051 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +// Draw crosses to check layout calculation +#define DRAW_X_ + +#if DEBUG +// Test drawing in a bitmap. This is just a hack - don't use it! +#define DRAW_BMP_ +#endif + +using System; +using System.Diagnostics; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; +using PdfSharp.Drawing; + +#if !GDI +#error This file must only be included in GDI build. +#endif + +namespace PdfSharp.Forms +{ + /* TODOs + * + * o Call render event only once. -> introduce an UpdatePage() function + * + * Further stuff: set printable area; set text box (smallest rect that contains all content) + */ + /// + /// Represents a preview control for an XGraphics page. Can be used as an alternative to + /// System.Windows.Forms.PrintPreviewControl. + /// + public class PagePreview : UserControl + { + /// + /// A delegate for invoking the render function. + /// + public delegate void RenderEvent(XGraphics gfx); + + private Container components = null!; + + /// + /// Initializes a new instance of the class. + /// + public PagePreview() + { + _canvas = new PagePreviewCanvas(this); + Controls.Add(_canvas); + + _hScrollBar = new HScrollBar(); + _hScrollBar.Visible = _showScrollbars; + _hScrollBar.Scroll += OnScroll; + _hScrollBar.ValueChanged += OnValueChanged; + Controls.Add(_hScrollBar); + + _vScrollBar = new VScrollBar(); + _vScrollBar.Visible = _showScrollbars; + _vScrollBar.Scroll += OnScroll; + _vScrollBar.ValueChanged += OnValueChanged; + Controls.Add(_vScrollBar); + + InitializeComponent(); + //OnLayout(); + + _zoom = Zoom.FullPage; + _printableArea = new RectangleF(); + //virtPageSize = new Size(); + //showNonPrintableArea = false; + //virtualPrintableArea = new Rectangle(); + + _printableArea.GetType(); + //showNonPrintableArea.GetType(); + //virtualPrintableArea.GetType(); + + // Prevent bogus compiler warnings + _posOffset = new Point(); + _virtualPage = new Rectangle(); + } + + readonly PagePreviewCanvas _canvas; + readonly HScrollBar _hScrollBar; + readonly VScrollBar _vScrollBar; + + /// + /// Clean up any resources being used. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (components != null!) + components.Dispose(); + } + base.Dispose(disposing); + } + + /// + /// Gets or sets the border style of the control. + /// + /// + [DefaultValue((int)BorderStyle.Fixed3D), Description("Determines the style of the border."), Category("Preview Properties")] + public new BorderStyle BorderStyle + { + get { return _borderStyle; } + set + { + if (!Enum.IsDefined(typeof(BorderStyle), value)) + throw new InvalidEnumArgumentException("value", (int)value, typeof(BorderStyle)); + + if (value != _borderStyle) + { + _borderStyle = value; + LayoutChildren(); + } + } + } + BorderStyle _borderStyle = BorderStyle.Fixed3D; + + // [DefaultValue(2), Description("TODO..."), Category("Preview Properties")] + // public PageSize PageSize + // { + // get { return pageSize2; } + // set + // { + // if (!Enum.IsDefined(typeof(PageSize), value)) + // throw new InvalidEnumArgumentException("value", (int)value, typeof(PageSize)); + // + // if (value != pageSize2) + // { + // pageSize2 = value; + // // base.RecreateHandle(); + // // integralHeightAdjust = true; + // // try + // // { + // // base.Height = requestedHeight; + // // } + // // finally + // // { + // // integralHeightAdjust = false; + // // } + // } + // } + // } + // PageSize pageSize2; + + /// + /// Gets or sets the XGraphicsUnit of the page. + /// The default value is XGraphicsUnit.Point. + /// + public XGraphicsUnit PageGraphicsUnit + { + get { return _pageGraphicsUnit; } + set { _pageGraphicsUnit = value; } + } + XGraphicsUnit _pageGraphicsUnit = XGraphicsUnit.Point; + + /// + /// This property was renamed. Use new property PageGraphicsUnit. + /// + [Obsolete("Property renamed, use PageGraphicsUnit")] + public XGraphicsUnit PageUnit + { + get { return _pageGraphicsUnit; } + set { _pageGraphicsUnit = value; } + } + + /// + /// Gets or sets a predefined zoom factor. + /// + [DefaultValue((int)Zoom.FullPage), Description("Determines the zoom of the page."), Category("Preview Properties")] + public Zoom Zoom + { + get { return _zoom; } + set + { + if ((int)value < (int)Zoom.Mininum || (int)value > (int)Zoom.Maximum) + { + if (!Enum.IsDefined(typeof(Zoom), value)) + throw new InvalidEnumArgumentException("value", (int)value, typeof(Zoom)); + } + if (value != _zoom) + { + _zoom = value; + CalculatePreviewDimension(); + SetScrollBarRange(); + _canvas.Invalidate(); + } + } + } + Zoom _zoom; + + /// + /// Gets or sets an arbitrary zoom factor. The range is from 10 to 800. + /// + //[DefaultValue((int)Zoom.FullPage), Description("Determines the zoom of the page."), Category("Preview Properties")] + public int ZoomPercent + { + get { return _zoomPercent; } + set + { + if (value < (int)Zoom.Mininum || value > (int)Zoom.Maximum) + throw new ArgumentOutOfRangeException("value", value, + String.Format("Value must between {0} and {1}.", (int)Zoom.Mininum, (int)Zoom.Maximum)); + + if (value != _zoomPercent) + { + _zoom = (Zoom)value; + _zoomPercent = value; + CalculatePreviewDimension(); + SetScrollBarRange(); + _canvas.Invalidate(); + } + } + } + int _zoomPercent; + + /// + /// Gets or sets the color of the page. + /// + [Description("The background color of the page."), Category("Preview Properties")] + public Color PageColor + { + get { return _pageColor; } + set + { + if (value != _pageColor) + { + _pageColor = value; + Invalidate(); + } + } + } + Color _pageColor = Color.GhostWhite; + + /// + /// Gets or sets the color of the desktop. + /// + [Description("The color of the desktop."), Category("Preview Properties")] + public Color DesktopColor + { + get { return _desktopColor; } + set + { + if (value != _desktopColor) + { + _desktopColor = value; + Invalidate(); + } + } + } + internal Color _desktopColor = SystemColors.ControlDark; + + /// + /// Gets or sets a value indicating whether the scrollbars are visible. + /// + [DefaultValue(true), Description("Determines whether the scrollbars are visible."), Category("Preview Properties")] + public bool ShowScrollbars + { + get { return _showScrollbars; } + set + { + if (value != _showScrollbars) + { + _showScrollbars = value; + _hScrollBar.Visible = value; + _vScrollBar.Visible = value; + LayoutChildren(); + } + } + } + bool _showScrollbars = true; + + /// + /// Gets or sets a value indicating whether the page is visible. + /// + [DefaultValue(true), Description("Determines whether the page visible."), Category("Preview Properties")] + public bool ShowPage + { + get { return _showPage; } + set + { + if (value != _showPage) + { + _showPage = value; + _canvas.Invalidate(); + } + } + } + internal bool _showPage = true; + + /// + /// Gets or sets the page size in point. + /// + [Description("Determines the size (in points) of the page."), Category("Preview Properties")] + public XSize PageSize + { + get { return new XSize((int)_pageSize.Width, (int)_pageSize.Height); } + set + { + _pageSize = new SizeF((float)value.Width, (float)value.Height); + CalculatePreviewDimension(); + Invalidate(); + } + } + + /// + /// This is a hack for Visual Studio 2008. The designer uses reflection for setting the PageSize property. + /// This fails, even an implicit operator that converts Size to XSize exits. + /// + public Size PageSizeF + { + get { return new Size(Convert.ToInt32(_pageSize.Width), Convert.ToInt32(_pageSize.Height)); } + set + { + _pageSize = value; + CalculatePreviewDimension(); + Invalidate(); + } + } + + /// + /// Sets a delegate that is invoked when the preview wants to be painted. + /// + public void SetRenderFunction(Action renderEvent) + { + _renderAction = renderEvent; + Invalidate(); + } + Action _renderAction = null!; + + /// + /// Sets a delegate that is invoked when the preview wants to be painted. + /// + [Obsolete("Use SetRenderFunction")] + public void SetRenderEvent(RenderEvent renderEvent) + { + _renderAction = new Action(renderEvent); + Invalidate(); + } + + #region Component Designer generated code + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + Name = "PagePreview"; + Size = new System.Drawing.Size(228, 252); + } + #endregion + + /// + /// Raises the ZoomChanged event when the zoom factor changed. + /// + protected virtual void OnZoomChanged(EventArgs e) + { + if (ZoomChanged != null!) + ZoomChanged(this, e); + } + + /// + /// Occurs when the zoom factor changed. + /// + public event EventHandler ZoomChanged = null!; + + /// + /// Paints the background with the sheet of paper. + /// + protected override void OnPaintBackground(PaintEventArgs e) + { + // Accurate drawing prevents flickering + Graphics gfx = e.Graphics; + Rectangle clientRect = ClientRectangle; + int d = 0; + switch (_borderStyle) + { + case BorderStyle.FixedSingle: + gfx.DrawRectangle(SystemPens.WindowFrame, clientRect.X, clientRect.Y, clientRect.Width - 1, clientRect.Height - 1); + d = 1; + break; + + case BorderStyle.Fixed3D: + ControlPaint.DrawBorder3D(gfx, clientRect, Border3DStyle.Sunken); + d = 2; + break; + } + if (_showScrollbars) + { + int cxScrollbar = SystemInformation.VerticalScrollBarWidth; + int cyScrollbar = SystemInformation.HorizontalScrollBarHeight; + + gfx.FillRectangle(new SolidBrush(BackColor), + clientRect.Width - cxScrollbar - d, clientRect.Height - cyScrollbar - d, cxScrollbar, cyScrollbar); + } + } + + /// + /// Recalculates the preview dimension. + /// + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + CalculatePreviewDimension(); + SetScrollBarRange(); + } + + /// + /// Invalidates the canvas. + /// + protected override void OnInvalidated(InvalidateEventArgs e) + { + base.OnInvalidated(e); + _canvas.Invalidate(); + } + + /// + /// Layouts the child controls. + /// + protected override void OnLayout(LayoutEventArgs levent) + { + LayoutChildren(); + } + + void OnScroll(object? obj, ScrollEventArgs e) + { + ScrollBar? sc = obj as ScrollBar; + if (sc != null) + { + //Debug.WriteLine(String.Format("OnScroll: {0}, {1}", sc.Value, e.NewValue)); + } + } + + void OnValueChanged(object? obj, EventArgs e) + { + ScrollBar? sc = obj as ScrollBar; + if (sc != null) + { + //Debug.WriteLine(String.Format("OnValueChanged: {0}", sc.Value)); + if (sc == _hScrollBar) + _posOffset.X = sc.Value; + + else if (sc == _vScrollBar) + _posOffset.Y = sc.Value; + } + _canvas.Invalidate(); + } + + void LayoutChildren() + { + Invalidate(); + Rectangle clientRect = ClientRectangle; + switch (_borderStyle) + { + case BorderStyle.FixedSingle: + clientRect.Inflate(-1, -1); + break; + + case BorderStyle.Fixed3D: + clientRect.Inflate(-2, -2); + break; + } + int x = clientRect.X; + int y = clientRect.Y; + int cx = clientRect.Width; + int cy = clientRect.Height; + int cxScrollbar = 0; + int cyScrollbar = 0; + if (_showScrollbars && _vScrollBar != null! && _hScrollBar != null!) + { + cxScrollbar = _vScrollBar.Width; + cyScrollbar = _hScrollBar.Height; + _vScrollBar.Location = new Point(x + cx - cxScrollbar, y); + _vScrollBar.Size = new Size(cxScrollbar, cy - cyScrollbar); + _hScrollBar.Location = new Point(x, y + cy - cyScrollbar); + _hScrollBar.Size = new Size(cx - cxScrollbar, cyScrollbar); + } + if (_canvas != null!) + { + _canvas.Location = new Point(x, y); + _canvas.Size = new Size(cx - cxScrollbar, cy - cyScrollbar); + } + } + + /// + /// Calculates all values for drawing the page preview. + /// + internal void CalculatePreviewDimension(out bool zoomChanged) + { + // User may change display resolution while preview is running + Graphics gfx = Graphics.FromHwnd(IntPtr.Zero); + IntPtr hdc = gfx.GetHdc(); + DeviceInfos devInfo = DeviceInfos.GetInfos(hdc); + gfx.ReleaseHdc(hdc); + gfx.Dispose(); + int xdpiScreen = devInfo.LogicalDpiX; + int ydpiScreen = devInfo.LogicalDpiY; + //int cxScrollbar = SystemInformation.VerticalScrollBarWidth; + //int cyScrollbar = SystemInformation.HorizontalScrollBarHeight; + Rectangle rcCanvas = _canvas.ClientRectangle; + + Zoom zoomOld = _zoom; + int zoomPercentOld = _zoomPercent; + + // Border around virtual page in pixel. + const int leftBorder = 2; + const int rightBorder = 4; // because of shadow + const int topBorder = 2; + const int bottomBorder = 4; // because of shadow + const int horzBorders = leftBorder + rightBorder; + const int vertBorders = topBorder + bottomBorder; + + // Calculate new zoom factor. + switch (_zoom) + { + case Zoom.BestFit: + BestFit: + //zoomPercent = Convert.ToInt32(25400.0 * (rcCanvas.Width - (leftBorder + rightBorder)) / (this.pageSize.Width * xdpiScreen)); + _zoomPercent = (int)(7200f * (rcCanvas.Width - horzBorders) / (_pageSize.Width * xdpiScreen)); + //--zoomPercent; // prevent round up errors + break; + + case Zoom.TextFit: + // TODO_OLD: 'public Rectangle TextBox' property + goto BestFit; + //zoomPercent = LongFromReal (25400.0 / (_cxUsedPage + 0) * + // (rcWnd.CX () - 2 * cxScrollbar) / xdpiScreen) - 3; + //break; + + case Zoom.FullPage: + { + //int zoomX = Convert.ToInt32(25400.0 / (pageSize.Width) * + // (rcCanvas.Width - (leftBorder + rightBorder)) / xdpiScreen); + //int zoomY = Convert.ToInt32(25400.0 / (pageSize.Height) * + // (rcCanvas.Height - (topBorder + bottomBorder)) / ydpiScreen); + int zoomX = (int)(7200f * (rcCanvas.Width - horzBorders) / (_pageSize.Width * xdpiScreen)); + int zoomY = (int)(7200f * (rcCanvas.Height - vertBorders) / (_pageSize.Height * ydpiScreen)); + _zoomPercent = Math.Min(zoomX, zoomY); + //--zoomPercent; // prevent round up errors + } + break; + + case Zoom.OriginalSize: + _zoomPercent = (int)(0.5 + 200f / (devInfo.ScaleX + devInfo.ScaleY)); + _zoomPercent = (int)(0.5 + 100f / devInfo.ScaleX); + break; + + default: + _zoomPercent = (int)_zoom; + break; + } + + // Bound to zoom limits + _zoomPercent = Math.Max(Math.Min(_zoomPercent, (int)Zoom.Maximum), (int)Zoom.Mininum); + if ((int)_zoom > 0) + _zoom = (Zoom)_zoomPercent; + + // Size of page in preview window in pixel + _virtualPage.X = leftBorder; + _virtualPage.Y = topBorder; + _virtualPage.Width = (int)(_pageSize.Width * xdpiScreen * _zoomPercent / 7200); + _virtualPage.Height = (int)(_pageSize.Height * ydpiScreen * _zoomPercent / 7200); + + //// 2540 := (25.4mm * 100%) / 1mm + //m_VirtualPrintableArea.X = (int)printableArea.X * this.zoomPercent * xdpiScreen / 2540; + //m_VirtualPrintableArea.Y = (int)printableArea.Y * this.zoomPercent * xdpiScreen / 2540; + //m_VirtualPrintableArea.Width = (int)printableArea.Width * this.zoomPercent * xdpiScreen / 2540; + //m_VirtualPrintableArea.Height = (int)printableArea.Height * this.zoomPercent * xdpiScreen / 2540; + + // Border do not depend on zoom anymore + _virtualCanvas = new Size(_virtualPage.Width + horzBorders, _virtualPage.Height + vertBorders); + + // Adjust virtual canvas to at least actual window size + if (_virtualCanvas.Width < rcCanvas.Width) + { + _virtualCanvas.Width = rcCanvas.Width; + _virtualPage.X = leftBorder + (rcCanvas.Width - horzBorders - _virtualPage.Width) / 2; + } + if (_virtualCanvas.Height < rcCanvas.Height) + { + _virtualCanvas.Height = rcCanvas.Height; + _virtualPage.Y = topBorder + (rcCanvas.Height - vertBorders - _virtualPage.Height) / 2; + } + + zoomChanged = zoomOld != _zoom || zoomPercentOld != _zoomPercent; + if (zoomChanged) + OnZoomChanged(new EventArgs()); + } + + internal void CalculatePreviewDimension() + { + bool zoomChanged; + CalculatePreviewDimension(out zoomChanged); + } + + internal bool RenderPage(Graphics gfx) + { + //delete m_RenderContext; + //m_RenderContext = new HdcRenderContext(wdc.m_hdc); + + gfx.TranslateTransform(-_posOffset.X, -_posOffset.Y); + gfx.SetClip(new Rectangle(_virtualPage.X + 1, _virtualPage.Y + 1, _virtualPage.Width - 1, _virtualPage.Height - 1)); + + float scaleX = _virtualPage.Width / _pageSize.Width; + float scaleY = _virtualPage.Height / _pageSize.Height; + + //gfx.SetSmoothingMode(SmoothingModeAntiAlias); + //PaintBackground(gfx); + +#if DRAW_BMP + Matrix matrix = new Matrix(); + matrix.Translate(virtualPage.X, virtualPage.Y); + matrix.Translate(-posOffset.X, -this.posOffset.Y); + //matrix.Scale(scaleX, scaleY); + gfx.Transform = matrix; + +#if DRAW_X + gfx.DrawLine(Pens.Red, 0, 0, pageSize.Width, pageSize.Height); + gfx.DrawLine(Pens.Red, 0, pageSize.Height, pageSize.Width, 0); +#endif + if (renderEvent != null) + { + Bitmap bmp = new Bitmap(virtualPage.Width, this.virtualPage.Height, gfx); + Graphics gfx2 = Graphics.FromImage(bmp); + gfx2.Clear(pageColor); + gfx2.ScaleTransform(scaleX, scaleY); + gfx2.SmoothingMode = SmoothingMode.HighQuality; + XGraphics xgfx = XGraphics.FromGraphics(gfx2, new XSize(pageSize.Width, this.pageSize.Height)); + try + { + renderEvent(xgfx); + gfx.DrawImage(bmp, 0, 0); + } + finally + { + bmp.Dispose(); + } + } +#else + Matrix matrix = new Matrix(); + matrix.Translate(_virtualPage.X, _virtualPage.Y); + matrix.Translate(-_posOffset.X, -_posOffset.Y); + matrix.Scale(scaleX, scaleY); + gfx.Transform = matrix; + +#if DRAW_X + gfx.DrawLine(Pens.Red, 0, 0, pageSize.Width, pageSize.Height); + gfx.DrawLine(Pens.Red, 0, pageSize.Height, pageSize.Width, 0); +#endif + + if (_renderAction != null!) + { + gfx.SmoothingMode = SmoothingMode.HighQuality; + XGraphics xgfx = XGraphics.FromGraphics(gfx, new XSize(_pageSize.Width, _pageSize.Height), PageGraphicsUnit, null!); + try + { + _renderAction(xgfx); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Exception"); + } + } +#endif + + // Old C++ stuff, may be useful later... +#if false + switch (m_mode) + { + case RenderModeDirect: + { + delete m_PreviewMetafile; + m_PreviewMetafile = NULL; + + float cxPage = Metric::MillimetersToPoints(m_dimPage.cx / 10.0f); + float cyPage = Metric::MillimetersToPoints(m_dimPage.cy / 10.0f); + + float scaleX = virtualPage.Width / cxPage; + float scaleY = virtualPage.Height / cyPage; + + Graphics gfx(m_RenderContext); + gfx.SetSmoothingMode(SmoothingModeAntiAlias); + PaintBackground(gfx, &virtualPage); + + Matrix matrix; + matrix.Translate((float)virtualPage.X, (float)virtualPage.Y); + matrix.Translate((float) - m_posOffset.x, (float) -m_posOffset.y); + matrix.Scale(scaleX, scaleY); + + m_RenderContext->SetDefaultViewMatrix(&matrix); + gfx.ResetTransform(); + if (m_PreviewRenderer && m_PreviewRenderer->CanRender()) + m_PreviewRenderer->Render(&gfx, m_Page); + } + break; + + case RenderModeMetafile: + { + Graphics gfx(m_RenderContext); + if (m_PreviewMetafile == NULL) + { + float cxPage = Metric::MillimetersToPoints(m_dimPage.cx / 10.0f); + float cyPage = Metric::MillimetersToPoints(m_dimPage.cy / 10.0f); + + //float factor = 72.0f / 96.0f; + Rect rcLogicalPage(0, 0, (int)cxPage, (int)cyPage); + RectF rcFLogicalPage(0, 0, cxPage, cyPage); + + //DeviceName devname; + //DESKTOP::QueryDefaultPrinter(devname); //HACK DRUCKER MUSS DA SEIN! + //DeviceMode devmode(devname); + + //HDC hdc = ::CreateIC(devname.m_szDriver, devname.m_szDevice, devname.m_szOutput, devmode.m_pdm); + + //HDC hdc = m_Graphics->GetHDC(); + //HDC hdc = ::GetDC(NULL); + HDC hdc = ::CreateIC("DISPLAY", NULL, NULL, NULL); + + + float dpiX = gfx.GetDpiX(); + float dpiY = gfx.GetDpiY(); + + // Even Petzold would be surprised about that... + // Display | LaserJet + // DPI 96 : 120 | 300 + // physical device size in MM --------------------------------------------- + int horzSizeMM = ::GetDeviceCaps(hdc, HORZSIZE); // = 330 : 254 | 198 (not 210) + int vertSizeMM = ::GetDeviceCaps(hdc, VERTSIZE); // = 254 : 203 | 288 (hot 297) + + // device size in pixel + int horzSizePixel = ::GetDeviceCaps(hdc, HORZRES); // = 1280 : 1280 | 4676 + int vertSizePixel = ::GetDeviceCaps(hdc, VERTRES); // = 1024 : 1024 | 6814 + + // 'logical' device resolution in DPI + int logResX = ::GetDeviceCaps(hdc, LOGPIXELSX); // = 96 : 120 | 600 + int logResY = ::GetDeviceCaps(hdc, LOGPIXELSY); // = 96 : 120 | 600 + + // physical pixel size in .01 MM units + // accidentally(?) the result of GetPhysicalDimension! + //float X1 = 100.0f * horzSizeMM / horzSizePixel; // = 25.781250 : 19.843750 | 4.2343884 + //float Y1 = 100.0f * vertSizeMM / vertSizePixel; // = 24.804688 : 19.824219 | 4.2265925 + + // now we can get the 'physical' device resolution... + float phyResX = horzSizePixel / (horzSizeMM / 25.4f); // = 98.521210 : 128.00000 | 599.85052 + float phyResY = vertSizePixel / (vertSizeMM / 25.4f); // = 102.40000 : 128.12611 | 600.95691 + + // ...and rescale the size of the meta rectangle. + float magicX = logResX / phyResX; // = 0.97440946 : 0.93750000 | 1.0002491 + float magicY = logResY / phyResY; // = 0.93750000 : 0.93657720 | 0.99840766 + + // use A4 page in point + // adjust size of A4 page so that meta file fits with DrawImage... + RectF rcMagic(0, 0, magicX * cxPage, magicY * cyPage); + m_PreviewMetafile = new Metafile(hdc, rcMagic, MetafileFrameUnitPoint, + EmfTypeEmfPlusOnly, L"some description"); + + SizeF size; + float horzRes, vertRes; + float height, width; + MetafileHeader metafileHeader; + + // GetPhysicalDimension returns physical size of a pixel in .01 MM units!! + m_PreviewMetafile->GetPhysicalDimension(&size); + + horzRes = (float)m_PreviewMetafile->GetHorizontalResolution(); + vertRes = (float)m_PreviewMetafile->GetVerticalResolution(); + height = (float)m_PreviewMetafile->GetHeight(); + width = (float)m_PreviewMetafile->GetWidth(); + m_PreviewMetafile->GetMetafileHeader(&metafileHeader); + + Graphics gfxMf(m_PreviewMetafile); + dpiX = gfxMf.GetDpiX(); + dpiY = gfxMf.GetDpiY(); + + m_PreviewMetafile->GetPhysicalDimension(&size); + horzRes = (float)m_PreviewMetafile->GetHorizontalResolution(); + vertRes = (float)m_PreviewMetafile->GetVerticalResolution(); + height = (float)m_PreviewMetafile->GetHeight(); + width = (float)m_PreviewMetafile->GetWidth(); + m_PreviewMetafile->GetMetafileHeader(&metafileHeader); + + gfxMf.SetPageUnit(UnitPoint); + if (m_PreviewRenderer && m_PreviewRenderer->CanRender()) + m_PreviewRenderer->Render(&gfxMf, m_Page); + + ::DeleteDC(hdc); + } + if (m_PreviewMetafile) + { + gfx.SetSmoothingMode(SmoothingModeAntiAlias); + PaintBackground(gfx, &virtualPage); + //Matrix matrix(1, 0, 0, 1, (float) - m_posOffset.x, (float) - m_posOffset.y); + m_RenderContext->SetDefaultViewMatrix(&matrix); + gfx.ResetTransform(); + gfx.DrawImage(m_PreviewMetafile, virtualPage); + } + } + break; + + case RenderModeBitmap: + break; + } +#endif + return true; + } + + /// + /// Paints the background and the empty page. + /// + internal void PaintBackground(Graphics gfx) + { + // Draw sharp paper borders and shadow. + gfx.SmoothingMode = SmoothingMode.None; + //gfx.SetCompositingMode(CompositingModeSourceOver); // CompositingModeSourceCopy + //gfx.SetCompositingQuality(CompositingQualityHighQuality); + + gfx.TranslateTransform(-_posOffset.X, -_posOffset.Y); + + // Draw outer area. Use clipping to prevent flickering of page interior. + gfx.SetClip(new Rectangle(_virtualPage.X, _virtualPage.Y, _virtualPage.Width + 3, _virtualPage.Height + 3), CombineMode.Exclude); + gfx.SetClip(new Rectangle(_virtualPage.X + _virtualPage.Width + 1, _virtualPage.Y, 2, 2), CombineMode.Union); + gfx.SetClip(new Rectangle(_virtualPage.X, _virtualPage.Y + _virtualPage.Height + 1, 2, 2), CombineMode.Union); + gfx.Clear(_desktopColor); + +#if DRAW_X + gfx.DrawLine(Pens.Blue, 0, 0, virtualCanvas.Width, virtualCanvas.Height); + gfx.DrawLine(Pens.Blue, virtualCanvas.Width, 0, 0, virtualCanvas.Height); +#endif + gfx.ResetClip(); + +#if !DRAW_BMP + // Fill page interior. + SolidBrush brushPaper = new SolidBrush(_pageColor); + gfx.FillRectangle(brushPaper, _virtualPage.X + 1, _virtualPage.Y + 1, _virtualPage.Width - 1, _virtualPage.Height - 1); +#endif + + //// draw non printable area + //if (m_ShowNonPrintableArea) + //{ + //SolidBrush brushNPA(+DESKTOP::QuerySysColor((SYSCLR_3DLIGHT)) | 0xFF000000); + // + //gfx.FillRectangle(&brushNPA, virtualPage.X, virtualPage.Y, virtualPage.Width, rcPrintableArea.Y - virtualPage.Y); + //gfx.FillRectangle(&brushNPA, virtualPage.X, virtualPage.Y, rcPrintableArea.X - virtualPage.X, virtualPage.Height); + //gfx.FillRectangle(&brushNPA, rcPrintableArea.X + rcPrintableArea.Width, + //virtualPage.Y, virtualPage.X + virtualPage.Width - (rcPrintableArea.X + rcPrintableArea.Width), virtualPage.Height); + //gfx.FillRectangle(&brushNPA, virtualPage.X, rcPrintableArea.Y + rcPrintableArea.Height, + //virtualPage.Width, virtualPage.Y + virtualPage.Height - (rcPrintableArea.Y + rcPrintableArea.Height)); + //} + //DrawDash(gfx, virtualPage); + + // Draw page border and shadow. + Pen penPaperBorder = SystemPens.WindowText; + Brush brushShadow = SystemBrushes.ControlDarkDark; + gfx.DrawRectangle(penPaperBorder, _virtualPage); + gfx.FillRectangle(brushShadow, _virtualPage.X + _virtualPage.Width + 1, _virtualPage.Y + 2, 2, _virtualPage.Height + 1); + gfx.FillRectangle(brushShadow, _virtualPage.X + 2, _virtualPage.Y + _virtualPage.Height + 1, _virtualPage.Width + 1, 2); + } + + /// + /// Check clipping rectangle calculations. + /// + [Conditional("DEBUG")] + void DrawDash(Graphics gfx, Rectangle rect) + { + Pen pen = new Pen(Color.GreenYellow, 1); + pen.DashStyle = DashStyle.Dash; + gfx.DrawRectangle(pen, rect); + } + + /// + /// Adjusts scroll bars. + /// + void SetScrollBarRange() + { + // Windows 7 issue: Must invalidate scroll bars to ensure that + // the arrows are painted correctly. + + Rectangle clientRect = _canvas.ClientRectangle; + Size clientAreaSize = clientRect.Size; + + // Scroll range + int dx = _virtualCanvas.Width - clientAreaSize.Width; + int dy = _virtualCanvas.Height - clientAreaSize.Height; + + //bool extendX = clientAreaSize.Width < virtualCanvas.Width; + //bool extendY = clientAreaSize.Height < virtualCanvas.Height; + + if (ShowScrollbars && _hScrollBar != null!) + { + if (_posOffset.X > dx) + _hScrollBar.Value = _posOffset.X = dx; + + if (dx > 0) + { + _hScrollBar.Minimum = 0; + _hScrollBar.Maximum = _virtualCanvas.Width; + _hScrollBar.SmallChange = clientAreaSize.Width / 10; + _hScrollBar.LargeChange = clientAreaSize.Width; + _hScrollBar.Enabled = true; + } + else + { + _hScrollBar.Minimum = 0; + _hScrollBar.Maximum = 0; + _hScrollBar.Enabled = false; + } + _hScrollBar.Invalidate(); + } + + if (ShowScrollbars && _vScrollBar != null!) + { + if (_posOffset.Y > dy) + _vScrollBar.Value = _posOffset.Y = dy; + + if (dy > 0) + { + _vScrollBar.Minimum = 0; + _vScrollBar.Maximum = _virtualCanvas.Height; + _vScrollBar.SmallChange = clientAreaSize.Height / 10; + _vScrollBar.LargeChange = clientAreaSize.Height; + _vScrollBar.Enabled = true; + } + else + { + _vScrollBar.Minimum = 0; + _vScrollBar.Maximum = 0; + _vScrollBar.Enabled = false; + } + _vScrollBar.Invalidate(); + } + } + + ///// + ///// Calculates two interesting values... + ///// + //public static void GetMagicValues(IntPtr hdc, out float magicX, out float magicY) + //{ + // // Even Petzold would be surprised about that... + + // // Physical device size in MM + // int horzSizeMM = GetDeviceCaps(hdc, HORZSIZE); + // int vertSizeMM = GetDeviceCaps(hdc, VERTSIZE); + // // + // // Display size in pixels 1600 x 1200 1280 x 1024 + // // + // // My old Sony display with 96 DPI: --- 330 x 254 + // // My old Sony display with 120 DPI: --- 254 x 203 + // // My current Sony display with 96 DPI: 410 x 310 410 x 310 + // // My current Sony display with 120 DPI: 410 x 310 410 x 310 + // // My old Sony display with 96 DPI: --- 360 x 290 + // // My old Sony display with 120 DPI: --- 360 x 290 + // // My LaserJet 6L (300 DPI): 198 (not 210) x 288 (nscot 297) + + + // // Device size in pixel + // int horzSizePixel = GetDeviceCaps(hdc, HORZRES); + // int vertSizePixel = GetDeviceCaps(hdc, VERTRES); + // // + // // Display size in pixels 1600 x 1200 1280 x 1024 + // // + // // My old Sony display with 96 DPI: --- 1280 x 1024 + // // My old Sony display with 120 DPI: --- 1280 x 1024 + // // My current Sony display with 96 DPI: 1600 x 1200 1280 x 1024 + // // My current Sony display with 120 DPI: 1600 x 1200 1280 x 1024 + // // + // // My LaserJet 6L (600 DPI): 4676 x 6814 + + // // 'logical' device resolution in DPI + // int logResX = GetDeviceCaps(hdc, LOGPIXELSX); + // int logResY = GetDeviceCaps(hdc, LOGPIXELSY); + // // + // // Display size in pixels 1600 x 1200 1280 x 1024 + // // + // // My old Sony display with 96 DPI: --- 96 x 96 + // // My old Sony display with 120 DPI: --- 120 x 120 + // // My current Sony display with 96 DPI: 96 x 96 96 x 96 + // // My current Sony display with 120 DPI: 120 x 120 120 x 120 + // // + // // My LaserJet 6L (600 DPI): 600 x 600 + + // // physical pixel size in .01 MM units + // // accidentally(?) the result of GetPhysicalDimension! + // //float X1 = 100.0f * horzSizeMM / horzSizePixel; // = 25.781250 : 19.843750 | 4.2343884 + // //float Y1 = 100.0f * vertSizeMM / vertSizePixel; // = 24.804688 : 19.824219 | 4.2265925 + + // // Now we can get the 'physical' device resolution... + // float phyResX = horzSizePixel / (horzSizeMM / 25.4f); + // float phyResY = vertSizePixel / (vertSizeMM / 25.4f); + // // + // // Display size in pixels 1600 x 1200 1280 x 1024 + // // + // // My old Sony display with 96 DPI: --- 98.521210 x 102.40000 + // // My old Sony display with 120 DPI: --- 128.00000 x 128.12611 + // // My current Sony display with 96 DPI: 99.12195 x 98.32258 79.29756 x 83.90193 + // // My current Sony display with 120 DPI: 99.12195 x 98.32258 79.29756 x 83.90193 + // // + // // My LaserJet 6L (600 DPI): 599.85052 x 600.95691 + + // // ...and rescale the size of the meta rectangle. + // magicX = logResX / phyResX; + // magicY = logResY / phyResY; + // // + // // Display size in pixels 1600 x 1200 1280 x 1024 + // // + // // My old Sony display with 96 DPI: --- 0.97440946 x 0.93750000 + // // My old Sony display with 120 DPI: --- 0.93750000 x 0.93657720 + // // My current Sony display with 96 DPI: 0.968503952 x 0.976377964 1.21062994 x 1.14419293 + // // My current Sony display with 120 DPI: 1.21062994 x 1.22047246 1.51328743 x 1.43024123 + // // + // // My LaserJet 6L (600 DPI): 1.0002491 x 0.99840766 + //} + + //[DllImport("gdi32.dll")] + //static extern int GetDeviceCaps(IntPtr hdc, int capability); + //const int HORZSIZE = 4; + //const int VERTSIZE = 6; + //const int HORZRES = 8; + //const int VERTRES = 10; + //const int LOGPIXELSX = 88; + //const int LOGPIXELSY = 90; + + /// + /// Upper left corner of scroll area. + /// + Point _posOffset; + + /// + /// Real page size in point. + /// + SizeF _pageSize = PageSizeConverter.ToSize(PdfSharp.PageSize.A4).ToSizeF(); + + /// + /// Page in pixel relative to virtual canvas. + /// + Rectangle _virtualPage; + + /// + /// The size in pixels of an area that completely contains the virtual page and at least a small + /// border around it. If this area is larger than the canvas window, it is scrolled. + /// + Size _virtualCanvas; + + /// + /// Printable area in point. + /// + readonly RectangleF _printableArea; + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.resx b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.resx new file mode 100644 index 00000000..5d320b1a --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreview.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + False + + + False + + + PagePreview + + + False + + + 80 + + + (Default) + + + False + + + Private + + + 4, 4 + + \ No newline at end of file diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.cs b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.cs new file mode 100644 index 00000000..8b17ed04 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.cs @@ -0,0 +1,61 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using System; +using System.Collections; +using System.ComponentModel; +#if GDI +using System.Drawing; +using System.Windows.Forms; +#endif +#if Wpf +using System.Windows.Media; +#endif + +#if !GDI +#error This file must only be included in GDI build. +#endif + +namespace PdfSharp.Forms +{ + /// + /// Implements the control that previews the page. + /// + class PagePreviewCanvas : System.Windows.Forms.Control + { + public PagePreviewCanvas(PagePreview preview) + { + _preview = preview; + SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.DoubleBuffer, true); + } + PagePreview _preview; + + protected override void OnPaint(PaintEventArgs e) + { + if (!_preview._showPage) + return; + + Graphics gfx = e.Graphics; + bool zoomChanged; + _preview.CalculatePreviewDimension(out zoomChanged); + _preview.RenderPage(gfx); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + if (!_preview._showPage) + { + e.Graphics.Clear(_preview._desktopColor); + return; + } + bool zoomChanged; + _preview.CalculatePreviewDimension(out zoomChanged); + _preview.PaintBackground(e.Graphics); + } + + protected override void OnSizeChanged(EventArgs e) + { + Invalidate(); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.resx b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.resx new file mode 100644 index 00000000..3f337e08 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/PagePreviewCanvas.resx @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.0.0.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/RenderMode.cs b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/RenderMode.cs new file mode 100644 index 00000000..97adae46 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/RenderMode.cs @@ -0,0 +1,28 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +#if GDI +namespace PdfSharp.Forms +{ + /// + /// Specifies how to render the preview. + /// + public enum RenderMode + { + /// + /// Draw immediately. + /// + Direct = 0, + + /// + /// Draw using a metafile. + /// + Metafile = 1, + + /// + /// Draw using a bitmap image. + /// + Bitmap = 2 + } +} +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/Zoom.cs b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/Zoom.cs new file mode 100644 index 00000000..2cafee8f --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/Forms/enums/Zoom.cs @@ -0,0 +1,94 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +#if GDI +namespace PdfSharp.Forms +{ + /// + /// Defines a zoom factor used in the preview control. + /// + public enum Zoom + { + /// + /// The smallest possible zoom factor. + /// + Mininum = 10, + + /// + /// The largest possible zoom factor. + /// + Maximum = 800, + + /// + /// A pre-defined zoom factor. + /// + Percent800 = 800, + + /// + /// A pre-defined zoom factor. + /// + Percent600 = 600, + + /// + /// A pre-defined zoom factor. + /// + Percent400 = 400, + + /// + /// A pre-defined zoom factor. + /// + Percent200 = 200, + + /// + /// A pre-defined zoom factor. + /// + Percent150 = 150, + + /// + /// A pre-defined zoom factor. + /// + Percent100 = 100, + + /// + /// A pre-defined zoom factor. + /// + Percent75 = 75, + + /// + /// A pre-defined zoom factor. + /// + Percent50 = 50, + + /// + /// A pre-defined zoom factor. + /// + Percent25 = 25, + + /// + /// A pre-defined zoom factor. + /// + Percent10 = 10, + + /// + /// Sets the zoom factor so that the document fits horizontally into the window. + /// + BestFit = -1, + + /// + /// Sets the zoom factor so that the printable area of the document fits horizontally into the window. + /// Currently not yet implemented and the same as ZoomBestFit. + /// + TextFit = -2, + + /// + /// Sets the zoom factor so that the whole document fits completely into the window. + /// + FullPage = -3, + + /// + /// Sets the zoom factor so that the document is displayed in its real physical size (based on the DPI information returned from the OS for the current monitor). + /// + OriginalSize = -4, + } +} +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj index 7a0cb05a..ec4fc5d5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj @@ -13,6 +13,7 @@ + true @@ -145,7 +146,10 @@ - + + + + @@ -171,6 +175,7 @@ + @@ -178,9 +183,19 @@ - - - + + + + + + + + + + + + + @@ -243,11 +258,15 @@ + - + + + + @@ -405,6 +424,9 @@ + + + @@ -421,4 +443,19 @@ + + + True + True + FontResources.resx + + + + + + ResXFileCodeGenerator + FontResources.Designer.cs + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj index 9fc703b1..f17ebfcc 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj @@ -144,7 +144,10 @@ - + + + + @@ -170,6 +173,7 @@ + @@ -178,8 +182,18 @@ + + + + + + + + + - + + @@ -242,12 +256,16 @@ - - + + + + + - + + @@ -421,4 +439,19 @@ + + + True + True + FontResources.resx + + + + + + ResXFileCodeGenerator + FontResources.Designer.cs + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/Windows/PagePreview.xaml.cs b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/Windows/PagePreview.xaml.cs index 9e68764e..75b9ce7b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/Windows/PagePreview.xaml.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/Windows/PagePreview.xaml.cs @@ -75,7 +75,7 @@ public Canvas Canvas /// /// Sets the render function. /// - public void SetRenderFunction(Action renderFunction, RenderEvents renderEvents) + public void SetRenderFunction(Action renderFunction, RenderEvents? renderEvents = null) { if (canvas.Children.Count > 0) canvas.Children.Clear(); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/!internal/Directives.cs b/src/foundation/src/PDFsharp/src/PdfSharp/!internal/Directives.cs index 0ca45b67..da95b90e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/!internal/Directives.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/!internal/Directives.cs @@ -6,12 +6,20 @@ // Checks correct settings and obsolete conditional compilation symbols. // -#if !DEBUG && (TEST_CODE || TEST_CODE_) -// Ensure not to accidentally rename TEST_CODE to TEST_CODE_ -// This would compile code previously disabled with #if TEST_CODE_ -#warning ********************************************************* -#warning ***** TEST_CODE MUST BE UNDEFINED FOR FINAL RELEASE ***** -#warning ********************************************************* +#if !DEBUG && TEST_CODE +#warning *********************************************************** +#warning ***** TEST_CODE MUST BE UNDEFINED FOR FINAL RELEASE ***** +#warning *********************************************************** +#endif + +#if TEST_CODE_ // +// Ensure not to accidentally rename TEST_CODE to TEST_CODE_. +// This would compile code previously disabled with #if TEST_CODE_. +// Rename TEST_CODE always to TEST_CODE_xxx in Directory.Build.targets. +#warning ***************************************************** +#warning ***** TEST_CODE_ MUST NEVER BE DEFINED ***** +#warning ***** THIS ACCIDENTALLY ACTIVATES EXCLUDED CODE ***** +#warning ***************************************************** #endif #if GDI && WPF diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/IImageImporter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/IImageImporter.cs index a92c2298..ce495960 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/IImageImporter.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/IImageImporter.cs @@ -1,6 +1,8 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using Microsoft.Extensions.Logging; +using PdfSharp.Logging; using PdfSharp.Pdf; namespace PdfSharp.Drawing @@ -35,83 +37,103 @@ internal StreamReaderHelper(byte[] data) internal StreamReaderHelper(Stream stream, int streamLength) { - // TODO_OLD: Use the Stream as it is or ensure it is a MemoryStream? -#if CORE || GDI || WPF OriginalStream = stream; - // Only copy when necessary. - //MemoryStream ms; - if (stream is not MemoryStream ms) + + if (stream is MemoryStream ms) { - OwnedMemoryStream = ms = streamLength > -1 ? new MemoryStream(streamLength) : new(); -#if false - CopyStream(stream, ms); -#else - // For .NET 4: - stream.CopyTo(ms); -#endif + // If the given stream is a MemoryStream, work with it. + if (ms.TryGetBuffer(out var buffer)) + { + // Buffer is accessible - use it. + Data = buffer.Array ?? throw new ArgumentNullException(nameof(stream), "Stream has no content byte array."); + Length = (int)ms.Length; + } + else + { + // Buffer of given stream is not accessible, so read stream into new buffer. + OwnedMemoryStream = new(streamLength); + stream.CopyTo(OwnedMemoryStream); + Data = OwnedMemoryStream.GetBuffer(); + Length = (int)OwnedMemoryStream.Length; + PdfSharpLogHost.Logger.LogWarning("LoadImage: MemoryStream with buffer that is not publicly visible was used. " + + "For better performance, set 'publiclyVisible' to true when creating the MemoryStream."); + } } - Data = ms.GetBuffer(); - Length = (int)ms.Length; - if (Data.Length > Length) + else { - var tmp = new Byte[Length]; - Buffer.BlockCopy(Data, 0, tmp, 0, Length); - Data = tmp; + // If the given stream is not a MemoryStream, copy the stream to a new MemoryStream. + if (streamLength > -1) + { + // Simple case: length of stream is known, create a MemoryStream with correct buffer size. + OwnedMemoryStream = new(streamLength); + stream.CopyTo(OwnedMemoryStream); + Data = OwnedMemoryStream.GetBuffer(); + Length = (int)OwnedMemoryStream.Length; + } + else + { + // Complex case: length of stream is not known. + // This only occurs with streams that do not support the Length property. + OwnedMemoryStream = new(); + stream.CopyTo(OwnedMemoryStream); + Data = OwnedMemoryStream.GetBuffer(); + Length = (int)OwnedMemoryStream.Length; + // If buffer is larger than needed, create a new buffer with required size. + if (Data.Length > Length) + { + var tmp = new Byte[Length]; + Buffer.BlockCopy(Data, 0, tmp, 0, Length); + Data = tmp; + } + } } -#else - // For Win_RT there is no GetBuffer() => alternative implementation for Win_RT. - // TODO_OLD: Are there advantages of GetBuffer()? It should reduce LOH fragmentation. - this.stream = stream; - this.stream.Position = 0; - if (this.stream.Length > Int32.MaxValue) - throw new ArgumentException("Stream is too large.", nameof(stream)); - Length = (int)this.stream.Length; - Data = new byte[Length]; - this.stream.Read(Data, 0, Length); -#endif } internal byte GetByte(int offset) { if (CurrentOffset + offset >= Length) - { - Debug.Assert(false); - return 0; - } + throw new InvalidOperationException("Index out of range."); + return Data[CurrentOffset + offset]; } internal ushort GetWord(int offset, bool bigEndian) { - return (ushort)(bigEndian ? - (GetByte(offset) << 8) + GetByte(offset + 1) : - GetByte(offset) + (GetByte(offset + 1) << 8)); + if (CurrentOffset + offset + 1 >= Length) + throw new InvalidOperationException("Index out of range."); + + return (ushort)(bigEndian + ? (Data[CurrentOffset + offset++] << 8) + Data[CurrentOffset + offset] + : Data[CurrentOffset + offset++] + (Data[CurrentOffset + offset] << 8)); } internal uint GetDWord(int offset, bool bigEndian) { - return (uint)(bigEndian ? - (GetWord(offset, true) << 16) + GetWord(offset + 2, true) : - GetWord(offset, false) + (GetWord(offset + 2, false) << 16)); + if (CurrentOffset + offset + 3 >= Length) + throw new InvalidOperationException("Index out of range."); + + // Are you a good developer? + // What’s wrong with this code? + //return (bigEndian + // ? ((uint)Data[CurrentOffset + offset++] << 24) + ((uint)Data[CurrentOffset + offset++] << 16) + // + ((uint)Data[CurrentOffset + offset++] << 8) + Data[CurrentOffset + offset] + // : Data[CurrentOffset + offset++] + ((uint)Data[CurrentOffset + offset++] << 8)) + // + ((uint)Data[CurrentOffset + offset++] << 16) + ((uint)Data[CurrentOffset + offset] << 24); + return (uint)(bigEndian + ? (Data[CurrentOffset + offset++] << 24) + + (Data[CurrentOffset + offset++] << 16) + + (Data[CurrentOffset + offset++] << 8) + + Data[CurrentOffset + offset] + : Data[CurrentOffset + offset++] + + (Data[CurrentOffset + offset++] << 8) + + (Data[CurrentOffset + offset++] << 16) + + (Data[CurrentOffset + offset] << 24)); } - //static void CopyStream(Stream input, Stream output) - //{ - // var buffer = new byte[65536]; - // int read; - // while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - // { - // output.Write(buffer, 0, read); - // } - //} - /// /// Resets this instance. /// - public void Reset() - { - CurrentOffset = 0; - } + public void Reset() => CurrentOffset = 0; /// /// Gets the original stream. @@ -154,19 +176,18 @@ abstract class ImportedImage /// /// Initializes a new instance of the class. /// - protected ImportedImage(IImageImporter importer, ImagePrivateData? data) + protected ImportedImage(ImagePrivateData? data) { Data = data; if (data != null) data.Image = this; - //_importer = importer; } /// /// Initializes a new instance of the class. /// - protected ImportedImage(IImageImporter importer) - : this(importer, null) + protected ImportedImage() + : this(null) { } /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterBmp.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterBmp.cs index 27b689b5..a7e6c25b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterBmp.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterBmp.cs @@ -19,7 +19,7 @@ class ImageImporterBmp : ImageImporterRoot, IImageImporter // Note: TestBitmapFileHeader updates stream.CurrentOffset on success. ImagePrivateDataBitmap ipd = new ImagePrivateDataBitmap(stream.Data, stream.Length); - ImportedImage ii = new ImportedImageBitmap(this, ipd); + ImportedImage ii = new ImportedImageBitmap(ipd); ii.Information.DefaultDPI = 96; // Assume 96 DPI if information not provided in the file. if (TestBitmapInfoHeader(stream, ii, offsetImageData)) @@ -174,8 +174,8 @@ class ImportedImageBitmap : ImportedImage /// /// Initializes a new instance of the class. /// - public ImportedImageBitmap(IImageImporter importer, ImagePrivateDataBitmap data) - : base(importer, data) + public ImportedImageBitmap(ImagePrivateDataBitmap data) + : base(data) { } internal override ImageData PrepareImageData(PdfDocumentOptions options) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs index 667baf67..76330bac 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs @@ -26,7 +26,7 @@ class ImageImporterJpeg : ImageImporterRoot, IImageImporter stream.CurrentOffset += 2; var ipd = new ImagePrivateDataDct(stream.Data, stream.Length); - var ii = new ImportedImageJpeg(this, ipd); + var ii = new ImportedImageJpeg(ipd); ii.Information.DefaultDPI = 72; // Assume 72 DPI if information not provided in the file. if (TestJfifHeader(stream, ii)) { @@ -400,8 +400,8 @@ class ImportedImageJpeg : ImportedImage /// /// Initializes a new instance of the class. /// - public ImportedImageJpeg(IImageImporter importer, ImagePrivateDataDct data) - : base(importer, data) + public ImportedImageJpeg(ImagePrivateDataDct data) + : base(data) { } internal override ImageData PrepareImageData(PdfDocumentOptions options) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterPng.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterPng.cs index 3057e77a..d63d4f0a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterPng.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterPng.cs @@ -2,7 +2,7 @@ // See the LICENSE file in the solution root for more information. #if CORE -using PdfSharp.BigGustave; +using PdfSharp.Internal.Png.BigGustave; #endif using PdfSharp.Pdf; @@ -25,7 +25,7 @@ class ImageImporterPng : ImageImporterRoot, IImageImporter stream.CurrentOffset = 0; if (TestPngFileHeader(stream)) { - ImportedImage ii = new ImportedImagePng(this); + ImportedImage ii = new ImportedImagePng(); if (TestPngInfoHeader(stream, ii)) { return ii; @@ -551,8 +551,8 @@ class ImportedImagePng : ImportedImage /// /// Initializes a new instance of the class. /// - public ImportedImagePng(IImageImporter importer) - : base(importer) + public ImportedImagePng() + : base() { } internal override ImageData PrepareImageData(PdfDocumentOptions options) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs index b38c583f..bdb8b5f5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs @@ -10,7 +10,7 @@ namespace PdfSharp.Drawing.Layout /// /// Represents a very simple text formatter. /// If this class does not satisfy your needs on formatting paragraphs, I recommend taking a look - /// at MigraDoc Foundation. Alternatively, you should copy this class in your own source code and modify it. + /// at MigraDoc. Alternatively, you should copy this class in your own source code and modify it. /// public class XTextFormatter { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs index 280f8ff3..3ba76fe1 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs @@ -46,7 +46,7 @@ public XGraphicsPdfRenderer(PdfPage page, XGraphics gfx, XGraphicsPdfPageOptions _options = options; _gfx = gfx; _content = new StringBuilder(); - page.RenderContent!._pdfRenderer = this; // NRT + page.RenderContent!.SetRenderer(this); _gfxState = new PdfGraphicsState(this); } @@ -78,17 +78,17 @@ public void Close() var content2 = _page.RenderContent!; // NRT content2.CreateStream(PdfEncoders.RawEncoding.GetBytes(GetContent())); - _gfx = default!; - _page.RenderContent!._pdfRenderer = default!; - _page.RenderContent = default!; - _page = default!; + _gfx = null!; + _page.RenderContent!.SetRenderer(null); + _page.RenderContent = null!; + _page = null!; } else if (_form != null!) { _form._pdfForm!.CreateStream(PdfEncoders.RawEncoding.GetBytes(GetContent())); // NRT - _gfx = default!; - _form.PdfRenderer = default!; - _form = default!; + _gfx = null!; + _form.PdfRenderer = null!; + _form = null!; } } @@ -510,6 +510,11 @@ public void DrawString(string s, XFont font, XBrush brush, XRect rect, XStringFo isAnsi = fontType == FontType.TrueTypeWinAnsi; } } + if (font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont) + { + fontType = FontType.Type1StandardFont; + isAnsi = true; + } //Realize(font, brush, boldSimulation ? 2 : 0); //Realize(glyphTypeface, font.Size, brush, boldSimulation ? 2 : 0, font.FontTypeFromUnicodeFlag); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs index 329474fa..93711f7b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs @@ -19,6 +19,7 @@ using PdfSharp.Fonts; using PdfSharp.Fonts.Internal; using PdfSharp.Fonts.OpenType; +using PdfSharp.Fonts.StandardFonts; using PdfSharp.Internal; using PdfSharp.Pdf; using PdfSharp.Pdf.Advanced; @@ -33,6 +34,18 @@ namespace PdfSharp.Drawing [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] public sealed class XFont { + /// + /// Creates a new XFont based on an existing XFont with the specified emSize and options. + /// + /// + /// + /// + /// + public static XFont FromExisting(XFont existingFont, double emSize, XPdfFontOptions? pdfOptions = null) + { + return new XFont(existingFont.GlyphTypeface, emSize, pdfOptions ?? existingFont.PdfOptions); + } + /// /// Initializes a new instance of the class. /// @@ -59,6 +72,9 @@ public XFont(string familyName, double emSize, XFontStyleEx style) /// The em size. /// The font style. /// Additional PDF options. + /// + /// is specified in the and is not one of the standard fonts defined in + /// public XFont(string familyName, double emSize, XFontStyleEx style, XPdfFontOptions pdfOptions) { _familyName = familyName; @@ -128,7 +144,7 @@ public XFont(XTypeface typeface, double emSize, XPdfFontOptions? pdfOptions = nu public XFont(XGlyphTypeface glyphTypeface, double emSize, XPdfFontOptions? pdfOptions = null, XStyleSimulations? styleSimulations = null) { GlyphTypeface = glyphTypeface; - _familyName = glyphTypeface.FamilyName; + _familyName = glyphTypeface.XFamilyName ?? glyphTypeface.FamilyName; _emSize = emSize; _style = (glyphTypeface.IsBold ? XFontStyleEx.Bold : XFontStyleEx.Regular) | (glyphTypeface.IsItalic ? XFontStyleEx.Italic : XFontStyleEx.Regular); @@ -336,6 +352,13 @@ void Initialize() if (_familyName == "Segoe UI Semilight" && (_style & XFontStyleEx.BoldItalic) == XFontStyleEx.Italic) _ = typeof(int); #endif + if (_pdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont && + !StandardFontData.IsStandardFont(_familyName)) + { + throw new InvalidOperationException( + $"The font '{_familyName}' is not one of the 14 standard-fonts and cannot be used with the embedding-option '{nameof(PdfFontEmbedding.OmitStandardFont)}'"); + } + FontResolvingOptions fontResolvingOptions = OverrideStyleSimulations ? new FontResolvingOptions(_style, StyleSimulations) : new FontResolvingOptions(_style); @@ -344,6 +367,7 @@ void Initialize() { // In principle an XFont is an XGlyphTypeface plus an em-size. GlyphTypeface = XGlyphTypeface.GetOrCreateFrom(_familyName, fontResolvingOptions); + GlyphTypeface.XFamilyName = _familyName; GlyphTypeface.FontFace.SetFontEmbedding(_pdfOptions.FontEmbedding); } @@ -765,5 +789,20 @@ string DebuggerDisplay { get => Invariant($"font=('{Name2}' {Size:0.##}{(Bold ? " bold" : "")}{(Italic ? " italic" : "")} {GlyphTypeface.StyleSimulations})"); } + + /// + /// Gets the list of characters supported by this font.

+ ///
+ /// The list of characters supported by this font + /// + /// Applications may use to convert these values to a valid string. + /// + public HashSet GetSupportedCharacters() + { + characterSet ??= OpenTypeDescriptor.FontFace.cmap.GetSupportedCharacters(); + return characterSet; + } + + HashSet characterSet = null!; } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFontSource.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFontSource.cs index 47aaa679..f3fff30a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFontSource.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFontSource.cs @@ -45,14 +45,20 @@ public class XFontSource /// Gets an existing font source or creates a new one. /// A new font source is cached in font factory. ///
- public static XFontSource GetOrCreateFrom(byte[] bytes) + /// The bytes of the font + /// If true, a new font source is cached in font factory. + /// + /// Setting to false allows inspecting the font-data without caching the whole font + /// + public static XFontSource GetOrCreateFrom(byte[] bytes, bool cache = true) { ulong key = FontHelper.CalcChecksum(bytes); if (!FontFactory.TryGetFontSourceByKey(key, out var fontSource)) { fontSource = new XFontSource(bytes, key); // Theoretically the font source could be created by a different thread in the meantime. - fontSource = FontFactory.CacheFontSource(fontSource); + if (cache) + fontSource = FontFactory.CacheFontSource(fontSource); } return fontSource; } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGlyphTypeface.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGlyphTypeface.cs index 45a7df93..4ff075db 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGlyphTypeface.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGlyphTypeface.cs @@ -22,10 +22,13 @@ #endif using Microsoft.Extensions.Logging; using PdfSharp.Fonts; +using PdfSharp.Fonts.StandardFonts; using PdfSharp.Fonts.Internal; using PdfSharp.Fonts.OpenType; using PdfSharp.Internal; using PdfSharp.Logging; +using PdfSharp.Pdf; +using PdfSharp.Pdf.Advanced; namespace PdfSharp.Drawing { @@ -50,7 +53,7 @@ public sealed class XGlyphTypeface FontFamily = fontFamily; FontSource = fontSource; - FontFace = OpenTypeFontFace.CetOrCreateFrom(fontSource); + FontFace = OpenTypeFontFace.GetOrCreateFrom(fontSource); // Check why it fails. //Debug.Assert(ReferenceEquals(FontSource.FontFace, FontFace)); @@ -67,7 +70,7 @@ public sealed class XGlyphTypeface FontFamily = fontFamily; FontSource = fontSource; - FontFace = OpenTypeFontFace.CetOrCreateFrom(fontSource); + FontFace = OpenTypeFontFace.GetOrCreateFrom(fontSource); Debug.Assert(ReferenceEquals(FontSource.FontFace, FontFace)); _gdiFont = gdiFont; @@ -105,7 +108,7 @@ public XGlyphTypeface(XFontSource fontSource) FontSource = fontSource; StyleSimulations = styleSimulations; - FontFace = OpenTypeFontFace.CetOrCreateFrom(fontSource); + FontFace = OpenTypeFontFace.GetOrCreateFrom(fontSource); Debug.Assert(ReferenceEquals(FontSource.FontFace, FontFace)); WpfTypeface = wpfTypeface; @@ -411,6 +414,16 @@ void Initialize() ///
public string FamilyName { get; private set; } = default!; + /// + /// When constructed from an XFont, gets the family-name with which the XFont was initialized.

+ /// This is a HACK to pass the family-name used to create an to + /// .

+ /// Required for the in conjuction with + /// where the actual face-name (as stored in the font) + /// differs from the name used to create an . + ///
+ internal string XFamilyName { get; set; } = default!; + /// /// Gets the English subfamily name of the font, /// for example "Bold". diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs index 595fbae8..880ed3ef 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs @@ -185,7 +185,7 @@ public sealed class XGraphics : IDisposable /// The page unit. /// The page direction. /// The render events. - XGraphics(Canvas canvas, XSize size, XGraphicsUnit pageUnit, XPageDirection pageDirection, RenderEvents renderEvents) + XGraphics(Canvas canvas, XSize size, XGraphicsUnit pageUnit, XPageDirection pageDirection, RenderEvents? renderEvents = null) { //throw new ArgumentNullException("canvas"); if (canvas == null!) @@ -412,7 +412,7 @@ public sealed class XGraphics : IDisposable /// /// Initializes a new instance of the XGraphics class used for drawing on a form. /// - XGraphics(XForm form, RenderEvents renderEvents) + XGraphics(XForm form, RenderEvents? renderEvents = null) { if (form == null!) throw new ArgumentNullException(nameof(form)); @@ -571,7 +571,7 @@ public static XGraphics CreateMeasureContext(XSize size, XGraphicsUnit pageUnit, /// /// Creates a new instance of the XGraphics class from a System.Drawing.Graphics object. /// - public static XGraphics FromGraphics(Graphics graphics, XSize size, RenderEvents renderEvents) + public static XGraphics FromGraphics(Graphics graphics, XSize size, RenderEvents? renderEvents = null) { // Creating a new instance is by design. var gfx = new XGraphics(graphics, size, XGraphicsUnit.Point, XPageDirection.Downwards, renderEvents); @@ -584,7 +584,7 @@ public static XGraphics FromGraphics(Graphics graphics, XSize size, RenderEvents /// /// Creates a new instance of the XGraphics class from a System.Drawing.Graphics object. /// - public static XGraphics FromGraphics(Graphics graphics, XSize size, XGraphicsUnit unit, RenderEvents renderEvents) + public static XGraphics FromGraphics(Graphics graphics, XSize size, XGraphicsUnit unit, RenderEvents? renderEvents = null) { // Creating a new instance is by design. var gfx = new XGraphics(graphics, size, unit, XPageDirection.Downwards, renderEvents); @@ -618,7 +618,7 @@ public static XGraphics FromGraphics(Graphics graphics, XSize size, XGraphicsUni /// /// Creates a new instance of the XGraphics class from a System.Windows.Media.DrawingContext object. /// - public static XGraphics FromDrawingContext(DrawingContext drawingContext, XSize size, XGraphicsUnit unit, RenderEvents renderEvents) + public static XGraphics FromDrawingContext(DrawingContext drawingContext, XSize size, XGraphicsUnit unit, RenderEvents? renderEvents = null) { var gfx = new XGraphics(drawingContext, size, unit, XPageDirection.Downwards, renderEvents); @@ -632,7 +632,7 @@ public static XGraphics FromDrawingContext(DrawingContext drawingContext, XSize /// /// Creates a new instance of the XGraphics class from a System.Windows.Media.DrawingContext object. /// - public static XGraphics FromCanvas(Canvas canvas, XSize size, XGraphicsUnit unit, RenderEvents renderEvents) + public static XGraphics FromCanvas(Canvas canvas, XSize size, XGraphicsUnit unit, RenderEvents? renderEvents = null) { var gfx = new XGraphics(canvas, size, unit, XPageDirection.Downwards, renderEvents); @@ -791,13 +791,13 @@ public static XGraphics FromForm(XForm form) /// /// Creates a new instance of the XGraphics class from a PdfSharp.Drawing.XForm object. /// - public static XGraphics? FromImage(XImage image, RenderEvents renderEvents) + public static XGraphics? FromImage(XImage image, RenderEvents? renderEvents = null) => FromImage(image, XGraphicsUnit.Point, renderEvents); /// /// Creates a new instance of the XGraphics class from a PdfSharp.Drawing.XImage object. /// - public static XGraphics? FromImage(XImage image, XGraphicsUnit unit, RenderEvents renderEvents) + public static XGraphics? FromImage(XImage image, XGraphicsUnit unit, RenderEvents? renderEvents = null) { if (image == null) throw new ArgumentNullException(nameof(image)); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XMatrix.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XMatrix.cs index 5685bb27..f03a5f43 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XMatrix.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XMatrix.cs @@ -181,9 +181,7 @@ public void Multiply(XMatrix matrix, XMatrixOrder order) /// /// Appends a translation of the specified offsets to this matrix. /// - [Obsolete( - "Use TranslateAppend or TranslatePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use TranslateAppend or TranslatePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void Translate(double offsetX, double offsetY) { throw new InvalidOperationException("Temporarily out of order."); @@ -261,8 +259,7 @@ public void Translate(double offsetX, double offsetY, XMatrixOrder order) /// /// Appends the specified scale vector to this matrix. /// - [Obsolete("Use ScaleAppend or ScalePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use ScaleAppend or ScalePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void Scale(double scaleX, double scaleY) { this = CreateScaling(scaleX, scaleY) * this; @@ -315,8 +312,7 @@ public void Scale(double scaleX, double scaleY, XMatrixOrder order) /// /// Scales the matrix with the specified scalar. /// - [Obsolete("Use ScaleAppend or ScalePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use ScaleAppend or ScalePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] // ReSharper disable InconsistentNaming public void Scale(double scaleXY) // ReSharper restore InconsistentNaming @@ -358,8 +354,7 @@ public void Scale(double scaleXY, XMatrixOrder order) /// /// Function is obsolete. /// - [Obsolete("Use ScaleAtAppend or ScaleAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use ScaleAtAppend or ScaleAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void ScaleAt(double scaleX, double scaleY, double centerX, double centerY) { throw new InvalidOperationException("Temporarily out of order."); @@ -386,8 +381,7 @@ public void ScaleAtPrepend(double scaleX, double scaleY, double centerX, double /// /// Function is obsolete. /// - [Obsolete("Use RotateAppend or RotatePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use RotateAppend or RotatePrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void Rotate(double angle) { throw new InvalidOperationException("Temporarily out of order."); @@ -457,9 +451,7 @@ public void Rotate(double angle, XMatrixOrder order) /// /// Function is obsolete. /// - [Obsolete( - "Use RotateAtAppend or RotateAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use RotateAtAppend or RotateAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void RotateAt(double angle, double centerX, double centerY) { throw new InvalidOperationException("Temporarily out of order."); @@ -488,9 +480,7 @@ public void RotateAtPrepend(double angle, double centerX, double centerY) /// /// Rotates the matrix with the specified angle at the specified point. /// - [Obsolete( - "Use RotateAtAppend or RotateAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use RotateAtAppend or RotateAtPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void RotateAt(double angle, XPoint point) { throw new InvalidOperationException("Temporarily out of order."); @@ -539,8 +529,7 @@ public void RotateAt(double angle, XPoint point, XMatrixOrder order) /// /// Function is obsolete. /// - [Obsolete("Use ShearAppend or ShearPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", - true)] + [Obsolete("Use ShearAppend or ShearPrepend explicitly, because in GDI+ and WPF the defaults are contrary.", true)] public void Shear(double shearX, double shearY) { throw new InvalidOperationException("Temporarily out of order."); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPoint.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPoint.cs index e3e67b5b..b80d06a6 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPoint.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPoint.cs @@ -109,18 +109,12 @@ public override bool Equals(object? o) /// /// Indicates whether this instance and a specified point are equal. /// - public bool Equals(XPoint value) - { - return Equals(this, value); - } + public bool Equals(XPoint value) => Equals(this, value); /// /// Returns the hash code for this instance. /// - public override int GetHashCode() - { - return X.GetHashCode() ^ Y.GetHashCode(); - } + public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); /// /// Parses the point from a string. @@ -177,45 +171,30 @@ public double Y /// /// Converts this XPoint to a System.Drawing.Point. /// - public PointF ToPointF() - { - return new PointF((float)_x, (float)_y); - } + public PointF ToPointF() => new((float)_x, (float)_y); #endif #if WPF || WUI /// /// Converts this XPoint to a System.Windows.Point. /// - public SysPoint ToPoint() - { - return new SysPoint(_x, _y); - } + public SysPoint ToPoint() => new(_x, _y); #endif /// - /// Converts this XPoint to a human readable string. + /// Converts this XPoint to a human-readable string. /// - public override string ToString() - { - return ConvertToString(null, null); - } + public override string ToString() => ConvertToString(null, null); /// - /// Converts this XPoint to a human readable string. + /// Converts this XPoint to a human-readable string. /// - public string ToString(IFormatProvider provider) - { - return ConvertToString(null, provider); - } + public string ToString(IFormatProvider provider) => ConvertToString(null, provider); /// - /// Converts this XPoint to a human readable string. + /// Converts this XPoint to a human-readable string. /// - string IFormattable.ToString(string? format, IFormatProvider? provider) - { - return ConvertToString(format, provider); - } + string IFormattable.ToString(string? format, IFormatProvider? provider) => ConvertToString(format, provider); /// /// Implements ToString. @@ -242,132 +221,96 @@ public void Offset(double offsetX, double offsetY) /// /// Adds a point and a vector. /// - public static XPoint operator +(XPoint point, XVector vector) - { - return new XPoint(point._x + vector.X, point._y + vector.Y); - } + public static XPoint operator +(XPoint point, XVector vector) => new(point._x + vector.X, point._y + vector.Y); /// /// Adds a point and a size. /// public static XPoint operator +(XPoint point, XSize size) // TODO_OLD: make obsolete - { - return new XPoint(point._x + size.Width, point._y + size.Height); - } + => new(point._x + size.Width, point._y + size.Height); /// /// Adds a point and a vector. /// public static XPoint Add(XPoint point, XVector vector) - { - return new XPoint(point._x + vector.X, point._y + vector.Y); - } + => new(point._x + vector.X, point._y + vector.Y); /// /// Subtracts a vector from a point. /// - public static XPoint operator -(XPoint point, XVector vector) - { - return new XPoint(point._x - vector.X, point._y - vector.Y); - } + public static XPoint operator -(XPoint point, XVector vector) + => new(point._x - vector.X, point._y - vector.Y); /// /// Subtracts a vector from a point. /// - public static XPoint Subtract(XPoint point, XVector vector) - { - return new XPoint(point._x - vector.X, point._y - vector.Y); - } + public static XPoint Subtract(XPoint point, XVector vector) + => new(point._x - vector.X, point._y - vector.Y); /// /// Subtracts a point from a point. /// - public static XVector operator -(XPoint point1, XPoint point2) - { - return new XVector(point1._x - point2._x, point1._y - point2._y); - } + public static XVector operator -(XPoint point1, XPoint point2) + => new(point1._x - point2._x, point1._y - point2._y); /// /// Subtracts a size from a point. /// [Obsolete("Use XVector instead of XSize as second parameter.")] - public static XPoint operator -(XPoint point, XSize size) - { - return new XPoint(point._x - size.Width, point._y - size.Height); - } + public static XPoint operator -(XPoint point, XSize size) + => new(point._x - size.Width, point._y - size.Height); /// /// Subtracts a point from a point. /// - public static XVector Subtract(XPoint point1, XPoint point2) - { - return new XVector(point1._x - point2._x, point1._y - point2._y); - } + public static XVector Subtract(XPoint point1, XPoint point2) + => new(point1._x - point2._x, point1._y - point2._y); /// /// Multiplies a point with a matrix. /// - public static XPoint operator *(XPoint point, XMatrix matrix) - { - return matrix.Transform(point); - } + public static XPoint operator *(XPoint point, XMatrix matrix) + => matrix.Transform(point); /// /// Multiplies a point with a matrix. /// - public static XPoint Multiply(XPoint point, XMatrix matrix) - { - return matrix.Transform(point); - } + public static XPoint Multiply(XPoint point, XMatrix matrix) + => matrix.Transform(point); /// /// Multiplies a point with a scalar value. /// - public static XPoint operator *(XPoint point, double value) - { - return new XPoint(point._x * value, point._y * value); - } + public static XPoint operator *(XPoint point, double value) + => new(point._x * value, point._y * value); /// /// Multiplies a point with a scalar value. /// - public static XPoint operator *(double value, XPoint point) - { - return new XPoint(value * point._x, value * point._y); - } + public static XPoint operator *(double value, XPoint point) + => new(value * point._x, value * point._y); /// /// Performs an explicit conversion from XPoint to XSize. /// - public static explicit operator XSize(XPoint point) - { - return new XSize(Math.Abs(point._x), Math.Abs(point._y)); - } + public static explicit operator XSize(XPoint point) + => new(Math.Abs(point._x), Math.Abs(point._y)); /// /// Performs an explicit conversion from XPoint to XVector. /// - public static explicit operator XVector(XPoint point) - { - return new XVector(point._x, point._y); - } + public static explicit operator XVector(XPoint point) => new(point._x, point._y); #if WPF || WUI /// /// Performs an implicit conversion from XPoint to Point. /// - public static implicit operator SysPoint(XPoint point) - { - return new SysPoint(point.X, point.Y); - } + public static implicit operator SysPoint(XPoint point) => new(point.X, point.Y); /// /// Performs an implicit conversion from Point to XPoint. /// - public static implicit operator XPoint(SysPoint point) - { - return new XPoint(point.X, point.Y); - } + public static implicit operator XPoint(SysPoint point) => new(point.X, point.Y); #endif /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPrivateFontCollection.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPrivateFontCollection.cs index b83b1b0c..09e3a0c1 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPrivateFontCollection.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XPrivateFontCollection.cs @@ -1,6 +1,7 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if false // DELETE 2025-12-31 #if GDI using System.Runtime.InteropServices; using PdfSharp.Logging; @@ -28,3 +29,4 @@ namespace PdfSharp.Drawing public sealed class XPrivateFontCollection { } } +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XRect.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XRect.cs index 07379df5..10f1641a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XRect.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XRect.cs @@ -136,27 +136,20 @@ public XRect(SysRect rect) // ReSharper disable once IdentifierTypo public static XRect FromLTRB(double left, double top, double right, double bottom) // ReSharper restore InconsistentNaming - { - return new XRect(left, top, right - left, bottom - top); - } + => new(left, top, right - left, bottom - top); /// /// Determines whether the two rectangles are equal. /// - public static bool operator ==(XRect rect1, XRect rect2) - { // ReSharper disable CompareOfFloatsByEqualityOperator - return rect1.X == rect2.X && rect1.Y == rect2.Y && rect1.Width == rect2.Width && rect1.Height == rect2.Height; - // ReSharper restore CompareOfFloatsByEqualityOperator - } + public static bool operator ==(XRect rect1, XRect rect2) => + rect1.X == rect2.X && rect1.Y == rect2.Y && rect1.Width == rect2.Width && rect1.Height == rect2.Height; + // ReSharper restore CompareOfFloatsByEqualityOperator /// /// Determines whether the two rectangles are not equal. /// - public static bool operator !=(XRect rect1, XRect rect2) - { - return !(rect1 == rect2); - } + public static bool operator !=(XRect rect1, XRect rect2) => !(rect1 == rect2); /// /// Determines whether the two rectangles are equal. @@ -181,10 +174,7 @@ public override bool Equals(object? o) /// /// Determines whether this instance and the specified rect are equal. /// - public bool Equals(XRect value) - { - return Equals(this, value); - } + public bool Equals(XRect value) => Equals(this, value); /// /// Returns the hash code for this instance. @@ -214,28 +204,19 @@ public static XRect Parse(string source) } /// - /// Converts this XRect to a human readable string. + /// Converts this XRect to a human-readable string. /// - public override string ToString() - { - return ConvertToString(null, null); - } + public override string ToString() => ConvertToString(null, null); /// - /// Converts this XRect to a human readable string. + /// Converts this XRect to a human-readable string. /// - public string ToString(IFormatProvider provider) - { - return ConvertToString(null, provider); - } + public string ToString(IFormatProvider provider) => ConvertToString(null, provider); /// - /// Converts this XRect to a human readable string. + /// Converts this XRect to a human-readable string. /// - string IFormattable.ToString(string? format, IFormatProvider? provider) - { - return ConvertToString(format, provider); - } + string IFormattable.ToString(string? format, IFormatProvider? provider) => ConvertToString(format, provider); internal string ConvertToString(string? format, IFormatProvider? provider) { @@ -277,7 +258,6 @@ public XPoint Location /// /// Gets or sets the size of the rectangle. /// - //[Browsable(false)] public XSize Size { get @@ -313,7 +293,6 @@ public double X _x = value; } } - double _x; /// @@ -329,7 +308,6 @@ public double Y _y = value; } } - double _y; /// @@ -348,7 +326,6 @@ public double Width _width = value; } } - double _width; /// @@ -366,7 +343,6 @@ public double Height _height = value; } } - double _height; /// @@ -434,10 +410,7 @@ public double Bottom /// /// Indicates whether the rectangle contains the specified point. /// - public bool Contains(XPoint point) - { - return Contains(point.X, point.Y); - } + public bool Contains(XPoint point) => Contains(point.X, point.Y); /// /// Indicates whether the rectangle contains the specified point. @@ -541,10 +514,7 @@ public static XRect Union(XRect rect1, XRect rect2) /// /// Sets current rectangle to the union of the current rectangle and the specified point. /// - public void Union(XPoint point) - { - Union(new XRect(point, point)); - } + public void Union(XPoint point) => Union(new XRect(point, point)); /// /// Returns the union of a rectangle and a point. @@ -598,28 +568,19 @@ public static XRect Offset(XRect rect, double offsetX, double offsetY) /// /// Translates the rectangle by adding the specified point. /// - //[Obsolete("Use Offset.")] public static XRect operator +(XRect rect, XPoint point) - { - return new XRect(rect._x + point.X, rect.Y + point.Y, rect._width, rect._height); - } + => new(rect._x + point.X, rect.Y + point.Y, rect._width, rect._height); /// /// Translates the rectangle by subtracting the specified point. /// - //[Obsolete("Use Offset.")] public static XRect operator -(XRect rect, XPoint point) - { - return new XRect(rect._x - point.X, rect.Y - point.Y, rect._width, rect._height); - } + => new(rect._x - point.X, rect.Y - point.Y, rect._width, rect._height); /// /// Expands the rectangle by using the specified Size, in all directions. /// - public void Inflate(XSize size) - { - Inflate(size.Width, size.Height); - } + public void Inflate(XSize size) => Inflate(size.Width, size.Height); /// /// Expands or shrinks the rectangle by using the specified width and height amounts, in all directions. @@ -669,9 +630,7 @@ public static XRect Transform(XRect rect, XMatrix matrix) /// Transforms the rectangle by applying the specified matrix. /// public void Transform(XMatrix matrix) - { - XMatrix.MatrixHelper.TransformRect(ref this, ref matrix); - } + => XMatrix.MatrixHelper.TransformRect(ref this, ref matrix); /// /// Multiplies the size of the current rectangle by the specified x and y values. @@ -701,57 +660,42 @@ public void Scale(double scaleX, double scaleY) /// /// Converts this instance to a System.Drawing.RectangleF. /// - public RectangleF ToRectangleF() - { - return new RectangleF((float)_x, (float)_y, (float)_width, (float)_height); - } + public RectangleF ToRectangleF() + => new((float)_x, (float)_y, (float)_width, (float)_height); #endif #if GDI /// /// Performs an implicit conversion from a System.Drawing.Rectangle to an XRect. /// - public static implicit operator XRect(Rectangle rect) - { - return new XRect(rect.X, rect.Y, rect.Width, rect.Height); - } + public static implicit operator XRect(Rectangle rect) + => new(rect.X, rect.Y, rect.Width, rect.Height); /// /// Performs an implicit conversion from a System.Drawing.RectangleF to an XRect. /// - public static implicit operator XRect(RectangleF rect) - { - return new XRect(rect.X, rect.Y, rect.Width, rect.Height); - } + public static implicit operator XRect(RectangleF rect) + => new(rect.X, rect.Y, rect.Width, rect.Height); #endif #if WPF || WUI /// /// Performs an implicit conversion from System.Windows.Rect to XRect. /// - public static implicit operator XRect(SysRect rect) - { - return new XRect(rect.X, rect.Y, rect.Width, rect.Height); - } + public static implicit operator XRect(SysRect rect) + => new(rect.X, rect.Y, rect.Width, rect.Height); #endif bool ContainsInternal(double x, double y) - { - return x >= _x && x - _width <= _x && y >= _y && y - _height <= _y; - } + => x >= _x && x - _width <= _x && y >= _y && y - _height <= _y; - static XRect CreateEmptyRect() + static readonly XRect s_empty = new() { - return new() - { - _x = double.PositiveInfinity, - _y = double.PositiveInfinity, - _width = double.NegativeInfinity, - _height = double.NegativeInfinity - }; - } - - static readonly XRect s_empty = CreateEmptyRect(); + _x = Double.PositiveInfinity, + _y = Double.PositiveInfinity, + _width = Double.NegativeInfinity, + _height = Double.NegativeInfinity + }; /// /// Gets the DebuggerDisplayAttribute text. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XSize.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XSize.cs index 17fb61cc..86d851eb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XSize.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XSize.cs @@ -50,10 +50,7 @@ public XSize(double width, double height) /// /// Determines whether two size objects are not equal. /// - public static bool operator !=(XSize size1, XSize size2) - { - return !(size1 == size2); - } + public static bool operator !=(XSize size1, XSize size2) => !(size1 == size2); /// /// Indicates whether these two instances are equal. @@ -78,10 +75,7 @@ public override bool Equals(object? o) /// /// Indicates whether this instance and a specified size are equal. /// - public bool Equals(XSize value) - { - return Equals(this, value); - } + public bool Equals(XSize value) => Equals(this, value); /// /// Returns the hash code for this instance. @@ -123,56 +117,38 @@ public PointF ToPointF() /// /// Converts this XSize to an XPoint. /// - public XPoint ToXPoint() - { - return new XPoint(_width, _height); - } + public XPoint ToXPoint() => new(_width, _height); /// /// Converts this XSize to an XVector. /// - public XVector ToXVector() - { - return new XVector(_width, _height); - } + public XVector ToXVector() => new(_width, _height); #if GDI /// /// Converts this XSize to a SizeF. /// - public SizeF ToSizeF() - { - return new SizeF((float)_width, (float)_height); - } + public SizeF ToSizeF() => new((float)_width, (float)_height); #endif #if WPF || WUI /// /// Converts this XSize to a System.Windows.Size. /// - public SysSize ToSize() - { - return new SysSize(_width, _height); - } + public SysSize ToSize() => new(_width, _height); #endif #if GDI /// /// Creates an XSize from a System.Drawing.Size. /// - public static XSize FromSize(Size size) - { - return new XSize(size.Width, size.Height); - } + public static XSize FromSize(Size size) => new(size.Width, size.Height); /// /// Implicit conversion from XSize to System.Drawing.Size. The conversion must be implicit because the /// WinForms designer uses it. /// - public static implicit operator XSize(Size size) - { - return new XSize(size.Width, size.Height); - } + public static implicit operator XSize(Size size) => new(size.Width, size.Height); #endif #if WPF || WUI @@ -189,35 +165,24 @@ public static XSize FromSize(SysSize size) /// /// Creates an XSize from a System.Drawing.Size. /// - public static XSize FromSizeF(SizeF size) - { - return new XSize(size.Width, size.Height); - } + public static XSize FromSizeF(SizeF size) => new(size.Width, size.Height); #endif /// - /// Converts this XSize to a human readable string. + /// Converts this XSize to a human-readable string. /// - public override string ToString() - { - return ConvertToString(null, null); - } + public override string ToString() => ConvertToString(null, null); /// - /// Converts this XSize to a human readable string. + /// Converts this XSize to a human-readable string. /// - public string ToString(IFormatProvider provider) - { - return ConvertToString(null, provider); - } + public string ToString(IFormatProvider provider) => ConvertToString(null, provider); /// - /// Converts this XSize to a human readable string. + /// Converts this XSize to a human-readable string. /// - string IFormattable.ToString(string? format, IFormatProvider? provider) - { - return ConvertToString(format, provider); - } + string IFormattable.ToString(string? format, IFormatProvider? provider) + => ConvertToString(format, provider); internal string ConvertToString(string? format, IFormatProvider? provider) { @@ -285,27 +250,18 @@ public double Height /// /// Performs an explicit conversion from XSize to XVector. /// - public static explicit operator XVector(XSize size) - { - return new XVector(size._width, size._height); - } + public static explicit operator XVector(XSize size) => new(size._width, size._height); /// /// Performs an explicit conversion from XSize to XPoint. /// - public static explicit operator XPoint(XSize size) - { - return new XPoint(size._width, size._height); - } + public static explicit operator XPoint(XSize size) => new(size._width, size._height); #if WPF || WUI /// /// Performs an explicit conversion from Size to XSize. /// - public static explicit operator XSize(SysSize size) - { - return new XSize(size.Width, size.Height); - } + public static explicit operator XSize(SysSize size) => new(size.Width, size.Height); #endif /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XUnit.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XUnit.cs index 46c57ff0..ea9a35df 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XUnit.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XUnit.cs @@ -273,16 +273,12 @@ string GetSuffix() /// public static implicit operator XUnit(string value) { - XUnit unit = default; value = value.Trim(); - // #DELETE 2024-12-31 - Replace this special treatment for German numbers with an exception. + // No commas allowed anymore. ',' as decimal separator was a special hack for German numbers. if (value.Contains(',')) - { - PdfSharpLogHost.Logger.LogError("A number string contains an illegal ','. It is replaced by '.'. Will throw exception in the future."); - value = value.Replace(',', '.'); - } + throw new FormatException($"value '{value}' must not contain a comma as decimal or thousands separator."); int count = value.Length; int valLen = 0; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XVector.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XVector.cs index ae81cfee..c3f94ebb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XVector.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XVector.cs @@ -49,12 +49,10 @@ public XVector(double x, double y) /// /// The first vector to compare. /// The second vector to compare. - public static bool operator !=(XVector vector1, XVector vector2) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - return vector1._x != vector2._x || vector1._y != vector2._y; - // ReSharper restore CompareOfFloatsByEqualityOperator - } + // ReSharper disable CompareOfFloatsByEqualityOperator + public static bool operator !=(XVector vector1, XVector vector2) => + vector1._x != vector2._x || vector1._y != vector2._y; + // ReSharper restore CompareOfFloatsByEqualityOperator /// /// Compares two vectors for equality. @@ -84,20 +82,15 @@ public override bool Equals(object? o) /// Compares two vectors for equality. /// /// The vector to compare with this vector. - public bool Equals(XVector value) - { - return Equals(this, value); - } + public bool Equals(XVector value) => Equals(this, value); /// /// Returns the hash code for this instance. /// - public override int GetHashCode() - { // ReSharper disable NonReadonlyFieldInGetHashCode - return _x.GetHashCode() ^ _y.GetHashCode(); - // ReSharper restore NonReadonlyFieldInGetHashCode - } + public override int GetHashCode() => + _x.GetHashCode() ^ _y.GetHashCode(); + // ReSharper restore NonReadonlyFieldInGetHashCode /// /// Converts a string representation of a vector into the equivalent Vector structure. @@ -135,19 +128,16 @@ public double Y /// /// Returns the string representation of this Vector structure. /// - public override string ToString() - { - return ConvertToString(null, null); - } + public override string ToString() => ConvertToString(null, null); /// /// Returns the string representation of this Vector structure with the specified formatting information. /// /// The culture-specific formatting information. - public string ToString(IFormatProvider provider) + public string ToString(IFormatProvider provider) => ConvertToString(null, provider); - string IFormattable.ToString(string? format, IFormatProvider? provider) + string IFormattable.ToString(string? format, IFormatProvider? provider) => ConvertToString(format, provider); internal string ConvertToString(string? format, IFormatProvider? provider) @@ -163,22 +153,20 @@ internal string ConvertToString(string? format, IFormatProvider? provider) /// /// Gets the length of this vector. /// - public double Length - => Math.Sqrt(_x * _x + _y * _y); + public double Length => Math.Sqrt(_x * _x + _y * _y); /// /// Gets the square of the length of this vector. /// - public double LengthSquared - => _x * _x + _y * _y; + public double LengthSquared => _x * _x + _y * _y; /// /// Normalizes this vector. /// public void Normalize() { - this = this / Math.Max(Math.Abs(_x), Math.Abs(_y)); - this = this / Length; + this /= Math.Max(Math.Abs(_x), Math.Abs(_y)); + this /= Length; } /// @@ -238,7 +226,7 @@ public static XVector Add(XVector vector1, XVector vector2) /// /// The vector from which vector2 is subtracted. /// The vector to subtract from vector1. - public static XVector operator -(XVector vector1, XVector vector2) + public static XVector operator -(XVector vector1, XVector vector2) => new(vector1._x - vector2._x, vector1._y - vector2._y); /// @@ -254,7 +242,7 @@ public static XVector Subtract(XVector vector1, XVector vector2) /// /// The vector used to translate point. /// The point to translate. - public static XPoint operator +(XVector vector, XPoint point) + public static XPoint operator +(XVector vector, XPoint point) => new(point.X + vector._x, point.Y + vector._y); /// @@ -262,7 +250,7 @@ public static XVector Subtract(XVector vector1, XVector vector2) /// /// The vector used to translate point. /// The point to translate. - public static XPoint Add(XVector vector, XPoint point) + public static XPoint Add(XVector vector, XPoint point) => new(point.X + vector._x, point.Y + vector._y); /// @@ -270,7 +258,7 @@ public static XPoint Add(XVector vector, XPoint point) /// /// The vector to multiply. /// The scalar to multiply. - public static XVector operator *(XVector vector, double scalar) + public static XVector operator *(XVector vector, double scalar) => new(vector._x * scalar, vector._y * scalar); /// @@ -278,7 +266,7 @@ public static XPoint Add(XVector vector, XPoint point) /// /// The vector to multiply. /// The scalar to multiply. - public static XVector Multiply(XVector vector, double scalar) + public static XVector Multiply(XVector vector, double scalar) => new(vector._x * scalar, vector._y * scalar); /// @@ -286,7 +274,7 @@ public static XVector Multiply(XVector vector, double scalar) /// /// The scalar to multiply. /// The vector to multiply. - public static XVector operator *(double scalar, XVector vector) + public static XVector operator *(double scalar, XVector vector) => new(vector._x * scalar, vector._y * scalar); /// @@ -294,7 +282,7 @@ public static XVector Multiply(XVector vector, double scalar) /// /// The scalar to multiply. /// The vector to multiply. - public static XVector Multiply(double scalar, XVector vector) + public static XVector Multiply(double scalar, XVector vector) => new(vector._x * scalar, vector._y * scalar); /// @@ -302,7 +290,7 @@ public static XVector Multiply(double scalar, XVector vector) /// /// The vector to divide. /// The scalar by which vector will be divided. - public static XVector operator /(XVector vector, double scalar) + public static XVector operator /(XVector vector, double scalar) => vector * (1.0 / scalar); /// @@ -310,7 +298,7 @@ public static XVector Multiply(double scalar, XVector vector) /// /// The vector structure to divide. /// The amount by which vector is divided. - public static XVector Divide(XVector vector, double scalar) + public static XVector Divide(XVector vector, double scalar) => vector * (1.0 / scalar); /// @@ -318,7 +306,7 @@ public static XVector Divide(XVector vector, double scalar) /// /// The vector to transform. /// The transformation to apply to vector. - public static XVector operator *(XVector vector, XMatrix matrix) + public static XVector operator *(XVector vector, XMatrix matrix) => matrix.Transform(vector); /// @@ -326,7 +314,7 @@ public static XVector Divide(XVector vector, double scalar) /// /// The vector to transform. /// The transformation to apply to vector. - public static XVector Multiply(XVector vector, XMatrix matrix) + public static XVector Multiply(XVector vector, XMatrix matrix) => matrix.Transform(vector); /// @@ -334,7 +322,7 @@ public static XVector Multiply(XVector vector, XMatrix matrix) /// /// The first vector to multiply. /// The second vector to multiply. - public static double operator *(XVector vector1, XVector vector2) + public static double operator *(XVector vector1, XVector vector2) => vector1._x * vector2._x + vector1._y * vector2._y; /// @@ -342,7 +330,7 @@ public static XVector Multiply(XVector vector, XMatrix matrix) /// /// The first vector to multiply. /// The second vector structure to multiply. - public static double Multiply(XVector vector1, XVector vector2) + public static double Multiply(XVector vector1, XVector vector2) => vector1._x * vector2._x + vector1._y * vector2._y; /// @@ -350,21 +338,21 @@ public static double Multiply(XVector vector1, XVector vector2) /// /// The first vector to evaluate. /// The second vector to evaluate. - public static double Determinant(XVector vector1, XVector vector2) + public static double Determinant(XVector vector1, XVector vector2) => vector1._x * vector2._y - vector1._y * vector2._x; /// /// Creates a Size from the offsets of this vector. /// /// The vector to convert. - public static explicit operator XSize(XVector vector) + public static explicit operator XSize(XVector vector) => new(Math.Abs(vector._x), Math.Abs(vector._y)); /// /// Creates a Point with the X and Y values of this vector. /// /// The vector to convert. - public static explicit operator XPoint(XVector vector) + public static explicit operator XPoint(XVector vector) => new(vector._x, vector._y); /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.Internal/FontHelper.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.Internal/FontHelper.cs index 767cd125..1114e3ba 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.Internal/FontHelper.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.Internal/FontHelper.cs @@ -39,7 +39,7 @@ namespace PdfSharp.Fonts.Internal /// static class FontHelper { -#if true_ // #DELETE 24-12-31 +#if true_ // #DELETE 25-12-31 /// /// Measure string directly from font data. /// @@ -65,7 +65,7 @@ public static XSize MeasureString(string text, XFont font) //codePoints = codeRun.Items; return MeasureString(codePoints, font); -#if true_ // Keep until 2024-12-31 for reference +#if true_ // Keep until 2025-12-31 for reference var size = new XSize(); var descriptor = FontDescriptorCache.GetOrCreateDescriptorFor(font) as OpenTypeDescriptor; if (descriptor != null) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/IRefFontTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/IRefFontTable.cs index 27336a8c..9078682f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/IRefFontTable.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/IRefFontTable.cs @@ -35,11 +35,11 @@ public override void PrepareForCompilation() // Check the checksum algorithm. if (DirectoryEntry.Tag != TableTagNames.Head) { - byte[] bytes = new byte[DirectoryEntry.PaddedLength]; - Buffer.BlockCopy(_irefDirectoryEntry.FontTable._fontData!.FontSource.Bytes, _irefDirectoryEntry.Offset, bytes, 0, DirectoryEntry.PaddedLength); - uint checkSum1 = DirectoryEntry.CheckSum; - uint checkSum2 = CalcChecksum(bytes); - // TODO_OLD: Sometimes this Assert fails, + //byte[] bytes = new byte[DirectoryEntry.PaddedLength]; + //Buffer.BlockCopy(_irefDirectoryEntry.FontTable._fontData!.FontSource.Bytes, _irefDirectoryEntry.Offset, bytes, 0, DirectoryEntry.PaddedLength); + //uint checkSum1 = DirectoryEntry.CheckSum; + //uint checkSum2 = CalcChecksum(bytes); + // TODO: Sometimes this Assert fails, //Debug.Assert(checkSum1 == checkSum2, "Bug in checksum algorithm."); } #endif @@ -50,7 +50,7 @@ public override void PrepareForCompilation() /// public override void Write(OpenTypeFontWriter writer) { - writer.Write(_irefDirectoryEntry.FontTable._fontData!.FontSource.Bytes, _irefDirectoryEntry.Offset, _irefDirectoryEntry.PaddedLength); + writer.Write(_irefDirectoryEntry.FontTable._fontData!.FontSource.Bytes, _irefDirectoryEntry.Offset, _irefDirectoryEntry.Length); } } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs index 0b2db0cc..b7322a54 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs @@ -2,7 +2,6 @@ // See the LICENSE file in the solution root for more information. using System.Text; -using PdfSharp.Internal; using PdfSharp.Drawing; using Fixed = System.Int32; @@ -111,6 +110,25 @@ internal void Read() throw new InvalidOperationException(PsMsgs.ErrorReadingFontData, ex); } } + + internal HashSet GetSupportedCharacters() + { + var characterSet = new HashSet(); + + int segCount = segCountX2 / 2; + for (var i = 0; i < segCount; i++) + { + var start = startCount[i]; + var end = endCount[i]; + if (start == 0xffff && end == 0xffff) + break; + for (var c = start; c <= end; c++) + { + characterSet.Add(c); + } + } + return characterSet; + } } /// @@ -168,6 +186,22 @@ internal void Read() throw new InvalidOperationException(PsMsgs.ErrorReadingFontData, ex); } } + + public HashSet GetSupportedCharacters() + { + var characterSet = new HashSet(); + + foreach (var group in groups) + { + var start = group.startCharCode; + var end = group.endCharCode; + for (var c = start; c <= end; c++) + { + characterSet.Add((int)c); + } + } + return characterSet; + } } /// @@ -251,6 +285,13 @@ internal void Read() throw new InvalidOperationException(PsMsgs.ErrorReadingFontData, ex); } } + + internal HashSet GetSupportedCharacters() + { + return cmap12?.GetSupportedCharacters() ?? cmap4?.GetSupportedCharacters() ?? emptySet; + } + + private static readonly HashSet emptySet = []; } /// @@ -604,7 +645,7 @@ public void Read() } } - // UNDONE + // Not used in PDFsharp. class VerticalHeaderTable : OpenTypeFontTable { public const string Tag = TableTagNames.VHea; @@ -697,9 +738,9 @@ public void Read() /// information (the advance heights and top sidebearings) for the vertical layout of each /// of the glyphs in the font. /// + // Not used in PDFsharp. class VerticalMetricsTable : OpenTypeFontTable { - // UNDONE public const string Tag = TableTagNames.VMtx; // Code comes from HorizontalMetricsTable. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs index 4643237d..215ffbdb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs @@ -78,7 +78,7 @@ public OpenTypeFontFace(XFontSource fontSource) _fullFaceName = name.FullFontName; } - public static OpenTypeFontFace CetOrCreateFrom(XFontSource fontSource) + public static OpenTypeFontFace GetOrCreateFrom(XFontSource fontSource) { if (OpenTypeFontFaceCache.TryGetFontFace(fontSource.Key, out var fontFace)) return fontFace; @@ -111,7 +111,7 @@ public ulong CheckSum public void SetFontEmbedding(PdfFontEmbedding fontEmbedding) { - Debug.Assert(fontEmbedding is PdfFontEmbedding.TryComputeSubset or PdfFontEmbedding.EmbedCompleteFontFile); + Debug.Assert(fontEmbedding is PdfFontEmbedding.TryComputeSubset or PdfFontEmbedding.EmbedCompleteFontFile or PdfFontEmbedding.OmitStandardFont); if (_fontEmbedding == (PdfFontEmbedding)(-1)) { @@ -127,6 +127,10 @@ public void SetFontEmbedding(PdfFontEmbedding fontEmbedding) // Case: _fontEmbedding is already set to EmbedCompleteFontFile. PdfSharpLogHost.Logger.LogError("Font embedding option was already set to EmbedCompleteFontFile. Setting to TryComputeSubset is ignored."); } + else if (fontEmbedding == PdfFontEmbedding.OmitStandardFont) + { + PdfSharpLogHost.Logger.LogError("Font embedding option was already set to {embedding}. Setting to OmitStandardFont is ignored.", _fontEmbedding); + } else { // Case: _fontEmbedding is already set to TryComputeSubset. @@ -398,7 +402,7 @@ public OpenTypeFontFace CreateFontSubset(Dictionary glyphs, boo { // Do not create a subset? // No loca table means font has postscript outline. - if (_fontEmbedding == PdfFontEmbedding.EmbedCompleteFontFile || loca == null!) + if (glyphs.Count == 0 || _fontEmbedding != PdfFontEmbedding.TryComputeSubset || loca == null!) return this; // Create new font image. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.Designer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.Designer.cs new file mode 100644 index 00000000..ccfe784c --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.Designer.cs @@ -0,0 +1,203 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PdfSharp.Fonts.StandardFonts { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class FontResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal FontResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PdfSharp.Fonts.StandardFonts.FontResources", typeof(FontResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] D050000L { + get { + object obj = ResourceManager.GetObject("D050000L", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusMonoPS_Bold { + get { + object obj = ResourceManager.GetObject("NimbusMonoPS_Bold", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusMonoPS_BoldItalic { + get { + object obj = ResourceManager.GetObject("NimbusMonoPS_BoldItalic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusMonoPS_Italic { + get { + object obj = ResourceManager.GetObject("NimbusMonoPS_Italic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusMonoPS_Regular { + get { + object obj = ResourceManager.GetObject("NimbusMonoPS_Regular", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusRoman_Bold { + get { + object obj = ResourceManager.GetObject("NimbusRoman_Bold", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusRoman_BoldItalic { + get { + object obj = ResourceManager.GetObject("NimbusRoman_BoldItalic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusRoman_Italic { + get { + object obj = ResourceManager.GetObject("NimbusRoman_Italic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusRoman_Regular { + get { + object obj = ResourceManager.GetObject("NimbusRoman_Regular", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusSans_Bold { + get { + object obj = ResourceManager.GetObject("NimbusSans_Bold", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusSans_BoldItalic { + get { + object obj = ResourceManager.GetObject("NimbusSans_BoldItalic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusSans_Italic { + get { + object obj = ResourceManager.GetObject("NimbusSans_Italic", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] NimbusSans_Regular { + get { + object obj = ResourceManager.GetObject("NimbusSans_Regular", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] StandardSymbolsPS { + get { + object obj = ResourceManager.GetObject("StandardSymbolsPS", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsgs.de.resx b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.resx similarity index 61% rename from src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsgs.de.resx rename to src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.resx index 1b7707d9..27ef5199 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/MdPdfMsgs.de.resx +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/FontResources.resx @@ -33,7 +33,7 @@ Each data row contains a name, and value. The row also contains a type or mimetype. Type corresponds to a .NET class that support text/value conversion through the TypeConverter architecture. - Classes that don’t support this are serialized and stored with the + Classes that don't support this are serialized and stored with the mimetype set. The mimetype is used for serialized objects, and tells the @@ -117,44 +117,47 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Ihre Benutzereingaben führten zu einem leeren Bildbereich. + + + ..\..\Resources\D050000L.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Bild nicht gefunden. + + ..\..\Resources\NimbusMonoPS-Bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Bild nicht eingelesen. + + ..\..\Resources\NimbusMonoPS-BoldItalic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Die Zahl {0} ist zu groß für eine Darstellung in Buchstaben. + + ..\..\Resources\NimbusMonoPS-Italic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Lesezeichen '{0}' ist innerhalb des Dokuments nicht definiert. + + ..\..\Resources\NimbusMonoPS-Regular.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Ungültiger Bildtyp: '{0}'. + + ..\..\Resources\NimbusRoman-Bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Die Zahl {0} ist zu groß für eine Darstellung in römischen Ziffern. + + ..\..\Resources\NimbusRoman-BoldItalic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Bild '{0}' nicht gefunden. + + ..\..\Resources\NimbusRoman-Italic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - '{0}' muss vor dem Aufruf von '{1}' gesetzt werden. + + ..\..\Resources\NimbusRoman-Regular.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Leerer Bildbereich. + + ..\..\Resources\NimbusSans-Bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Ungültiger Bildtyp. + + ..\..\Resources\NimbusSans-BoldItalic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Nur Bilder, Textrahmen, Diagramme und Absätze können frei gerendert werden. + + ..\..\Resources\NimbusSans-Italic.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Bild '{0}' konnte nicht eingelesen werden. Es trat eine Ausnahme mit der folgenden Meldung auf: -{1} + + ..\..\Resources\NimbusSans-Regular.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\..\Resources\StandardSymbolsPS.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/OFL.txt b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/OFL.txt new file mode 100644 index 00000000..187990b7 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/OFL.txt @@ -0,0 +1,92 @@ +Copyright 2016 by (URW)++ Design & Development. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontData.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontData.cs new file mode 100644 index 00000000..fd34053c --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontData.cs @@ -0,0 +1,70 @@ +using PdfSharp.Pdf.AcroForms; + +namespace PdfSharp.Fonts.StandardFonts +{ + /// + /// Provides the data for the 14 Standard-Fonts defined in the PDF-specification.

+ /// Pdf spec. 1.7, Chapter 9.6.2.2: Standard Type 1 Fonts (Standard 14 Fonts)

+ /// Mainly intended for existing documents which use one or more of the standard-fonts without embedding them.

+ /// Most often used in fillable forms, i.e. a + ///
+ public static class StandardFontData + { + /// + /// Gets the data for the specified font + /// + /// Name of the font. A leading slash is automatically stripped + /// Font-data or null if a font with the specified name could not be found + public static byte[]? GetFontData(string fontName) + { + if (string.IsNullOrWhiteSpace(fontName)) + return null; + + // if the name comes from a resource-dictionary... + if (fontName.StartsWith("/")) + fontName = fontName.TrimStart('/'); + + if (fontData.TryGetValue(fontName, out var data)) + return data; + return null; + } + + /// + /// Indicates, whether the specified is one of the 14 Standard-Fonts + /// + /// Font-name to check. A leading slash it automatically stripped + /// true, if is one of the 14 Standard-Fonts, otherwise false + public static bool IsStandardFont(string fontName) + { + if (string.IsNullOrWhiteSpace(fontName)) + return false; + return FontNames.Contains(fontName.TrimStart('/')); + } + + /// + /// Get the names of the supported standard-fonts + /// + public static IEnumerable FontNames => fontData.Keys; + + private static readonly Dictionary fontData = new() + { + { StandardFontNames.Courier, FontResources.NimbusMonoPS_Regular }, + { StandardFontNames.CourierBold, FontResources.NimbusMonoPS_Bold }, + { StandardFontNames.CourierItalic, FontResources.NimbusMonoPS_Italic }, + { StandardFontNames.CourierBoldItalic, FontResources.NimbusMonoPS_BoldItalic }, + + { StandardFontNames.Helvetica, FontResources.NimbusSans_Regular }, + { StandardFontNames.HelveticaBold, FontResources.NimbusSans_Bold }, + { StandardFontNames.HelveticaItalic, FontResources.NimbusSans_Italic }, + { StandardFontNames.HelveticaBoldItalic, FontResources.NimbusSans_BoldItalic }, + + { StandardFontNames.Times, FontResources.NimbusRoman_Regular }, + { StandardFontNames.TimesBold, FontResources.NimbusRoman_Bold }, + { StandardFontNames.TimesItalic, FontResources.NimbusRoman_Italic }, + { StandardFontNames.TimesBoldItalic, FontResources.NimbusRoman_BoldItalic }, + + { StandardFontNames.ZapfDingbats, FontResources.D050000L }, + { StandardFontNames.Symbol, FontResources.StandardSymbolsPS } + }; + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontNames.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontNames.cs new file mode 100644 index 00000000..cd0a650b --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontNames.cs @@ -0,0 +1,69 @@ +namespace PdfSharp.Fonts.StandardFonts +{ + /// + /// Contains the names of the 14 Standard-fonts as defined in the PDF-Specification.

+ /// See PdfReference 1.7, Chapter 9.6.2.2 + ///
+ public static class StandardFontNames + { + /// + /// Courier + /// + public const string Courier = "Courier"; + /// + /// CourierBold + /// + public const string CourierBold = "Courier-Bold"; + /// + /// CourierItalic + /// + public const string CourierItalic = "Courier-Oblique"; + /// + /// CourierBoldItalic + /// + public const string CourierBoldItalic = "Courier-BoldOblique"; + + /// + /// Helvetica + /// + public const string Helvetica = "Helvetica"; + /// + /// HelveticaBold + /// + public const string HelveticaBold = "Helvetica-Bold"; + /// + /// HelveticaItalic + /// + public const string HelveticaItalic = "Helvetica-Oblique"; + /// + /// HelveticaBoldItalic + /// + public const string HelveticaBoldItalic = "Helvetica-BoldOblique"; + + /// + /// Times + /// + public const string Times = "Times-Roman"; + /// + /// TimesBold + /// + public const string TimesBold = "Times-Bold"; + /// + /// TimesItalic + /// + public const string TimesItalic = "Times-Italic"; + /// + /// TimesBoldItalic + /// + public const string TimesBoldItalic = "Times-BoldItalic"; + + /// + /// Symbol + /// + public const string Symbol = "Symbol"; + /// + /// ZapfDingbats + /// + public const string ZapfDingbats = "ZapfDingbats"; + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontResolver.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontResolver.cs new file mode 100644 index 00000000..d8d11b21 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/StandardFonts/StandardFontResolver.cs @@ -0,0 +1,190 @@ +using PdfSharp.Drawing; +using PdfSharp.Fonts.OpenType; +using System.Diagnostics.CodeAnalysis; + +namespace PdfSharp.Fonts.StandardFonts +{ + /// + /// Resolves the 14 standard-fonts and fonts that were pre-registered + /// + public class StandardFontResolver : IFontResolver + { + private readonly Dictionary localFonts = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary registeredFontFiles = new(StringComparer.OrdinalIgnoreCase); + private string? fallbackFontName; + private readonly Dictionary> characterSets = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a new instance of the + /// + public StandardFontResolver() + { + // register character-sets of standard-fonts + foreach (var fontName in StandardFontData.FontNames) + { + var fontData = StandardFontData.GetFontData(fontName)!; + var fontSource = XFontSource.GetOrCreateFrom(fontData, false); + var typeFace = new OpenTypeFontFace(fontSource); + var characterSet = typeFace.cmap.GetSupportedCharacters(); + characterSets[fontName] = characterSet; + } + } + + /// + /// Registers a new font + /// + /// The name of the font + /// The font-data + /// Specifies, whether the font is bold + /// Specifies, whether the font is italic + public void Register(string fontName, byte[] fontData, bool isBold = false, bool isItalic = false) + { + var localName = MakeLocalName(fontName, isBold, isItalic); + localFonts[localName] = fontData; + // get the name stored in the font itself + var fontSource = XFontSource.GetOrCreateFrom(fontData, false); + var typeFace = new OpenTypeFontFace(fontSource); + if (!string.IsNullOrEmpty(typeFace.FullFaceName)) + { + localFonts[typeFace.FullFaceName] = fontData; + var characterSet = typeFace.cmap.GetSupportedCharacters(); + characterSets[typeFace.FullFaceName] = characterSet; + } + } + + /// + /// Registers all (TrueType)-fonts from the specified folder and all sub-folders.

+ /// In an , these fonts may be referenced by their filename or their full face-name. + ///
+ /// The base path to load fonts from + /// The number of fonts that were found + /// + /// + public int RegisterFolder(string folderPath) + { + if (string.IsNullOrEmpty(folderPath)) + throw new ArgumentNullException(nameof(folderPath), "Folder name may not be null or empty"); + if (!Directory.Exists(folderPath)) + throw new ArgumentException($"The folder '{folderPath}' does not exist", nameof(folderPath)); + + var fontFiles = Directory.GetFiles(folderPath, "*.ttf", SearchOption.AllDirectories); + var count = 0; + foreach (var file in fontFiles) + { + try + { + var data = File.ReadAllBytes(file); + // create Font-source without caching it + var fontSource = XFontSource.GetOrCreateFrom(data, false); + var font = new OpenTypeFontFace(fontSource); + var fileName = Path.GetFileNameWithoutExtension(file); + registeredFontFiles[fileName] = file; + registeredFontFiles[font.FullFaceName] = file; + + var characterSet = font.cmap.GetSupportedCharacters(); + characterSets[font.FullFaceName] = characterSet; + count++; + } + catch + { + } + } + return count; + } + + /// + /// Registers the font with the specified name as the fallback font.

+ /// The font must be either one of the or one of the pre-registered fonts. + ///
+ /// + /// + public void RegisterFallbackFont(string fontName) + { + if (StandardFontData.IsStandardFont(fontName) + || localFonts.ContainsKey(fontName) + || registeredFontFiles.ContainsKey(fontName)) + fallbackFontName = fontName; + else + throw new ArgumentException($"'{fontName}' is not one of the standard font names and none of the registered fonts"); + } + + /// + /// Tries to find a font suitable for rendering all characters specified in .

+ /// The returned font-name can be used to construct a new . + ///
+ /// + /// + /// The name of a font or null if no appropiate font was found + /// + /// If no font was found it could mean that the specified text contains characters for different languages.

+ /// e.g. if the text contains a mix of Arabic and Korean, it is unlikely to find a single font that suits both languages.

+ /// In this case you could try to split the text into multiple substrings, each one containing only characters for a single language.

+ /// Then call this function for each substring. + ///
+ public bool TryFindAppropiateFont(string text, [MaybeNullWhen(false)] out string fontName) + { + fontName = null!; + if (string.IsNullOrWhiteSpace(text)) + return false; + // try to find a character-set containing all characters of text + var kv = characterSets.FirstOrDefault(it => text.All(c => it.Value.Contains(c))); + fontName = kv.Key; + return kv.Key != null; + } + + /// + /// Gets the data for the specified font. + /// + /// Name of the font + /// Font data or null, if no font with the specified name could be found + public byte[]? GetFont(string faceName) + { + var result = Resolve(faceName, false, false); + if (result.Item1 == null && !string.IsNullOrEmpty(fallbackFontName)) + result = Resolve(fallbackFontName, false, false); + return result.Item1; + } + + /// + /// Get a for the specified font + /// + /// Name of the font + /// + /// + /// A or null, if no font with the specified name could be found + public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic) + { + var result = Resolve(familyName, isBold, isItalic); + if (result.Item2 == null && !string.IsNullOrEmpty(fallbackFontName)) + result = Resolve(fallbackFontName, isBold, isItalic); + return result.Item2; + } + + private static string MakeLocalName(string fontName, bool isBold, bool isItalic) + { + return XGlyphTypeface.ComputeGtfKey(fontName, isBold, isItalic); + } + + private Tuple Resolve(string fontName, bool isBold, bool isItalic) + { + var localName = MakeLocalName(fontName, isBold, isItalic); + if (localFonts.TryGetValue(localName, out var localData) || localFonts.TryGetValue(fontName, out localData)) + return new Tuple(localData, new FontResolverInfo(fontName, isBold, isItalic)); + + if (registeredFontFiles.TryGetValue(fontName, out var fileName)) + { + var fileData = System.IO.File.ReadAllBytes(fileName); + return new Tuple(fileData, new FontResolverInfo(fontName, isBold, isItalic)); + } + + var data = StandardFontData.GetFontData(fontName); + if (data != null) + { + Register(fontName, data, isBold, isItalic); + return new Tuple(data, new FontResolverInfo(fontName, isBold, isItalic)); + } + + return new Tuple(null, null); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Internal/ErrorHelpers.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/ErrorHelpers.cs index 12e0a06e..471812bd 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Internal/ErrorHelpers.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/ErrorHelpers.cs @@ -12,12 +12,15 @@ namespace PdfSharp.Internal // ReSharper disable once InconsistentNaming static class TH { - private const string SendUsTheFile = "\nPDFsharp cannot read this PDF file. " + + const string SendUsTheFile = "\nPDFsharp cannot read this PDF file. " + "If you think your file is a valid PDF file please send it to us so that we can fix this bug in the PDF parser."; public static InvalidOperationException InvalidOperationException_CouldNotFindMetadataDictionary() => new("Could not find document’s metadata dictionary." + SendUsTheFile); + public static InvalidOperationException InvalidOperationException_ReferenceMustNotBeNull() => + new("The reference must not be null."); + #region Reader Messages public static ObjectNotAvailableException ObjectNotAvailableException_CannotRetrieveStreamLength(Exception? innerException = null) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adam7.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adam7.cs similarity index 95% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adam7.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adam7.cs index f0320fb2..48af2e6f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adam7.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adam7.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System.Collections.Generic; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adler32Checksum.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adler32Checksum.cs similarity index 84% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adler32Checksum.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adler32Checksum.cs index 6e7b9a19..f137043e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Adler32Checksum.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Adler32Checksum.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System.Collections.Generic; @@ -13,7 +15,7 @@ namespace PdfSharp.BigGustave public static class Adler32Checksum { // Both sums (s1 and s2) are done modulo 65521. - private const int AdlerModulus = 65521; + const int AdlerModulus = 65521; /// /// Calculate the Adler-32 checksum for some data. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ChunkHeader.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ChunkHeader.cs similarity index 91% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ChunkHeader.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ChunkHeader.cs index 61e864d2..8e1ef7f0 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ChunkHeader.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ChunkHeader.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ColorType.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ColorType.cs similarity index 79% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ColorType.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ColorType.cs index f8a50d0d..f127ab93 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ColorType.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ColorType.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/CompressionMethod.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/CompressionMethod.cs similarity index 69% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/CompressionMethod.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/CompressionMethod.cs index 81dff90e..fe0701a5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/CompressionMethod.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/CompressionMethod.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// The method used to compress the image data. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Crc32.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Crc32.cs similarity index 91% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Crc32.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Crc32.cs index 9b424654..fc69b5e8 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Crc32.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Crc32.cs @@ -1,15 +1,17 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// 32-bit Cyclic Redundancy Code used by the PNG for checking the data is intact. /// public static class Crc32 { - private const uint Polynomial = 0xEDB88320; + const uint Polynomial = 0xEDB88320; static readonly uint[] Lookup; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Decoder.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Decoder.cs similarity index 97% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Decoder.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Decoder.cs index 768a54b0..cf8cf98d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Decoder.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Decoder.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterMethod.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterMethod.cs similarity index 69% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterMethod.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterMethod.cs index 6c7e78d2..1a6a25f7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterMethod.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterMethod.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// Indicates the pre-processing method applied to the image data before compression. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterType.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterType.cs similarity index 79% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterType.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterType.cs index 326f49d0..608dce8b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/FilterType.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/FilterType.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { internal enum FilterType { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/HeaderValidationResult.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/HeaderValidationResult.cs similarity index 88% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/HeaderValidationResult.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/HeaderValidationResult.cs index 3c4a2be4..fb8915b7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/HeaderValidationResult.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/HeaderValidationResult.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { internal readonly struct HeaderValidationResult { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/IChunkVisitor.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/IChunkVisitor.cs similarity index 72% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/IChunkVisitor.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/IChunkVisitor.cs index 10c9257f..7c2cd606 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/IChunkVisitor.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/IChunkVisitor.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System.IO; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ImageHeader.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ImageHeader.cs similarity index 94% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ImageHeader.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ImageHeader.cs index 3a99313f..67e7c281 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/ImageHeader.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/ImageHeader.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; using System.Collections.Generic; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/InterlaceMethod.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/InterlaceMethod.cs similarity index 70% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/InterlaceMethod.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/InterlaceMethod.cs index 01a63405..ab9dfc91 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/InterlaceMethod.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/InterlaceMethod.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// Indicates the transmission order of the image data. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/LICENSE b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/LICENSE similarity index 100% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/LICENSE rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/LICENSE diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Palette.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Palette.cs similarity index 89% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Palette.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Palette.cs index 3fb6e620..359c144e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Palette.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Palette.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// The Palette class. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Pixel.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Pixel.cs similarity index 95% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Pixel.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Pixel.cs index ea9833de..a8e1b6af 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Pixel.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Pixel.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// A pixel in a image. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Png.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Png.cs similarity index 97% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Png.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Png.cs index 0eb19fde..90db658e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/Png.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/Png.cs @@ -1,12 +1,11 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave -{ - using System; - using System.IO; +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. +namespace PdfSharp.Internal.Png.BigGustave +{ /// /// A PNG image. Call to open from file or bytes. /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngBuilder.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngBuilder.cs similarity index 99% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngBuilder.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngBuilder.cs index 752532a3..1d7f34cd 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngBuilder.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngBuilder.cs @@ -1,10 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -#nullable enable annotations +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +namespace PdfSharp.Internal.Png.BigGustave { using System; using System.Collections.Generic; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpener.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpener.cs similarity index 97% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpener.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpener.cs index eae2f528..6e0673b2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpener.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpener.cs @@ -1,16 +1,14 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -#nullable enable annotations +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave -{ - using System; - using System.IO; - using System.IO.Compression; - using System.Text; +using System.IO.Compression; +using System.Text; +namespace PdfSharp.Internal.Png.BigGustave +{ internal static class PngOpener { public static Png Open(Stream stream, IChunkVisitor? chunkVisitor = null) => Open(stream, new PngOpenerSettings diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpenerSettings.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpenerSettings.cs similarity index 79% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpenerSettings.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpenerSettings.cs index 52bf0a91..c6d0187a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngOpenerSettings.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngOpenerSettings.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { /// /// Settings to use when opening a PNG using diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngStreamWriteHelper.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngStreamWriteHelper.cs similarity index 90% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngStreamWriteHelper.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngStreamWriteHelper.cs index 88d4db5f..419dcb6e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/PngStreamWriteHelper.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/PngStreamWriteHelper.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; using System.Collections.Generic; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/RawPngData.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/RawPngData.cs similarity index 96% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/RawPngData.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/RawPngData.cs index b11d47ba..1f0940ad 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/RawPngData.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/RawPngData.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/StreamHelper.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/StreamHelper.cs similarity index 84% rename from src/foundation/src/PDFsharp/src/PdfSharp/PngLib/StreamHelper.cs rename to src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/StreamHelper.cs index bb8d18ab..d8a7f0e6 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PngLib/StreamHelper.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/Png/BigGustave/StreamHelper.cs @@ -1,8 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -// ReSharper disable once CheckNamespace -namespace PdfSharp.BigGustave +// BigGustave is distributed with PDFsharp, but was published under a different license. +// See file LICENSE in the folder containing this file. + +namespace PdfSharp.Internal.Png.BigGustave { using System; using System.IO; @@ -17,8 +19,8 @@ public static int ReadBigEndianInt32(Stream stream) public static int ReadBigEndianInt32(byte[] bytes, int offset) { - return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16) - + (bytes[2 + offset] << 8) + bytes[3 + offset]; + return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16) + + (bytes[2 + offset] << 8) + bytes[3 + offset]; } public static void WriteBigEndianInt32(Stream stream, int value) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Internal/StringExtensions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/StringExtensions.cs new file mode 100644 index 00000000..abc20690 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/StringExtensions.cs @@ -0,0 +1,37 @@ +namespace PdfSharp +{ + internal static class StringExtensions + { + /// + /// Add a number-suffix to the provided string.

+ /// If the string already has a number-suffix, the suffix is incremented by one. + ///
+ /// + /// + public static string AddIncrementalSuffix(this string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + var numberSuffix = 1; // will be incremented to 2 if text does not have a suffix + var idx = text.Length - 1; + // check if value ends with a number. if it does, increment it + while (idx >= 0 && char.IsDigit(text[idx])) + idx--; + idx++; + if (idx < text.Length) +#if NET6_0_OR_GREATER + numberSuffix = int.Parse(text[idx..]); +#else + numberSuffix = int.Parse(text.Substring(idx)); +#endif + numberSuffix++; +#if NET6_0_OR_GREATER + var namePrefix = text[..idx]; +#else + var namePrefix = text.Substring(0, idx); +#endif + return namePrefix + numberSuffix.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Internal/SuppressExceptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/SuppressExceptions.cs index 980c0090..7d0ada44 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Internal/SuppressExceptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Internal/SuppressExceptions.cs @@ -37,7 +37,6 @@ public static bool HasError(SuppressExceptions? suppressExceptions) } // Experimental - not yet in use. - #if true_ interface IErrorResult// TODO_OLD: Find final name diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Logging/LogMessages.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Logging/LogMessages.cs index d3572040..0e3d70e3 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Logging/LogMessages.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Logging/LogMessages.cs @@ -9,7 +9,7 @@ namespace PdfSharp.Logging { /// - /// Defines the logging event ids of PDFsharp. + /// Defines the logging event IDs of PDFsharp. /// public static class PdfSharpEventId // draft... { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs index 7f8223f6..1c7c5954 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs @@ -1,7 +1,15 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Fonts.OpenType; +using PdfSharp.Fonts.StandardFonts; using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Content; +using PdfSharp.Pdf.Content.Objects; +using PdfSharp.Pdf.Internal; +using System.Diagnostics.CodeAnalysis; namespace PdfSharp.Pdf.AcroForms { @@ -22,7 +30,9 @@ internal PdfAcroField(PdfDocument document) ///
protected PdfAcroField(PdfDictionary dict) : base(dict) - { } + { + DetermineAppearance(); + } /// /// Gets the name of this field. @@ -34,12 +44,99 @@ public string Name string name = Elements.GetString(Keys.T); return name; } + set + { + //if (value.Contains('.')) + // throw new ArgumentException("Field-names should not contain dots (.)", nameof(value)); + Debug.Assert(!value.Contains('.'), "Field-names should not contain dots (.)"); + Elements.SetString(Keys.T, value); + } + } + + /// + /// An alternate field name, to be used in place of the actual + /// field name wherever the field must be identified in the user interface (such as + /// in error or status messages referring to the field). This text is also useful + /// when extracting the document’s contents in support of accessibility to disabled + /// users or for other purposes. + /// + public string AlternateName + { + get { return Elements.GetString(Keys.TU); } + set { Elements.SetString(Keys.TU, value); } + } + + /// + /// The mapping name to be used when exporting interactive form field data from the document. + /// + public string MappingName + { + get { return Elements.GetString(Keys.TM); } + set { Elements.SetString(Keys.TM, value); } + } + + /// + /// Gets the fully qualified name of this field, that is: "parent-name.field-name" + /// If the field has no parent, this is equal to + /// + /// + /// Note: These names are not required to be unique for a given document.

+ /// The spec states (12.7.3.2):

+ /// It is possible for different field dictionaries to have the same fully qualified field name if they are descendants of + /// a common ancestor with that name and have no partial field names of their own. + /// Such field dictionaries are different representations of the same underlying field; + /// they should differ only in properties that specify their visual appearance. + ///
+ public string FullyQualifiedName + { + get + { + var fqn = Name; + var parent = Elements.GetObject(Keys.Parent) as PdfDictionary; + while (parent != null) + { + var parentName = parent.Elements.GetString(Keys.T); + if (!string.IsNullOrEmpty(parentName)) + fqn = parentName + "." + fqn; + parent = parent.Elements.GetObject(Keys.Parent) as PdfDictionary; + } + return fqn; + } + } + + /// + /// Gets the Parent of this field or null, if the field has no parent + /// + public PdfAcroField? Parent + { + get + { + var parentRef = Elements.GetReference(Keys.Parent); + if (parentRef != null && parentRef.Value is PdfDictionary pDict) + return PdfAcroFieldCollection.CreateAcroField(pDict); + return null; + } + internal set + { + if (value != null) + Elements.SetReference(Keys.Parent, value); + else + Elements.Remove(Keys.Parent); + } } /// /// Gets the field flags of this instance. /// - public PdfAcroFieldFlags Flags => (PdfAcroFieldFlags)Elements.GetInteger(Keys.Ff); + public PdfAcroFieldFlags Flags //=> (PdfAcroFieldFlags)Elements.GetInteger(Keys.Ff); + { + get + { + var ancestor = FindParentHavingKey(Keys.Ff); + return (PdfAcroFieldFlags)ancestor.Elements.GetInteger(Keys.Ff); + } + set => Elements.SetInteger(Keys.Ff, (int)value); + } internal PdfAcroFieldFlags SetFlags { @@ -52,7 +149,11 @@ internal PdfAcroFieldFlags SetFlags ///
public virtual PdfItem? Value { - get => Elements[Keys.V]; + get + { + var ancestor = FindParentHavingKey(Keys.V); + return ancestor.Elements[Keys.V]; + } set { if (ReadOnly) @@ -79,6 +180,91 @@ public bool ReadOnly } } + /// + /// Gets or sets the font used to draw the text of the field.

+ /// The font-size may be adjusted during rendering when is set to true.

+ /// In this case, the size of the field's widget determines the actual font-size. + ///
+ public XFont? Font + { + get { return font; } + set { font = value; } + } + XFont? font; + + /// + /// Gets the font size that was obtained by analyzing the Fields' content-stream.

+ /// May be zero. This means, during rendering, the font-size should be calculated based on the height of the field's widget.

+ /// PdfReference 2.0 states in chapter 12.7.4.3:

+ /// A zero value for size means that the font shall be auto-sized: + /// its size shall be computed as an implementation dependent function. + ///
+ public double? FontSize + { + get { return _fontSize; } + set + { + if (value <= 0.0) + throw new ArgumentOutOfRangeException(nameof(value), "FontSize must not be smaller than or equal to zero"); + _fontSize = value; + } + } + private double? _fontSize; + + /// + /// Gets or sets a value that determines whether the font should be auto-sized when rendered.

+ /// Note: It is not specified, how exactly the font-size should be calculated, + /// but it would typically be a function of the height of the field's widget.

+ /// Unless you have a very specific reason to do so, you should not set this to true for new fields.

+ /// (support for this seems to be very poor in most common PDF-viewers) + ///
+ public bool AutomaticFontSize { get; set; } + + /// + /// Gets or sets the foreground (i.e. text-) color of the field. + /// + /// + /// Note: This is not a real property of an AcroField, but a property of an AcroField's annotation.

+ /// If is included here for convenience so is doesn't has to be set for every annotation separately. + ///
+ public XColor ForeColor + { + get { return foreColor; } + set { foreColor = value; } + } + XColor foreColor = XColors.Black; + + /// + /// Gets or sets the default value of the field. + /// + public virtual PdfItem? DefaultValue + { + get + { + var ancestor = FindParentHavingKey(Keys.DV); + return ancestor.Elements.ContainsKey(Keys.DV) ? ancestor.Elements.GetValue(Keys.DV) : new PdfString(""); + } + set { Elements[Keys.DV] = value; } + } + + /// + /// Gets or sets the alignment for the text of this field. + /// + public virtual PdfAcroFieldTextAlignment TextAlign + { + get + { + var alignment = PdfAcroFieldTextAlignment.Left; // default + var ancestor = FindParentHavingKey(Keys.Q); + if (ancestor.Elements.ContainsKey(Keys.Q)) + alignment = (PdfAcroFieldTextAlignment)ancestor.Elements.GetInteger(Keys.Q); + else if (_document.AcroForm != null && _document.AcroForm.Elements.ContainsKey(Keys.Q)) + alignment = (PdfAcroFieldTextAlignment)_document.AcroForm.Elements.GetInteger(Keys.Q); + return alignment; + } + set { Elements[Keys.Q] = new PdfInteger((int)value); } + } + /// /// Gets the field with the specified name. /// @@ -91,182 +277,716 @@ public bool ReadOnly { if (String.IsNullOrEmpty(name)) return this; - if (HasKids) + if (HasChildFields) return Fields.GetValue(name); return null; } + /// + /// Indicates whether the field has annotations, i.e. visible representations of the field.

+ /// Not all fields have visible representations of their own, e.g. when a field acts only as a container for other fields. + ///
+ public bool HasAnnotations => Annotations.Elements.Count > 0; + /// /// Indicates whether the field has child fields. /// - public bool HasKids + public bool HasChildFields { get { - PdfItem? item = Elements[Keys.Kids]; - if (item == null) - return false; - if (item is PdfArray array) - return array.Elements.Count > 0; + var kidsArray = Elements.GetArray(Keys.Kids); + if (kidsArray != null) + { + for (var i = 0; i < kidsArray.Elements.Count; i++) + { + if (kidsArray.Elements.GetObject(i) is PdfDictionary kid && IsField(kid)) + return true; + } + } return false; } } /// - /// Gets the names of all descendants of this field. + /// Gets the collection of fields within this field. /// - [Obsolete("Use GetDescendantNames")] - public string[] DescendantNames // Properties should not return arrays. - => - GetDescendantNames(); + public PdfAcroFieldCollection Fields + { + get + { + if (_fields == null) + { + // owner may be a widget annotation, we have to make sure, the owner is correct, + // otherwise an exception occurs (/Kids is not a valid key for Annotations) + if (Elements.Owner != this) + Elements.ChangeOwner(this); + object o = Elements.GetValue(Keys.Kids, VCF.CreateIndirect)!; + _fields = (PdfAcroFieldCollection)o; + } + // TODO: It would be nice if the FieldCollection contains only "real" fields. + // Currently, the items are a mix of fields and annotations... + return _fields; + } + } + PdfAcroFieldCollection? _fields; + + /// + /// Gets the annotations for this field. + /// The elements in this list are of type . + /// + public PdfAnnotationArray Annotations + { + get + { + if (_annotations == null) + { + _annotations = new PdfAnnotationArray(); + var childs = Elements.GetArray(Keys.Kids); + if (childs != null && childs.Elements.Count > 0) + { + for (var i = 0; i < childs.Elements.Count; i++) + { + var obj = childs.Elements.GetDictionary(i); + if (obj is PdfWidgetAnnotation annotation) + _annotations.Elements.Add(annotation); + else if (obj != null && "/Widget".Equals(obj.Elements.GetString(PdfAnnotation.Keys.Subtype), StringComparison.OrdinalIgnoreCase)) + { + var annot = new PdfWidgetAnnotation(obj); + if (annot.Page != null) + annot.Parent = annot.Page.Annotations; + _annotations.Elements.Add(annot); + // must reset the value in the reference after type-transformation so a reference to this field points to the field, not the widget + obj.Reference!.Value = obj; + } + } + } + // if the dictionaries are merged (no childs), use current field as Widget + if ("/Widget".Equals(Elements.GetString(PdfAnnotation.Keys.Subtype), StringComparison.OrdinalIgnoreCase)) + { + var annot = new PdfWidgetAnnotation(this); + if (annot.Page != null) + annot.Parent = annot.Page.Annotations; + _annotations.Elements.Add(annot); + // must reset the value in the reference after type-transformation + Reference!.Value = this; + } + } + return _annotations; + } + } + PdfAnnotationArray? _annotations; + + /// + /// Adds a new Annotation to this field. + /// + /// A method that is used to configure the Annotation + /// The created and configured Annotation + /// + public virtual PdfWidgetAnnotation AddAnnotation(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var annotation = new PdfWidgetAnnotation(_document) + { + ParentField = this + }; + // must add to iref-table here because we need a valid Reference for the field's Kids-array + Owner.IrefTable.Add(annotation); + + configure(annotation); + if (!Elements.ContainsKey(Keys.Kids)) + Elements.GetValue(Keys.Kids, VCF.CreateIndirect); + var childs = Elements.GetArray(Keys.Kids)!; + childs.Elements.Add(annotation.Reference!); + // re-create updated annotations the next time the "Annotations"-Property is accessed + _annotations = null; + return annotation; + } + + /// + /// Removes the specified annotation from this field + /// + /// The annotation to remove + /// true, if the annotation was removed, false otherwise + public bool RemoveAnnotation(PdfWidgetAnnotation annotation) + { + if (annotation == null) + return false; + Debug.Assert(Annotations.Elements.IndexOf(annotation) >= 0, "Annotation is not part of this field"); + + var kids = Elements.GetArray(Keys.Kids); + if (kids != null) + { + for (var i = 0; i < kids.Elements.Count; i++) + { + var kid = kids.Elements.GetObject(i); + if (kid != null && kid.ObjectID == annotation.ObjectID) + { + kids.Elements.RemoveAt(i--); + } + } + } + var removed = Annotations.Elements.Remove(annotation); + if (removed) + annotation.Page?.Annotations.Remove(annotation); + // re-create updated annotations the next time the "Annotations"-Property is accessed + _annotations = null; + + return removed; + } + + /// + /// Determines whether the specified field is an actual AcroField.

+ /// Used to tell apart actual fields from WidgetAnnotations.

+ /// PdfReference 1.7, Chapter 12.7.1: + /// As a convenience, when a field has only a single associated widget annotation, the + /// contents of the field dictionary and the annotation dictionary(12.5.2, 'Annotation Dictionaries') + /// may be merged into a single dictionary containing entries that pertain to both a field and an annotation.

+ /// This means, a PdfObject may be a PdfAcroField AND a PdfWidgetAnnotation at the same time. + /// We consider a dictionary to be a Field, if one of the following is true:

+ /// - the /Subtype is missing (which is required for Annotations),

+ /// - it has an /T or /FT entry (which is required for terminal fields) -> Chapter 12.7.3.1 in PdfReference

+ /// - it has a /Kids entry (which is invalid for Annotations) -> indicates a container for other fields + ///
+ /// + /// + internal static bool IsField(PdfDictionary field) + { + return !field.Elements.ContainsKey(PdfAnnotation.Keys.Subtype) + || field.Elements.ContainsKey(Keys.FT) + || field.Elements.ContainsKey(Keys.T) + || field.Elements.ContainsKey(Keys.Kids); + } + + /// + /// Used to retrieve inherited field-properties from parent-fields.

+ /// If no parent is found having the specified key, this is returned. + ///
+ /// + /// + protected PdfAcroField FindParentHavingKey(string key) + { + var field = this; + if (!field.Elements.ContainsKey(key)) + field = Parent?.FindParentHavingKey(key); + return field ?? this; + } + + /// + /// Adds the specified to the list of child-fields of this field + /// + /// + /// + public void AddChild(PdfAcroField childField) + { + var existingField = Fields.GetValue(childField.Name); + if (existingField != null) + throw new InvalidOperationException($"Field '{Name}' already has a child-field named '{childField.Name}'"); + Fields.Elements.Add(childField); + childField.Parent = this; + } /// - /// Gets the names of all descendants of this field. + /// Removes this field, all child-fields and associated annotations from the document /// - public string[] GetDescendantNames() + public void Remove() { - List names = []; - if (HasKids) + var annots = Annotations.Elements.ToArray(); + foreach (var annot in annots) + RemoveAnnotation(annot); + + // delete childs + for (var i = 0; i < Fields.Count; i++) + { + var child = Fields[i]; + child.Remove(); + } + Fields.Elements.Clear(); + + var fieldsList = new[] { Parent?.Fields, Owner.AcroForm?.Fields }; + foreach (var fields in fieldsList) { - PdfAcroFieldCollection fields = Fields; - fields.GetDescendantNames(ref names, null); + for (var i = 0; i < fields?.Elements.Count; i++) + { + var kid = fields.Elements.GetObject(i); + if (kid != null && kid.ObjectID == ObjectID) + { + fields.Elements.RemoveAt(i--); + } + } } - List temp = []; - foreach (string name in names) - temp.Add(name); - return temp.ToArray(); } /// - /// Gets the names of all appearance dictionaries of this AcroField. + /// Tries to determine the Appearance of the Field by checking elements of its dictionary /// - public string[] GetAppearanceNames() + protected virtual void DetermineAppearance() { - Dictionary names = new(); - if (Elements["/AP"] is PdfDictionary dict) + try + { + var ancestor = FindParentHavingKey(Keys.DA); + var da = ancestor.Elements.GetString(Keys.DA); // 12.7.3.3 + if (string.IsNullOrEmpty(da) && _document.AcroForm != null) + { + // if Field does not contain appearance dictionary, check AcroForm + da = _document.AcroForm.Elements.GetString(Keys.DA); + if (da == null) + { + // no appearance found, use some default + font = new XFont("Helvetica", 10, XFontStyleEx.Regular); + return; + } + } + if (!string.IsNullOrEmpty(da)) + DetermineFontFromContent(PdfEncoders.RawEncoding.GetBytes(da)); + } + catch { - AppDict(dict, names); + font = new XFont("Helvetica", 10, XFontStyleEx.Regular); + } + } - if (HasKids) + /// + /// Attempts to determine the font, font-size and fore-color of this AcroField + /// + /// + protected void DetermineFontFromContent(byte[] contentBytes) + { + string? fontName = null; + double fontSize = 10.0; + var content = ContentReader.ReadContent(contentBytes); + for (var i = 0; i < content.Count; i++) + { + if (content[i] is COperator op) + { + switch (op.OpCode.OpCodeName) + { + case OpCodeName.Tf: + fontName = op.Operands[0].ToString(); + fontSize = double.Parse(op.Operands[1].ToString()!, CultureInfo.InvariantCulture); + break; + case OpCodeName.g: // gray value (0.0 = black, 1.0 = white) + if (op.Operands.Count > 0) + ForeColor = XColor.FromGrayScale(double.Parse(op.Operands[0].ToString()!, CultureInfo.InvariantCulture)); + break; + case OpCodeName.rg: // rgb color (Chapter 8.6.8) + if (op.Operands.Count > 2) + { + var r = double.Parse(op.Operands[0].ToString()!, CultureInfo.InvariantCulture); + var g = double.Parse(op.Operands[1].ToString()!, CultureInfo.InvariantCulture); + var b = double.Parse(op.Operands[2].ToString()!, CultureInfo.InvariantCulture); + ForeColor = XColor.FromArgb((int)Math.Round(r * 255.0), (int)Math.Round(g * 255.0), (int)Math.Round(b * 255.0)); + } + break; + case OpCodeName.k: + if (op.Operands.Count > 3) + { + var c = double.Parse(op.Operands[0].ToString()!, CultureInfo.InvariantCulture); + var m = double.Parse(op.Operands[1].ToString()!, CultureInfo.InvariantCulture); + var y = double.Parse(op.Operands[2].ToString()!, CultureInfo.InvariantCulture); + var k = double.Parse(op.Operands[3].ToString()!, CultureInfo.InvariantCulture); + ForeColor = XColor.FromCmyk((int)Math.Round(c * 255.0), (int)Math.Round(m * 255.0), (int)Math.Round(y * 255.0), (int)Math.Round(k * 255.0)); + } + break; + } + } + } + AutomaticFontSize = fontSize == 0; + _fontSize = fontSize; + if (!string.IsNullOrEmpty(fontName)) + { + if (!TryGetFont(fontName, out font)) { - PdfItem[] kids = Fields.Elements.Items; - foreach (PdfItem pdfItem in kids) + // if not found or not supported (e.g. not a TrueTypeFont) create new font + try { - if (pdfItem is PdfReference) + // try to create font (may use a custom FontResolver) + font = new XFont(fontName, AutomaticFontSize ? 10 : fontSize, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEncoding.Automatic, PdfFontEmbedding.EmbedCompleteFontFile)); + } + catch + { + // unable to resolve font, use one of the Standard-Fonts + var defaultFontName = StandardFontNames.Helvetica; + // manually cache font-data so it can be found even without a FontResolver + var typefaceKey = XGlyphTypeface.ComputeGtfKey(defaultFontName, false, false); + if (!Globals.Global.Fonts.GlyphTypefacesByKey.ContainsKey(typefaceKey)) { - PdfDictionary? xxx = ((PdfReference)pdfItem).Value as PdfDictionary; - if (xxx != null) - AppDict(xxx, names); + var fontData = StandardFontData.GetFontData(defaultFontName)!; + var fontSource = XFontSource.GetOrCreateFrom(fontData); + Globals.Global.Fonts.GlyphTypefacesByKey[typefaceKey] = new XGlyphTypeface(fontSource); } + font = new XFont(defaultFontName, + AutomaticFontSize ? 10 : fontSize, + XFontStyleEx.Regular, // should match the options for the typefaceKey + new XPdfFontOptions(PdfFontEncoding.Automatic, PdfFontEmbedding.EmbedCompleteFontFile)); } - //((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1); + } + } + } + /// + /// Tries to create an XFont from the information stored in the AcroForm's Font-Resources + /// + /// The key in the AcroForms Font-Resources that identifies the font + /// Contains the font used by this field or null if the font could not be resolved + /// + internal bool TryGetFont(string resourceKey, [MaybeNullWhen(false)] out XFont? xFont) + { + xFont = null; + if (Owner.AcroForm != null && Owner.AcroForm.Resources != null) + { + var fontDict = Owner.AcroForm.Resources.Fonts.Elements.GetDictionary(resourceKey); + if (fontDict != null) + { + var subType = fontDict.Elements.GetName(PdfFont.Keys.Subtype); + var fontName = fontDict.Elements.GetName(PdfFont.Keys.BaseFont); + var fontDescriptor = fontDict.Elements.GetDictionary(PdfFont.Keys.FontDescriptor); + var descendantFonts = fontDict.Elements.GetArray(PdfType0Font.Keys.DescendantFonts); + byte[]? fontData = null; + FontType fontType = FontType.TrueTypeWinAnsi; + // Standard-font that is not embedded + if (subType == "/Type1" && fontDescriptor == null && !string.IsNullOrEmpty(fontName)) + { + fontName = fontName.TrimStart('/'); + if (StandardFontData.IsStandardFont(fontName)) + { + fontData = StandardFontData.GetFontData(fontName)!; + fontType = FontType.Type1StandardFont; + } + } + // embedded true-type font + else if (subType == "/TrueType" && fontDescriptor != null) + { + var fileRef = fontDescriptor.Elements.GetDictionary(PdfFontDescriptor.Keys.FontFile2); + if (fileRef != null) + { + fontData = fileRef?.Stream.UnfilteredValue; + } + } + else if (subType == "/Type0" && descendantFonts != null) + { + // entries like those generated by PDFsharp itself + var descFont = descendantFonts.Elements.GetDictionary(0); + if (descFont != null) + { + fontDescriptor = descFont.Elements.GetDictionary(PdfFont.Keys.FontDescriptor); + if (fontDescriptor != null) + { + var fileRef = fontDescriptor.Elements.GetDictionary(PdfFontDescriptor.Keys.FontFile2); + if (fileRef != null) + { + fontData = fileRef?.Stream.UnfilteredValue; + } + } + } + } + if (fontData != null) + { + var fontSource = XFontSource.GetOrCreateFrom(fontData); + var typeFace = new XGlyphTypeface(fontSource); + // cache the typeFace + if (!GlyphTypefaceCache.TryGetGlyphTypeface(typeFace.Key, out _)) + GlyphTypefaceCache.AddGlyphTypeface(typeFace); + // cache the font + Owner.FontTable.CacheExistingFont(fontDict, typeFace, fontType); + var style = XFontStyleEx.Regular; + if (typeFace.IsBold) + style |= XFontStyleEx.Bold; + if (typeFace.IsItalic) + style |= XFontStyleEx.Italic; + if (string.IsNullOrEmpty(fontName) || fontType != FontType.Type1StandardFont) + fontName = typeFace.FamilyName; + xFont = new XFont(fontName, Math.Max(_fontSize ?? 0, 10.0), style, + new XPdfFontOptions(PdfFontEncoding.Automatic, + fontType == FontType.Type1StandardFont ? PdfFontEmbedding.OmitStandardFont : PdfFontEmbedding.EmbedCompleteFontFile)); + return true; + } } } - string[] array = new string[names.Count]; - names.Keys.CopyTo(array, 0); - return array; + return false; } - //static string[] AppearanceNames(PdfDictionary dictIn) - //{ - // Dictionary names = new Dictionary(); - // PdfDictionary dict = dictIn["/AP"] as PdfDictionary; - // if (dict != null) - // { - // AppDict(dict, names); + /// + /// Calculates the optimal font-size based on the height of the specified widget.

+ /// If a font-size greater than 0 is specified in the field's appearance or default appearance, this size is returned.

+ /// Otherwise the returned font-size is set to be 80% of the widget's height for single-line fields and a fixed value of 10 for multi-line fields.

+ /// Always returns a value greater than 0 + ///
+ /// The widget, the font-size should be based on + /// + internal double DetermineFontSize(PdfWidgetAnnotation? widget) + { + if (FontSize > 0.0) + return FontSize.Value; - // if (HasKids) - // { - // PdfItem[] kids = Fields.Elements.Items; - // foreach (PdfItem pdfItem in kids) - // { - // if (pdfItem is PdfReference) - // { - // PdfDictionary xxx = ((PdfReference)pdfItem).Value as PdfDictionary; - // if (xxx != null) - // AppDict(xxx, names); - // } - // } - // //((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1); + var fontSize = 10.0; + if (widget != null && !widget.Rectangle.IsZero) + { + var refValue = widget.Rotation == 0 + || widget.Rotation == 180 + || (widget.Flags & PdfAnnotationFlags.NoRotate) != 0 + ? widget.Rectangle.Height : widget.Rectangle.Width; - // } - // } - // string[] array = new string[names.Count]; - // names.Keys.CopyTo(array, 0); - // return array; - //} + if (this is not PdfTextField field || !field.MultiLine) + // Rects were spotted with negative height + fontSize = Math.Abs(refValue * 0.80); // set font size to 80% of the widget height + return Math.Max(1.0, fontSize); + } + return fontSize; + } - static void AppDict(PdfDictionary dict, Dictionary names) + /// + /// This may switch the encoding for the current font to Unicode from the default WinAnsi. + /// + /// + internal void SetFontType(FontType fontType) { - PdfDictionary? sub; - if ((sub = dict.Elements["/D"] as PdfDictionary) != null) - AppDict2(sub, names); - if ((sub = dict.Elements["/N"] as PdfDictionary) != null) - AppDict2(sub, names); + if (Font is null) + return; + + // do not change the encoding for standard-fonts + if (Font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont) + return; + + if (Font.PdfOptions.FontEncoding != PdfFontEncoding.Unicode && fontType == FontType.Type0Unicode) + Font = new XFont(Font.GlyphTypeface, Font.Size, + new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.EmbedCompleteFontFile)); + } + + /// + /// Adds the font of the current AcroField to the specified XForm object + /// + /// + internal void SetXFormFont(XForm form) + { + if (Font is null) + return; + + var fontType = Font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : Font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var docFont = _document.FontTable.GetOrCreateFont(Font.GlyphTypeface, fontType); + form.PdfForm.Resources.AddFont(docFont); } - static void AppDict2(PdfDictionary dict, Dictionary names) + internal override void PrepareForSave() { - foreach (string key in dict.Elements.Keys) + base.PrepareForSave(); + // add the font to the AcroForm's resources + if (Font != null && _document.AcroForm != null) { - if (!names.ContainsKey(key)) - names.Add(key, null!); + var formResources = _document.AcroForm.GetOrCreateResources(); + var fontType = Font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : Font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var pdfFont = _document.FontTable.GetOrCreateFont(Font.GlyphTypeface, fontType); + formResources.AddFont(pdfFont); } + // TODO: as a small optimization, we may merge field and annotation if there is only a single annotation + // -> leave this for a future version + if (HasChildFields) + { + for (var i = 0; i < Fields.Elements.Count; i++) + { + var field = Fields[i]; + field.PrepareForSave(); + } + } + // accessing the Fields-property may have created a new empty array, remove that + if (Fields.Elements.Count == 0) + Elements.Remove(Keys.Kids); + // handle annotations + foreach (var annot in Annotations.Elements) + annot.PrepareForSave(); } - internal virtual void GetDescendantNames(ref List names, string? partialName) + internal virtual void Flatten() { - if (HasKids) + // Copy Font-Resources to the Page + // This is neccessary, because Fonts used by AcroFields may be referenced only by the AcroForm, which is deleted after flattening + for (var i = 0; i < Annotations.Elements.Count; i++) { - PdfAcroFieldCollection fields = Fields; - string t = Elements.GetString(Keys.T); - Debug.Assert(t != ""); - if (t.Length > 0) + var widget = Annotations.Elements[i]; + if ((widget.Flags & PdfAnnotationFlags.Hidden) != 0 || (widget.Flags & PdfAnnotationFlags.NoView) != 0) { - if (!String.IsNullOrEmpty(partialName)) - partialName += "." + t; - else - partialName = t; - fields.GetDescendantNames(ref names, partialName); + RemoveAnnotation(widget); + i--; + continue; + } + if (widget.Page != null) + { + var acroResources = _document.AcroForm?.Elements.GetDictionary(PdfAcroForm.Keys.DR); + var pageResources = widget.Page.Elements.GetDictionary(PdfPage.Keys.Resources); + if (acroResources != null && pageResources != null) + { + var acroFontList = acroResources.Elements.GetDictionary(PdfResources.Keys.Font); + var pageFontList = pageResources.Elements.GetDictionary(PdfResources.Keys.Font); + if (acroFontList != null) + { + pageFontList ??= new PdfDictionary(Owner); + pageResources.Elements.SetObject(PdfResources.Keys.Font, pageFontList); + foreach (var fontKey in acroFontList.Elements.Keys) + { + if (!pageFontList.Elements.ContainsKey(fontKey)) + pageFontList.Elements.Add(fontKey, acroFontList.Elements[fontKey]); + } + } + } } } - else + + for (var i = 0; i < Annotations.Elements.Count; i++) { - string t = Elements.GetString(Keys.T); - Debug.Assert(t != ""); - if (t.Length > 0) + var widget = Annotations.Elements[i]; + // Remove annotation + widget.Parent?.Remove(widget); + widget.Page?.Annotations.Remove(widget); + } + + if (HasChildFields) + { + for (var i = 0; i < Fields.Elements.Count; i++) { - if (!String.IsNullOrEmpty(partialName)) - names.Add(partialName + "." + t); - else - names.Add(t); + var field = Fields[i]; + field.Flatten(); } } + + if (Reference != null) + _document.IrefTable.Remove(Reference); + + RenderAppearance(); + RenderAppearanceToPage(); } /// - /// Gets the collection of fields within this field. + /// Must be overridden by subclasses /// - public PdfAcroFieldCollection Fields + protected virtual void RenderAppearance() { - get + } + + /// + /// Renders the widget-appearances of this field directly onto the page.

+ /// Used by the method. + ///
+ protected void RenderAppearanceToPage() + { + // /N -> Normal appearance, /R -> Rollover appearance, /D -> Down appearance + const string normalName = "/N"; + + for (var i = 0; i < Annotations.Elements.Count; i++) { - if (_fields == null) + var widget = Annotations.Elements[i]; + if (widget.Page != null) { - var o = Elements.GetValue(Keys.Kids, VCF.CreateIndirect); - _fields = (PdfAcroFieldCollection?)o ?? NRT.ThrowOnNull(); + var appearances = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + if (appearances != null) + { + var normalAppearance = appearances.Elements.GetDictionary(normalName); + var appeareanceState = widget.Elements.GetName(PdfAnnotation.Keys.AS); + // if state is unset, treat normal appearance as the appearance itself + if (normalAppearance != null && string.IsNullOrEmpty(appeareanceState)) + RenderContentStream(widget.Page, normalAppearance, widget.Rectangle); + else if (normalAppearance != null) + { + // the state is used by radio-buttons and checkboxes, which have a checked and an unchecked state + var selectedAppearance = normalAppearance.Elements.GetDictionary(appeareanceState); + if (selectedAppearance != null) + RenderContentStream(widget.Page, selectedAppearance, widget.Rectangle); + } + } } - return _fields; } } - PdfAcroFieldCollection? _fields; + + /// + /// Renders the contents of the supplied Stream to the Page at the position specified by the provided Rectangle + /// + /// Page to render the content onto + /// A containing a stream with drawing-operators and associated resources + /// + protected virtual void RenderContentStream(PdfPage page, PdfDictionary streamDict, PdfRectangle rect) + { + if (streamDict == null || streamDict.Stream == null || rect.IsZero) + return; + var stream = streamDict.Stream; + var content = ContentReader.ReadContent(stream.UnfilteredValue); + // check for graphical objects and copy them to the pages resources + foreach (var obj in content) + { + if (obj is COperator op) + { + if (op.OpCode.OpCodeName == OpCodeName.Do) + { + var arg = op.Operands[0].ToString()!; + var resources = streamDict.Elements.GetDictionary("/Resources"); + if (resources != null) + { + var xobjDict = resources.Elements.GetDictionary("/XObject"); + if (xobjDict != null && xobjDict.Elements.ContainsKey(arg)) + { + var objDict = xobjDict.Elements.GetDictionary(arg)!; + if (!page.Resources.Elements.ContainsKey("/XObject")) + page.Resources.Elements.Add("/XObject", new PdfDictionary()); + xobjDict = page.Resources.Elements.GetDictionary("/XObject")!; + // create new unique name for the xobject + var objKey = arg + Guid.NewGuid().ToString("N"); + objDict.Elements.SetName("/Name", objKey); + xobjDict.Elements[objKey] = objDict; + op.Operands[0] = new CName(objKey); + } + } + } + } + } + // TODO: use W or W* operator for clipping + var matrix = new XMatrix(); + matrix.TranslateAppend(rect.X1, rect.Y1); + var matElements = matrix.GetElements(); + var matrixOp = OpCodes.OperatorFromName("cm"); + foreach (var el in matElements) + matrixOp.Operands.Add(new CReal { Value = el }); + content.Insert(0, matrixOp); + + // Save and restore Graphics state + var appendedContent = page.Contents.AppendContent(); + appendedContent.CreateStream(content.ToContent()); + appendedContent.PreserveGraphicsState(); // wrap in q/Q + } + + /// + /// Holds the collection of WidgetAnnotations for a field + /// + public sealed class PdfAnnotationArray + { + private readonly List elements = []; + + /// + /// Gets the list of of the array + /// + public List Elements + { + get { return elements; } + } + } /// /// Holds a collection of interactive fields. /// public sealed class PdfAcroFieldCollection : PdfArray { + internal PdfAcroFieldCollection(PdfDocument document) + : base(document) + { } + internal PdfAcroFieldCollection(PdfArray array) : base(array) { } @@ -284,40 +1004,22 @@ public string[] Names get { int count = Elements.Count; - string[] names = new string[count]; + var names = new List(count); for (int idx = 0; idx < count; idx++) - names[idx] = ((PdfDictionary)((PdfReference)Elements[idx]).Value).Elements.GetString(Keys.T); - return names; - } - } - - /// - /// Gets an array of all descendant names. - /// - public string[] DescendantNames - { - get - { - var names = new List(); - GetDescendantNames(ref names, null); - //List temp = new List(); - //foreach (PdfName name in names) - // temp.Add(name.ToString()); + { + var dict = Elements.GetDictionary(idx); + // the element may be a WidgetAnnotation lacking the /T key, skip these + if (dict != null && dict.Elements.ContainsKey(Keys.T)) + { + var name = dict.Elements.GetString(Keys.T); + if (!string.IsNullOrEmpty(name)) + names.Add(name); + } + } return names.ToArray(); } } - internal void GetDescendantNames(ref List names, string? partialName) - { - int count = Elements.Count; - for (int idx = 0; idx < count; idx++) - { - var field = this[idx]; - if (field != null!) - field.GetDescendantNames(ref names, partialName); - } - } - /// /// Gets a field from the collection. For your convenience an instance of a derived class like /// PdfTextField or PdfCheckBox is returned if PDFsharp can guess the actual type of the dictionary. @@ -372,7 +1074,7 @@ public PdfAcroField this[int index] /// If the actual cannot be guessed by PDFsharp the function returns an instance /// of PdfGenericField. /// - PdfAcroField CreateAcroField(PdfDictionary dict) + internal static PdfAcroField CreateAcroField(PdfDictionary dict) { string ft = dict.Elements.GetName(Keys.FT); PdfAcroFieldFlags flags = (PdfAcroFieldFlags)dict.Elements.GetInteger(Keys.Ff); @@ -400,6 +1102,7 @@ PdfAcroField CreateAcroField(PdfDictionary dict) return new PdfSignatureField(dict); default: + // this is either a non-terminal field or a WidgetAnnotation return new PdfGenericField(dict); } } @@ -415,11 +1118,11 @@ public class Keys : KeysBase /// /// (Required for terminal fields; inheritable) The type of field that this dictionary - /// describes: - /// Btn Button - /// Tx Text - /// Ch Choice - /// Sig (PDF 1.3) Signature + /// describes:

+ /// Btn Button

+ /// Tx Text

+ /// Ch Choice

+ /// Sig (PDF 1.3) Signature

/// Note: This entry may be present in a nonterminal field (one whose descendants /// are themselves fields) in order to provide an inheritable FT value. However, a /// nonterminal field does not logically have a type of its own; it is merely a container @@ -439,7 +1142,12 @@ public class Keys : KeysBase public const string Parent = "/Parent"; /// - /// (Optional) An array of indirect references to the immediate children of this field. + /// (Sometimes required, as described below) An array of indirect references to the immediate + /// children of this field. In a non-terminal field, the Kids array shall refer to field dictionaries + /// that are immediate descendants of this field. In a terminal field, the Kids array ordinarily + /// shall refer to one or more separate widget annotations that are associated with this field. + /// However, if there is only one associated widget annotation, and its contents have been merged + /// into the field dictionary, Kids shall be omitted. /// [KeyInfo(KeyType.Array | KeyType.Optional, typeof(PdfAcroFieldCollection))] public const string Kids = "/Kids"; @@ -498,15 +1206,6 @@ public class Keys : KeysBase // ----- Additional entries to all fields containing variable text -------------------------- - /// - /// (Required; inheritable) A resource dictionary containing default resources - /// (such as fonts, patterns, or color spaces) to be used by the appearance stream. - /// At a minimum, this dictionary must contain a Font entry specifying the resource - /// name and font dictionary of the default font for displaying the field’s text. - /// - [KeyInfo(KeyType.Dictionary | KeyType.Required)] - public const string DR = "/DR"; - /// /// (Required; inheritable) The default appearance string, containing a sequence of /// valid page-content graphics or text state operators defining such properties as @@ -526,6 +1225,22 @@ public class Keys : KeysBase [KeyInfo(KeyType.Integer | KeyType.Optional)] public const string Q = "/Q"; + // Keys specific to fields containing variable text + + /// + /// (Optional; PDF 1.5) A default style string, as described in Adobe XML + /// Architecture, XML Forms Architecture(XFA) Specification, version 3.3. + /// + [KeyInfo(KeyType.String | KeyType.Optional)] + public const string DS = "/DS"; + + /// + /// (Optional; PDF 1.5) A rich text string, as described in Adobe XML + /// Architecture, XML Forms Architecture(XFA) Specification, version 3.3. + /// + [KeyInfo(KeyType.String | KeyType.Optional)] + public const string RV = "/RV"; + // ReSharper restore InconsistentNaming } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs index d1763a9d..e4d11c31 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs @@ -1,6 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.AcroForms.Rendering; +using PdfSharp.Pdf.Advanced; + namespace PdfSharp.Pdf.AcroForms { /// @@ -23,7 +27,8 @@ internal PdfAcroForm(PdfDictionary dictionary) { } /// - /// Gets the fields collection of this form. + /// Gets the fields collection (i.e. the root fields) of this AcroForm.

+ /// To retrieve all fields (including child-fields), use ///
public PdfAcroField.PdfAcroFieldCollection Fields { @@ -39,6 +44,402 @@ public PdfAcroField.PdfAcroFieldCollection Fields } PdfAcroField.PdfAcroFieldCollection? _fields; + /// + /// Gets or sets the default text-alignment for variable text fields. + /// + public PdfAcroFieldTextAlignment? DefaultTextAlign + { + get + { + if (Elements.ContainsKey(Keys.Q)) + return (PdfAcroFieldTextAlignment)Elements.GetInteger(Keys.Q); + return null; + } + set + { + if (value is null) + Elements.Remove(Keys.Q); + else + Elements[Keys.Q] = new PdfInteger((int)value); + } + } + + /// + /// Sets the default appearance for variable text fields. + /// + public void SetDefaultAppearance(XFont font, double fontSize, XColor textColor) + { + if (font is null) + throw new ArgumentNullException(nameof(font)); + if (fontSize < 0.0) + throw new ArgumentException("Font size must be greater or equal to zero", nameof(fontSize)); + + var formResources = GetOrCreateResources(); + var fontType = font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var docFont = _document.FontTable.GetOrCreateFont(font.GlyphTypeface, fontType); + var fontName = formResources.AddFont(docFont); + var da = string.Format(CultureInfo.InvariantCulture, "{0} {1:F2} Tf {2:F4} {3:F4} {4:F4} rg", + fontName, fontSize, textColor.R / 255, textColor.G / 255, textColor.B / 255); + Elements.SetString(Keys.DA, da); + } + + /// + /// Gets the flattened field-hierarchy of this AcroForm + /// + public IEnumerable GetAllFields() + { + var fields = new List(); + if (Fields != null) + { + for (var i = 0; i < Fields.Elements.Count; i++) + { + var field = Fields[i]; + TraverseFields(field, fields); + } + } + return fields; + } + + private static void TraverseFields(PdfAcroField parentField, List fieldList) + { + fieldList.Add(parentField); + for (var i = 0; i < parentField.Fields.Elements.Count; i++) + { + var field = parentField.Fields[i]; + if (PdfAcroField.IsField(field)) + TraverseFields(field, fieldList); + } + } + + internal PdfResources? Resources + { + get + { + if (resources == null) + resources = (PdfResources?)Elements.GetValue(Keys.DR, VCF.None); + return resources; + } + } + PdfResources? resources; + + /// + /// Gets the of this or creates a new one if none exist + /// + /// The of this AcroForm + internal PdfResources GetOrCreateResources() + { + var resources = Resources; + if (resources == null) + Elements.Add(Keys.DR, new PdfResources(_document)); + return Resources!; + } + + private PdfAcroFieldRenderer? fieldRenderer; + /// + /// Gets the used to render s + /// + public PdfAcroFieldRenderer FieldRenderer + { + get + { + fieldRenderer ??= new PdfAcroFieldRenderer(); + return fieldRenderer; + } + } + + internal override void PrepareForSave() + { + // Need to create "Fields" Entry after importing fields from external documents + if (_fields != null && _fields.Elements.Count > 0 && !Elements.ContainsKey(Keys.Fields)) + { + Elements.Add(Keys.Fields, _fields); + } + // do not use the Fields-Property, as that may create a new unwanted fields-array ! + var fieldsArray = Elements.GetArray(Keys.Fields); + if (fieldsArray != null) + { + for (var i = 0; i < fieldsArray.Elements.Count; i++) + { + if (fieldsArray.Elements[i] is PdfReference field && field.Value != null) + field.Value.PrepareForSave(); + } + } + base.PrepareForSave(); + } + + /// + /// Flattens the AcroForm by rendering Field-contents directly onto the page + /// + internal void Flatten() + { + for (var i = 0; i < Fields.Elements.Count; i++) + { + var field = Fields[i]; + field.Flatten(); + } + _document.Catalog.AcroForm = null; + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddTextField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfTextField AddTextField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfTextField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddCheckBoxField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfCheckBoxField AddCheckBoxField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfCheckBoxField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddRadioButtonField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfRadioButtonField AddRadioButtonField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfRadioButtonField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddComboBoxField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfComboBoxField AddComboBoxField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfComboBoxField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddListBoxField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfListBoxField AddListBoxField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfListBoxField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddPushButtonField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfPushButtonField AddPushButtonField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfPushButtonField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the + /// + /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddSignatureField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfSignatureField AddSignatureField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfSignatureField(_document); + return AddToFieldList(field, configure); + } + + /// + /// Adds a new to the

+ /// Typically used as a container for other fields + ///
+ /// + /// A method that receives the new for further customization

+ /// + /// The created and configured + /// + /// + /// This method adds the new field to the root of the field-hierarchy.

+ /// To add the new field as a child to another field, create the parent-field first and + /// then add the new field to the parent inside the action. + ///

+ /// Like so: + ///

+ /// acroForm.AddGenericField(field =>

+ /// {

+ /// ... // configure the field as needed

+ /// parentField.AddChild(field);

+ /// });

+ ///
+ public PdfGenericField AddGenericField(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + var field = new PdfGenericField(_document); + return AddToFieldList(field, configure); + } + + private T AddToFieldList(T field, Action configure) where T : PdfAcroField + { + _document.IrefTable.Add(field); + configure(field); + if (field.Parent == null) + { + // ensure names of root-fields are unique + var existingField = Fields.GetValue(field.Name); + if (existingField != null) + { + var name = existingField.Name.AddIncrementalSuffix(); + // search for next free number + while (Fields.GetValue(name) != null) + { + name = name.AddIncrementalSuffix(); + } + field.Name = name; + } + Fields.Elements.Add(field); + } + return field; + } + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. @@ -55,9 +456,9 @@ public sealed class Keys : KeysBase public const string Fields = "/Fields"; /// - /// (Optional) A flag specifying whether to construct appearance streams and - /// appearance dictionaries for all widget annotations in the document. - /// Default value: false. + /// (Optional) A flag specifying whether to construct appearance streams and appearance dictionaries + /// for all widget annotations in the document(see 12.7.3.3, “Variable Text”). + /// Default value: false. /// [KeyInfo(KeyType.Boolean | KeyType.Optional)] public const string NeedAppearances = "/NeedAppearances"; @@ -80,9 +481,13 @@ public sealed class Keys : KeysBase public const string CO = "/CO"; /// - /// (Optional) A document-wide default value for the DR attribute of variable text fields. + /// (Optional) A resource dictionary (see 7.8.3, "Resource Dictionaries") + /// containing default resources(such as fonts, patterns, or colour spaces) + /// that shall be used by form field appearance streams. + /// At a minimum, this dictionary shall contain a Font entry specifying the resource name + /// and font dictionary of the default font for displaying text. /// - [KeyInfo(KeyType.Dictionary | KeyType.Optional)] + [KeyInfo(KeyType.Dictionary | KeyType.Optional, typeof(PdfResources))] public const string DR = "/DR"; /// @@ -97,6 +502,16 @@ public sealed class Keys : KeysBase [KeyInfo(KeyType.Integer | KeyType.Optional)] public const string Q = "/Q"; + /// + /// (Optional; PDF 1.5) A stream or array containing an XFA resource, + /// whose format shall be described by the Data Package (XDP) Specification. (see the Bibliography).

+ /// The value of this entry shall be either a stream representing the entire contents + /// of the XML Data Package or an array of text string and stream pairs + /// representing the individual packets comprising the XML Data Package. + ///
+ [KeyInfo(KeyType.ArrayOrDictionary | KeyType.Optional)] + public const string XFA = "/XFA"; + /// /// Gets the KeysMeta for these keys. /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroFormImporter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroFormImporter.cs new file mode 100644 index 00000000..5afc373e --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroFormImporter.cs @@ -0,0 +1,250 @@ +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms +{ + /// + /// Imports s + /// + internal sealed class PdfAcroFormImporter : PdfObject + { + /// + /// Create a new for importing into the specified + /// + /// + internal PdfAcroFormImporter(PdfDocument targetDocument) + : base(targetDocument) + { + } + + internal void ImportAcroForm(PdfAcroForm remoteForm, + Func? fieldFilter = null, + Action? fieldHandler = null) + { + // skip, if there is no AcroForm or an AcroForm without fields + if (remoteForm == null || !remoteForm.Fields.Names.Any()) + return; + + var importedObjectTable = Owner.FormTable.GetImportedObjectTable(remoteForm.Owner); + var needNewForm = Owner.Catalog.AcroForm == null; + var localForm = Owner.GetOrCreateAcroForm(); + if (needNewForm) + { + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.CO)) + localForm.Elements[PdfAcroForm.Keys.CO] = ImportClosure(importedObjectTable, _document, remoteForm.Elements.GetObject(PdfAcroForm.Keys.CO)!); + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.DA)) + localForm.Elements[PdfAcroForm.Keys.DA] = remoteForm.Elements[PdfAcroForm.Keys.DA]; + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.DR)) + localForm.Elements[PdfAcroForm.Keys.DR] = ImportClosure(importedObjectTable, _document, remoteForm.Elements.GetObject(PdfAcroForm.Keys.DR)!); + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.NeedAppearances)) + localForm.Elements[PdfAcroForm.Keys.NeedAppearances] = remoteForm.Elements[PdfAcroForm.Keys.NeedAppearances]; + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.Q)) + localForm.Elements[PdfAcroForm.Keys.Q] = remoteForm.Elements[PdfAcroForm.Keys.Q]; + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) + localForm.Elements[PdfAcroForm.Keys.SigFlags] = remoteForm.Elements[PdfAcroForm.Keys.SigFlags]; + if (remoteForm.Elements.ContainsKey(PdfAcroForm.Keys.XFA)) + localForm.Elements[PdfAcroForm.Keys.XFA] = ImportClosure(importedObjectTable, Owner, remoteForm.Elements.GetObject(PdfAcroForm.Keys.XFA)!); + } + else + { + // copy resources from the imported AcroForm to the local form + var extResources = remoteForm.Elements.GetDictionary(PdfAcroForm.Keys.DR); + if (extResources != null) + { + var localResources = localForm.Elements.GetDictionary(PdfAcroForm.Keys.DR) ?? new PdfDictionary(Owner); + var resourceKeys = new[] { PdfResources.Keys.Font, PdfResources.Keys.XObject }; + foreach (var resKey in resourceKeys) + { + var extResDict = extResources.Elements.GetDictionary(resKey); + if (extResDict != null) + { + var localResDict = localResources.Elements.GetDictionary(resKey) ?? new PdfDictionary(Owner); + foreach (var key in extResDict.Elements.Keys) + { + if (!localResDict.Elements.ContainsKey(key)) + localResDict.Elements.Add(key, ImportClosure(importedObjectTable, Owner, extResDict.Elements.GetObject(key)!)); + } + if (!localResources.Elements.ContainsKey(resKey)) + localResources.Elements.Add(resKey, localResDict); + } + } + if (!localForm.Elements.ContainsKey(PdfAcroForm.Keys.DR)) + localForm.Elements.Add(PdfAcroForm.Keys.DR, localResources); + } + } + + for (var f = 0; f < remoteForm.Fields.Elements.Count; f++) + { + var remoteField = remoteForm.Fields[f]; + if (fieldFilter != null && !fieldFilter(remoteField)) + continue; + ImportAcroField(localForm, remoteField, null, fieldHandler); + } + } + + private void ImportAcroField(PdfAcroForm localForm, PdfAcroField remoteField, PdfAcroField? parentField = null, + Action? fieldHandler = null) + { + var importedObjectTable = Owner.FormTable.GetImportedObjectTable(remoteField.Owner); + var annotationsImported = false; + + PdfAcroField importedField = remoteField.GetType().Name switch + { + nameof(PdfCheckBoxField) => localForm.AddCheckBoxField(checkBoxField => + { + var externalCheckBoxField = (PdfCheckBoxField)remoteField; + checkBoxField.Name = remoteField.Name; + checkBoxField.Checked = externalCheckBoxField.Checked; + parentField?.AddChild(checkBoxField); + }), + nameof(PdfComboBoxField) => localForm.AddComboBoxField(comboBoxField => + { + var externalComboBoxField = (PdfComboBoxField)remoteField; + comboBoxField.Name = remoteField.Name; + comboBoxField.Options = externalComboBoxField.Options; + comboBoxField.SelectedIndex = externalComboBoxField.SelectedIndex; + if (remoteField.Elements.ContainsKey(PdfChoiceField.Keys.Opt)) + comboBoxField.Elements[PdfChoiceField.Keys.Opt] = remoteField.Elements[PdfChoiceField.Keys.Opt]!.Clone(); + parentField?.AddChild(comboBoxField); + }), + nameof(PdfListBoxField) => localForm.AddListBoxField(listBoxField => + { + var externalListBoxField = (PdfListBoxField)remoteField; + listBoxField.Name = remoteField.Name; + listBoxField.Options = externalListBoxField.Options; + listBoxField.SelectedIndices = externalListBoxField.SelectedIndices; + if (remoteField.Elements.ContainsKey(PdfChoiceField.Keys.Opt)) + listBoxField.Elements[PdfChoiceField.Keys.Opt] = remoteField.Elements[PdfChoiceField.Keys.Opt]!.Clone(); + parentField?.AddChild(listBoxField); + }), + nameof(PdfRadioButtonField) => localForm.AddRadioButtonField(radioButtonField => + { + var extRadioButtonField = (PdfRadioButtonField)remoteField; + // must copy annotations here, because SelectedIndex relies on them + ImportFieldAnnotations(radioButtonField, remoteField); + annotationsImported = true; + radioButtonField.Name = remoteField.Name; + radioButtonField.SelectedIndex = extRadioButtonField.SelectedIndex; + if (remoteField.Elements.ContainsKey(PdfRadioButtonField.Keys.Opt)) + radioButtonField.Elements[PdfRadioButtonField.Keys.Opt] = remoteField.Elements[PdfRadioButtonField.Keys.Opt]!.Clone(); + parentField?.AddChild(radioButtonField); + }), + nameof(PdfSignatureField) => localForm.AddSignatureField(signatureField => + { + signatureField.Name = remoteField.Name; + if (remoteField.Elements.ContainsKey(PdfSignatureField.Keys.Lock)) + signatureField.Elements[PdfSignatureField.Keys.Lock] = ImportClosure(importedObjectTable, Owner, remoteField.Elements.GetObject(PdfSignatureField.Keys.Lock)!); + if (remoteField.Elements.ContainsKey(PdfSignatureField.Keys.SV)) + signatureField.Elements[PdfSignatureField.Keys.SV] = ImportClosure(importedObjectTable, Owner, remoteField.Elements.GetObject(PdfSignatureField.Keys.SV)!); + parentField?.AddChild(signatureField); + }), + nameof(PdfGenericField) => localForm.AddGenericField(genericField => + { + genericField.Name = remoteField.Name; + parentField?.AddChild(genericField); + }), + nameof(PdfTextField) => localForm.AddTextField(textField => + { + var externalTextField = (PdfTextField)remoteField; + textField.Name = remoteField.Name; + textField.MaxLength = externalTextField.MaxLength; + textField.Text = externalTextField.Text; + parentField?.AddChild(textField); + }), + nameof(PdfPushButtonField) => localForm.AddPushButtonField(pushButton => + { + pushButton.Name = remoteField.Name; + parentField?.AddChild(pushButton); + }), + _ => throw new NotImplementedException($"Field type {remoteField.GetType().Name} is not handled"), + }; + // copy common properties + if (!string.IsNullOrEmpty(importedField.AlternateName)) + importedField.AlternateName = remoteField.AlternateName; + if (!string.IsNullOrEmpty(importedField.MappingName)) + importedField.MappingName = remoteField.MappingName; + if (remoteField.DefaultValue != null && importedField is not PdfPushButtonField) + importedField.DefaultValue = remoteField.DefaultValue; + if (remoteField.Elements.ContainsKey(PdfAcroField.Keys.DA)) + importedField.Elements[PdfAcroField.Keys.DA] = remoteField.Elements[PdfAcroField.Keys.DA]; + if (remoteField.Elements.ContainsKey(PdfAcroField.Keys.DS)) + importedField.Elements[PdfAcroField.Keys.DS] = remoteField.Elements[PdfAcroField.Keys.DS]; + if (remoteField.Elements.ContainsKey(PdfAcroField.Keys.RV)) + importedField.Elements[PdfAcroField.Keys.RV] = remoteField.Elements[PdfAcroField.Keys.RV]; + if (remoteField.Elements.ContainsKey(PdfAcroField.Keys.AA)) + importedField.Elements[PdfAcroField.Keys.AA] = ImportClosure(importedObjectTable, Owner, remoteField.Elements.GetObject(PdfAcroField.Keys.AA)!); + importedField.SetFlags = remoteField.Flags; + importedField.Font = remoteField.Font; + importedField.FontSize = remoteField.FontSize; + importedField.ForeColor = remoteField.ForeColor; + importedField.TextAlign = remoteField.TextAlign; + + if (!annotationsImported) + ImportFieldAnnotations(importedField, remoteField); + + fieldHandler?.Invoke(remoteField, importedField); + + if (remoteField.HasChildFields) + { + for (var i = 0; i < remoteField.Fields.Elements.Count; i++) + ImportAcroField(localForm, remoteField.Fields[i], importedField, fieldHandler); + } + } + + private void ImportFieldAnnotations(PdfAcroField localField, PdfAcroField remoteField) + { + var importedObjectTable = Owner.FormTable.GetImportedObjectTable(remoteField.Owner); + foreach (var remoteAnnot in remoteField.Annotations.Elements) + { + // skip annotation if it is associated with a page that was not imported + if (remoteAnnot.Page != null && !importedObjectTable.Contains(remoteAnnot.Page.ObjectID)) + continue; + + localField.AddAnnotation(annot => + { + annot.BackColor = remoteAnnot.BackColor; + annot.BorderColor = remoteAnnot.BorderColor; + annot.Border = new PdfAnnotationBorder + { + BorderStyle = remoteAnnot.Border.BorderStyle, + DashPattern = remoteAnnot.Border.DashPattern, + HorizontalRadius = remoteAnnot.Border.HorizontalRadius, + VerticalRadius = remoteAnnot.Border.VerticalRadius, + Width = remoteAnnot.Border.Width + }; + annot.Color = remoteAnnot.Color; + annot.Flags = remoteAnnot.Flags; + annot.Opacity = remoteAnnot.Opacity; + annot.Rectangle = remoteAnnot.Rectangle; + annot.Rotation = remoteAnnot.Rotation; + if (remoteAnnot.Elements.ContainsKey(PdfAnnotation.Keys.AP)) + annot.Elements[PdfAnnotation.Keys.AP] = ImportClosure(importedObjectTable, _document, remoteAnnot.Elements.GetObject(PdfAnnotation.Keys.AP)!); + if (remoteAnnot.Elements.ContainsKey(PdfAnnotation.Keys.AS)) + annot.Elements[PdfAnnotation.Keys.AS] = remoteAnnot.Elements[PdfAnnotation.Keys.AS]; + if (remoteAnnot.Elements.ContainsKey(PdfAnnotation.Keys.NM)) + annot.Elements[PdfAnnotation.Keys.NM] = remoteAnnot.Elements[PdfAnnotation.Keys.NM]; + if (remoteAnnot.Elements.ContainsKey(PdfAnnotation.Keys.Contents)) + annot.Elements[PdfAnnotation.Keys.Contents] = remoteAnnot.Elements[PdfAnnotation.Keys.Contents]; + if (remoteAnnot.Elements.ContainsKey(PdfAnnotation.Keys.A)) + annot.Elements[PdfAnnotation.Keys.A] = ImportClosure(importedObjectTable, Owner, remoteAnnot.Elements.GetObject(PdfAnnotation.Keys.A)!); + if (remoteAnnot.Elements.ContainsKey(PdfWidgetAnnotation.Keys.H)) + annot.Elements[PdfWidgetAnnotation.Keys.H] = remoteAnnot.Elements[PdfWidgetAnnotation.Keys.H]; + if (remoteAnnot.Elements.ContainsKey(PdfWidgetAnnotation.Keys.MK)) + annot.Elements[PdfWidgetAnnotation.Keys.MK] = ImportClosure(importedObjectTable, _document, remoteAnnot.Elements.GetObject(PdfWidgetAnnotation.Keys.MK)!); + if (remoteAnnot.Page != null && importedObjectTable.Contains(remoteAnnot.Page.ObjectID)) + { + var localPage = importedObjectTable[remoteAnnot.Page.ObjectID]!.Value as PdfPage; + // avoid duplicate annotations (page-import already imported annotations) + if (localPage != null + && importedObjectTable.Contains(remoteAnnot.ObjectID) + && importedObjectTable[remoteAnnot.ObjectID].Value is PdfDictionary importedAnnot) + { + localPage.Annotations.Remove(new PdfGenericAnnotation(importedAnnot)); + } + annot.Page = localPage; + } + }); + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfButtonField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfButtonField.cs index 1982cf1f..32232078 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfButtonField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfButtonField.cs @@ -1,9 +1,5 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; using PdfSharp.Pdf.Annotations; namespace PdfSharp.Pdf.AcroForms @@ -18,7 +14,9 @@ public abstract class PdfButtonField : PdfAcroField ///
protected PdfButtonField(PdfDocument document) : base(document) - { } + { + Elements.SetName(PdfAcroField.Keys.FT, "Btn"); + } /// /// Initializes a new instance of the class. @@ -27,40 +25,96 @@ protected PdfButtonField(PdfDictionary dict) : base(dict) { } + /// + /// Gets the name which represents the opposite of Off. + /// + public string? GetNonOffValue() + { + return GetNonOffValueInternal().TrimStart('/'); + } + /// /// Gets the name which represents the opposite of /Off. /// - protected string GetNonOffValue() + protected string GetNonOffValueInternal() { // Try to get the information from the appearance dictionary. // Just return the first key that is not /Off. - // Im not sure what is the right solution to get this value. - var ap = Elements[PdfAnnotation.Keys.AP] as PdfDictionary; - if (ap != null) + // I'm not sure what is the right solution to get this value. + if (Annotations.Elements.Count > 0) + { + var widget = Annotations.Elements[0]; + if (widget != null) + { + var ap = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + if (ap != null) + { + var n = ap.Elements.GetDictionary("/N"); + if (n != null) + { + foreach (string name in n.Elements.Keys) + if (name != "/Off") + return name; + } + } + } + } + return "/Yes"; + } + + /// + /// Gets the name which represents the opposite of /Off for the specified widget. + /// + /// + /// + protected static string? GetNonOffValue(PdfWidgetAnnotation widget) + { + if (widget != null) { - var n = ap.Elements["/N"] as PdfDictionary; - if (n != null) + var ap = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + if (ap != null) { - foreach (string name in n.Elements.Keys) - if (name != "/Off") - return name; + var n = ap.Elements.GetDictionary("/N"); + if (n != null) + { + return n.Elements.Keys.FirstOrDefault(name => name != "/Off"); + } } } - return null!; + return null; } - internal override void GetDescendantNames(ref List names, string? partialName) + /// + /// Attempts to determine the visual appearance for this AcroField + /// + protected override void DetermineAppearance() { - string t = Elements.GetString(PdfAcroField.Keys.T); - if (t == "") - t = "???"; - Debug.Assert(t != ""); - if (t.Length > 0) + base.DetermineAppearance(); + for (var i = 0; i < Annotations.Elements.Count; i++) { - if (!String.IsNullOrEmpty(partialName)) - names.Add(partialName + "." + t); - else - names.Add(t); + var widget = Annotations.Elements[i]; + if (widget.Page != null) + { + var appearance = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + if (appearance != null) + { + // /N -> Normal appearance, /R -> Rollover appearance, /D -> Down appearance + var normalAppearance = appearance.Elements.GetDictionary("/N"); + if (normalAppearance != null) + { + var activeAppearance = normalAppearance.Elements.GetDictionary(GetNonOffValueInternal()); + if (activeAppearance != null) + { + try + { + DetermineFontFromContent(activeAppearance.Stream.UnfilteredValue); + } + catch + { } + } + } + } + } } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfCheckBoxField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfCheckBoxField.cs index 85980bb1..b246a9a8 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfCheckBoxField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfCheckBoxField.cs @@ -1,8 +1,8 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; using PdfSharp.Pdf.Annotations; -using PdfSharp.Pdf.Advanced; namespace PdfSharp.Pdf.AcroForms { @@ -24,156 +24,29 @@ internal PdfCheckBoxField(PdfDictionary dict) : base(dict) { } -#if true_ + /// - /// Indicates whether the field is checked. + /// Gets or sets the value of this field. This should be either Off or + /// the result of (typically Yes) /// - public bool Checked //R080317 // TODO_OLD + public new string Value { - get - { - if (!HasKids) - { - string value = Elements.GetString(Keys.V); - //return !String.IsNullOrEmpty(value) && value != UncheckedValue; - return !String.IsNullOrEmpty(value) && value == CheckedName; - } - - if (Fields.Elements.Items.Length == 2) - { - string value = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.GetString(Keys.V); - //bool bReturn = value.Length != 0 && value != UncheckedValue; //R081114 (3Std.!!) auch auf Nein prüfen; //TODO_OLD woher kommt der Wert? - bool bReturn = value.Length != 0 && value == CheckedName; - return bReturn; - } - - // NYI: Return false in any other case. - return false; - } - + get { return (base.Value?.ToString() ?? "Off").TrimStart('/'); } set { - if (!HasKids) + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + if (value != null && value.Equals(GetNonOffValueInternal().TrimStart('/'), StringComparison.OrdinalIgnoreCase)) { - //string name = value ? GetNonOffValue() : "/Off"; - string name = value ? CheckedName : UncheckedName; - Elements.SetName(Keys.V, name); - Elements.SetName(PdfAnnotation.Keys.AS, name); + Elements.SetName(PdfAcroField.Keys.V, value); } + else if (value != "Off" && value != GetNonOffValueInternal()) + throw new ArgumentException($"'{value}' is not a valid value for field '{FullyQualifiedName}'. Valid values are either '/Off' or '{GetNonOffValueInternal()}'"); else - { - // Here we have to handle fields that exist twice with the same name. - // Checked must be set for both fields, using /Off for one field and skipping /Off for the other, - // to have only one field with a check mark. - // Finding this took me two working days. - if (Fields.Elements.Items.Length == 2) - { - if (value) - { - //Element 0 behandeln -> auf checked setzen - string name1 = ""; - PdfDictionary o = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements["/AP"] as PdfDictionary; - if (o != null) - { - PdfDictionary n = o.Elements["/N"] as PdfDictionary; - if (n != null) - { - foreach (string name in n.Elements.Keys) - { - //if (name != UncheckedValue) - if (name == CheckedName) - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - - //Element 1 behandeln -> auf unchecked setzen - o = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements["/AP"] as PdfDictionary; - if (o != null) - { - PdfDictionary n = o.Elements["/N"] as PdfDictionary; - if (n != null) - { - foreach (string name in n.Elements.Keys) - { - if (name == UncheckedName) - { - name1 = name; - break; - } - } - } - } - if (!String.IsNullOrEmpty(name1)) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - } - else - { - //Element 0 behandeln -> auf unchecked setzen - string name1 = ""; - PdfDictionary o = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements["/AP"] as PdfDictionary; - if (o != null) - { - PdfDictionary n = o.Elements["/N"] as PdfDictionary; - if (n != null) - { - foreach (string name in n.Elements.Keys) - { - //if (name != UncheckedValue) - if (name == CheckedName) - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - - //Element 1 behandeln -> auf checked setzen - o = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements["/AP"] as PdfDictionary; - if (o != null) - { - PdfDictionary n = o.Elements["/N"] as PdfDictionary; - if (n != null) - { - foreach (string name in n.Elements.Keys) - { - if (name == UncheckedName) - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - } - } - } + Elements.SetName(PdfAcroField.Keys.V, "/Off"); } } -#else /// /// Indicates whether the field is checked. /// @@ -181,139 +54,90 @@ public bool Checked { get { - if (!HasKids) //R080317 + var value = Elements.GetString(PdfAcroField.Keys.V); + var widget = Annotations.Elements.Count > 0 ? Annotations.Elements[0] : null; + if (widget != null) { - string value = Elements.GetString(PdfAcroField.Keys.V); - return value.Length != 0 && value != "/Off"; - } - else //R080317 - { - if (Fields.Elements.Items.Length == 2) + if (string.IsNullOrEmpty(value)) + value = widget.Elements.GetString(PdfAnnotation.Keys.AS); + var appearances = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + if (appearances != null) { - string value = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.GetString(PdfAcroField.Keys.V); - bool bReturn = value.Length != 0 && value != "/Off" && value != "/Nein"; //R081114 (3Std.!!) auch auf Nein prüfen; //TODO_OLD woher kommt der Wert? - return bReturn; + var normalState = appearances.Elements.GetDictionary("/N"); + if (normalState != null) + return value.Length != 0 && value != "/Off" && normalState.Elements.ContainsKey(value); } - else - return false; } + return value.Length != 0 && value != "/Off"; } set { - if (!HasKids) + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + var name = value ? GetNonOffValueInternal() : "/Off"; + Elements.SetName(PdfAcroField.Keys.V, name); + } + } + + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; + + // existing/imported field ? + if (widget.Elements.ContainsKey(PdfAnnotation.Keys.AP)) { - string name = value ? GetNonOffValue() : "/Off"; - Elements.SetName(PdfAcroField.Keys.V, name); - Elements.SetName(PdfAnnotation.Keys.AS, name); + widget.Elements.SetName(PdfAnnotation.Keys.AS, Checked ? GetNonOffValueInternal() : "/Off"); } - else + else // newly created field { - // Here we have to handle fields that exist twice with the same name. - // Checked must be set for both fields, using /Off for one field and skipping /Off for the other, - // to have only one field with a check mark. - // Finding this took me two working days. - if (Fields.Elements.Items.Length == 2) + var xRect = new XRect(0, 0, width, height); + // checked state + var formChecked = new XForm(_document, xRect); + using (var gfx = XGraphics.FromForm(formChecked)) { - if (value) - { - //Element 0 behandeln -> auf checked setzen - string name1 = ""; - if (((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements["/AP"] is PdfDictionary obj1) - { - if (obj1.Elements["/N"] is PdfDictionary n) - { - foreach (string name in n.Elements.Keys) - { - if (name != "/Off") - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAcroField.Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - - //Element 1 behandeln -> auf unchecked setzen - var obj2 = ((PdfDictionary?)((PdfReference?)Fields.Elements.Items[1])?.Value)?.Elements["/AP"] as PdfDictionary; - if (obj2 != null) - { - var n = obj2.Elements["/N"] as PdfDictionary; - if (n != null) - { - foreach (string name in n.Elements.Keys) - { - if (name == "/Off") - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAcroField.Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - - } - else - { - //Element 0 behandeln -> auf unchecked setzen - string name1 = ""; - PdfDictionary? o = ((PdfDictionary)((PdfReference)(Fields.Elements.Items[1])).Value).Elements["/AP"] as PdfDictionary; - if (o != null) - { - if (o.Elements["/N"] is PdfDictionary n) - { - foreach (string name in n.Elements.Keys) - { - if (name != "/Off") - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAcroField.Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - - //Element 1 behandeln -> auf checked setzen - o = ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements["/AP"] as PdfDictionary; - if (o != null) - { - if (o.Elements["/N"] is PdfDictionary n) - { - foreach (string name in n.Elements.Keys) - { - if (name == "/Off") - { - name1 = name; - break; - } - } - } - } - if (name1.Length != 0) - { - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAcroField.Keys.V, name1); - ((PdfDictionary)(((PdfReference)(Fields.Elements.Items[0])).Value)).Elements.SetName(PdfAnnotation.Keys.AS, name1); - } - } + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.CheckBoxFieldRenderer.RenderCheckedState(this, widget, gfx, xRect); } + formChecked.DrawingFinished(); + SetXFormFont(formChecked); + // unchecked state + var formUnchecked = new XForm(_document, rect.ToXRect()); + using (var gfx = XGraphics.FromForm(formUnchecked)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.CheckBoxFieldRenderer.RenderUncheckedState(this, widget, gfx, xRect); + } + formUnchecked.DrawingFinished(); + SetXFormFont(formUnchecked); + + var ap = new PdfDictionary(_document); + var nDict = new PdfDictionary(_document); + ap.Elements.SetValue("/N", nDict); + // the names /Off and /Yes should be used according to the spec (1.7, 12.7.4.2.3) + nDict.Elements["/Yes"] = formChecked.PdfForm.Reference; + nDict.Elements["/Off"] = formUnchecked.PdfForm.Reference; + widget.Elements[PdfAnnotation.Keys.AP] = ap; + widget.Elements.SetName(PdfAnnotation.Keys.AS, Checked ? "/Yes" : "/Off"); // set appearance state } } } -#endif + + internal override void PrepareForSave() + { + base.PrepareForSave(); + RenderAppearance(); + } /// /// Gets or sets the name of the dictionary that represents the Checked state. @@ -346,8 +170,15 @@ public string UncheckedName public new class Keys : PdfButtonField.Keys { /// - /// (Optional; inheritable; PDF 1.4) A text string to be used in place of the V entry for the - /// value of the field. + /// (Optional; inheritable; PDF 1.4) An array containing one entry for each + /// widget annotation in the Kids array of the radio button or check box field. + /// Each entry shall be a text string representing the on state of the + /// corresponding widget annotation.

+ /// When this entry is present, the names used to represent the on state in the + /// AP dictionary of each annotation may use numerical position (starting with 0) + /// of the annotation in the Kids array, encoded as a name object (for example: /0, /1).

+ /// This allows distinguishing between the annotations even if two or more of them + /// have the same value in the Opt array. ///
[KeyInfo(KeyType.TextString | KeyType.Optional)] public const string Opt = "/Opt"; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfChoiceField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfChoiceField.cs index dcb24c62..ce5d7110 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfChoiceField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfChoiceField.cs @@ -1,6 +1,9 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Advanced; + namespace PdfSharp.Pdf.AcroForms { /// @@ -13,7 +16,9 @@ public abstract class PdfChoiceField : PdfAcroField /// protected PdfChoiceField(PdfDocument document) : base(document) - { } + { + Elements.SetName(Keys.FT, "Ch"); + } /// /// Initializes a new instance of the class. @@ -23,41 +28,58 @@ protected PdfChoiceField(PdfDictionary dict) { } /// - /// Gets the index of the specified string in the /Opt array or -1, if no such string exists. + /// Sets the default appearance for this field. /// - protected int IndexInOptArray(string value) + public void SetDefaultAppearance(XFont font, double fontSize, XColor textColor) { - var opt = Elements.GetArray(Keys.Opt); + if (font is null) + throw new ArgumentNullException(nameof(font)); + if (fontSize < 0.0) + throw new ArgumentException("Font size must be greater or equal to zero", nameof(fontSize)); + if (Owner.AcroForm is null) + throw new InvalidOperationException("AcroForm has to be created first"); -#if DEBUG // Check with //R080317 implementation - PdfArray? opt2 = null; - if (Elements[Keys.Opt] is PdfArray) - opt2 = Elements[Keys.Opt] as PdfArray; - else if (Elements[Keys.Opt] is Advanced.PdfReference) - { - // If the array is not stored in the element directly, - // fetch the array from the referenced element. - opt2 = ((Advanced.PdfReference?)Elements[Keys.Opt])?.Value as PdfArray; - } - Debug.Assert(ReferenceEquals(opt, opt2)); -#endif + var formResources = Owner.AcroForm.GetOrCreateResources(); + var fontType = font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var docFont = _document.FontTable.GetOrCreateFont(font.GlyphTypeface, fontType); + var fontName = formResources.AddFont(docFont); + var da = string.Format(CultureInfo.InvariantCulture, "{0} {1:F2} Tf {2:F4} {3:F4} {4:F4} rg", + fontName, fontSize, textColor.R / 255, textColor.G / 255, textColor.B / 255); + Elements.SetString(PdfAcroField.Keys.DA, da); + } + + /// + /// Gets the index of the specified string in the /Opt array or -1, if no such string exists. + /// + /// Value, for which the index should be retrieved + /// true if value is the export value, false if value is the text shown in the UI + protected int IndexInOptArray(string value, bool useExportValue) + { + var ancestor = FindParentHavingKey(Keys.Opt); + var opt = ancestor.Elements.GetArray(Keys.Opt); if (opt != null) { int count = opt.Elements.Count; for (int idx = 0; idx < count; idx++) { - var item = opt.Elements[idx]; - if (item is PdfString) + PdfItem item = opt.Elements[idx]; + if (item is PdfString pdfString) { - if (item.ToString() == value) + if (pdfString.Value == value) return idx; } - else if (item is PdfArray) + else if (item is PdfArray array) { - var array = (PdfArray)item; - if (array.Elements.Count != 0) + if (array.Elements.Count > 0) { - if (array.Elements[0].ToString() == value) + // Pdf Reference 1.7, Section 12.7.4.4: Should be a 2-element Array. + // Second value is the text shown in the UI. + if ((!useExportValue && array.Elements.Count > 1 && array.Elements.GetString(1) == value) || + (array.Elements.Count > 0 && array.Elements.GetString(0) == value)) return idx; } } @@ -67,29 +89,169 @@ protected int IndexInOptArray(string value) } /// - /// Gets the value from the index in the /Opt array. + /// Gets the value or the display text from the index in the /Opt array. /// - protected string ValueInOptArray(int index) + /// Index of the value that should be retrieved + /// true to get the export value, false to get the text shown in the UI + internal string ValueInOptArray(int index, bool useExportValue) { - var opt = Elements.GetArray(Keys.Opt); + var ancestor = FindParentHavingKey(Keys.Opt); + var opt = ancestor.Elements.GetArray(Keys.Opt); if (opt != null) { int count = opt.Elements.Count; if (index < 0 || index >= count) throw new ArgumentOutOfRangeException(nameof(index)); - var item = opt.Elements[index]; - if (item is PdfString) - return item.ToString() ?? ""; + PdfItem item = opt.Elements[index]; + if (item is PdfString pdfString) + return pdfString.Value; + else if (item is PdfArray array) + { + return !useExportValue && array.Elements.Count > 1 + ? array.Elements.GetString(1) + : array.Elements.GetString(0); + } + } + return string.Empty; + } - if (item is PdfArray) + /// + /// Gets or sets the Value for the Field. + /// For fields supporting multiple values (e.g. ListBox) use instead + /// + public override PdfItem? Value + { + get + { + var item = base.Value; + if (item is PdfArray pdfArray) + { + if (pdfArray.Elements.Count > 0) + item = pdfArray.Elements[0]; + } + if (item is PdfString pdfString) { - PdfArray array = (PdfArray)item; - if (array.Elements.Count != 0) - return array.Elements[0].ToString() ?? ""; + // First try the export value + var idx = IndexInOptArray(pdfString.Value, true); + // If that is not found, try the string shown in the UI + if (idx < 0) + idx = IndexInOptArray(pdfString.Value, false); + if (idx < 0) + return null; + // return the display text + return new PdfString(ValueInOptArray(idx, true)); } + return null; } - return ""; + set { base.Value = value; } + } + + /// + /// Gets or sets the Default value for the field + /// + public override PdfItem? DefaultValue + { + get + { + var item = base.DefaultValue; + if (item is PdfArray pdfArray) + { + if (pdfArray.Elements.Count > 0) + item = pdfArray.Elements[0]; + } + if (item is PdfString pdfString) + { + // First try the export value + var idx = IndexInOptArray(pdfString.Value, true); + // If that is not found, try the string shown in the UI + if (idx < 0) + idx = IndexInOptArray(pdfString.Value, false); + if (idx < 0) + return null; + // return the display text + return new PdfString(ValueInOptArray(idx, true)); + } + return null; + } + set { base.DefaultValue = value; } + } + + /// + /// Gets or sets the List of options (entries) available for selection. + /// This is the list of values shown in the UI. + /// + public ICollection Options + { + get + { + var result = new List(); + var ancestor = FindParentHavingKey(Keys.Opt); + var options = ancestor.Elements.GetArray(Keys.Opt); + if (options != null) + { + foreach (var item in options) + { + if (item is PdfString s) + result.Add(s.Value); + else + { + if (item is PdfArray array) + { + // Pdf Reference 1.7, Section 12.7.4.4 : Value is the SECOND entry in the Array + // (the first value is the exported value) + var v = array.Elements.GetString(array.Elements.Count > 1 ? 1 : 0); + if (string.IsNullOrEmpty(v)) + v = string.Empty; + result.Add(v); + } + } + } + } + return result; + } + set + { + var ary = new PdfArray(_document); + foreach (var item in value) + ary.Elements.Add(new PdfString(item)); + Elements.SetObject(Keys.Opt, ary); + } + } + + /// + /// Gets the list of values for this Field.

+ /// May or may not be equal to but has always the same amount of items. + ///
+ public ICollection Values + { + get + { + var result = new List(); + var ancestor = FindParentHavingKey(Keys.Opt); + var options = ancestor.Elements.GetArray(Keys.Opt); + if (options != null) + { + foreach (var item in options) + { + if (item is PdfString s) + result.Add(s.Value); + else + { + if (item is PdfArray array) + { + var ary = array; + var v = ary.Elements.GetString(0); + if (string.IsNullOrEmpty(v)) + v = string.Empty; + result.Add(v); + } + } + } + } + return result; + } + } /// @@ -101,29 +263,31 @@ protected string ValueInOptArray(int index) // ReSharper disable InconsistentNaming /// - /// (Required; inheritable) An array of options to be presented to the user. Each element of - /// the array is either a text string representing one of the available options or a two-element - /// array consisting of a text string together with a default appearance string for constructing - /// the item’s appearance dynamically at viewing time. + /// (Optional) An array of options that shall be presented to the user. + /// Each element of the array is either a text string representing one of the available + /// options or an array consisting of two text strings: the option’s export value + /// and the text that shall be displayed as the name of the option.

+ /// If this entry is not present, no choices should be presented to the user. ///
[KeyInfo(KeyType.Array | KeyType.Optional)] public const string Opt = "/Opt"; /// - /// (Optional; inheritable) For scrollable list boxes, the top index (the index in the Opt array - /// of the first option visible in the list). + /// (Optional) For scrollable list boxes, the top index (the index in the Opt array + /// of the first option visible in the list). Default value: 0. /// [KeyInfo(KeyType.Integer | KeyType.Optional)] public const string TI = "/TI"; /// - /// (Sometimes required, otherwise optional; inheritable; PDF 1.4) For choice fields that allow - /// multiple selection (MultiSelect flag set), an array of integers, sorted in ascending order, - /// representing the zero-based indices in the Opt array of the currently selected option - /// items. This entry is required when two or more elements in the Opt array have different - /// names but the same export value, or when the value of the choice field is an array; in - /// other cases, it is permitted but not required. If the items identified by this entry differ - /// from those in the V entry of the field dictionary (see below), the V entry takes precedence. + /// (Sometimes required, otherwise optional; PDF 1.4) For choice fields that allow + /// multiple selection (MultiSelect flag set), an array of integers, sorted in ascending order, + /// representing the zero-based indices in the Opt array of the currently selected option items.

+ /// This entry shall be used when two or more elements in the Opt array have different names + /// but the same export value or when the value of the choice field is an array.

+ /// This entry should not be used for choice fields that do not allow multiple selection.

+ /// If the items identified by this entry differ from those in the V entry of the + /// field dictionary (see discussion following this Table), the V entry shall be used. ///
[KeyInfo(KeyType.Array | KeyType.Optional)] public const string I = "/I"; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfComboBoxField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfComboBoxField.cs index a9e4c250..47a0fdfa 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfComboBoxField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfComboBoxField.cs @@ -1,6 +1,11 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Internal; + namespace PdfSharp.Pdf.AcroForms { /// @@ -13,64 +18,129 @@ public sealed class PdfComboBoxField : PdfChoiceField /// internal PdfComboBoxField(PdfDocument document) : base(document) - { } + { + SetFlags |= PdfAcroFieldFlags.Combo; + } internal PdfComboBoxField(PdfDictionary dict) : base(dict) { } /// - /// Gets or sets the index of the selected item. + /// Gets or sets the index of the selected item /// public int SelectedIndex { get { - string value = Elements.GetString(PdfAcroField.Keys.V); - return IndexInOptArray(value); + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + string value = ancestor.Elements.GetString(PdfAcroField.Keys.V); + // try export value first + var index = IndexInOptArray(value, true); + if (index < 0) + index = IndexInOptArray(value, false); + return index; } set { - if (value != -1) + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + if (value >= 0) { - string key = ValueInOptArray(value); + if (value >= Options.Count) + throw new ArgumentOutOfRangeException(nameof(value), value, + $"SelectedIndex for field '{FullyQualifiedName}' must be smaller than {Options.Count}"); + + string key = ValueInOptArray(value, true); Elements.SetString(PdfAcroField.Keys.V, key); - Elements.SetInteger("/I", value); + } + else + Elements.SetString(PdfAcroField.Keys.V, string.Empty); } } /// - /// Gets or sets the value of the field. + /// Gets or sets the value of this field. This should be an item from the list.

///
- public override PdfItem? Value + public new string Value { - get => Elements[PdfAcroField.Keys.V]!; + get + { + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + return ancestor.Elements.GetString(PdfAcroField.Keys.V); + } set { if (ReadOnly) throw new InvalidOperationException("The field is read only."); - if (value is PdfString or PdfName) + if (!string.IsNullOrWhiteSpace(value)) { - Elements[PdfAcroField.Keys.V] = value; - SelectedIndex = SelectedIndex; - if (SelectedIndex == -1) - { - try - { - ((PdfArray)((PdfItem[])Elements.Values)[2]).Elements.Add(Value!); // NRT Value - SelectedIndex = SelectedIndex; - } - // ReSharper disable once EmptyGeneralCatchClause - catch - { } - } + var index = IndexInOptArray(value, true); + if (index < 0) + throw new ArgumentException($"'{value}' is not a valid value for field '{FullyQualifiedName}'. Valid values are: [{string.Join(",", Options)}]"); + + Elements.SetString(PdfAcroField.Keys.V, index >= 0 ? value : string.Empty); + SelectedIndex = index; } else - throw new NotImplementedException("Values other than string cannot be set."); + SelectedIndex = -1; } } + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + if (Font is null) + return; + + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget == null) + continue; + + var fontSize = DetermineFontSize(widget); + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; + + var preferredFontType = Options.All(s => AnsiEncoding.IsAnsi(s)) ? FontType.TrueTypeWinAnsi : FontType.Type0Unicode; + SetFontType(preferredFontType); + + var xRect = new XRect(0, 0, width, height); + var form = new XForm(_document, xRect); + using (var gfx = XGraphics.FromForm(form)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.ComboBoxFieldRenderer.Render(this, widget, gfx, xRect); + } + form.DrawingFinished(); + SetXFormFont(form); + + var ap = new PdfDictionary(Owner); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + // Set XRef to normal state + ap.Elements["/N"] = form.PdfForm.Reference; + + var xobj = form.PdfForm; + var s = xobj.Stream.ToString(); + s = "/Tx BMC\n" + s + "\nEMC"; + xobj.Stream.Value = new RawEncoding().GetBytes(s); + } + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + RenderAppearance(); + } + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfGenericField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfGenericField.cs index 97501e84..6f2d0573 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfGenericField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfGenericField.cs @@ -1,6 +1,8 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Pdf.Annotations; + namespace PdfSharp.Pdf.AcroForms { /// @@ -19,6 +21,24 @@ internal PdfGenericField(PdfDictionary dict) : base(dict) { } + internal override void Flatten() + { + base.Flatten(); + + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget.Page != null) + { + var appearances = widget.Elements.GetDictionary(PdfAnnotation.Keys.AP); + var normalAppearance = appearances?.Elements.GetDictionary("/N"); + var activeAppearance = widget.Elements.GetString(PdfAnnotation.Keys.AS); + if (!String.IsNullOrEmpty(activeAppearance) && normalAppearance != null && normalAppearance.Elements.ContainsKey(activeAppearance)) + RenderContentStream(widget.Page, normalAppearance.Elements.GetDictionary(activeAppearance)!, widget.Rectangle); + } + } + } + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfListBoxField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfListBoxField.cs index 789a2488..051b2f91 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfListBoxField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfListBoxField.cs @@ -1,6 +1,11 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Internal; + namespace PdfSharp.Pdf.AcroForms { /// @@ -20,22 +25,222 @@ internal PdfListBoxField(PdfDictionary dict) { } /// - /// Gets or sets the index of the selected item. + /// Gets or sets the background color for selected items of the field. + /// + public XColor HighlightColor + { + get { return this.highlightColor; } + set { this.highlightColor = value; } + } + XColor highlightColor = XColors.DarkBlue; + + /// + /// Gets or sets the text-color for selected items of the field. + /// + public XColor HighlightTextColor + { + get { return this.highlightTextColor; } + set { this.highlightTextColor = value; } + } + XColor highlightTextColor = XColors.White; + + /// + /// Gets or sets the value for this ListBox

+ /// Note: As a may have multiple values selected, + /// this is an enumerable instead of a single value + ///
+ public new IEnumerable Value + { + get + { + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + if (ancestor.Elements.ContainsKey(PdfAcroField.Keys.V)) + { + var val = ancestor.Elements[PdfAcroField.Keys.V]; + if (val is PdfString valString) + return [valString.Value]; + if (val is PdfArray valArray) + { + return valArray.Elements.Select(v => (v?.ToString() ?? "").TrimStart('(').TrimEnd(')')); + } + } + return []; + } + set + { + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + if (value == null || !value.Any()) + { + Elements.Remove(PdfAcroField.Keys.V); + Elements.Remove(PdfChoiceField.Keys.I); + if (value == null) + return; + } + var indices = new List(); + foreach (var v in value) + { + var index = IndexInOptArray(v, true); + if (index < 0) + throw new ArgumentException($"'{v}' is not a valid value for field '{FullyQualifiedName}'. Valid values are: [{string.Join(",", Options)}]"); + + indices.Add(index); + } + SelectedIndices = indices; + } + } + + /// + /// Gets or sets the Indices of the selected items of this Field + /// + public IEnumerable SelectedIndices + { + get + { + var result = new List(); + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + var value = ancestor.Elements[PdfAcroField.Keys.V]; + if (value is PdfString valString) + { + result.Add(IndexInOptArray(valString.Value, true)); + } + else + { + var ary = ancestor.Elements.GetArray(PdfAcroField.Keys.V); // /V takes precedence over /I + if (ary != null) + { + for (var i = 0; i < ary.Elements.Count; i++) + { + int idx; + var val = ary.Elements.GetString(i); + if (val != null && (idx = IndexInOptArray(val, true)) >= 0) + result.Add(idx); + } + } + + if (result.Count > 0) + return result; + + ancestor = FindParentHavingKey(PdfChoiceField.Keys.I); + ary = ancestor.Elements.GetArray(PdfChoiceField.Keys.I); + if (ary != null) + { + foreach (var item in ary.Elements) + { + if (item is PdfInteger pdfInt) + result.Add(pdfInt.Value); + } + } + } + return result; + } + set + { + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + if (value == null) + { + Elements.Remove(PdfChoiceField.Keys.I); + Elements.Remove(PdfAcroField.Keys.V); + return; + } + if (value.Any(v => v < 0 || v >= Options.Count)) + throw new ArgumentOutOfRangeException($"At least one of the indices [{string.Join(",", value)}] is out of range. Valid values are in the range 0..{Options.Count - 1}"); + + Elements.Remove(PdfChoiceField.Keys.I); + Elements.Remove(PdfAcroField.Keys.V); + + var indexList = new List(value.Distinct()); + if (indexList.Count > 0) + { + indexList.Sort(); + var indices = new PdfArray(_document); + var values = new PdfArray(_document); + foreach (var index in indexList) + { + indices.Elements.Add(new PdfInteger(index)); + values.Elements.Add(new PdfString(ValueInOptArray(index, true))); + } + if (indexList.Count > 1) + { + Elements.SetObject(PdfChoiceField.Keys.I, indices); + Elements.SetObject(PdfAcroField.Keys.V, values); + } + else + Elements.SetString(PdfAcroField.Keys.V, ValueInOptArray(indexList[0], true)); + } + } + } + + /// + /// Gets or sets the index of the first visible item in the ListBox /// - public int SelectedIndex + public int TopIndex { get { - string value = Elements.GetString(Keys.V); - return IndexInOptArray(value); + var ancestor = FindParentHavingKey(PdfChoiceField.Keys.TI); + return ancestor.Elements.GetInteger(PdfChoiceField.Keys.TI); } set { - string key = ValueInOptArray(value); - Elements.SetString(Keys.V, key); + if (value < 0) + throw new ArgumentException("TopIndex must not be less than zero"); + Elements.SetInteger(PdfChoiceField.Keys.TI, value); } } + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + if (Font is null) + return; + + for (var idx = 0; idx < Annotations.Elements.Count; idx++) + { + var widget = Annotations.Elements[idx]; + if (widget == null) + continue; + + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; + + var preferredFontType = Options.All(s => AnsiEncoding.IsAnsi(s)) ? FontType.TrueTypeWinAnsi : FontType.Type0Unicode; + SetFontType(preferredFontType); + + var xRect = new XRect(0, 0, width, height); + var form = new XForm(_document, xRect); + using (var gfx = XGraphics.FromForm(form)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.ListBoxFieldRenderer.Render(this, widget, gfx, xRect); + } + form.DrawingFinished(); + SetXFormFont(form); + + var ap = new PdfDictionary(Owner); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + ap.Elements["/N"] = form.PdfForm.Reference; + + var xobj = form.PdfForm; + var s = xobj.Stream.ToString(); + s = "/Tx BMC\n" + s + "\nEMC"; + xobj.Stream.Value = new RawEncoding().GetBytes(s); + } + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + RenderAppearance(); + } + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfPushButtonField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfPushButtonField.cs index 0079fdc7..96ebbc44 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfPushButtonField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfPushButtonField.cs @@ -1,6 +1,11 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Internal; + namespace PdfSharp.Pdf.AcroForms { /// @@ -15,12 +20,167 @@ internal PdfPushButtonField(PdfDocument document) : base(document) { _document = document; + SetFlags |= PdfAcroFieldFlags.Pushbutton; } internal PdfPushButtonField(PdfDictionary dict) : base(dict) { } + private string? caption; + /// + /// Gets or sets the Caption of this Button + /// + public string? Caption + { + get + { + if (caption == null) + { + foreach (var widget in Annotations.Elements) + { + if (!string.IsNullOrWhiteSpace(widget.NormalCaption)) + { + caption = widget.NormalCaption; + break; + } + } + } + return caption; + } + set + { + if (caption != value) + { + caption = value; + foreach (var widget in Annotations.Elements) + { + widget.NormalCaption = value; + } + } + } + } + + /// + /// Adds a new Annotation to this field. + /// + /// A method that is used to configure the Annotation + /// The created and configured Annotation + /// + public override PdfWidgetAnnotation AddAnnotation(Action configure) + { + var annot = base.AddAnnotation(configure); + if (Caption != null) + annot.NormalCaption = Caption; + return annot; + } + + /// + /// Determines the visual appearance of this field, i.e. font and text-color + /// + protected override void DetermineAppearance() + { + var dict = this; + base.DetermineAppearance(); + } + + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + if (Font is null) + return; + + for (var idx = 0; idx < Annotations.Elements.Count; idx++) + { + var widget = Annotations.Elements[idx]; + // if widget already has an appearance, use that (imported field) + if (widget == null || widget.Elements.ContainsKey(PdfAnnotation.Keys.AP)) + continue; + + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; + + var preferredFontType = AnsiEncoding.IsAnsi(Caption ?? string.Empty) ? FontType.TrueTypeWinAnsi : FontType.Type0Unicode; + SetFontType(preferredFontType); + + var xRect = new XRect(0, 0, width, height); + + var formNormal = new XForm(_document, xRect); + using (var gfx = XGraphics.FromForm(formNormal)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.PushButtonFieldRenderer.RenderNormalState(this, widget, gfx, xRect); + } + formNormal.DrawingFinished(); + SetXFormFont(formNormal); + + // Note: implementing RenderRolloverState and RenderDownState is optional for the PushButtonFieldRenderer + // the current implementation throws a NotImplementedException for these methods + var formRollover = new XForm(_document, xRect); + try + { + using (var gfx = XGraphics.FromForm(formRollover)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.PushButtonFieldRenderer.RenderRolloverState(this, widget, gfx, xRect); + } + formRollover.DrawingFinished(); + SetXFormFont(formRollover); + } + catch (NotImplementedException) + { + formRollover = null; + } + + var formDown = new XForm(_document, xRect); + try + { + using (var gfx = XGraphics.FromForm(formDown)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.PushButtonFieldRenderer.RenderDownState(this, widget, gfx, xRect); + } + formDown.DrawingFinished(); + SetXFormFont(formDown); + } + catch (NotImplementedException) + { + formDown = null; + } + + var ap = new PdfDictionary(Owner); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + ap.Elements["/N"] = formNormal.PdfForm.Reference; + if (formRollover != null) + ap.Elements["/R"] = formRollover.PdfForm.Reference; + if (formDown != null) + ap.Elements["/D"] = formDown.PdfForm.Reference; + + foreach (var form in new[] { formNormal, formRollover, formDown }) + { + if (form == null) + continue; + var xobj = form.PdfForm; + var s = xobj.Stream.ToString(); + s = "/Tx BMC\n" + s + "\nEMC"; + xobj.Stream.Value = new RawEncoding().GetBytes(s); + } + } + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + RenderAppearance(); + } + + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfRadioButtonField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfRadioButtonField.cs index 85ab84a5..431317b1 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfRadioButtonField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfRadioButtonField.cs @@ -1,6 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; +using System.Collections.ObjectModel; + namespace PdfSharp.Pdf.AcroForms { /// @@ -15,55 +19,250 @@ internal PdfRadioButtonField(PdfDocument document) : base(document) { _document = document; + SetFlags |= PdfAcroFieldFlags.Radio; } internal PdfRadioButtonField(PdfDictionary dict) : base(dict) - { } + { + if (!Elements.ContainsKey(Keys.Opt)) + { + var array = new PdfArray(_document); + foreach (var val in Options) + array.Elements.Add(new PdfString(val)); + Elements.Add(Keys.Opt, array); + } + } /// - /// Gets or sets the index of the selected radio button in a radio button group. + /// Gets or sets the value of this field. This should be an item from the list.

+ /// Setting this to null or an empty string unchecks all radio-buttons. ///
- public int SelectedIndex + public new string Value { get { - string value = Elements.GetString(PdfAcroField.Keys.V); - return IndexInOptStrings(value); + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + var name = ancestor.Elements.GetName(PdfAcroField.Keys.V); + return (string.IsNullOrEmpty(name) ? "/Off" : name).TrimStart('/'); } set { - var opt = Elements[Keys.Opt] as PdfArray; + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + if (!string.IsNullOrWhiteSpace(value)) + { + var index = IndexInFieldValues(value); + if (index < 0) + throw new ArgumentException($"'{value}' is not a valid value for field '{FullyQualifiedName}'. Valid values are: [{string.Join(",", Options)}]"); + + Elements.SetName(PdfAcroField.Keys.V, value); + SelectedIndex = index; + } + else + SelectedIndex = -1; + } + } - if (opt == null) - opt = Elements[PdfAcroField.Keys.Kids] as PdfArray; + private List? options; + /// + /// Gets the option-names of this RadioButton

+ /// Use one of these values when setting

+ /// You cannot manipulate this collection directly. + /// To change the elements you have to manipulate the of this field. + ///
+ public ReadOnlyCollection Options + { + get + { + if (options != null) + return options.AsReadOnly(); + + var values = new List(); + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget == null) + continue; + // convert names to ordinary strings by removing the slash + values.Add((GetNonOffValue(widget) ?? i.ToString()).TrimStart('/')); + } + options = values; + return options.AsReadOnly(); + } + } + + /// + /// Gets or sets the (optional) export-values for each entry in this radio button group.

+ /// If the field does not specify these, is returned. + ///
+ public ICollection ExportValues + { + get + { + var ancestor = FindParentHavingKey(Keys.Opt); + var opt = ancestor.Elements.GetArray(Keys.Opt); if (opt != null) { - int count = opt.Elements.Count; - if (value < 0 || value >= count) - throw new ArgumentOutOfRangeException(nameof(value)); - Elements.SetName(PdfAcroField.Keys.V, opt.Elements[value].ToString() ?? NRT.ThrowOnNull()); + var list = new List(); + for (var i = 0; i < opt.Elements.Count; i++) + list.Add(opt.Elements.GetString(i)); + return list; } + return Options; + } + set + { + if (value.Count != Options.Count) + throw new ArgumentException("Length of Opt-Array must match length of Options"); + var optArray = new PdfArray(); + foreach (var val in value) + optArray.Elements.Add(new PdfString(val)); + Elements[Keys.Opt] = optArray; } } - int IndexInOptStrings(string value) + /// + /// Gets or sets the (zero-based) index of the selected radio button in a radio button group.

+ /// Use -1 to deselect all items.

+ /// This is an alternative to the + ///
+ public int SelectedIndex { - if (Elements[Keys.Opt] is PdfArray opt) + get + { + return IndexInFieldValues(Value); + } + set { - int count = opt.Elements.Count; - for (int idx = 0; idx < count; idx++) + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + var values = Options; + var count = values.Count; + if (value < -1 || value >= count) + throw new ArgumentOutOfRangeException(nameof(value), value, + $"SelectedIndex for field '{FullyQualifiedName}' must be greater or equal to -1 and smaller than {Options.Count}"); + + var name = value == -1 ? "/Off" : '/' + values.ElementAt(value); + Elements.SetName(PdfAcroField.Keys.V, name); + // first, set all annotations to /Off + for (var i = 0; i < Annotations.Elements.Count; i++) { - var item = opt.Elements[idx]; - if (item is PdfString) + var widget = Annotations.Elements[i]; + widget?.Elements.SetName(PdfAnnotation.Keys.AS, "/Off"); + } + if ((Flags & PdfAcroFieldFlags.RadiosInUnison) != 0) + { + // Then set all Widgets with the same Appearance to the checked state + for (var i = 0; i < Annotations.Elements.Count; i++) { - if (item.ToString() == value) - return idx; + var widget = Annotations.Elements[i]; + if (name == values.ElementAt(i) && widget != null) + widget.Elements.SetName(PdfAnnotation.Keys.AS, name); } } + else + { + if (value >= 0 && value < Annotations.Elements.Count) + { + var widget = Annotations.Elements[value]; + widget?.Elements.SetName(PdfAnnotation.Keys.AS, name); + } + } + } + } + + private int IndexInFieldValues(string value) + { + return Options.IndexOf(value); + } + + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + var rect = widget.Rectangle; + if (widget.Page != null && !rect.IsZero) + { + // existing/imported field ? + if (widget.Elements.ContainsKey(PdfAnnotation.Keys.AP)) + { + widget.Elements.SetName(PdfAnnotation.Keys.AS, i == SelectedIndex ? Options.ElementAt(i) : "/Off"); + } + else + CreateAppearance(widget, GetNonOffValue(widget) ?? "/Yes"); + } + } + } + + /// + /// Creates the appearance-stream for the specified Widget. + /// + /// + /// + private void CreateAppearance(PdfWidgetAnnotation widget, string nameOfOnState) + { + // remove possible leading slashes (will be re-added later) + nameOfOnState = nameOfOnState.TrimStart('/'); + + var rect = widget.Rectangle; + if (widget.Page != null && !rect.IsZero) + { + var xRect = new XRect(0, 0, Math.Max(1, rect.Width), Math.Max(1, rect.Height)); + // checked state + var formChecked = new XForm(_document, xRect); + using (var gfx = XGraphics.FromForm(formChecked)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.RadioButtonFieldRenderer.RenderCheckedState(this, widget, gfx, xRect); + } + formChecked.DrawingFinished(); + + // unchecked state + var formUnchecked = new XForm(_document, rect.ToXRect()); + using (var gfx = XGraphics.FromForm(formUnchecked)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.RadioButtonFieldRenderer.RenderUncheckedState(this, widget, gfx, xRect); + } + formUnchecked.DrawingFinished(); + + var ap = new PdfDictionary(_document); + var nDict = new PdfDictionary(_document); + ap.Elements.SetValue("/N", nDict); + nDict.Elements[new PdfName("/" + nameOfOnState)] = formChecked.PdfForm.Reference; + nDict.Elements["/Off"] = formUnchecked.PdfForm.Reference; + widget.Elements[PdfAnnotation.Keys.AP] = ap; } - return -1; + } + + /// + /// A special overload for RadioButtons that allows specifying the name for the "On"-State of an individual radio-button + /// + /// Name of the "On"-State of this RadioButton + /// A method that is used to configure the Annotation + /// The created and configured Annotation + /// + public PdfWidgetAnnotation AddAnnotation(string nameOfOnState, Action configure) + { + if (string.IsNullOrWhiteSpace(nameOfOnState)) + throw new ArgumentNullException(nameof(nameOfOnState), "Name of 'On' state must not be null or empty"); + + var annot = AddAnnotation(configure); + CreateAppearance(annot, nameOfOnState); + options = null; // reset. next access to Options will re-create them + return annot; + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + RenderAppearance(); } /// @@ -73,11 +272,15 @@ int IndexInOptStrings(string value) public new class Keys : PdfButtonField.Keys { /// - /// (Optional; inheritable; PDF 1.4) An array of text strings to be used in - /// place of the V entries for the values of the widget annotations representing - /// the individual radio buttons. Each element in the array represents - /// the export value of the corresponding widget annotation in the - /// Kids array of the radio button field. + /// (Optional; inheritable; PDF 1.4) An array containing one entry for each + /// widget annotation in the Kids array of the radio button or check box field. + /// Each entry shall be a text string representing the on state of the + /// corresponding widget annotation.

+ /// When this entry is present, the names used to represent the on state in the + /// AP dictionary of each annotation may use numerical position (starting with 0) + /// of the annotation in the Kids array, encoded as a name object (for example: /0, /1).

+ /// This allows distinguishing between the annotations even if two or more of them + /// have the same value in the Opt array. ///
[KeyInfo(KeyType.Array | KeyType.Optional)] public const string Opt = "/Opt"; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs index 740d272b..05660a88 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs @@ -1,9 +1,11 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -using PdfSharp.Pdf.IO; using PdfSharp.Drawing; +using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.IO; +using PdfSharp.Pdf.Signatures; namespace PdfSharp.Pdf.AcroForms { @@ -18,6 +20,7 @@ public sealed class PdfSignatureField : PdfAcroField internal PdfSignatureField(PdfDocument document) : base(document) { + Elements.SetName(PdfAcroField.Keys.FT, "Sig"); CustomAppearanceHandler = null!; } @@ -27,6 +30,15 @@ internal PdfSignatureField(PdfDictionary dict) CustomAppearanceHandler = null!; } + /// + /// Gets or sets the for this signature field + /// + public new PdfSignature2? Value + { + get => Elements.GetValue(PdfAcroField.Keys.V) as PdfSignature2; + set => Elements[PdfAcroField.Keys.V] = value; + } + /// /// Handler that creates the visual representation of the digital signature in PDF. /// @@ -38,35 +50,42 @@ internal PdfSignatureField(PdfDictionary dict) ///
void RenderCustomAppearance() { - var rect = Elements.GetRectangle(PdfAnnotation.Keys.Rect); + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget == null) + continue; - var visible = rect.X1 + rect.X2 + rect.Y1 + rect.Y2 != 0; + var rect = widget.Rectangle; - if (!visible) - return; + var visible = rect.X1 + rect.X2 + rect.Y1 + rect.Y2 != 0; - if (CustomAppearanceHandler == null) - throw new Exception("AppearanceHandler is not set."); + if (!visible) + continue; - var form = new XForm(_document, rect.Size); - var gfx = XGraphics.FromForm(form); + if (CustomAppearanceHandler == null) + throw new Exception("AppearanceHandler is null"); - CustomAppearanceHandler.DrawAppearance(gfx, rect.ToXRect()); + var form = new XForm(_document, rect.Size); + var gfx = XGraphics.FromForm(form); - form.DrawingFinished(); + CustomAppearanceHandler.DrawAppearance(gfx, rect.ToXRect()); - // Get existing or create new appearance dictionary - if (Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) - { - ap = new PdfDictionary(_document); - Elements[PdfAnnotation.Keys.AP] = ap; - } + form.DrawingFinished(); + + // Get existing or create new appearance dictionary + if (widget.Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) + { + ap = new PdfDictionary(_document); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + } - // Set XRef to normal state - ap.Elements["/N"] = form.PdfForm.Reference; + // Set XRef to normal state + ap.Elements["/N"] = form.PdfForm.Reference; - // PdfRenderer can be null. - form.PdfRenderer?.Close(); + // PdfRenderer can be null. + form.PdfRenderer?.Close(); + } } internal override void PrepareForSave() @@ -74,6 +93,62 @@ internal override void PrepareForSave() base.PrepareForSave(); if (CustomAppearanceHandler != null!) RenderCustomAppearance(); + else + RenderAppearance(); + } + + /// + /// Renders the appearance of this field + /// + protected override void RenderAppearance() + { + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget == null) + continue; + + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; + + var xRect = new XRect(0, 0, width, height); + var form = (widget.Rotation == 90 || widget.Rotation == 270) && (widget.Flags & PdfAnnotationFlags.NoRotate) == 0 + ? new XForm(_document, XUnit.FromPoint(rect.Height), XUnit.FromPoint(rect.Width)) + : new XForm(_document, xRect); + + if (widget.Rotation != 0 && (widget.Flags & PdfAnnotationFlags.NoRotate) == 0) + { + // I could not get this to work using gfx.Rotate/Translate Methods... + const double deg2Rad = 0.01745329251994329576923690768489; // PI/180 + var sr = Math.Sin(widget.Rotation * deg2Rad); + var cr = Math.Cos(widget.Rotation * deg2Rad); + // see PdfReference 1.7, Chapter 8.3.3 (Common Transformations) + // TODO: Is this always correct ? I had only the chance to test this with a 90 degree rotation... + form.PdfForm.Elements.SetMatrix(PdfFormXObject.Keys.Matrix, new XMatrix(cr, sr, -sr, cr, xRect.Width, 0)); + if (widget.Rotation == 90 || widget.Rotation == 270) + xRect = new XRect(0, 0, rect.Height, rect.Width); + } + + using (var gfx = XGraphics.FromForm(form)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.SignatureFieldRenderer.Render(this, widget, gfx, xRect); + } + form.DrawingFinished(); + + // Get existing or create new appearance dictionary. + if (widget.Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) + { + ap = new PdfDictionary(_document); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + } + + ap.Elements["/N"] = form.PdfForm.Reference; + } } /// @@ -99,6 +174,25 @@ internal override void WriteDictionaryElement(PdfWriter writer, PdfName key) /// public new class Keys : PdfAcroField.Keys { + /// + /// (Optional; shall be an indirect reference; PDF 1.5) A signature field lock dictionary + /// that specifies a set of form fields that shall be locked when this signature field is signed. + /// + [KeyInfo(KeyType.Dictionary | KeyType.Optional)] + public const string Lock = "/Lock"; + + /// + /// (Optional; shall be an indirect reference; PDF 1.5) A seed value dictionary (see Table 234) + /// containing information that constrains the properties of a signature that is applied to this field. + /// + [KeyInfo(KeyType.Dictionary | KeyType.Optional)] + public const string SV = "/SV"; + + // + // NOTE: The following entries are not part of a Signature field. + // Rather, these are the key of a signature-dictionary (see PdfReference 1.7, Chapter 12.8) + // + /// /// (Optional) The type of PDF object that this dictionary describes; if present, /// must be Sig for a signature dictionary. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfTextField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfTextField.cs index cefef326..7bd1429b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfTextField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfTextField.cs @@ -18,35 +18,41 @@ public sealed class PdfTextField : PdfAcroField /// internal PdfTextField(PdfDocument document) : base(document) - { } + { + Elements.SetName(PdfAcroField.Keys.FT, "Tx"); + } internal PdfTextField(PdfDictionary dict) : base(dict) { } /// - /// Gets or sets the text value of the text field. + /// Same as (which should be used instead) /// - public string Text + public new string Value { - get => Elements.GetString(PdfAcroField.Keys.V); - set { Elements.SetString(PdfAcroField.Keys.V, value); RenderAppearance(); } //HACK_OLD in PdfTextField + get => Text; + set => Text = value; } /// - /// Gets or sets the font used to draw the text of the field. - /// - public XFont Font { get; set; } = new XFont("Courier New", 10); - - /// - /// Gets or sets the foreground color of the field. - /// - public XColor ForeColor { get; set; } = XColors.Black; - - /// - /// Gets or sets the background color of the field. + /// Gets or sets the text value of the text field. /// - public XColor BackColor { get; set; } = XColor.Empty; + public string Text + { + get + { + var ancestor = FindParentHavingKey(PdfAcroField.Keys.V); + return ancestor.Elements.GetString(PdfAcroField.Keys.V); + } + set + { + if (ReadOnly) + throw new InvalidOperationException("The field is read only."); + // TODO: check MaxLength ? (risky -> potential data-loss) + Elements.SetString(PdfAcroField.Keys.V, value ?? string.Empty); + } + } /// /// Gets or sets the maximum length of the field. @@ -54,8 +60,12 @@ public string Text /// The length of the max. public int MaxLength { - get => Elements.GetInteger(Keys.MaxLen); - set => Elements.SetInteger(Keys.MaxLen, value); + get + { + var ancestor = FindParentHavingKey(Keys.MaxLen); + return ancestor.Elements.GetInteger(Keys.MaxLen); + } + set { Elements.SetInteger(Keys.MaxLen, value); } } /// @@ -63,7 +73,7 @@ public int MaxLength /// public bool MultiLine { - get => (Flags & PdfAcroFieldFlags.Multiline) != 0; + get { return (Flags & PdfAcroFieldFlags.Multiline) != 0; } set { if (value) @@ -78,7 +88,7 @@ public bool MultiLine /// public bool Password { - get => (Flags & PdfAcroFieldFlags.Password) != 0; + get { return (Flags & PdfAcroFieldFlags.Password) != 0; } set { if (value) @@ -89,138 +99,128 @@ public bool Password } /// - /// Creates the normal appearance form X object for the annotation that represents - /// this acro form text field. + /// Gets or sets a value indicating whether this field is a combined field. + /// A combined field is a text field made up of multiple "combs" of equal width. The number of combs is determined by . /// - void RenderAppearance() + public bool Combined { -#if true_ - PdfFormXObject xobj = new PdfFormXObject(Owner); - Owner.Internals.AddObject(xobj); - xobj.Elements["/BBox"] = new PdfLiteral("[0 0 122.653 12.707]"); - xobj.Elements["/FormType"] = new PdfLiteral("1"); - xobj.Elements["/Matrix"] = new PdfLiteral("[1 0 0 1 0 0]"); - PdfDictionary res = new PdfDictionary(Owner); - xobj.Elements["/Resources"] = res; - res.Elements["/Font"] = new PdfLiteral("<< /Helv 28 0 R >> /ProcSet [/PDF /Text]"); - xobj.Elements["/Subtype"] = new PdfLiteral("/Form"); - xobj.Elements["/Type"] = new PdfLiteral("/XObject"); - - string s = - "/Tx BMC " + '\n' + - "q" + '\n' + - "1 1 120.653 10.707 re" + '\n' + - "W" + '\n' + - "n" + '\n' + - "BT" + '\n' + - "/Helv 7.93 Tf" + '\n' + - "0 g" + '\n' + - "2 3.412 Td" + '\n' + - "(Hello ) Tj" + '\n' + - "20.256 0 Td" + '\n' + - "(XXX) Tj" + '\n' + - "ET" + '\n' + - "Q" + '\n' + - "";//"EMC"; - int length = s.Length; - byte[] stream = new byte[length]; - for (int idx = 0; idx < length; idx++) - stream[idx] = (byte)s[idx]; - xobj.CreateStream(stream); - - // Get existing or create new appearance dictionary - PdfDictionary ap = Elements[PdfAnnotation.Keys.AP] as PdfDictionary; - if (ap == null) + get { return (Flags & PdfAcroFieldFlags.CombTextField) != 0; } + set { - ap = new PdfDictionary(_document); - Elements[PdfAnnotation.Keys.AP] = ap; + if (value) + SetFlags |= PdfAcroFieldFlags.CombTextField; + else + SetFlags &= ~PdfAcroFieldFlags.CombTextField; } + } - // Set XRef to normal state - ap.Elements["/N"] = xobj.Reference; + /// + /// Sets the default appearance for this field. + /// + public void SetDefaultAppearance(XFont font, double fontSize, XColor textColor) + { + if (font is null) + throw new ArgumentNullException(nameof(font)); + if (fontSize < 0.0) + throw new ArgumentException("Font size must be greater or equal to zero", nameof(fontSize)); + if (Owner.AcroForm is null) + throw new InvalidOperationException("AcroForm has to be created first"); - //string m = - //"" + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" PDFsharp 1.40.2150-g (www.pdfsharp.com) (Original: Powered By Crystal) " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" 2011-07-11T23:15:09+02:00 " + '\n' + - //" 2011-05-19T16:26:51+03:00 " + '\n' + - //" 2011-07-11T23:15:09+02:00 " + '\n' + - //" Crystal Reports " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" application/pdf " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" uuid:68249d89-baed-4384-9a2d-fbf8ace75c45 " + '\n' + - //" uuid:3d5f2f46-c140-416f-baf2-7f9c970cef1d " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //" " + '\n' + - //""; + var formResources = Owner.AcroForm.GetOrCreateResources(); + var fontType = font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var docFont = _document.FontTable.GetOrCreateFont(font.GlyphTypeface, fontType); + var fontName = formResources.AddFont(docFont); + var da = string.Format(CultureInfo.InvariantCulture, "{0} {1:F2} Tf {2:F4} {3:F4} {4:F4} rg", + fontName, fontSize, textColor.R / 255, textColor.G / 255, textColor.B / 255); + Elements.SetString(PdfAcroField.Keys.DA, da); + } - //PdfDictionary mdict = (PdfDictionary)_document.Internals.GetObject(new PdfObjectID(32)); + /// + /// Creates the normal appearance form X object for the annotation that represents + /// this acro form text field. + /// + protected override void RenderAppearance() + { + if (Font == null || string.IsNullOrEmpty(Text)) + return; - //length = m.Length; - //stream = new byte[length]; - //for (int idx = 0; idx < length; idx++) - // stream[idx] = (byte)m[idx]; + for (var i = 0; i < Annotations.Elements.Count; i++) + { + var widget = Annotations.Elements[i]; + if (widget == null) + continue; - //mdict.Stream.Value = stream; -#else - var rect = Elements.GetRectangle(PdfAnnotation.Keys.Rect); - var form = new XForm(_document, rect.Size); - var gfx = XGraphics.FromForm(form); + var rect = widget.Rectangle; + var width = Math.Abs(rect.Width); + var height = Math.Abs(rect.Height); + // ensure a minimum size of 1x1, otherwise an exception is thrown + if (width < 1.0 || height < 1.0) + continue; - if (BackColor != XColor.Empty) - gfx.DrawRectangle(new XSolidBrush(BackColor), rect.ToXRect() - rect.Location); + var xRect = new XRect(0, 0, width, height); + var form = (widget.Rotation == 90 || widget.Rotation == 270) && (widget.Flags & PdfAnnotationFlags.NoRotate) == 0 + ? new XForm(_document, XUnit.FromPoint(rect.Height), XUnit.FromPoint(rect.Width)) + : new XForm(_document, xRect); - string text = Text; - if (text.Length > 0) - gfx.DrawString(Text, Font, new XSolidBrush(ForeColor), - rect.ToXRect() - rect.Location + new XPoint(2, 0), XStringFormats.TopLeft); + if (widget.Rotation != 0 && (widget.Flags & PdfAnnotationFlags.NoRotate) == 0) + { + // I could not get this to work using gfx.Rotate/Translate Methods... + const double deg2Rad = 0.01745329251994329576923690768489; // PI/180 + var sr = Math.Sin(widget.Rotation * deg2Rad); + var cr = Math.Cos(widget.Rotation * deg2Rad); + // see PdfReference 1.7, Chapter 8.3.3 (Common Transformations) + // TODO: Is this always correct ? I had only the chance to test this with a 90 degree rotation... + form.PdfForm.Elements.SetMatrix(PdfFormXObject.Keys.Matrix, new XMatrix(cr, sr, -sr, cr, xRect.Width, 0)); + if (widget.Rotation == 90 || widget.Rotation == 270) + xRect = new XRect(0, 0, rect.Height, rect.Width); + } - form.DrawingFinished(); - form.PdfForm.Elements.Add("/FormType", new PdfLiteral("1")); + var preferredFontType = AnsiEncoding.IsAnsi(Text) ? FontType.TrueTypeWinAnsi : FontType.Type0Unicode; + SetFontType(preferredFontType); - // Get existing or create new appearance dictionary. - if (Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) - { - ap = new PdfDictionary(_document); - Elements[PdfAnnotation.Keys.AP] = ap; - } + using (var gfx = XGraphics.FromForm(form)) + { + gfx.IntersectClip(xRect); + Owner.AcroForm?.FieldRenderer.TextFieldRenderer.Render(this, widget, gfx, xRect); + } + form.DrawingFinished(); + SetXFormFont(form); - // Set XRef to normal state. - ap.Elements["/N"] = form.PdfForm.Reference; + // Get existing or create new appearance dictionary. + if (widget.Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) + { + ap = new PdfDictionary(_document); + widget.Elements[PdfAnnotation.Keys.AP] = ap; + } - form.PdfRenderer.Close(); + ap.Elements["/N"] = form.PdfForm.Reference; - var xobj = form.PdfForm; - string s = xobj.Stream?.ToString() ?? ""; - // Thank you Adobe: Without putting the content in 'EMC brackets' - // the text is not rendered by PDF Reader 9 or higher. - s = "/Tx BMC\n" + s + "\nEMC"; - if (xobj.Stream != null) + var xobj = form.PdfForm; + var s = xobj.Stream.ToString(); + // Thank you Adobe: Without putting the content in 'EMC brackets' + // the text is not rendered by PDF Reader 9 or higher. + s = "/Tx BMC\n" + s + "\nEMC"; xobj.Stream.Value = new RawEncoding().GetBytes(s); -#endif + } + // create DefaultAppearance for newly created fields (required according to the spec) + if (!Elements.ContainsKey(PdfAcroField.Keys.DA) && _document.AcroForm != null) + { + var fontType = Font.PdfOptions.FontEmbedding == PdfFontEmbedding.OmitStandardFont + ? FontType.Type1StandardFont + : Font.PdfOptions.FontEncoding == PdfFontEncoding.Unicode + ? FontType.Type0Unicode + : FontType.TrueTypeWinAnsi; + var pdfFont = _document.FontTable.GetOrCreateFont(Font.GlyphTypeface, fontType); + var formResources = _document.AcroForm.GetOrCreateResources(); + var fontName = formResources.AddFont(pdfFont); + Elements.Add(PdfAcroField.Keys.DA, new PdfString(string.Format( + CultureInfo.InvariantCulture, "{0} {1:0.###} Tf {2:0.###} {3:0.###} {4:0.###} rg", + fontName, FontSize ?? Font.Size, ForeColor.R / 255.0, ForeColor.G / 255.0, ForeColor.B / 255.0))); + } } internal override void PrepareForSave() diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfAcroFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfAcroFieldRenderer.cs new file mode 100644 index 00000000..50129ca2 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfAcroFieldRenderer.cs @@ -0,0 +1,87 @@ +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Contains the renderers for s

+ /// Individual renderers may be overriden by their respective implementations + ///
+ public class PdfAcroFieldRenderer + { + private PdfCheckBoxFieldRenderer checkBoxFieldRenderer = new(); + private PdfComboBoxFieldRenderer comboBoxFieldRenderer = new(); + private PdfListBoxFieldRenderer listBoxFieldRenderer = new(); + private PdfPushButtonFieldRenderer pushButtonFieldRenderer = new(); + private PdfRadioButtonFieldRenderer radioButtonFieldRenderer = new(); + private PdfSignatureFieldRenderer signatureFieldRenderer = new(); + private PdfTextFieldRenderer textFieldRenderer = new(); + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfCheckBoxFieldRenderer CheckBoxFieldRenderer + { + get => checkBoxFieldRenderer; + set => checkBoxFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfComboBoxFieldRenderer ComboBoxFieldRenderer + { + get => comboBoxFieldRenderer; + set => comboBoxFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfListBoxFieldRenderer ListBoxFieldRenderer + { + get => listBoxFieldRenderer; + set => listBoxFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfPushButtonFieldRenderer PushButtonFieldRenderer + { + get => pushButtonFieldRenderer; + set => pushButtonFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfRadioButtonFieldRenderer RadioButtonFieldRenderer + { + get => radioButtonFieldRenderer; + set => radioButtonFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfSignatureFieldRenderer SignatureFieldRenderer + { + get => signatureFieldRenderer; + set => signatureFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets the renderer for s + ///

Trying to set this to null will throw an Exception + ///
+ public PdfTextFieldRenderer TextFieldRenderer + { + get => textFieldRenderer; + set => textFieldRenderer = value ?? throw new ArgumentNullException(nameof(value)); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfCheckBoxFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfCheckBoxFieldRenderer.cs new file mode 100644 index 00000000..c7df6f4b --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfCheckBoxFieldRenderer.cs @@ -0,0 +1,57 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ ///
+ /// + /// Inheritors should override the methods + /// and + /// + public class PdfCheckBoxFieldRenderer + { + /// + /// Renders the ckecked state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderCheckedState(PdfCheckBoxField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + // draw border + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + var borderPen = new XPen(widget.BorderColor, widget.Border.Width); + gfx.DrawRectangle(borderPen, 0, 0, rect.Width, rect.Height); + } + // draw an X-shape + var pen = new XPen(field.ForeColor, rect.Width * 0.125) + { + LineCap = XLineCap.Round + }; + var pad = widget.Border.Width + 1; + gfx.DrawLine(pen, 0 + pad, pad, rect.Width - pad, rect.Height - pad); + gfx.DrawLine(pen, 0 + pad, rect.Height - pad, rect.Width - pad, pad); + } + + /// + /// Renders the unckecked state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderUncheckedState(PdfCheckBoxField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + // draw border + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + var borderPen = new XPen(widget.BorderColor, widget.Border.Width); + gfx.DrawRectangle(borderPen, 0, 0, rect.Width, rect.Height); + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfComboBoxFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfComboBoxFieldRenderer.cs new file mode 100644 index 00000000..4c3a02a2 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfComboBoxFieldRenderer.cs @@ -0,0 +1,51 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ ///
+ /// + /// Inheritors should override the method + /// + public class PdfComboBoxFieldRenderer + { + /// + /// Renders the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void Render(PdfComboBoxField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + if (field.Font == null) + return; + + if (widget.BackColor != XColor.Empty) + gfx.DrawRectangle(new XSolidBrush(widget.BackColor), rect); + // Draw Border + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + gfx.DrawRectangle(new XPen(widget.BorderColor, widget.Border.Width), rect); + rect.Inflate(-widget.Border.Width, -widget.Border.Width); + } + + var index = field.SelectedIndex; + if (index > 0) + { + var text = field.ValueInOptArray(index, false); + if (!String.IsNullOrEmpty(text)) + { + var format = field.TextAlign == PdfAcroFieldTextAlignment.Left + ? XStringFormats.CenterLeft + : field.TextAlign == PdfAcroFieldTextAlignment.Center + ? XStringFormats.Center + : XStringFormats.CenterRight; + gfx.DrawString(text, field.Font, new XSolidBrush(field.ForeColor), rect, format); + } + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfListBoxFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfListBoxFieldRenderer.cs new file mode 100644 index 00000000..1808135c --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfListBoxFieldRenderer.cs @@ -0,0 +1,62 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ ///
+ /// + /// Inheritors should override the method + /// + public class PdfListBoxFieldRenderer + { + /// + /// Renders the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void Render(PdfListBoxField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + if (field.Font == null) + return; + + var format = field.TextAlign == PdfAcroFieldTextAlignment.Left + ? XStringFormats.CenterLeft + : field.TextAlign == PdfAcroFieldTextAlignment.Center + ? XStringFormats.Center + : XStringFormats.CenterRight; + if (widget.BackColor != XColor.Empty) + gfx.DrawRectangle(new XSolidBrush(widget.BackColor), rect); + // Draw Border + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + gfx.DrawRectangle(new XPen(widget.BorderColor, widget.Border.Width), rect); + + var lineHeight = field.Font.Height; + var y = 0.0; + var startIndex = Math.Max(0, Math.Min(field.TopIndex, field.Options.Count - (int)(rect.Height / lineHeight))); + for (var i = startIndex; i < field.Options.Count; i++) + { + var text = field.Options.ElementAt(i); + // offset and shrink a bit to not draw on top of the outer border + var lineRect = new XRect(widget.Border.Width, y + widget.Border.Width, + rect.Width - 2 * widget.Border.Width, lineHeight - 1); + var selected = false; + if (text.Length > 0) + { + if (field.SelectedIndices.Contains(i)) + { + gfx.DrawRectangle(new XSolidBrush(field.HighlightColor), lineRect); + selected = true; + } + lineRect.Inflate(-2, 0); + gfx.DrawString(text, field.Font, + new XSolidBrush(selected ? field.HighlightTextColor : field.ForeColor), lineRect, format); + y += lineHeight; + } + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfPushButtonFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfPushButtonFieldRenderer.cs new file mode 100644 index 00000000..1e79d6e8 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfPushButtonFieldRenderer.cs @@ -0,0 +1,130 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Annotations.enums; +using PdfSharp.Pdf.Internal; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ /// The current implementation does not render the Rollover- and Down- states + /// so the button will always appear in Normal state.

+ ///
+ /// + /// Inheritors should override the methods , + /// and + ///



+ /// It is allowed to throw a in the methods + /// and + /// , + /// only the implementation of + /// is mandatory. + ///
+ public class PdfPushButtonFieldRenderer + { + static XColor ShadeLight = XColors.White; + static XColor ShadeDark = XColors.DimGray; + const double shadeWidth = 2; + + /// + /// Renders the normal state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderNormalState(PdfPushButtonField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + var text = field.Caption; + if (string.IsNullOrWhiteSpace(text) || field.Font == null) + return; + + var format = XStringFormats.Center; + if (widget.NormalIcon != null) + { + format = widget.CaptionPlacement == PdfButtonCaptionPosition.LeftOfIcon + ? XStringFormats.CenterLeft + : widget.CaptionPlacement == PdfButtonCaptionPosition.RightOfIcon + ? XStringFormats.CenterRight + : widget.CaptionPlacement == PdfButtonCaptionPosition.AboveIcon + ? XStringFormats.TopCenter + : XStringFormats.BottomCenter; + } + + if (!widget.BackColor.IsEmpty) + gfx.DrawRectangle(new XSolidBrush(widget.BackColor), rect); + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + gfx.DrawRectangle(new XPen(widget.BorderColor, widget.Border.Width), rect); + //var prevRect = new XRect(rect.X, rect.Y, rect.Width, rect.Height); + rect.Inflate(-widget.Border.Width, -widget.Border.Width); + //gfx.TranslateTransform(widget.Border.Width, widget.Border.Width); + //gfx.ScaleAtTransform(rect.Width / prevRect.Width, rect.Height / prevRect.Height, rect.Width / 2, rect.Height / 2); + } + var tlColor = XColor.Empty; + var brColor = XColor.Empty; + if (widget.Border.BorderStyle == Annotations.enums.PdfAnnotationBorderStyle.Beveled) + { + tlColor = ShadeLight; + brColor = ShadeDark; + } + else if (widget.Border.BorderStyle == Annotations.enums.PdfAnnotationBorderStyle.Inset) + { + tlColor = ShadeDark; + brColor = ShadeLight; + } + if (!tlColor.IsEmpty) + { + // draw additional beveled or inset border + var tlPen = new XPen(tlColor, shadeWidth) + { + LineCap = XLineCap.Square, + LineJoin = XLineJoin.Miter + }; var brPen = new XPen(brColor, shadeWidth) + { + LineCap = XLineCap.Square, + LineJoin = XLineJoin.Miter + }; + // top + gfx.DrawLine(tlPen, rect.X, rect.Top, rect.Right, rect.Top); + // left + gfx.DrawLine(tlPen, rect.X, rect.Y, rect.X, rect.Bottom); + // bottom + gfx.DrawLine(brPen, rect.X + shadeWidth, rect.Bottom, rect.Right, rect.Bottom); + // right + gfx.DrawLine(brPen, rect.Right, rect.Y, rect.Right, rect.Bottom); + rect.Inflate(-brPen.Width, -brPen.Width); + } + // TODO: offset icon based on CaptionPosition + if (widget.NormalIcon != null && widget.CaptionPlacement != PdfButtonCaptionPosition.CaptionOnly) + gfx.AppendToContentStream(PdfEncoders.RawEncoding.GetString(widget.NormalIcon.Stream.UnfilteredValue)); + + if (widget.CaptionPlacement != PdfButtonCaptionPosition.IconOnly) + gfx.DrawString(text, field.Font, new XSolidBrush(field.ForeColor), rect, format); + } + + /// + /// Renders the rollover state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderRolloverState(PdfPushButtonField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + throw new NotImplementedException(); + } + + /// + /// Renders the down state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderDownState(PdfPushButtonField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfRadioButtonFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfRadioButtonFieldRenderer.cs new file mode 100644 index 00000000..7563c4aa --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfRadioButtonFieldRenderer.cs @@ -0,0 +1,54 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ ///
+ /// + /// Inheritors should override the methods + /// and + /// + /// + public class PdfRadioButtonFieldRenderer + { + /// + /// Renders the ckecked state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderCheckedState(PdfRadioButtonField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + // draw border + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + var borderPen = new XPen(widget.BorderColor, widget.Border.Width); + gfx.DrawEllipse(borderPen, 0, 0, rect.Width, rect.Height); + } + // draw a dot in the middle + var dotRect = new XRect(rect.Location, rect.Size); + dotRect.Inflate(-rect.Width * 0.25, -rect.Height * 0.25); + gfx.DrawEllipse(new XSolidBrush(field.ForeColor), dotRect); + } + + /// + /// Renders the unckecked state of the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void RenderUncheckedState(PdfRadioButtonField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + // draw border + if (!widget.BorderColor.IsEmpty) + { + var borderPen = new XPen(widget.BorderColor); + gfx.DrawEllipse(borderPen, 0, 0, rect.Width, rect.Height); + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfSignatureFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfSignatureFieldRenderer.cs new file mode 100644 index 00000000..3ef6a06e --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfSignatureFieldRenderer.cs @@ -0,0 +1,38 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a

+ ///
+ /// + /// Inheritors should override the method + /// + public class PdfSignatureFieldRenderer + { + /// + /// Renders the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void Render(PdfSignatureField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + const double pad = 4.0; + + if (!widget.BackColor.IsEmpty) + gfx.DrawRectangle(new XSolidBrush(widget.BackColor), rect); + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + gfx.DrawRectangle(new XPen(widget.BorderColor, widget.Border.Width), rect); + rect.Inflate(-widget.Border.Width, -widget.Border.Width); + } + + // render a horizontal line where a (handwritten) signature may be placed on + var linePen = new XPen(XColors.Black, 1.0); + gfx.DrawLine(linePen, rect.Left + pad, rect.Bottom - pad, rect.Right - pad, rect.Bottom - pad); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfTextFieldRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfTextFieldRenderer.cs new file mode 100644 index 00000000..a1bdf0a7 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/Rendering/PdfTextFieldRenderer.cs @@ -0,0 +1,75 @@ +using PdfSharp.Drawing; +using PdfSharp.Drawing.Layout; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.AcroForms.Rendering +{ + /// + /// Renders a + /// + /// + /// Inheritors should override the method + /// + public class PdfTextFieldRenderer + { + /// + /// Renders the + /// + /// The field being rendered + /// The of the field that is being rendered + /// The used to perform drawing operations + /// The spcifying the position and dimensions of the field + public virtual void Render(PdfTextField field, PdfWidgetAnnotation widget, XGraphics gfx, XRect rect) + { + var text = field.Text; + if (string.IsNullOrWhiteSpace(text) || field.Font == null) + return; + + if (field.MaxLength > 0) + text = text.Substring(0, Math.Min(text.Length, field.MaxLength)); + + var format = field.TextAlign == PdfAcroFieldTextAlignment.Left + ? XStringFormats.CenterLeft + : field.TextAlign == PdfAcroFieldTextAlignment.Center + ? XStringFormats.Center + : XStringFormats.CenterRight; + + if (field.MultiLine) + format.LineAlignment = XLineAlignment.Near; + if (!widget.BackColor.IsEmpty) + gfx.DrawRectangle(new XSolidBrush(widget.BackColor), rect); + if (!widget.BorderColor.IsEmpty && widget.Border.Width > 0) + { + gfx.DrawRectangle(new XPen(widget.BorderColor, widget.Border.Width), rect); + rect.Inflate(-widget.Border.Width, -widget.Border.Width); + } + + var renderFont = field.Font; + if (field.AutomaticFontSize) + renderFont = XFont.FromExisting(renderFont, field.DetermineFontSize(widget)); + + if (field.Combined && field.MaxLength > 0) + { + var combWidth = rect.Width / field.MaxLength; + var cBrush = new XSolidBrush(field.ForeColor); + var count = Math.Min(text.Length, field.MaxLength); + for (var ci = 0; ci < count; ci++) + { + var cRect = new XRect(ci * combWidth, 0, combWidth, rect.Height); + gfx.DrawString(text[ci].ToString(), renderFont, cBrush, cRect, XStringFormats.Center); + } + } + else + { + // for Multiline fields, we use XTextFormatter to handle line-breaks and a fixed TextFormat (only TopLeft is supported) + if (field.MultiLine) + { + var tf = new XTextFormatter(gfx); + tf.DrawString(text, renderFont, new XSolidBrush(field.ForeColor), rect, XStringFormats.TopLeft); + } + else + gfx.DrawString(text, renderFont, new XSolidBrush(field.ForeColor), rect, format); + } + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/enums/PdfAcroFieldTextAlignment.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/enums/PdfAcroFieldTextAlignment.cs new file mode 100644 index 00000000..2b8fe0ca --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/enums/PdfAcroFieldTextAlignment.cs @@ -0,0 +1,21 @@ +namespace PdfSharp.Pdf.AcroForms +{ + /// + /// Specifies the horizontal Text-Alignment for a + /// + public enum PdfAcroFieldTextAlignment + { + /// + /// Text is left-aligned + /// + Left, + /// + /// Text is centered inside the Field + /// + Center, + /// + /// Text is right-aligned + /// + Right + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs index 1b500a58..6d772e3f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs @@ -148,7 +148,7 @@ public PdfNameDictionary Names { _names = new PdfNameDictionary(Owner); Owner.Internals.AddObject(_names); - Elements.SetReference(Keys.Names, _names.Reference); + Elements.SetReference(Keys.Names, _names.Reference ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); } } return _names; @@ -173,7 +173,7 @@ public PdfNamedDestinations Destinations _dests = new PdfNamedDestinations(); _dests = new PdfNamedDestinations(); Owner.Internals.AddObject(_dests); - Elements.SetReference(Keys.Dests, _dests.Reference); + Elements.SetReference(Keys.Dests, _dests.Reference ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); } } return _dests; @@ -184,14 +184,30 @@ public PdfNamedDestinations Destinations /// /// Gets the AcroForm dictionary of this document. /// - public PdfAcroForm AcroForm + public PdfAcroForm? AcroForm { get { if (_acroForm == null) - _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm) ?? NRT.ThrowOnNull(); + _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm); return _acroForm; } + internal set + { + if (value != null) + { + if (!value.IsIndirect) + throw new InvalidOperationException("Setting the AcroForm requires an indirect object"); + Elements.SetReference(Keys.AcroForm, value); + } + else + { + if (AcroForm != null && AcroForm.Reference != null) + _document.IrefTable.Remove(AcroForm.Reference); + Elements.Remove(Keys.AcroForm); + _acroForm = null; + } + } } PdfAcroForm? _acroForm; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs index dc022f0c..902f72b8 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs @@ -67,8 +67,7 @@ public bool Compressed ///
void Decode() { - //if (Stream != null && Stream.Value != null) - if (Stream is { Value: { } }) // StL: Is this really more readable??? + if (Stream is { Value: not null }) { var item = Elements["/Filter"]; if (item != null) @@ -92,7 +91,7 @@ internal void PreserveGraphicsState() { // If a content stream is touched by PDFsharp it is typically because graphical operations are // prepended or appended. Some nasty PDF tools do not preserve the graphical state correctly. - // Therefore we try to relieve the problem by surrounding the content stream with push/restore + // Therefore, we try to relieve the problem by surrounding the content stream with push/restore // graphic state operation. if (Stream != null!) { @@ -128,7 +127,6 @@ internal override void WriteObject(PdfWriter writer) if (Stream != null!) { - //if (Owner.Options.CompressContentStreams) if (Owner.Options.CompressContentStreams && Elements.GetName("/Filter").Length == 0) { Stream.Value = Filtering.FlateDecode.Encode(Stream.Value, _document.Options.FlateEncodeMode); @@ -141,7 +139,8 @@ internal override void WriteObject(PdfWriter writer) base.WriteObject(writer); } - internal XGraphicsPdfRenderer? _pdfRenderer; + internal void SetRenderer(XGraphicsPdfRenderer? renderer) => _pdfRenderer = renderer; + XGraphicsPdfRenderer? _pdfRenderer; /// /// Predefined keys of this dictionary. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs index 37416217..c22d7e65 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs @@ -1,6 +1,13 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +// Turn on all test code at one place and ensure, that it does not come accidentally to the release build. +#if true && TEST_CODE +#define TEST_CODE // The #define is redundant, but clarifies what it means. +#else +#undef TEST_CODE +#endif + using System.Collections; using Microsoft.Extensions.Logging; using PdfSharp.Logging; @@ -12,8 +19,19 @@ namespace PdfSharp.Pdf.Advanced /// Represents the cross-reference table of a PDF document. /// It contains all indirect objects of a document. /// + /// + /// New implementation:
+ /// * No deep nesting recursion anymore.
+ /// * Uses a Stack<PdfObject>.
+ ///
+ /// We use Dictionary<PdfReference, object?> instead of Set<PdfReference> because a dictionary + /// with an unused value is faster than a set. + ///
sealed class PdfCrossReferenceTable(PdfDocument document) // Must not be derived from PdfObject. { +#if TEST_CODE + readonly Stopwatch _stopwatch = new(); +#endif /// /// Gets or sets a value indicating whether this table is under construction. /// It is true while reading a PDF file. @@ -21,17 +39,21 @@ sealed class PdfCrossReferenceTable(PdfDocument document) // Must not be derived internal bool IsUnderConstruction { get; set; } /// - /// Gets the number of objects in the table. + /// Gets the current number of references in the table. /// public int Count => _objectTable.Count; /// - /// Adds a cross-reference entry to the table. Used when parsing the trailer. + /// Adds a cross-reference entry to the table. Used when parsing a trailer. /// public void Add(PdfReference iref) { if (iref.ObjectID.IsEmpty) + { + // When happens this? iref.ObjectID = new(GetNewObjectNumber()); + PdfSharpLogHost.DocumentProcessingLogger.LogWarning("iRef with empty object ID found."); + } // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because it would not build with .NET Framework. if (_objectTable.ContainsKey(iref.ObjectID)) @@ -59,16 +81,32 @@ public void Add(PdfReference iref) /// public void Add(PdfObject value) { + // ReSharper disable once NullableWarningSuppressionIsUsed if (value.Owner == null!) + { + PdfSharpLogHost.PdfReadingLogger.LogWarning("Object without owner gets owned by the document it was added to."); value.Document = document; + } else + { Debug.Assert(value.Owner == document); + if (value.Owner != document) + { + PdfSharpLogHost.PdfReadingLogger.LogError("Object not owned by the document it was added to."); + } + } if (value.ObjectID.IsEmpty) + { + // Create new object number. value.SetObjectID(GetNewObjectNumber(), 0); + } if (_objectTable.ContainsKey(value.ObjectID)) + { + // This must not happen. throw new InvalidOperationException("Object already in table."); + } _objectTable.Add(value.ObjectID, value.ReferenceNotNull); @@ -90,9 +128,14 @@ public bool TryAdd(PdfObject value) return false; } - public void Remove(PdfReference iref) + /// + /// Removes a PdfObject from the table. + /// + /// + public bool Remove(PdfReference iref) { - _objectTable.Remove(iref.ObjectID); + // Remove the reference by its ID. + return _objectTable.Remove(iref.ObjectID); } /// @@ -198,7 +241,7 @@ internal int Compact() int removed = _objectTable.Count; PdfReference[] irefs = TransitiveClosure(document.Trailer); -#if DEBUG +#if DEBUG_ // Turn on again // Have any two objects the same ID? Dictionary ids = []; foreach (PdfObjectID objID in _objectTable.Keys) @@ -248,7 +291,7 @@ internal int Compact() MaxObjectNumber = 0; _objectTable.Clear(); - foreach (PdfReference iref in irefs) + foreach (var iref in irefs) { // This if-statement is needed for corrupt PDF files from the wild. // Without the if-statement, an exception will be thrown if the file contains duplicate IDs ("An item with the same key has already been added to the dictionary."). @@ -277,7 +320,7 @@ internal void Renumber() int count = irefs.Length; for (int idx = 0; idx < count; idx++) { - PdfReference iref = irefs[idx]; + var iref = irefs[idx]; iref.ObjectID = new PdfObjectID(idx + 1); // Rehash with new number. _objectTable.Add(iref.ObjectID, iref); @@ -292,7 +335,7 @@ internal void Renumber() /// internal SizeType GetPositionOfObjectBehind(PdfObject obj, SizeType position) { -#if DEBUG +#if DEBUG_ if (obj.Reference == null) _ = typeof(int); #endif @@ -356,198 +399,308 @@ public void CheckConsistence() /// /// Calculates the transitive closure of the specified PdfObject with the specified depth, i.e. all indirect objects - /// recursively reachable from the specified object in up to maximally depth steps. + /// recursively reachable from the specified object. /// - public PdfReference[] TransitiveClosure(PdfObject pdfObject /*, int depth = Int16.MaxValue*/) + public PdfReference[] TransitiveClosure(PdfObject pdfObject) { + var logger = PdfSharpLogHost.DocumentProcessingLogger; + CheckConsistence(); - Dictionary objects = new(); - //_overflow = new(); -#if DEBUG - _nestingLevel = _maxNestingLevel = 0; +#if TEST_CODE + // ReSharper disable once InconsistentNaming because it is test-code. + Dictionary references_old = new(); + _stopwatch.Reset(); + _stopwatch.Start(); + TransitiveClosureImplementation_old(references_old, pdfObject); + _stopwatch.Stop(); + logger.LogInformation(nameof(TransitiveClosureImplementation_old) + " runs {MS}ms.", _stopwatch.ElapsedMilliseconds); + logger.LogInformation($"TC: {references_old.Count}"); + logger.LogInformation("--------------------"); #endif - TransitiveClosureImplementation(objects, pdfObject); -#if DEBUG - // TODO Check - PdfSharpLogHost.Logger.LogInformation("Transitive closure max nesting level is {NestingLevel}.", _maxNestingLevel); + +#if TEST_CODE + _stopwatch.Reset(); + _stopwatch.Start(); +#endif + Dictionary references = new(); + TransitiveClosureImplementation(references, pdfObject); +#if TEST_CODE + _stopwatch.Stop(); + logger.LogInformation(nameof(TransitiveClosureImplementation) + " runs {MS}ms.", _stopwatch.ElapsedMilliseconds); + logger.LogInformation($"TC: {references.Count}"); + logger.LogInformation("--------------------"); + + Debug.Assert(references.Count == references_old.Count); #endif -#if false - TryAgain: - if (_overflow.Count > 0) + +#if TEST_CODE + _stopwatch.Reset(); + _stopwatch.Start(); + foreach (var val in references_old) { - var array = new PdfObject[_overflow.Count]; - _overflow.Keys.CopyTo(array, 0); - _overflow = new(); - for (int idx = 0; idx < array.Length; idx++) - { - PdfObject obj = array[idx]; - TransitiveClosureImplementation(objects, obj); - } - goto TryAgain; + var valNew = references.ContainsKey((PdfReference)val.Key); + if (valNew == false) + _ = typeof(int); + Debug.Assert(valNew); } + _stopwatch.Stop(); + logger.LogInformation("references_old check runs {MS}ms.", _stopwatch.ElapsedMilliseconds); + #endif CheckConsistence(); - ICollection collection = objects.Keys; + ICollection collection = references.Keys; int count = collection.Count; PdfReference[] irefs = new PdfReference[count]; collection.CopyTo(irefs, 0); -#if true_ - for (int i = 0; i < count; i++) - for (int j = 0; j < count; j++) - if (i != j) - { - Debug.Assert(ReferenceEquals(irefs[i].Document, _document)); - Debug.Assert(irefs[i] != irefs[j]); - Debug.Assert(!ReferenceEquals(irefs[i], irefs[j])); - Debug.Assert(!ReferenceEquals(irefs[i].Value, irefs[j].Value)); - Debug.Assert(!Equals(irefs[i].ObjectID, irefs[j].Value.ObjectID)); - Debug.Assert(irefs[i].ObjectNumber != irefs[j].Value.ObjectNumber); - Debug.Assert(ReferenceEquals(irefs[i].Document, irefs[j].Document)); - _ = typeof(int); - } -#endif return irefs; } - //// TODO: Delete next two lines? - //int _nestingLevel; - //Dictionary _overflow_ = new(); -#if DEBUG - int _nestingLevel; - int _maxNestingLevel; -#endif - - // TODO: Write new non-recursive function and counter check with this implementation. - void TransitiveClosureImplementation(Dictionary objects, PdfObject pdfObject) +#if TEST_CODE + /// + /// This is the old implementation of the transitive closure. It is a recursive implementation. + /// We keep it some time to use it for counter-checking the new non-recursive implementation. + /// + /// + /// + void TransitiveClosureImplementation_old(Dictionary references, PdfObject pdfObject) { - try + IEnumerable? enumerable = null; + PdfDictionary? dict; + PdfArray? array; + if ((dict = pdfObject as PdfDictionary) != null) + enumerable = dict.Elements.Values; + else if ((array = pdfObject as PdfArray) != null) + enumerable = array.Elements; + else + Debug.Assert(false, "Should not come here."); + + if (enumerable != null!) { + foreach (PdfItem item in enumerable) + { + if (item is PdfReference iref) + { + // Is this an indirect reference to an object that does not exist? + //if (iref.Document == null) + //{ + // Deb/ug.WriteLine("Dead object detected: " + iref.ObjectID.ToString()); + // PdfReference dead = DeadObject; + // iref.ObjectID = dead.ObjectID; + // iref.Document = _document; + // iref.SetObject(dead.Value); + // PdfDictionary dict = (PdfDictionary)dead.Value; + + // dict.Elements["/DeadObjectCount"] = + // new PdfInteger(dict.Elements.GetInteger("/DeadObjectCount") + 1); + + // iref = dead; + //} + + if (!ReferenceEquals(iref.Document, document)) + { + //Debug.WriteLine($"Bad iref: {iref.ObjectID.ToString()}"); + PdfSharpLogHost.PdfReadingLogger.LogError($"Bad iref: {iref.ObjectID.ToString()}"); + } + Debug.Assert(ReferenceEquals(iref.Document, document) || iref.Document == null, "External object detected!"); + + if (references.ContainsKey(iref) is false) + { + PdfObject value = iref.Value; + + // Ignore unreachable objects. + if (iref.Document != null) + { + // ... from trailer hack + if (value == null!) // Can it be null? + { + iref = _objectTable[iref.ObjectID]; + Debug.Assert(iref.Value != null); + value = iref.Value; + } + Debug.Assert(ReferenceEquals(iref.Document, document)); + references.Add(iref, null); + //Debug.WriteLine(String.Format("objects.Add('{0}', null);", iref.ObjectID.ToString())); + if (value is PdfArray or PdfDictionary) + TransitiveClosureImplementation_old(references, value); + } #if DEBUG - _nestingLevel++; - _maxNestingLevel = Math.Max(_maxNestingLevel, _nestingLevel); + else + { + _ = typeof(int); + } #endif -#if false - if (_nestingLevel >= 1000) - { - // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because it would not build with .NET Framework. - if (!_overflow.ContainsKey(pdfObject)) - _overflow.Add(pdfObject, null); - return; + } + } + else + { + if (item is PdfObject pdfObj and (PdfDictionary or PdfArray)) + TransitiveClosureImplementation_old(references, pdfObj); + } } + } + } #endif -#if DEBUG_ - //enterCount++; - if (enterCount == 5400) - _ = typeof(int); - //if (!Object.ReferenceEquals(pdfObject.Owner, _document)) - // _ = typeof(int); - //////Debug.Assert(Object.ReferenceEquals(pdfObject27.Document, _document)); - // if (item is PdfObject && ((PdfObject)item).ObjectID.ObjectNumber == 5) - // Deb/ug.WriteLine("items: " + ((PdfObject)item).ObjectID.ToString()); - //if (pdfObject.ObjectNumber == 5) - // _ = typeof(int); + /// + /// The new non-recursive implementation. + /// + /// + /// + void TransitiveClosureImplementation(Dictionary references, PdfObject pdfObject) + { + var logger = PdfSharpLogHost.DocumentProcessingLogger; +#if TEST_CODE + //Dictionary doubleCheckReferences = []; + //Dictionary pivots = []; + int loopCounter = 0; + int maxStackLength = 0; + int alreadyInTable = 0; + int elementsAdded = 0; #endif - IEnumerable? enumerable = null; + // Initialize the stack. + Stack stack = []; + FindReferencedItems(pdfObject); + + // Loop until no more new references are found. + while (stack.Count > 0) + { + var pivot = stack.Pop(); +#if TEST_CODE + maxStackLength = Math.Max(maxStackLength, stack.Count); + loopCounter++; + + //// ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because TryAdd does not exist in .NET Framework. + //if (!pivots.ContainsKey(pivot)) + //{ + // pivots.Add(pivot, null); + //} + //else + //{ + // _ = pivot.Equals(null); + // _ = typeof(int); + //} +#endif + FindReferencedItems(pivot); + } +#if TEST_CODE + logger.LogInformation( + "LoopCounter: {LoopCounter}, MaxStackLength: {MaxStackLength}, AlreadyInTable: {AlreadyInTable}, ElementsAdded: {ElementsAdded}", + loopCounter, maxStackLength, alreadyInTable, elementsAdded); + + //Dictionary test = []; + //foreach (var pivotsValue in pivots.Keys) + //{ + // if (pivotsValue.ObjectID.ObjectNumber == 0) + // continue; + + // test.Add(pivotsValue.ObjectID.ObjectNumber, null); + //} + //int ids = test.Count; +#endif + return; + + // Add all dictionaries and arrays referenced by the specified object + // that are not already in references to the stack. + void FindReferencedItems(PdfObject pdfObj) + { + Debug.Assert(pdfObj is PdfDictionary or PdfArray, "Call with dictionary or array only."); + + IEnumerable? items = null; PdfDictionary? dict; PdfArray? array; - if ((dict = pdfObject as PdfDictionary) != null) - enumerable = dict.Elements.Values; - else if ((array = pdfObject as PdfArray) != null) - enumerable = array.Elements; + if ((dict = pdfObj as PdfDictionary) is not null) + items = dict.Elements.Values; + else if ((array = pdfObj as PdfArray) is not null) + items = array.Elements; else Debug.Assert(false, "Should not come here."); - if (enumerable != null!) + foreach (PdfItem item in items) { - foreach (PdfItem item in enumerable) + if (item is PdfReference iref) { - if (item is PdfReference iref) + // Case: The item is an indirect object. + + // Check if the reference belongs to the current document. + if (!ReferenceEquals(iref.Document, document)) { - // Is this an indirect reference to an object that does not exist? - //if (iref.Document == null) - //{ - // Deb/ug.WriteLine("Dead object detected: " + iref.ObjectID.ToString()); - // PdfReference dead = DeadObject; - // iref.ObjectID = dead.ObjectID; - // iref.Document = _document; - // iref.SetObject(dead.Value); - // PdfDictionary dict = (PdfDictionary)dead.Value; - - // dict.Elements["/DeadObjectCount"] = - // new PdfInteger(dict.Elements.GetInteger("/DeadObjectCount") + 1); - - // iref = dead; - //} - - if (!ReferenceEquals(iref.Document, document)) + logger.LogError($"Bad iref: {iref.ObjectID.ToString()}"); + } + + Debug.Assert(ReferenceEquals(iref.Document, document) || iref.Document == null, + "External object detected!"); + + // Is the reference not yet in the collection of referenced objects? + if (references.ContainsKey(iref)) + { +#if TEST_CODE + alreadyInTable++; +#endif + continue; + } + + var newObject = iref.Value; + + // Ignore unreachable objects. + if (iref.Document != null) + { + // ... from trailer hack + if (newObject == null!) // Can it be null? { - //Debug.WriteLine($"Bad iref: {iref.ObjectID.ToString()}"); - PdfSharpLogHost.PdfReadingLogger.LogError($"Bad iref: {iref.ObjectID.ToString()}"); + logger.LogInformation("Value of a PdfReference is null."); + iref = _objectTable[iref.ObjectID]; + Debug.Assert(iref.Value != null); + newObject = iref.Value; } - Debug.Assert(ReferenceEquals(iref.Document, document) || iref.Document == null, "External object detected!"); -#if DEBUG_ - if (iref.ObjectID.ObjectNumber == 23) + + Debug.Assert(ReferenceEquals(iref.Document, document)); + if (newObject.ObjectID.ObjectNumber != 0) + references.Add(iref, null); +#if TEST_CODE__ + // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because auf .NET Framework / Standard + if (!doubleCheckReferences.ContainsKey(value)) + doubleCheckReferences.Add(value, null); + else _ = typeof(int); #endif - if (objects.ContainsKey(iref) is false) - { - PdfObject value = iref.Value; - // Ignore unreachable objects. - if (iref.Document != null) - { - // ... from trailer hack - if (value == null!) // Can it be null? - { - iref = _objectTable[iref.ObjectID]; - Debug.Assert(iref.Value != null); - value = iref.Value; - } - Debug.Assert(ReferenceEquals(iref.Document, document)); - objects.Add(iref, null); - //Debug.WriteLine(String.Format("objects.Add('{0}', null);", iref.ObjectID.ToString())); - if (value is PdfArray or PdfDictionary) - TransitiveClosureImplementation(objects, value); - } -#if DEBUG - else - { - _ = typeof(int); - // objects2.Add(this[iref.ObjectID], null); - } + if (newObject is PdfDictionary or PdfArray) + { + stack.Push(newObject); +#if TEST_CODE + elementsAdded++; #endif - } } else { - //var pdfObject28 = item as PdfObject; - ////if (pdfObject28 != null) - //// Debug.Assert(Object.ReferenceEquals(pdfObject28.Document, _document)); - //if (pdfObject28 != null && (pdfObject28 is PdfDictionary || pdfObject28 is PdfArray)) - //if (pdfObject28 != null) - // Debug.Assert(Object.ReferenceEquals(pdfObject28.Document, _document)); - if (item is PdfObject pdfObj and (PdfDictionary or PdfArray)) - TransitiveClosureImplementation(objects, pdfObj); + // Can we come here? + logger.LogWarning("Document has no owner."); } } - } - } - finally - { -#if DEBUG - _nestingLevel--; + else + { + // Case: The item is a direct object. + + if (item is PdfObject pdfDictionaryOrArray and (PdfDictionary or PdfArray)) + { +#if TEST_CODE_ + // Not useful, is too slow. + if (stack.Contains(pdfDictionaryOrArray)) + _ = typeof(int); +#endif + stack.Push(pdfDictionaryOrArray); +#if TEST_CODE + elementsAdded++; #endif + } + } + } } } - void TransitiveClosureImplementationNew(Dictionary objects, PdfObject pdfObject) - { - // ... - } - +#if true_ // Not used. /// /// Gets the cross-reference to an object used for undefined indirect references. /// @@ -565,39 +718,10 @@ public PdfReference DeadObject } } PdfDictionary? _deadObject; - +#endif /// /// Represents the relation between PdfObjectID and PdfReference for a PdfDocument. /// readonly Dictionary _objectTable = []; } - - ///// - ///// Represents the cross-reference table of a PDF document. - ///// It contains all indirect objects of a document. - ///// - //internal sealed class PdfCrossReferenceStreamTable // Must not be derived from PdfObject. - //{ - // public PdfCrossReferenceStreamTable(PdfDocument document) - // { - // _document = document; - // } - // readonly PdfDocument _document; - - // public class Item - // { - // public PdfReference Reference; - - // public readonly List Entries = new List(); - // } - //} - - //struct CrossReferenceStreamEntry - //{ - // public int Type; - - // public int Field2; - - // public int Field3; - //} } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFileSpecification.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFileSpecification.cs index a197d86b..09da5b41 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFileSpecification.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFileSpecification.cs @@ -29,7 +29,7 @@ void Initialize() var embeddedFileDictionary = new PdfDictionary(Owner); Owner.Internals.AddObject(_embeddedFileStream); - embeddedFileDictionary.Elements.SetReference(Keys.F, _embeddedFileStream.Reference); + embeddedFileDictionary.Elements.SetReference(Keys.F, _embeddedFileStream.Reference ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); embeddedFileDictionary.Elements.SetReference(Keys.UF, _embeddedFileStream.Reference); Elements.SetObject(Keys.EF, embeddedFileDictionary); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFont.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFont.cs index c3cceb6a..fef7ffe7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFont.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFont.cs @@ -18,6 +18,15 @@ protected PdfFont(PdfDocument document) : base(document) { } + internal PdfFont(PdfDictionary dict, PdfFontDescriptor fontDescriptor, PdfFontEncoding encoding) + :base(dict) + { + FontDescriptor = fontDescriptor; + FontEncoding = encoding; + _cmapInfo = new CMapInfo(fontDescriptor.Descriptor); + _toUnicodeMap = new PdfToUnicodeMap(Owner); + } + internal PdfFontDescriptor FontDescriptor { get @@ -36,7 +45,7 @@ internal PdfFontDescriptor FontDescriptor ///
public bool IsSymbolFont => FontDescriptor.IsSymbolFont; - internal void AddChars(CodePointGlyphIndexPair[] codePoints) + internal virtual void AddChars(CodePointGlyphIndexPair[] codePoints) { _cmapInfo.AddChars(codePoints); _fontDescriptor.CMapInfo.AddChars(codePoints); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFontTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFontTable.cs index 791d35dc..bd9f93ef 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFontTable.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFontTable.cs @@ -2,6 +2,11 @@ // See the LICENSE file in the solution root for more information. using PdfSharp.Drawing; +using PdfSharp.Fonts; +using PdfSharp.Fonts.OpenType; +using PdfSharp.Fonts.StandardFonts; +using PdfSharp.Internal; +using System.Diagnostics.CodeAnalysis; namespace PdfSharp.Pdf.Advanced { @@ -18,6 +23,11 @@ enum FontType /// TrueType with Identity-H or Identity-V encoding (Unicode). ///
Type0Unicode = 2, // #RENAME better name + + /// + /// One of the 14 standard-fonts with WinAnsi encoding + /// + Type1StandardFont = 3 } /// @@ -30,7 +40,8 @@ sealed class PdfFontTable : PdfResourceTable /// public PdfFontTable(PdfDocument document) : base(document) - { } + { + } /// /// Gets a PdfFont from an XFont. If no PdfFont already exists, a new one is created. @@ -45,14 +56,39 @@ public PdfFont GetOrCreateFont(XGlyphTypeface glyphTypeface, FontType fontType) pdfFont = new PdfType0Font(Owner, glyphTypeface, false); else if (fontType == FontType.TrueTypeWinAnsi) pdfFont = new PdfTrueTypeFont(Owner, glyphTypeface); + else if (fontType == FontType.Type1StandardFont) + { + Debug.Assert(!string.IsNullOrEmpty(glyphTypeface.XFamilyName), "XFamilyName should be set here"); + pdfFont = new PdfType1Font(Owner) { BaseFont = glyphTypeface.XFamilyName }; + } else - throw new InvalidOperationException($"Invalid font type '{fontType.ToString()}'."); + throw new InvalidOperationException($"Invalid font type '{fontType}'."); Debug.Assert(pdfFont.Owner == Owner); _fonts[selector] = pdfFont; } return pdfFont; } + /// + /// Caches a font from an existing document.

+ /// Used to prevent adding new fonts when filling existing AcroForms. + ///
+ /// + /// + /// + internal void CacheExistingFont(PdfDictionary fontDict, XGlyphTypeface glyphTypeface, FontType fontType) + { + var selector = ComputePdfFontKey(glyphTypeface, fontType); + if (!_fonts.ContainsKey(selector)) + { + var otDescriptor = (OpenTypeDescriptor)FontDescriptorCache.GetOrCreateDescriptorFor(glyphTypeface); + var descriptor = Owner.PdfFontDescriptorCache.GetOrCreatePdfDescriptorFor(otDescriptor, glyphTypeface.GetBaseName()); + + var font = new PdfFont(fontDict, descriptor, PdfFontEncoding.Automatic); + _fonts[selector] = font; + } + } + #if true /// /// Gets a PdfFont from a font program. If no PdfFont already exists, a new one is created. @@ -99,7 +135,7 @@ public PdfFont GetFont(string idName, byte[] fontData) internal static string ComputePdfFontKey(XGlyphTypeface glyphTypeface, FontType fontType) { // fontType must be defined to compute the key. - Debug.Assert(fontType is FontType.TrueTypeWinAnsi or FontType.Type0Unicode); + Debug.Assert(fontType is FontType.TrueTypeWinAnsi or FontType.Type0Unicode or FontType.Type1StandardFont); // TODO_OLD Check if StringBuilder is more efficient here. //var glyphTypeface = font.GlyphTypeface; @@ -109,7 +145,7 @@ internal static string ComputePdfFontKey(XGlyphTypeface glyphTypeface, FontType var faceName = glyphTypeface.FontFace.FullFaceName.ToLowerInvariant(); var bold = glyphTypeface.IsBold; var italic = glyphTypeface.IsItalic; - var type = fontType == FontType.TrueTypeWinAnsi ? "+A" : "+U"; + var type = fontType is FontType.TrueTypeWinAnsi or FontType.Type1StandardFont ? "+A" : "+U"; var key = bold switch { false when !italic => faceName + type, diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFormXObject.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFormXObject.cs index 98f89b6c..de2bb33f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFormXObject.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfFormXObject.cs @@ -35,6 +35,14 @@ internal PdfFormXObject(PdfDocument thisDocument, XForm form) //{ } } + internal PdfFormXObject(PdfDictionary dict) + : base(dict.Owner) + { + if (dict.Elements.GetName(Keys.Subtype) != "/Form") + throw new ArgumentException("Dictionary does not specify the required Subtype", nameof(dict)); + } + + internal double DpiX { get; set; } = 72; internal double DpiY { get; set; } = 72; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfInternals.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfInternals.cs index 2a447206..376520ee 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfInternals.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfInternals.cs @@ -162,7 +162,8 @@ public PdfObject[] GetAllObjects() /// Creates the indirect object of the specified type, adds it to the document, /// and returns the object. /// - public T CreateIndirectObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>() where T : PdfObject + public T CreateIndirectObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>() + where T : PdfObject { #if true T obj = Activator.CreateInstance(); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfNameDictionary.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfNameDictionary.cs index 515f085a..760122f3 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfNameDictionary.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfNameDictionary.cs @@ -36,7 +36,7 @@ internal void AddNamedDestination(string destinationName, int destinationPage, P { _dests = new PdfNameTreeNode(); Owner.Internals.AddObject(_dests); - Elements.SetReference(Keys.Dests, _dests.Reference); + Elements.SetReference(Keys.Dests, _dests.Reference ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); } // destIndex > Owner.PageCount can happen when rendering pages using PDFsharp directly. @@ -68,7 +68,7 @@ internal void AddEmbeddedFile(string name, Stream stream) { _embeddedFiles = new PdfNameTreeNode(); Owner.Internals.AddObject(_embeddedFiles); - Elements.SetReference(Keys.EmbeddedFiles, _embeddedFiles.Reference); + Elements.SetReference(Keys.EmbeddedFiles, _embeddedFiles.Reference ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); } var embeddedFileStream = new PdfEmbeddedFileStream(Owner, stream); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfObjectStream.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfObjectStream.cs index 19fc8103..76775b86 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfObjectStream.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfObjectStream.cs @@ -37,7 +37,7 @@ internal PdfObjectStream(PdfDictionary dict, Parser documentParser) int first = Elements.GetInteger(Keys.First); Stream.TryUncompress(); - var parser = new Parser(_document, new MemoryStream(Stream.Value), documentParser); + var parser = new Parser(_document, new MemoryStream(Stream.UnfilteredValue), documentParser); _header = parser.ReadObjectStreamHeader(n, first); #if DEBUG_ && CORE @@ -65,11 +65,9 @@ internal ICollection> ReadReferencesAndOffse var objectID = new PdfObjectID(objectNumber); - // HACK_OLD: -1 indicates compressed object. - var iref = new PdfReference(objectID, -1); - ////iref.ObjectID = objectID; - ////iref.Value = xrefStream; - + // -1 indicates compressed object. + var iref = PdfReference.CreateForObjectID(objectID, -1); + _objectOffsets.Add(objectID, offset); if (!xrefTable.Contains(iref.ObjectID)) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs index 4422f748..7240a8b5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs @@ -16,12 +16,12 @@ namespace PdfSharp.Pdf.Advanced [DebuggerDisplay("iref({ObjectNumber}, {GenerationNumber})")] public sealed class PdfReference : PdfItem { - // About PdfReference - // + // About PdfReference + // // * A PdfReference holds either the ObjectID or the PdfObject or both. - // + // // * Each PdfObject has a PdfReference if and only if it is an indirect object. Direct objects have - // no PdfReference, because they are embedded in a parent objects. + // no PdfReference, because they are embedded in a parent object. // // * PdfReference objects are used to reference PdfObject instances. A value in a PDF dictionary // or array that is a PdfReference represents an indirect reference. A value in a PDF dictionary @@ -37,13 +37,25 @@ public sealed class PdfReference : PdfItem /// /// Initializes a new PdfReference instance for the specified indirect object. + /// An indirect PDF object has one and only one reference. + /// You cannot create an instance of PdfReference. /// - public PdfReference(PdfObject pdfObject) + PdfReference(PdfObject pdfObject, PdfObjectID objectID, SizeType position) { if (pdfObject.Reference != null) throw new InvalidOperationException("Must not create iref for an object that already has one."); _value = pdfObject; pdfObject.Reference = this; + _objectID = objectID; + + if (position != 0) + _position = position; + //else + //{ + // // Can be 0 or -1. + // //_ = typeof(int); + // //PdfSharpLogHost.DocumentProcessingLogger.LogError("Position is 0."); + //} #if UNIQUE_IREF && DEBUG _uid = ++s_counter; #endif @@ -52,7 +64,7 @@ public PdfReference(PdfObject pdfObject) /// /// Initializes a new PdfReference instance from the specified object identifier and file position. /// - public PdfReference(PdfObjectID objectID, SizeType position) + PdfReference(PdfObjectID objectID, SizeType position) { _objectID = objectID; _position = position; @@ -61,6 +73,25 @@ public PdfReference(PdfObjectID objectID, SizeType position) #endif } + /// + /// Creates a PdfReference from a PdfObject. + /// + /// + /// + /// + /// + internal static PdfReference CreateFromObject(PdfObject pdfObject, PdfObjectID objectID, SizeType position) + => new(pdfObject, objectID, position); + + /// + /// Creates a PdfReference from a PdfObjectID. + /// + /// + /// + /// + internal static PdfReference CreateForObjectID(PdfObjectID objectID, SizeType position) + => new(objectID, position); + /// /// Writes the object in PDF iref table format. /// @@ -121,7 +152,7 @@ public PdfObjectID ObjectID public SizeType Position { get => _position; - set + internal set { Debug.Assert(value >= 0); _position = value; @@ -135,7 +166,7 @@ public SizeType Position public PdfObject Value { get => _value; - set + internal set { Debug.Assert(value != null, "The value of a PdfReference must never be null."); Debug.Assert(value.Reference == null || ReferenceEquals(value.Reference, this), "The reference of the value must be null or this."); @@ -144,7 +175,7 @@ public PdfObject Value value.Reference = this; } } - PdfObject _value = default!; + PdfObject _value = null!; /// /// Resets value to null. Used when reading object stream. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs index bab525f9..1c57bada 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs @@ -41,7 +41,7 @@ public PdfTrailer(PdfCrossReferenceStream trailer) if (iref != null) Elements.SetReference(Keys.Info, iref); - Elements.SetReference(Keys.Root, trailer.Elements.GetReference(Keys.Root)); + Elements.SetReference(Keys.Root, trailer.Elements.GetReference(Keys.Root) ?? throw TH.InvalidOperationException_ReferenceMustNotBeNull()); Elements.SetInteger(Keys.Size, trailer.Elements.GetInteger(Keys.Size)); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs index 1332cf79..30a39007 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs @@ -27,6 +27,8 @@ public PdfType0Font(PdfDocument document, XGlyphTypeface glyphTypeface, bool ver var otDescriptor = (OpenTypeDescriptor)FontDescriptorCache.GetOrCreateDescriptorFor(glyphTypeface); FontDescriptor = document.PdfFontDescriptorCache.GetOrCreatePdfDescriptorFor(otDescriptor, glyphTypeface.GetBaseName()); + // TODO: even if we specify "Full" FontEmbedding in the XFont, the generated ToUnicodeMap + // and /W Array will only contain the "used" characters (specified in the cmapInfo) //FontOptions = font.PdfOptions; //Debug.Assert(FontOptions != null); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType1Font.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType1Font.cs index 8012c067..8619dd4d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType1Font.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType1Font.cs @@ -1,33 +1,38 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -#if true_ // Not yet implemented- +using PdfSharp.Fonts; +using PdfSharp.Internal; using System; using System.Collections; using System.Text; -using PdfSharp.Internal; namespace PdfSharp.Pdf.Advanced { /// - /// Not implemented - just for illustration of the class hierarchy. + /// Type1 font used for the 14 standard fonts defined in the PDF-specification. /// internal sealed class PdfType1Font : PdfFont { public PdfType1Font(PdfDocument document) : base(document) { - Elements["\\Type"] = new PdfName("Font"); - Elements["\\Subtype"] = new PdfName("Type1"); + Elements.SetName(Keys.Type, "Font"); + Elements.SetName(Keys.Subtype, "Type1"); + Elements.SetName(Keys.Encoding, "WinAnsiEncoding"); } - //public string BaseFont - //{ - // get {return baseFont;} - // set {baseFont = value;} - //} - //string baseFont; + public string BaseFont + { + get { return Elements.GetName(Keys.BaseFont); } + set { Elements.SetName(Keys.BaseFont, value); } + } + + internal override void AddChars(CodePointGlyphIndexPair[] codePoints) + { + // nothing to do here + } // internal override void AssignObjectID(ref int objectID) // { @@ -153,4 +158,3 @@ internal override DictionaryMeta Meta } } } -#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs index db6efe7c..4350028b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs @@ -3,6 +3,7 @@ using PdfSharp.Drawing; using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations.enums; namespace PdfSharp.Pdf.Annotations { @@ -52,6 +53,11 @@ public void Delete() Parent.Remove(this); } + /// + /// Gets or sets the border-properties of this Annotation + /// + public PdfAnnotationBorder Border { get; set; } = new PdfAnnotationBorder(); + /// /// Gets or sets the annotation flags of this instance. /// @@ -76,6 +82,40 @@ public PdfAnnotations Parent PdfAnnotations? _parent; + /// + /// Gets or sets the page for this Annotation + /// + public PdfPage? Page + { + get + { + var pageRef = Elements.GetReference(Keys.Page); + if (pageRef == null) + { + var page = TryFindPage(); + if (page != null) + { + Elements.SetReference(Keys.Page, page); + pageRef = page.Reference; + } + } + return pageRef != null && pageRef.Value is PdfDictionary + ? Owner.Pages.FindPage(pageRef.ObjectID) + : null; + } + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + var curPage = Page; + curPage?.Annotations.Remove(this); + Elements.SetReference(Keys.Page, value); + if (Reference != null && !value.Annotations.Elements.Contains(Reference)) + value.Annotations.Add(this); + } + } + /// /// Gets or sets the annotation rectangle, defining the location of the annotation /// on the page in default user space units. @@ -191,6 +231,136 @@ public double Opacity } } + /// + /// Convenience-method that serves 2 purposes:

+ /// 1: It allows setting the and the with one call

+ /// 2: It eases placing the annotation on the page by using the top-left of the page as the origin

+ /// (as opposed to the bottom-left, which would be the case when using directly) + ///
+ /// The the annotation should be placed on + /// The rectangle of the Annotation. The position should be relative to the top-left of the page + public void AddToPage(PdfPage page, PdfRectangle rectangle) + { + Page = page ?? throw new ArgumentNullException(nameof(page)); + if (rectangle == null) + throw new ArgumentNullException(nameof(rectangle)); + + var location = new XPoint(rectangle.X1, page.Height.Point - rectangle.Y2); + Rectangle = new PdfRectangle(location, rectangle.Size); + } + + private PdfPage? TryFindPage() + { + if (_document != null) + { + for (var i = 0; i < _document.PageCount; i++) + { + var page = _document.Pages[i]; + if (page.Annotations != null && page.Annotations.Count > 0) + { + for (var a = 0; a < page.Annotations.Count; a++) + { + var annot = page.Annotations[a]; + if (annot.Reference == Reference) + return page; + } + } + } + } + return null; + } + + /// + /// Determines the border-characteristics of this annotation

+ /// PdfReference 1.7, Chapter 12.5.2 and 12.5.4 + ///
+ protected void DetermineBorder() + { + var bs = Elements.GetDictionary(Keys.BS); + if (bs != null) + { + if (bs.Elements.ContainsKey("/W")) + Border.Width = Math.Max(0.0, bs.Elements.GetReal("/W")); + if (bs.Elements.ContainsKey("/S")) + { + var styleName = bs.Elements.GetName("/S"); + Border.BorderStyle = styleName switch + { + "/S" => PdfAnnotationBorderStyle.Solid, + "/D" => PdfAnnotationBorderStyle.Dashed, + "/B" => PdfAnnotationBorderStyle.Beveled, + "/I" => PdfAnnotationBorderStyle.Inset, + "/U" => PdfAnnotationBorderStyle.Underline, + _ => PdfAnnotationBorderStyle.None + }; + } + if (bs.Elements.ContainsKey("/D")) + { + var patternArray = bs.Elements.GetArray("/D"); + if (patternArray?.Elements.Count > 0) + { + var numbers = new List(patternArray.Elements.Count); + foreach (var item in patternArray) + { + if (item is PdfInteger intItem) + numbers.Add(intItem.Value); + } + Border.DashPattern = numbers.ToArray(); + } + } + return; // BS takes precedence over Border + } + if (Elements.ContainsKey(Keys.Border)) + { + var borderArray = Elements.GetArray(Keys.Border); + var hRadius = 0.0; + var vRadius = 0.0; + var width = 0.0; + int[]? dashPattern = null; + for (var i = 0; i < borderArray?.Elements.Count; i++) + { + var val = borderArray.Elements[i]; + if (i == 0) + { + if (val is PdfInteger hItem) + hRadius += hItem.Value; + else if (val is PdfReal hRealItem) + hRadius += hRealItem.Value; + } + if (i == 1) + { + if (val is PdfInteger vItem) + vRadius = vItem.Value; + else if (val is PdfReal vRealItem) + vRadius += vRealItem.Value; + } + if (i == 2) + { + if (val is PdfInteger widthItem) + width = widthItem.Value; + else if (val is PdfReal widthRealItem) + width = widthRealItem.Value; + } + if (i == 3 && val is PdfArray arrayItem) + { + var dash = new List(); + foreach (var item in arrayItem) + { + if (item is PdfInteger intItem) + dash.Add(intItem.Value); + } + if (dash.Count > 0) + dashPattern = dash.ToArray(); + } + } + Border.Width = width; + Border.HorizontalRadius = hRadius; + Border.VerticalRadius = vRadius; + if (dashPattern != null) + Border.DashPattern = dashPattern; + } + } + /// /// Predefined keys of this dictionary. /// @@ -228,7 +398,14 @@ public class Keys : KeysBase [KeyInfo(KeyType.TextString | KeyType.Optional)] public const string Contents = "/Contents"; - // P + /// + /// (Optional except as noted below; PDF 1.3; not used in FDF files) + /// An indirect reference to the page object with which this annotation is associated. + /// This entry shall be present in screen annotations associated with rendition actions + /// (PDF 1.5; see 12.5.6.18, 'Screen Annotations' and 12.6.4.13, 'Rendition Actions') + /// + [KeyInfo(KeyType = KeyType.Optional)] + public const string Page = "/P"; /// /// (Optional; PDF 1.4) The annotation name, a text string uniquely identifying it diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotationBorder.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotationBorder.cs new file mode 100644 index 00000000..541bc7a7 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotationBorder.cs @@ -0,0 +1,35 @@ +using PdfSharp.Pdf.Annotations.enums; + +namespace PdfSharp.Pdf.Annotations +{ + /// + /// Specifies the characteristics of an 's border + /// + public class PdfAnnotationBorder + { + /// + /// The width of the border in points + /// + public double Width { get; set; } = 1; + + /// + /// Horizontal radius of the border + /// + public double HorizontalRadius { get; set; } = 0; + + /// + /// Vertical radius of the border + /// + public double VerticalRadius { get; set; } = 0; + + /// + /// The border-style + /// + public PdfAnnotationBorderStyle BorderStyle { get; set; } = PdfAnnotationBorderStyle.Solid; + + /// + /// A dash array defining a pattern of dashes and gaps that shall be used in drawing a dashed border + /// + public int[]? DashPattern { get; set; } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotations.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotations.cs index 45f1d8dc..e72ba299 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotations.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotations.cs @@ -25,9 +25,10 @@ internal PdfAnnotations(PdfArray array) /// The annotation. public void Add(PdfAnnotation annotation) { - annotation.Document = Owner; - Owner.IrefTable.Add(annotation); + if (annotation.ObjectID.IsEmpty || !Owner.IrefTable.Contains(annotation.ObjectID)) + Owner.IrefTable.Add(annotation); Elements.Add(annotation.ReferenceNotNull); + annotation.Parent = this; } /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfWidgetAnnotation.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfWidgetAnnotation.cs index 24d26f45..1f609e7b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfWidgetAnnotation.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfWidgetAnnotation.cs @@ -1,29 +1,386 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations.enums; + namespace PdfSharp.Pdf.Annotations { /// /// Represents a text annotation. /// - sealed class PdfWidgetAnnotation : PdfAnnotation + public sealed class PdfWidgetAnnotation : PdfAnnotation { + /// + /// Creates a new that is not tied to a + /// public PdfWidgetAnnotation() { Initialize(); } + /// + /// Creates a new that is tied to the specified . + /// + /// public PdfWidgetAnnotation(PdfDocument document) : base(document) { Initialize(); } + /// + /// Initializes a new instance of the with the specified dictionary. + /// + /// + public PdfWidgetAnnotation(PdfDictionary dict) + : base(dict) + { + if (dict.Elements.GetString(PdfAnnotation.Keys.Subtype) != "/Widget") + throw new PdfSharpException("PdfWidgetAnnotation not initialized with the /Widget Subtype"); + DetermineAppearance(); + } + void Initialize() { Elements.SetName(Keys.Subtype, "/Widget"); } + /// + /// Gets or sets the background color of the field. + /// + public XColor BackColor + { + get { return backColor; } + set + { + backColor = value; + if (BackColor.IsEmpty) + return; + + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var colArray = new PdfArray(); + colArray.Elements.Add(new PdfReal(value.R / 255.0)); + colArray.Elements.Add(new PdfReal(value.G / 255.0)); + colArray.Elements.Add(new PdfReal(value.B / 255.0)); + var mk = Elements.GetDictionary(Keys.MK)!; + mk.Elements["/BG"] = colArray; + } + } + XColor backColor = XColor.Empty; + + /// + /// Gets or sets the border color of the field. + /// + public XColor BorderColor + { + get { return borderColor; } + set + { + borderColor = value; + if (borderColor.IsEmpty) + return; + + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var colArray = new PdfArray(); + colArray.Elements.Add(new PdfReal(value.R / 255.0)); + colArray.Elements.Add(new PdfReal(value.G / 255.0)); + colArray.Elements.Add(new PdfReal(value.B / 255.0)); + var mk = Elements.GetDictionary(Keys.MK)!; + mk.Elements["/BC"] = colArray; + } + } + XColor borderColor = XColor.Empty; + + /// + /// Gets or sets the Rotation of this Widget in counter-clockwise direction. + /// + public int Rotation + { + get + { + var mk = Elements.GetDictionary(Keys.MK); + if (mk != null && mk.Elements.ContainsKey("/R")) + return mk.Elements.GetInteger("/R"); + return 0; + } + set + { + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var mk = Elements.GetDictionary(Keys.MK)!; + mk.Elements.SetInteger("/R", value); + } + } + + /// + /// Gets or sets the placement of the widget's caption relative to it's icon

+ /// Only applies to s + ///
+ public PdfButtonCaptionPosition CaptionPlacement + { + get + { + var mk = Elements.GetDictionary(Keys.MK); + if (mk != null && mk.Elements.ContainsKey("/TP")) + return (PdfButtonCaptionPosition)mk.Elements.GetInteger("/TP"); + return PdfButtonCaptionPosition.CaptionOnly; + } + set + { + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var mk = Elements.GetDictionary(Keys.MK)!; + mk.Elements.SetInteger("/TP", (int)value); + } + } + + /// + /// Gets or sets the annotation's highlighting mode, the visual effect to be used when + /// the mouse button is pressed or held down inside its active area + /// + public PdfAnnotationHighlightingMode Highlighting + { + get + { + var mode = Elements.GetName(Keys.H); + return mode switch + { + "/N" => PdfAnnotationHighlightingMode.None, + "/I" => PdfAnnotationHighlightingMode.Invert, + "/O" => PdfAnnotationHighlightingMode.Outline, + "/P" => PdfAnnotationHighlightingMode.Push, + "/T" => PdfAnnotationHighlightingMode.Toggle, + _ => PdfAnnotationHighlightingMode.Invert + }; + } + set + { + switch (value) + { + case PdfAnnotationHighlightingMode.None: + Elements.SetName(Keys.H, "/N"); + break; + case PdfAnnotationHighlightingMode.Invert: + Elements.SetName(Keys.H, "/I"); + break; + case PdfAnnotationHighlightingMode.Outline: + Elements.SetName(Keys.H, "/O"); + break; + case PdfAnnotationHighlightingMode.Push: + Elements.SetName(Keys.H, "/P"); + break; + case PdfAnnotationHighlightingMode.Toggle: + Elements.SetName(Keys.H, "/T"); + break; + } + } + } + + /// + /// Gets or sets the normal Caption of this Annotation.

+ /// Only applies to button-fields, e.g. , and + ///
+ /// see PdfReference 1.7, Chapter 12.5.6.19: Widget Annotations + public string? NormalCaption + { + get { return GetMkString("/CA"); } + set { SetMkString("/CA", value); } + } + + /// + /// Gets or sets the rollover Caption of this Annotation which shall be displayed + /// when the user rolls the cursor into its active area without pressing the mouse button.

+ /// Only applies to s + ///
+ public string? RolloverCaption + { + get { return GetMkString("/RC"); } + set { SetMkString("/RC", value); } + } + + /// + /// Gets or sets the down Caption of this Annotation which shall be displayed + /// when the mouse button is pressed within its active area.

+ /// Only applies to s + ///
+ public string? DownCaption + { + get { return GetMkString("/AC"); } + set { SetMkString("/AC", value); } + } + + /// + /// The widget annotation's normal icon, which shall be displayed when it is not interacting with the user.

+ /// Only applies to s + ///
+ public PdfFormXObject? NormalIcon + { + get { return GetMkForm("/I"); } + set { SetMkForm("/I", value); } + } + + /// + /// The widget annotation's rollover icon, which shall be displayed + /// when the user rolls the cursor into its active area without pressing the mouse button.

+ /// Only applies to s + ///
+ public PdfFormXObject? RolloverIcon + { + get { return GetMkForm("/RI"); } + set { SetMkForm("/RI", value); } + } + + /// + /// The widget annotation's alternate (down) icon, which shall be displayed + /// when the mouse button is pressed within its active area.

+ /// Only applies to s + ///
+ public PdfFormXObject? DownIcon + { + get { return GetMkForm("/IX"); } + set { SetMkForm("/IX", value); } + } + + + /// + /// Get the parent-field of this Widget, if it is the child of a . + /// + public PdfObject ParentField + { + get + { + return Elements.GetObject(Keys.Parent) ?? new PdfNullObject(); + } + internal set { Elements.SetReference(Keys.Parent, value); } + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + if (Border.Width > 0 && Border.BorderStyle != PdfAnnotationBorderStyle.None && !BorderColor.IsEmpty) + { + // the /BS dictionary does not support border radii, use /Border for these cases + if (Border.HorizontalRadius > 0 || Border.VerticalRadius > 0) + { + var borderArray = new PdfArray(Owner); + borderArray.Elements.Add(new PdfReal(Border.HorizontalRadius)); + borderArray.Elements.Add(new PdfReal(Border.VerticalRadius)); + borderArray.Elements.Add(new PdfReal(Border.Width)); + if (Border.DashPattern?.Length > 0) + { + var dashArray = new PdfArray(Owner); + foreach (var a in Border.DashPattern) + dashArray.Elements.Add(new PdfInteger(a)); + borderArray.Elements.Add(dashArray); + } + Elements[Keys.Border] = borderArray; + Elements.Remove(Keys.BS); + } + else + { + var bs = new PdfDictionary(Owner); + bs.Elements.Add("/W", new PdfReal(Border.Width)); + bs.Elements.Add("/S", new PdfName(Border.BorderStyle switch + { + PdfAnnotationBorderStyle.Solid => "/S", + PdfAnnotationBorderStyle.Dashed => "/D", + PdfAnnotationBorderStyle.Beveled => "/B", + PdfAnnotationBorderStyle.Inset => "/I", + _ => "/U" + })); + if (Border.DashPattern?.Length > 0) + { + var dashArray = new PdfArray(Owner); + foreach (var a in Border.DashPattern) + dashArray.Elements.Add(new PdfInteger(a)); + bs.Elements.Add("/D", dashArray); + } + Elements[Keys.BS] = bs; + Elements.Remove(Keys.Border); + } + } + } + + private string? GetMkString(string key) + { + var mk = Elements.GetDictionary(Keys.MK); + if (mk != null && mk.Elements.ContainsKey(key)) + return mk.Elements.GetString(key); + return null; + } + + private void SetMkString(string key, string? value) + { + if (!Elements.ContainsKey(Keys.MK) && value == null) + return; + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var mk = Elements.GetDictionary(Keys.MK)!; + if (value != null) + mk.Elements.SetString(key, value); + else + mk.Elements.Remove(key); + } + + private PdfFormXObject? GetMkForm(string key) + { + var mk = Elements.GetDictionary(Keys.MK); + if (mk != null) + { + var dict = mk.Elements.GetDictionary(key); + if (dict != null) + return new PdfFormXObject(dict); + } + return null; + } + + private void SetMkForm(string key, PdfFormXObject? value) + { + if (!Elements.ContainsKey(Keys.MK) && value == null) + return; + if (!Elements.ContainsKey(Keys.MK)) + Elements.Add(Keys.MK, new PdfDictionary()); + var mk = Elements.GetDictionary(Keys.MK)!; + if (value != null) + mk.Elements.SetReference(key, value); + else + mk.Elements.Remove(key); + } + + private void DetermineAppearance() + { + DetermineBorder(); + + var mk = Elements.GetDictionary(Keys.MK); // 12.5.6.19 + if (mk != null) + { + var bc = mk.Elements.GetArray("/BC"); + if (bc == null || bc.Elements.Count == 0) + borderColor = XColor.Empty; + else if (bc.Elements.Count == 1) + borderColor = XColor.FromGrayScale(bc.Elements.GetReal(0)); + else if (bc.Elements.Count == 3) + borderColor = XColor.FromArgb((int)Math.Round(bc.Elements.GetReal(0) * 255.0), (int)Math.Round(bc.Elements.GetReal(1) * 255.0), (int)Math.Round(bc.Elements.GetReal(2) * 255.0)); + else if (bc.Elements.Count == 4) + borderColor = XColor.FromCmyk(bc.Elements.GetReal(0), bc.Elements.GetReal(1), bc.Elements.GetReal(2), bc.Elements.GetReal(3)); + + var bg = mk.Elements.GetArray("/BG"); + if (bg == null || bg.Elements.Count == 0) + backColor = XColor.Empty; + else if (bg.Elements.Count == 1) + backColor = XColor.FromGrayScale(bg.Elements.GetReal(0)); + else if (bg.Elements.Count == 3) + backColor = XColor.FromArgb((int)Math.Round(bg.Elements.GetReal(0) * 255.0), (int)Math.Round(bg.Elements.GetReal(1) * 255.0), (int)Math.Round(bg.Elements.GetReal(2) * 255.0)); + else if (bg.Elements.Count == 4) + backColor = XColor.FromCmyk(bg.Elements.GetReal(0), bg.Elements.GetReal(1), bg.Elements.GetReal(2), bg.Elements.GetReal(3)); + } + } + /// /// Predefined keys of this dictionary. /// @@ -31,15 +388,15 @@ void Initialize() { /// /// (Optional) The annotation’s highlighting mode, the visual effect to be used when - /// the mouse button is pressed or held down inside its active area: - /// N (None) No highlighting. - /// I (Invert) Invert the contents of the annotation rectangle. - /// O (Outline) Invert the annotation’s border. + /// the mouse button is pressed or held down inside its active area:

+ /// N (None) No highlighting.

+ /// I (Invert) Invert the contents of the annotation rectangle.

+ /// O (Outline) Invert the annotation’s border.

/// P (Push) Display the annotation’s down appearance, if any. If no down appearance is defined, /// offset the contents of the annotation rectangle to appear as if it were being pushed below - /// the surface of the page. - /// T (Toggle) Same as P (which is preferred). - /// A highlighting mode other than P overrides any down appearance defined for the annotation. + /// the surface of the page.

+ /// T (Toggle) Same as P (which is preferred).

+ /// A highlighting mode other than P overrides any down appearance defined for the annotation.

/// Default value: I. ///
[KeyInfo(KeyType.Name | KeyType.Optional)] @@ -47,12 +404,20 @@ void Initialize() /// /// (Optional) An appearance characteristics dictionary to be used in constructing a dynamic - /// appearance stream specifying the annotation’s visual presentation on the page. + /// appearance stream specifying the annotation’s visual presentation on the page.

/// The name MK for this entry is of historical significance only and has no direct meaning. ///
[KeyInfo(KeyType.Dictionary | KeyType.Optional)] public const string MK = "/MK"; + /// + /// (Required if this widget annotation is one of multiple children in a field; absent otherwise) + /// An indirect reference to the widget annotation's parent field.

+ /// A widget annotation may have at most one parent; that is, it can be included in the Kids array of at most one field + ///
+ [KeyInfo(KeyType.Optional)] + public const string Parent = "/Parent"; + public static DictionaryMeta Meta => _meta ??= CreateMeta(typeof(Keys)); static DictionaryMeta? _meta; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationBorderStyle.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationBorderStyle.cs new file mode 100644 index 00000000..a568e76d --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationBorderStyle.cs @@ -0,0 +1,34 @@ +namespace PdfSharp.Pdf.Annotations.enums +{ + /// + /// Specifies the border-style for a + /// + public enum PdfAnnotationBorderStyle + { + /// + /// No border + /// + None, + /// + /// A solid rectangle surrounding the annotation. + /// + Solid, + /// + /// A dashed rectangle surrounding the annotation. + /// The dash pattern may be specified by the D entry of the border-style dictionary. + /// + Dashed, + /// + /// A simulated embossed rectangle that appears to be raised above the surface of the page. + /// + Beveled, + /// + /// A simulated engraved rectangle that appears to be recessed below the surface of the page. + /// + Inset, + /// + /// A single line along the bottom of the annotation rectangle. + /// + Underline + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationHighlightingMode.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationHighlightingMode.cs new file mode 100644 index 00000000..48f9e67d --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfAnnotationHighlightingMode.cs @@ -0,0 +1,30 @@ +namespace PdfSharp.Pdf.Annotations.enums +{ + /// + /// The annotation's highlighting mode, the visual effect to be used when + /// the mouse button is pressed or held down inside its active area + /// + public enum PdfAnnotationHighlightingMode + { + /// + /// No highlighting + /// + None, + /// + /// Invert the contents of the annotation rectangle + /// + Invert, + /// + /// Invert the annotation's border + /// + Outline, + /// + /// Display the annotation's down appearance, if any + /// + Push, + /// + /// Same as (which is preferred) + /// + Toggle + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfButtonCaptionPosition.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfButtonCaptionPosition.cs new file mode 100644 index 00000000..659ac279 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/enums/PdfButtonCaptionPosition.cs @@ -0,0 +1,43 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Pdf.AcroForms; + +namespace PdfSharp.Pdf.Annotations.enums +{ + /// + /// s only:

+ /// Specifies where to position the text of the widget annotation's caption relative to its icon + ///
+ public enum PdfButtonCaptionPosition + { + /// + /// No icon, only caption + /// + CaptionOnly = 0, + /// + /// No caption, icon only + /// + IconOnly, + /// + /// Caption is placed below the icon + /// + BelowIcon, + /// + /// Caption is placed above the icon + /// + AboveIcon, + /// + /// Caption is placed right of the icon + /// + RightOfIcon, + /// + /// Caption is placed left of the icon + /// + LeftOfIcon, + /// + /// Caption is overlaid directly on the icon + /// + Overlaid + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Content/CLexer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Content/CLexer.cs index e7d0f47f..232e5f12 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Content/CLexer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Content/CLexer.cs @@ -3,23 +3,23 @@ using System.Text; using Microsoft.Extensions.Logging; -using PdfSharp.Internal; using PdfSharp.Logging; namespace PdfSharp.Pdf.Content { /// - /// Lexical analyzer for PDF content files. Adobe specifies no grammar, but it seems that it + /// Lexical analyzer for PDF content streams. Adobe specifies no grammar, but it seems that it /// is a simple post-fix notation. /// public class CLexer { /// - /// Initializes a new instance of the Lexer class. + /// Initializes a new instance of the CLexer class. /// public CLexer(byte[] content) { _content = content; + ContLength = _content.Length; _charIndex = 0; } @@ -28,7 +28,18 @@ public CLexer(byte[] content) ///
public CLexer(MemoryStream content) { - _content = content.ToArray(); + if (content.TryGetBuffer(out var buffer)) + { + _content = buffer.Array ?? throw new InvalidOperationException("Array must not be null."); + // Memory streams are 32-bit byte arrays. + ContLength = (int)content.Length; + } + else + { + _content = content.ToArray(); + ContLength = _content.Length; + } + _charIndex = 0; } @@ -49,11 +60,11 @@ public CSymbol ScanNextToken() goto Again; case '/': - return Symbol = ScanName(); + return ScanName(); case '+': case '-': - return Symbol = ScanNumber(); + return ScanNumber(); case '[': ScanNextChar(); @@ -64,25 +75,25 @@ public CSymbol ScanNextToken() return Symbol = CSymbol.EndArray; case '(': - return Symbol = ScanLiteralString(); + return ScanLiteralString(); case '<': if (_nextChar == '<') - return Symbol = ScanDictionary(); - return Symbol = ScanHexadecimalString(); + return ScanDictionary(); + return ScanHexadecimalString(); case '.': - return Symbol = ScanNumber(); + return ScanNumber(); case '"': case '\'': - return Symbol = ScanOperator(); + return ScanOperator(); } if (Char.IsDigit(ch)) - return Symbol = ScanNumber(); + return ScanNumber(); if (Char.IsLetter(ch)) - return Symbol = ScanOperator(); + return ScanOperator(); if (ch == Chars.EOF) return Symbol = CSymbol.Eof; @@ -279,11 +290,7 @@ protected CSymbol ScanDictionary() else { ScanNextChar(); -#if true - return CSymbol.Dictionary; -#else - return CSymbol.String; -#endif + return Symbol = CSymbol.Dictionary; } } } @@ -298,86 +305,173 @@ protected CSymbol ScanDictionary() public CSymbol ScanNumber() { // Note: This is a copy of Lexer.ScanNumber with minimal changes. Keep both versions in sync as far as possible. - const int maxDigitsForLong = 18; - const int maxDecimalDigits = 10; - long value = 0; - int totalDigits = 0; - int decimalDigits = 0; - bool period = false; - bool negative = false; + + // Parsing Strategy: + // Most real life numbers in PDF files have less than 19 digits. So we try to parse all digits as 64-bit integers + // in the first place. All leading zeros are skipped and not counted. + // If we found a decimal point we later divide the result by the appropriate power of 10 and covert is to Double. + // For edge cases, which are numbers with 19 digits, the token is parsed again with 'Int64.TryParse()' and + // if this fails with 'Double.TryParse'. + // If 'testForObjectReference' is 'true' and the value has up to 7 digits, no decimal point and no sign, + // we look ahead whether it is an object reference having the form '{object-number} {generation-number} R'. + + const int maxDigitsForLong = 18; // Up to 18 digits values can safely fit into 64-bit integer. + const int maxDecimalDigits = 10; // Maximum number of decimal digits we process. + var value = 0L; // The 64-bit value we have parsed. + var canBeLeadingZero = true; // True if a '0' is a leading zero and gets skipped. + var leadingZeros = 0; // The number of scanned leading zeros. Used for optional warning. + var totalDigits = 0; // The total number of digits scanned. E.g. is 7 for '123.4567'. + var decimalDigits = 0; // The total number of decimal digits scanned. E.g. is 4 for '123.4567'. + //var allDecimalDigitsAreZero = true; // Not used anymore, because '123.000' is always treated as double, never as integer. + var period = false; // The decimal point '.' was scanned. + var negative = false; // The value is negative and the scanned value is negated. + + var ch = _currChar; + Debug.Assert(ch is '+' or '-' or '.' or (>= '0' and <= '9')); ClearToken(); - char ch = _currChar; if (ch is '+' or '-') { if (ch == '-') negative = true; _token.Append(ch); ch = ScanNextChar(); + + // Never saw this in any PDF file. + if (ch is not ('.' or >= '0' and <= '9')) + { + PdfSharpLogHost.Logger.LogError("+/- not followed by a number or decimal point."); + } } + + // Scan the number. while (true) { - if (Char.IsDigit(ch)) + if (ch is >= '0' and <= '9') { _token.Append(ch); - ++totalDigits; + + if (canBeLeadingZero) + { + if (ch == '0') + { + leadingZeros++; + } + else + { + canBeLeadingZero = false; + ++totalDigits; + } + } + else + { + ++totalDigits; + } + if (decimalDigits < maxDecimalDigits) { // Calculate the value if it still fits into long. + // Lexer only: The value gets later parsed again by .NET if the total number of digits exceeds 18 digits. if (totalDigits <= maxDigitsForLong) value = 10 * value + ch - '0'; } + if (period) + { ++decimalDigits; + // A number with a decimal point is always considered as Symbol.Real, + // even if it fits in Symbol.Integer or Symbol.LongInteger. + // KEEP for documental purposes. + //// E.g. '123.0000' is real, but fits in an integer. + //if (ch is not '0') + // allDecimalDigitsAreZero = false; + } } else if (ch == '.') { + _token.Append(ch); + + // More than one period? if (period) - ContentReaderDiagnostics.ThrowContentReaderException("More than one period in number."); + ContentReaderDiagnostics.ThrowContentReaderException("More than one decimal point in number."); period = true; - _token.Append(ch); + canBeLeadingZero = false; } else break; ch = ScanNextChar(); } - if (totalDigits > maxDigitsForLong || - decimalDigits > maxDecimalDigits) + if (leadingZeros > 1) +#pragma warning disable CA2254 // This is a rare case. Therefor we use string interpolation. + PdfSharpLogHost.PdfReadingLogger.LogWarning($"Token '{_token}' has more than one leading zero."); +#pragma warning restore CA2254 + + // CLexer does not support LongInteger. + if (totalDigits > maxDigitsForLong || decimalDigits > maxDecimalDigits) { - // The number is too big for long or has too many decimal digits for our own code, so we provide it as real only. - // Number will be parsed here. + //// Case: It is not guarantied that the number fits in a 64-bit integer. + //// It is not integer if there is a period. + //if (period is false && totalDigits == maxDigitsForLong + 1) + //{ + // // Case: We have exactly 19 digits and no decimal point, which might fit in a 64-bit integer, + // // depending on the value. + // // If the 19-digit numbers is + // // in the range [1,000,000,000,000,000,000 .. 9,223,372,036,854,775,807] + // // or + // // in the range [-9,223,372,036,854,775,808 .. -1,000,000,000,000,000,000] + // // it is a 64-bit integer. Otherwise, it is not. + // // Because this is a super rare case we make life easy and parse the scanned token again by .NET. + // if (Int64.TryParse(_token.ToString(), out var result)) + // { + // _tokenAsLong = result; + // _tokenAsReal = result; + // return Symbol = Symbol.LongInteger; + // } + //} + + // Case: The number is too big for long or has too many decimal digits for our own code, + // so we provide it as real only. + // Number will be parsed by .NET. _tokenAsReal = Double.Parse(_token.ToString(), CultureInfo.InvariantCulture); - return CSymbol.Real; + + return Symbol = CSymbol.Real; } + // Case: The number is in range [0 .. 999,999,999,999,999,999] and fits into a 64-bit integer. if (negative) - value = -value; + value = -value; // Flipping 64-bit integer sign is safe here. if (period) { + // Case: A number with a period is always considered to be real value. if (decimalDigits > 0) { _tokenAsReal = value / PowersOf10[decimalDigits]; - //_tokenAsLong = value / PowersOf10[decimalDigits]; + // KEEP for documental purposes. + //// It is not integer if there is a period. + //if (allDecimalDigitsAreZero) + // _tokenAsLong = (long)_tokenAsReal; } else { _tokenAsReal = value; - _tokenAsLong = value; + // KEEP for documental purposes. + //// It is not integer if there is a period. + // _tokenAsLong = value; } - return CSymbol.Real; + return Symbol = CSymbol.Real; } _tokenAsLong = value; _tokenAsReal = Convert.ToDouble(value); Debug.Assert(Int64.Parse(_token.ToString(), CultureInfo.InvariantCulture) == value); - if (value is >= Int32.MinValue and < Int32.MaxValue) - return CSymbol.Integer; + if (value is >= Int32.MinValue and <= Int32.MaxValue) + return Symbol = CSymbol.Integer; - return CSymbol.Real; + return Symbol = CSymbol.Real; // CLexer returns "Real" because there is no "LongInteger". } static readonly double[] PowersOf10 = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000]; @@ -588,6 +682,7 @@ public CSymbol ScanLiteralString() goto SkipChar; default: + // Sync with ScanStringLiteral. if (Char.IsDigit(ch)) { // Octal character code. @@ -689,7 +784,7 @@ static char LogError(char ch) ///
char ScanNextChar() { - if (ContLength <= _charIndex) + if (_charIndex >= ContLength) { _currChar = _nextChar; // The last character we are now dealing with. _nextChar = Chars.EOF; // Next character is EOF. @@ -702,7 +797,7 @@ char ScanNextChar() { if (_nextChar == Chars.LF) { - // Treat CR LF as LF + // Treat CR LF as LF. _currChar = _nextChar; if (ContLength <= _charIndex) _nextChar = Chars.EOF; @@ -733,7 +828,8 @@ void ClearToken() /// Appends current character to the token and /// reads next byte as a character. ///
- internal char AppendAndScanNextChar() + /*internal*/ + char AppendAndScanNextChar() { _token.Append(_currChar); return ScanNextChar(); @@ -856,7 +952,7 @@ internal static bool IsDelimiter(char ch) /// /// Gets the length of the content. /// - public int ContLength => _content.Length; + public int ContLength { get; } /// /// Gets or sets the position in the content. @@ -866,9 +962,10 @@ public int Position get => _charIndex; set { + Debug.Assert(value >= 0); _charIndex = value; - _currChar = (char)_content[_charIndex - 1]; - _nextChar = (char)_content[_charIndex - 1]; + _currChar = _charIndex < ContLength ? (char)_content[_charIndex] : Chars.EOF; + _nextChar = _charIndex + 1 < ContLength ? (char)_content[_charIndex + 1] : Chars.EOF; } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs index 2f23feba..c02e6add 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs @@ -3,7 +3,6 @@ using System.Text; using Microsoft.Extensions.Logging; -using PdfSharp.Internal; using PdfSharp.Logging; using PdfSharp.Pdf.Internal; @@ -26,7 +25,7 @@ public Lexer(Stream pdfInputStream, ILogger? logger) _pdfStream = pdfInputStream; // ReSharper disable once RedundantCast because SizeType can be 32 bit depending on build. _pdfLength = (SizeType)_pdfStream.Length; - _idxChar = 0; + _charIndex = 0; Position = 0; _logger = logger ?? PdfSharpLogHost.PdfReadingLogger; } @@ -45,13 +44,13 @@ public SizeType Position { get { - Debug.Assert(_pdfStream.Position == _idxChar + 2); - return _idxChar; + Debug.Assert(_pdfStream.Position == _charIndex + 2); + return _charIndex; } set { Debug.Assert(value >= 0); - _idxChar = value; + _charIndex = value; _pdfStream.Position = value; // ReadByte return -1 (eof) at the end of the stream. @@ -83,15 +82,15 @@ public Symbol ScanNextToken(bool testForObjectReference) goto TryAgain; case '/': - return Symbol = ScanName(); + return ScanName(); case '+': case '-': // Cannot be an object reference if a sign was found. - return Symbol = ScanNumber(false); + return ScanNumber(false); case '(': - return Symbol = ScanStringLiteral(); + return ScanStringLiteral(); case '[': ScanNextChar(true); @@ -108,7 +107,7 @@ public Symbol ScanNextToken(bool testForObjectReference) ScanNextChar(true); return Symbol = Symbol.BeginDictionary; } - return Symbol = ScanHexadecimalString(); + return ScanHexadecimalString(); case '>': if (_nextChar == '>') @@ -122,19 +121,20 @@ public Symbol ScanNextToken(bool testForObjectReference) goto default; case >= '0' and <= '9': - Symbol = ScanNumber(testForObjectReference); + ScanNumber(testForObjectReference); Debug.Assert(Symbol is Symbol.Integer or Symbol.LongInteger or Symbol.Real or Symbol.ObjRef); return Symbol; case '.': // Cannot be an object reference if a decimal point was found. - Symbol = ScanNumber(false); + ScanNumber(false); Debug.Assert(Symbol == Symbol.Real); return Symbol; case >= 'a' and <= 'z': - return Symbol = ScanKeyword(); + return ScanKeyword(); +#if DEBUG case 'R': Debug.Assert(false, "'R' should not be parsed anymore."); // Note: "case 'R':" is not scanned, because it is only used in an object reference. @@ -142,6 +142,7 @@ public Symbol ScanNextToken(bool testForObjectReference) ScanNextChar(true); // The next line only exists for the 'UseOldCode' case in PdfReader. return Symbol = Symbol.R; +#endif case Chars.EOF: return Symbol = Symbol.Eof; @@ -178,9 +179,13 @@ public Symbol ScanComment() if (ch is Chars.LF or Chars.EOF) break; } - // TODO_OLD: not correct | StLa/24-01-23: Why? + // If someone writes '%%EOF' somewhere in the document this must not be + // interpreted as the end of the PDF file. if (_token.ToString().StartsWith("%%EOF", StringComparison.Ordinal)) - return Symbol.Eof; + { + //return Symbol.Eof; // This is wrong. + _logger.LogError("Unexpected '%%EOF' read."); + } return Symbol = Symbol.Comment; } @@ -252,14 +257,15 @@ static char LogError(char ch) /// Scans a number or an object reference. /// Returns one of the following symbols. /// Symbol.ObjRef if testForObjectReference is true and the pattern "nnn ggg R" can be found. - /// Symbol.Real if a decimal point exists or the number of digits is too large for 64-bit integer. + /// Symbol.Real if a decimal point exists or the number is too large for 64-bit integer. /// Symbol.Integer if the long value is in the range of 32-bit integer. /// Symbol.LongInteger otherwise. /// - public Symbol ScanNumber(bool testForObjectReference) + /*public */ + internal Symbol ScanNumber(bool testForObjectReference) { // We found a PDF file created with Acrobat 7 with this entry - // /Checksum 2996984786 # larger than 2.147.483.648 (2^31) + // /Checksum 2996984786 # larger than 2,147,483,648 (2^31) // // Also got an AutoCAD PDF file that contains // /C 264584027963392 # 15 digits @@ -269,38 +275,48 @@ public Symbol ScanNumber(bool testForObjectReference) // Note: This is a copy of CLexer.ScanNumber with minimal changes. Keep both versions in sync as far as possible. // Update StL: Function is revised for object reference look ahead. + // Parsing Strategy: + // Most real life numbers in PDF files have less than 19 digits. So we try to parse all digits as 64-bit integers + // in the first place. All leading zeros are skipped and not counted. + // If we found a decimal point we later divide the result by the appropriate power of 10 and covert is to Double. + // For edge cases, which are numbers with 19 digits, the token is parsed again with 'Int64.TryParse()' and + // if this fails with 'Double.TryParse'. + // If 'testForObjectReference' is 'true' and the value has up to 7 digits, no decimal point and no sign, + // we look ahead whether it is an object reference having the form '{object-number} {generation-number} R'. const int maxDigitsForObjectNumber = 7; // max: 8_388_608 / 0x_7F_FF_FF const int maxDigitsForGenerationNumber = 5; // max: 65_535 / 0x_FF_FF - const int maxDigitsForLong = 18; - const int maxDecimalDigits = 10; - var value = 0L; - var totalDigits = 0; - var decimalDigits = 0; - var period = false; - var negative = false; + const int maxDigitsForLong = 18; // Up to 18 digits values can safely fit into 64-bit integer. + const int maxDecimalDigits = 10; // Maximum number of decimal digits we process. + var value = 0L; // The 64-bit value we have parsed. + var canBeLeadingZero = true; // True if a '0' is a leading zero and gets skipped. + var leadingZeros = 0; // The number of scanned leading zeros. Used for optional warning. + var totalDigits = 0; // The total number of digits scanned. E.g. is 7 for '123.4567'. + var decimalDigits = 0; // The total number of decimal digits scanned. E.g. is 4 for '123.4567'. + //var allDecimalDigitsAreZero = true; // Not used anymore, because '123.000' is always treated as double, never as integer. + var period = false; // The decimal point '.' was scanned. + var negative = false; // The value is negative and the scanned value is negated. + var ch = _currChar; Debug.Assert(ch is '+' or '-' or '.' or (>= '0' and <= '9')); - // If first char is not a digit, it cannot be an object reference. + // If the first char is not a digit, it cannot be by definition an object reference. if (testForObjectReference && ch is not (>= '0' and <= '9')) testForObjectReference = false; -#if DEBUG_ - var pos = Position; - var neighborhood = GetNeighborhoodOfCurrentPosition(Position); - Console.W/riteLine(neighborhood); -#endif + ClearToken(); if (ch is '+' or '-') { + // 'testForObjectReference == false' is already ensured here. + if (ch == '-') negative = true; _token.Append(ch); ch = ScanNextChar(true); - // Never saw this in any PDF file, but possible. + // Never saw this in any PDF file. if (ch is not ('.' or >= '0' and <= '9')) { - PdfSharpLogHost.Logger.LogError("+/- not followed by a number."); + PdfSharpLogHost.Logger.LogError("+/- not followed by a number or decimal point."); } } @@ -310,83 +326,154 @@ public Symbol ScanNumber(bool testForObjectReference) if (ch is >= '0' and <= '9') { _token.Append(ch); - ++totalDigits; + + if (canBeLeadingZero) + { + if (ch == '0') + { + leadingZeros++; + } + else + { + canBeLeadingZero = false; + ++totalDigits; + } + } + else + { + ++totalDigits; + } + if (decimalDigits < maxDecimalDigits) { // Calculate the value if it still fits into long. + // The value gets later parsed again by .NET if the total number of digits exceeds 18 digits. if (totalDigits <= maxDigitsForLong) value = 10 * value + ch - '0'; } + if (period) + { ++decimalDigits; + // A number with a decimal point is always considered as Symbol.Real, + // even if it fits in Symbol.Integer or Symbol.LongInteger. + // KEEP for documental purposes. + //// E.g. '123.0000' is real, but fits in an integer. + //if (ch is not '0') + // allDecimalDigitsAreZero = false; + } } else if (ch == '.') { + _token.Append(ch); + // More than one period? if (period) - ContentReaderDiagnostics.ThrowContentReaderException("More than one period in number."); + ContentReaderDiagnostics.ThrowContentReaderException("More than one decimal point in number."); period = true; - _token.Append(ch); + canBeLeadingZero = false; } else break; ch = ScanNextChar(true); } +#if true_ // KEEP Maybe we warn in the future about leading zeros outside xref. + // xref has lots of leading zeros. + // Maybe we add a parameter 'warnAboutLeadingZeros', but not yet. + if (leadingZeros > 1) +#pragma warning disable CA2254 // This is a rare case outside xref. Therefor we use string interpolation. + PdfSharpLogHost.PdfReadingLogger.LogWarning($"Token '{_token}' has more than one leading zero."); +#pragma warning restore CA2254 +#endif // Can the scanned number be the first part of an object reference? if (testForObjectReference && period is false - && totalDigits <= maxDigitsForObjectNumber + && totalDigits <= maxDigitsForObjectNumber // Values in range [8_388_609..9_999_999] are checked in PdfObjectID. && IsWhiteSpace(_currChar)) { -#if DEBUG +#if DEBUG_ LexerHelper.TryCheckReferenceCount++; #endif int gen = TryReadReference(); if (gen >= 0) { -#if DEBUG +#if DEBUG_ LexerHelper.TryCheckReferenceSuccessCount++; #endif _tokenAsObjectID = ((int)value, gen); - return Symbol.ObjRef; + return Symbol = Symbol.ObjRef; } } if (totalDigits > maxDigitsForLong || decimalDigits > maxDecimalDigits) { - // The number is too big for long or has too many decimal digits for our own code, + // Case: It is not guarantied that the number fits in a 64-bit integer. + + // It is not integer if there is a period. + if (period is false && totalDigits == maxDigitsForLong + 1) + { + // Case: We have exactly 19 digits and no decimal point, which might fit in a 64-bit integer, + // depending on the value. + // If the 19-digit numbers is + // in the range [1,000,000,000,000,000,000 .. 9,223,372,036,854,775,807] + // or + // in the range [-9,223,372,036,854,775,808 .. -1,000,000,000,000,000,000] + // it is a 64-bit integer. Otherwise, it is not. + // Because this is a super rare case we make life easy and parse the scanned token again by .NET. + if (Int64.TryParse(_token.ToString(), out var result)) + { + _tokenAsLong = result; + _tokenAsReal = result; + return Symbol = Symbol.LongInteger; + } + } + + // Case: The number is too big for long or has too many decimal digits for our own code, // so we provide it as real only. // Number will be parsed by .NET. _tokenAsReal = Double.Parse(_token.ToString(), CultureInfo.InvariantCulture); - return Symbol.Real; + + return Symbol = Symbol.Real; } + // Case: The number is in range [0 .. 999,999,999,999,999,999] and fits into a 64-bit integer. if (negative) - value = -value; + value = -value; // Flipping 64-bit integer sign is safe here. if (period) { + // Case: A number with a period is always considered to be real value. if (decimalDigits > 0) { _tokenAsReal = value / PowersOf10[decimalDigits]; + // KEEP for documental purposes. + //// It is not integer if there is a period. + //if (allDecimalDigitsAreZero) + // _tokenAsLong = (long)_tokenAsReal; } else { _tokenAsReal = value; - _tokenAsLong = value; + // KEEP for documental purposes. + //// It is not integer if there is a period. + // _tokenAsLong = value; } - return Symbol.Real; + return Symbol = Symbol.Real; } _tokenAsLong = value; _tokenAsReal = Double.NaN; Debug.Assert(Int64.Parse(_token.ToString(), CultureInfo.InvariantCulture) == value); - if (value is >= Int32.MinValue and < Int32.MaxValue) - return Symbol.Integer; - return Symbol.LongInteger; + if (value is >= Int32.MinValue and <= Int32.MaxValue) + { + // Case: Fits in the range of a 32-bit integer. + return Symbol = Symbol.Integer; + } + + return Symbol = Symbol.LongInteger; // Try to read generation number followed by an 'R'. // Returns -1 if not an object reference. @@ -396,7 +483,7 @@ int TryReadReference() // A Reference has the form "nnn ggg R". The original implementation of the parser used a // reduce/shift algorithm in the first place. But this case is the only one we need to - // look ahead 3 tokens. + // look ahead 2 tokens. // This is a new implementation that checks whether a scanned integer is followed by // another integer and an 'R'. @@ -441,6 +528,7 @@ int TryReadReference() if (_currChar != 'R') goto NotAReference; + // Eat the 'R'. ScanNextChar(true); return generationNumber; @@ -453,6 +541,7 @@ int TryReadReference() return -1; } } + static readonly double[] PowersOf10 = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000]; /// @@ -481,7 +570,7 @@ public Symbol ScanKeyword() "null" => Symbol = Symbol.Null, "true" => Symbol = Symbol.Boolean, "false" => Symbol = Symbol.Boolean, - "R" => Symbol = Symbol.R, + "R" => Symbol = Symbol.R, // Not scanned anymore because it is handled in ScanNumber. "stream" => Symbol = Symbol.BeginStream, "endstream" => Symbol = Symbol.EndStream, "xref" => Symbol = Symbol.XRef, @@ -708,33 +797,10 @@ public bool TryScanLiterally(string literal) return true; } - ///// - ///// Tries to scan "\n", "\r" or "\r\n" and moves the Position to the next line. - ///// - //public bool TryScanEndOfLine() => TryScanEndOfLine(true, true, true); - - ///// - ///// Tries to scan the accepted end-of-line markers and moves the Position to the next line. - ///// - //public bool TryScanEndOfLine(bool acceptCR, bool acceptLF, bool acceptCRLF) - //{ - // if (acceptCRLF && _currChar == Chars.CR && _nextChar == Chars.LF) - // { - // Position += 2; - // return true; - // } - // if (acceptCR && _currChar == Chars.CR || acceptLF && _currChar == Chars.LF) - // { - // Position += 1; - // return true; - // } - // return false; - //} - /// /// Return the exact position where the content of the stream starts. /// The logical position is also set to this value when the function returns.
- /// Reference: 3.2.7 Stream Objects / Page 60 + /// Reference: 3.2.7 Stream Objects / Page 60
/// Reference 2.0: 7.3.8 Stream objects / Page 31 ///
public SizeType FindStreamStartPosition(PdfObjectID id) @@ -817,7 +883,7 @@ public byte[] ScanStream(SizeType position, int length, out int bytesRead) if (!ReadWholeStreamSequence(_pdfStream, length, out var bytes, out bytesRead)) { // EOF reached, so return what was read. - _logger.EndOfStreamReached(length,position,bytesRead); + _logger.EndOfStreamReached(length, position, bytesRead); Position = position + bytesRead; return bytes; @@ -853,7 +919,7 @@ static bool ReadWholeStreamSequence(Stream stream, int length, out byte[] conten // use a MemoryStream to cache the original stream if necessary. // However, without this workaround the file would be regarded as corrupt instead of the stream not being compatible. do - { + { // Log error only for retries. ConditionalLogOnRetry(bytesRead, "Stream.Read did not return the whole requested sequence. As a workaround, reading the missing bytes is tried again."); @@ -954,18 +1020,18 @@ public string ScanRawString(SizeType position, int length) /// return it as a character with high byte always zero. ///
// ReSharper disable once InconsistentNaming - internal char ScanNextChar(bool handleCRLF) // ScanNextByteAsChar + internal char ScanNextChar(bool handleCRLF) { - if (_idxChar >= _pdfLength) + if (_charIndex >= _pdfLength) { - _currChar = Chars.EOF; - _nextChar = Chars.EOF; + _currChar = _nextChar; // The last character we are now dealing with. + _nextChar = Chars.EOF; // Next character is EOF. } else { _currChar = _nextChar; _nextChar = (char)_pdfStream.ReadByte(); - _idxChar++; + _charIndex++; if (handleCRLF && _currChar == Chars.CR) { if (_nextChar == Chars.LF) @@ -973,7 +1039,7 @@ internal char ScanNextChar(bool handleCRLF) // ScanNextByteAsChar // Treat CR LF as LF. _currChar = _nextChar; _nextChar = (char)_pdfStream.ReadByte(); - _idxChar++; + _charIndex++; } else { @@ -1154,8 +1220,21 @@ public int TokenToInteger { get { - Debug.Assert(_tokenAsLong == Int32.Parse(_token.ToString(), CultureInfo.InvariantCulture)); - return (int)_tokenAsLong; + return Symbol switch + { + Symbol.Integer => (int)_tokenAsLong, + + // Should always fail, because if token fits into integer the symbol type would not be LongInteger. + Symbol.LongInteger => _tokenAsLong is >= Int32.MinValue and <= Int32.MaxValue ? + (int)_tokenAsLong : Throw(), + + // All other types fail. + //Symbol.Real => 42, + //Symbol.ObjRef => 42, + _ => throw new InvalidOperationException("Symbol type is not 'Integer'.") + }; + + static int Throw() => throw new InvalidOperationException("64-bit value too large for 32-bit value."); } } @@ -1164,11 +1243,12 @@ public int TokenToInteger ///
public long TokenToLongInteger { - get + get => Symbol switch { - Debug.Assert(_tokenAsLong == Int64.Parse(_token.ToString(), CultureInfo.InvariantCulture)); - return _tokenAsLong; - } + Symbol.Integer => _tokenAsLong, + Symbol.LongInteger => _tokenAsLong, + _ => throw new InvalidOperationException("Symbol type is not 'Integer' or 'LongInteger'.") + }; } /// @@ -1207,6 +1287,7 @@ internal static bool IsWhiteSpace(char ch) { return ch switch { + // Reference 2.0: 7.1 Table 1 — White-space characters / Page 22 Chars.NUL => true, // 0 Null Chars.HT => true, // 9 Horizontal Tab Chars.LF => true, // 10 Line Feed @@ -1222,6 +1303,7 @@ internal static bool IsWhiteSpace(char ch) /// internal static bool IsDelimiter(char ch) { + // Reference 2.0: 7.1 Table 2 — Delimiter characters / Page 23 return ch switch { '(' => true, @@ -1243,7 +1325,7 @@ internal static bool IsDelimiter(char ch) ///
public SizeType PdfLength => _pdfLength; readonly SizeType _pdfLength; - SizeType _idxChar; + SizeType _charIndex; char _currChar; char _nextChar; readonly StringBuilder _token = new(); @@ -1251,10 +1333,10 @@ internal static bool IsDelimiter(char ch) double _tokenAsReal; (int, int) _tokenAsObjectID; readonly Stream _pdfStream; - ILogger _logger; + readonly ILogger _logger; } -#if DEBUG +#if DEBUG_ public class LexerHelper { // Give me an idea of the try/success ratio. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs index 4b063b29..47b96239 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs @@ -362,8 +362,8 @@ void ReadDictionaryStream(PdfDictionary dict, SuppressExceptions? suppressObject int streamLength = GetStreamLength(dict, suppressObjectOrderExceptions); if (SuppressExceptions.HasError(suppressObjectOrderExceptions)) return; -//#warning THHO4STLA What to do if startPosition + streamLength is larger than length of stream? => Better not show "Please send us your PDF file" but another error message. -// TODO_OLD THHO4STLA What to do if startPosition + streamLength is larger than length of stream? => Better not show "Please send us your PDF file" but another error message. + //#warning THHO4STLA What to do if startPosition + streamLength is larger than length of stream? => Better not show "Please send us your PDF file" but another error message. + // TODO_OLD THHO4STLA What to do if startPosition + streamLength is larger than length of stream? => Better not show "Please send us your PDF file" but another error message. int retryCount = 0; RetryReadStream: // Step 3: We try to read the stream content. @@ -494,7 +494,7 @@ int GetStreamLength(PdfDictionary dict, SuppressExceptions? suppressObjectOrderE PdfSharpLogHost.Logger.LogError("Object '{Object}' has no valid /Length entry. Try to determine stream length by looking for 'endstream'.", dict.ObjectID.ToString()); #if TEST_CODE_ - TestStreamWithoutLengthEntry: + TestStreamWithoutLengthEntry: #endif // Try to determine an upper limit of the stream length. var behindPosition = _document.IrefTable.GetPositionOfObjectBehind(dict, _lexer.Position); @@ -707,10 +707,11 @@ List ParseObject(Symbol stopSymbol) items.Add(new PdfName(_lexer.Token)); break; +#if DEBUG case Symbol.R: - //Debug.Assert(_options.UseOldCode == true, "Must not come here anymore"); Debug.Assert(false, "Must not come here anymore"); break; +#endif case Symbol.ObjRef: { @@ -726,7 +727,7 @@ List ParseObject(Symbol stopSymbol) { // XRefTable not complete when trailer is read. Create temporary irefs that are // removed later in PdfTrailer.Finish(). - iref = new PdfReference(objectID, 0); + iref = PdfReference.CreateForObjectID(objectID, 0); items.Add(iref); } else @@ -1043,7 +1044,7 @@ PdfObject ReadIndirectObjectFromObjectStreamInternal(PdfObjectID objectID, Suppr throw TH.PdfReaderException_ObjectCouldNotBeFoundInObjectStreams(); #endif } - + /// /// Reads the PdfObjects of all known references, no matter if they are saved at document level or inside an ObjectStream. /// @@ -1153,25 +1154,37 @@ internal void ReadAllObjectStreamsAndTheirReferences() // ObjectStream could not be loaded. Maybe its stream length is saved in an ObjectStream not yet loaded. Try it again in the next round. nextObjectStreamIDsToLoad.Add(objectStreamID); - PdfSharpLogHost.PdfReadingLogger.LogWarning($"Loading ObjectStream with ID {objectStreamID} will be retried."); + PdfSharpLogHost.PdfReadingLogger.LogWarning("Loading ObjectStream with ID {objectStreamID} will be retried.", objectStreamID); // Read next ObjectStream. continue; } // Create the parser for the object stream. - var objectStreamParser = new Parser(_document, new MemoryStream(objectStream.Stream.Value), _documentParser); + var objectStreamParser = new Parser(_document, new MemoryStream(objectStream.Stream.UnfilteredValue), _documentParser); // Read and add all References to objects residing in the object stream and get all ObjectIDs and offsets . var objectIDsWithOffset = objectStream.ReadReferencesAndOffsets(_document.IrefTable); - + // Save all ObjectIDs with the parser of its ObjectStream and its offset. foreach (var objectIDWithOffset in objectIDsWithOffset) { var objectID = objectIDWithOffset.Key; var offset = objectIDWithOffset.Value; - _objectStreamObjectSources.Add(objectID, (objectStreamParser, offset)); + // PDFsharp reads objects from high addresses down to low addresses. + // Thus, the newest object should be read first. + // For duplicate IDs, we keep the first object and ignore objects read later. + if (!_objectStreamObjectSources.TryGetValue(objectID, out _)) + { + // Add object with new objectID. + _objectStreamObjectSources.Add(objectID, (objectStreamParser, offset)); + } + else + { + // Ignore object with objectID already on the list. + PdfSharpLogHost.PdfReadingLogger.LogWarning("Ignoring object with ID {objectID} because an object with that ID was already read.", objectID); + } } } @@ -1231,7 +1244,7 @@ internal void ReadAllObjectStreamsAndTheirReferences() continue; } - var objectStreamParser = new Parser(_document, new MemoryStream(objectStream.Stream.Value), _documentParser); + var objectStreamParser = new Parser(_document, new MemoryStream(objectStream.Stream.UnfilteredValue), _documentParser); _objectStreamsWithParsers.Add(objectStreamID, (objectStream, objectStreamParser)); // Read and add all References to objects residing in the object stream. @@ -1424,7 +1437,8 @@ internal PdfTrailer ReadTrailer() continue; int idToUse = id; -#if true // The following issue in PDF files is rare, but must be fixed here to prevent PdfReference with wrong IDs. +#if true + // The following issue in PDF files is rare, but must be fixed here to prevent PdfReference with wrong IDs. // We found PDF files where the ID of the referenced object was misaligned by one relative to // its number from the xref table. // Check if the object at the address has the correct ID and generation. @@ -1454,7 +1468,9 @@ internal PdfTrailer ReadTrailer() // Ignore the latter one. if (xrefTable.Contains(objectID)) continue; - xrefTable.Add(new PdfReference(objectID, position)); + + var iref = PdfReference.CreateForObjectID(objectID, position); + xrefTable.Add(iref); } } else if (symbol == Symbol.Trailer) @@ -1569,12 +1585,7 @@ PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable) // Usually, the cross-reference stream and its reference have not been read yet. if (!xrefTable.Contains(objectID)) { - var iref = new PdfReference(xrefStream) - { - ObjectID = objectID, - Value = xrefStream, - Position = xrefStart - }; + var iref = PdfReference.CreateFromObject(xrefStream, objectID, xrefStart); xrefTable.Add(iref); } // If a cross-reference stream B is referenced in the /Prev key of another cross-reference stream A dictionary, @@ -1710,12 +1721,13 @@ PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable) _ = typeof(int); #endif Debug.Assert(objectID.GenerationNumber == item.Field3); - + // Ignore the latter one. if (!xrefTable.Contains(objectID)) { // Add iref for all uncompressed objects. - xrefTable.Add(new PdfReference(objectID, position)); + var iref = PdfReference.CreateForObjectID(objectID, position); + xrefTable.Add(iref); } #if DEBUG_ else diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs index 26688b16..f9b9f6fc 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs @@ -398,7 +398,7 @@ PdfDocument OpenFromStream(Stream stream, string? password, PdfDocumentOpenMode // 7. Replace all document’s placeholder references by references knowing their objects. // Placeholder references are used, when reading indirect objects referring objects stored in object streams before reading and decoding them. - FinishReferences(); + FinalizeReferences(); RereadUnicodeStrings(); @@ -449,113 +449,66 @@ PdfDocument OpenFromStream(Stream stream, string? password, PdfDocumentOpenMode return _document; } - void FinishReferences() + /// + /// Ensures that all references in all objects refer to the actual object or to the null object (see ShouldUpdateReference method). + /// + void FinalizeReferences() { Debug.Assert(_document.IrefTable.IsUnderConstruction); - var finishedObjects = new HashSet(); - foreach (var iref in _document.IrefTable.AllReferences) { - Debug.Assert(iref.Value != null, - "All references saved in IrefTable should have been created when their referred PdfObject has been accessible."); + var pdfObject = iref.Value; - // Get and update object’s references. - FinishItemReferences(iref.Value, _document, finishedObjects); - } - - _document.IrefTable.IsUnderConstruction = false; - - // Fix references of trailer values and then objects and irefs are consistent. - _document.Trailer.Finish(); - } - - void FinishItemReferences(PdfItem? pdfItem, PdfDocument document, HashSet finishedObjects) - { - // Only PdfObjects may contain further PdfReferences. - if (pdfItem is not PdfObject pdfObject) - return; -#if true - // Try to add object to finished objects. - // Return, if this object was already processed. - if (!finishedObjects.Add(pdfObject)) - return; -#else - // Return, if this object is already processed. - if (finishedObjects.Contains(pdfObject)) - return; - - // Mark object as processed. - finishedObjects.Add(pdfObject); -#endif + Debug.Assert(pdfObject != null, + "All references saved in IrefTable should have been created when their referred PdfObject has been accessible."); -#if true - // For PdfDictionary and PdfArray, get and update child references. - switch (pdfObject) - { - case PdfDictionary childDictionary: - FinishChildReferences(childDictionary, finishedObjects); - break; - case PdfArray childArray: - FinishChildReferences(childArray, finishedObjects); - break; - } -#else - // For PdfDictionary and PdfArray, get and update child references. - if (pdfObject is PdfDictionary childDictionary) - FinishChildReferences(childDictionary, document, finishedObjects); - if (pdfObject is PdfArray childArray) - FinishChildReferences(childArray, document, finishedObjects); -#endif - } + // Update all references to PdfDictionary’s and PdfArray’s child objects. + switch (pdfObject) + { + case PdfDictionary dictionary: + // Dictionary elements are modified inside the loop. Avoid "Collection was modified; enumeration operation may not execute" error occuring in net 4.6.2. + // There is no way to access KeyValuePairs via index natively to use a for loop with. + // Instead, enumerate Keys and get value via Elements[key], which should be O(1). + foreach (var key in dictionary.Elements.Keys) + { + var item = dictionary.Elements[key]; - void FinishChildReferences(PdfDictionary dictionary, HashSet finishedObjects) - { - // Dictionary elements are modified inside loop. Avoid "Collection was modified; enumeration operation may not execute" error occuring in net 4.6.2. - // There is no way to access KeyValuePairs via index natively to use a for loop with. - // Instead, enumerate Keys and get value via Elements[key], which shall be O(1). - foreach (var key in dictionary.Elements.Keys) - { - var item = dictionary.Elements[key]; + // Replace each reference with its final item, if necessary. + if (item is PdfReference currentReference && ShouldUpdateReference(currentReference, out var finalItem)) + dictionary.Elements[key] = finalItem; + } + break; + case PdfArray array: + var elements = array.Elements; + for (var i = 0; i < elements.Count; i++) + { + var item = elements[i]; - // For PdfReference: Update reference, if necessary, and continue with referred item. - if (item is PdfReference iref) - { - if (FinishReference(iref, out var newIref, out var value)) - dictionary.Elements[key] = newIref; - item = value; + // Replace each reference with its final item, if necessary. + if (item is PdfReference currentReference && ShouldUpdateReference(currentReference, out var finalItem)) + elements[i] = finalItem; + } + break; } - - // Get and update item’s references. - FinishItemReferences(item, _document, finishedObjects); } - } - void FinishChildReferences(PdfArray array, HashSet finishedObjects) - { - var elements = array.Elements; - for (var i = 0; i < elements.Count; i++) - { - var item = elements[i]; - - // For PdfReference: Update reference, if necessary, and continue with referred item. - if (item is PdfReference iref) - { - if (FinishReference(iref, out var newIref, out var value)) - elements[i] = newIref; - item = value; - } + _document.IrefTable.IsUnderConstruction = false; - // Get and update item’s references. - FinishItemReferences(item, _document, finishedObjects); - } + // Fix references of trailer values and then objects and irefs are consistent. + _document.Trailer.Finish(); } - bool FinishReference(PdfReference currentReference, out PdfItem actualReference, out PdfItem value) + /// + /// Gets the final PdfItem that shall perhaps replace currentReference. It will be outputted in finalItem. + /// + /// True, if finalItem has changes compared to currentReference. + bool ShouldUpdateReference(PdfReference currentReference, out PdfItem finalItem) { var isChanged = false; PdfItem? reference = currentReference; + // Step 1: // The value of the reference may be null. // If a file level PdfObject refers object stream level PdfObjects, that were not yet decompressed when reading it, // placeholder references are used. @@ -565,32 +518,22 @@ bool FinishReference(PdfReference currentReference, out PdfItem actualReference, var newIref = _document.IrefTable[currentReference.ObjectID]; reference = newIref; isChanged = true; + // reference may be null. Don’t return yet. } + // Step: 2 // PDF Reference 2.0 section 7.3.10: // An indirect reference to an undefined object shall not be considered an error by a PDF processor; // it shall be treated as a reference to the null object. - if (reference is PdfReference { Value: null }) + // Addition: A reference replaced with null in step 1, as no reference for the object ID has been found in _document.IrefTable, + // is also considered as a reference to an undefined object here. + if (reference is PdfReference { Value: null } or null) { reference = PdfNull.Value; isChanged = true; } - if (reference == null) - { - reference = PdfNull.Value; - isChanged = true; - } - - actualReference = reference; - - if (!isChanged) - value = currentReference.Value; - else if (actualReference is PdfReference r) - value = r.Value; - else - value = PdfNull.Value; - + finalItem = reference; return isChanged; } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs index e6b5ab0b..dbbc1c19 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs @@ -102,17 +102,18 @@ public void Write(PdfInteger value) WriteRaw(value.Value.ToString(CultureInfo.InvariantCulture)); } - /// - /// Writes the specified value to the PDF stream. - /// -#pragma warning disable CS0618 // Type or member is obsolete - public void Write(PdfUInteger value) -#pragma warning restore CS0618 // Type or member is obsolete - { - WriteSeparator(CharCat.Character); - _lastCat = CharCat.Character; - WriteRaw(value.Value.ToString(CultureInfo.InvariantCulture)); - } + // DELETE + //// /// + //// /// Writes the specified value to the PDF stream. + //// /// + ////#pragma warning disable CS0618 // Type or member is obsolete + //// public void Write(PdfUInteger value) + ////#pragma warning restore CS0618 // Type or member is obsolete + //// { + //// WriteSeparator(CharCat.Character); + //// _lastCat = CharCat.Character; + //// WriteRaw(value.Value.ToString(CultureInfo.InvariantCulture)); + //// } /// /// Writes the specified value to the PDF stream. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/Symbol.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/Symbol.cs index 8196928b..ab311b82 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/Symbol.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/Symbol.cs @@ -15,7 +15,9 @@ public enum Symbol BeginStream, EndStream, BeginArray, EndArray, BeginDictionary, EndDictionary, - Obj, EndObj, R, XRef, Trailer, StartXRef, Eof, + Obj, EndObj, + R, // Is replaced by ObjRef. + XRef, Trailer, StartXRef, Eof, // The lexer now can parse references in the form "nnn ggg R" // as a symbol in one step. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Internal/PdfEncoders.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Internal/PdfEncoders.cs index 355ab378..31ad4021 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Internal/PdfEncoders.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Internal/PdfEncoders.cs @@ -35,22 +35,6 @@ public static Encoding WinAnsiEncoding { // We consistently use our own WinAnsiEncoding implementation in PDFsharp. get => _winAnsiEncoding ??= new AnsiEncoding(); - // #DELETE - // { - // if (_winAnsiEncoding == null) - // { - // //// Use own implementation because there is no ANSI encoding in .NET 6. - // //_winAnsiEncoding = new AnsiEncoding(); - //#if NET6_0_OR_GREATER___ // - // // There is ANSI encoding available with .NET 6. Use it. - // _winAnsiEncoding = CodePagesEncodingProvider.Instance.GetEncoding(1252)!; - //#else - // // StL 24-02-24: We are consistent on all platforms. - // _winAnsiEncoding = new AnsiEncoding(); - //#endif - // } - // return _winAnsiEncoding; - // } } static Encoding? _winAnsiEncoding; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs index c51bc051..82adadce 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs @@ -12,7 +12,8 @@ namespace PdfSharp.Pdf.Signatures public class DigitalSignatureOptions() { /// - /// Gets or sets the appearance handler that draws the visual representation of the signature in the PDF. + /// Gets or sets the appearance handler that draws the visual representation of the signature in the PDF.

+ /// This overrides the in ///
public IAnnotationAppearanceHandler? AppearanceHandler { get; init; } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index fb120ffd..897d3cfb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -149,64 +149,37 @@ internal async Task AddSignatureComponentsAsync() _signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, ByteRangePaddingLength, new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0)); var signatureDictionary = GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePdfArray); - var signatureField = GetSignatureField(signatureDictionary); - - var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots); - if (annotations == null) - Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); - else - annotations.Elements.Add(signatureField); // acroform - var catalog = Document.Catalog; - - if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) - catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); - - if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) - catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + var acroForm = Document.GetOrCreateAcroForm(); + if (!acroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) + acroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); else { - var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); + var sigFlagVersion = acroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); if (sigFlagVersion < 3) - catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); } - if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) - catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); - catalog.AcroForm.Fields.Elements.Add(signatureField); - } - - PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) - { - var signatureField = new PdfSignatureField(Document); - - signatureField.Elements.Add(PdfAcroField.Keys.V, signatureDic); - - // Annotation keys. - signatureField.Elements.Add(PdfAcroField.Keys.FT, new PdfName("/Sig")); - signatureField.Elements.Add(PdfAcroField.Keys.T, new PdfString("Signature1")); // TODO_OLD If already exists, will it cause error? implement a name chooser if yes. - signatureField.Elements.Add(PdfAcroField.Keys.Ff, new PdfInteger(132)); - signatureField.Elements.Add(PdfAcroField.Keys.DR, new PdfDictionary()); - signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot")); - signatureField.Elements.Add("/Subtype", new PdfName("/Widget")); - signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]); - - signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); - - signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + acroForm.AddSignatureField(signatureField => { - Location = Options.Location, - Reason = Options.Reason, - Signer = Signer.CertificateName - }; - // TODO_OLD Call RenderCustomAppearance(); here. - signatureField.PrepareForSave(); // TODO_OLD PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically. - - Document.Internals.AddObject(signatureField); - - return signatureField; + // Note: number-suffix will be added/incremented if a field with the same name already exist + signatureField.Name = "Signature1"; + signatureField.SetFlags = (PdfAcroFieldFlags)132; // TODO: what is that ? + signatureField.Value = signatureDictionary; + signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + { + Location = Options.Location, + Reason = Options.Reason, + Signer = Signer.CertificateName + }; + + signatureField.AddAnnotation(widget => + { + widget.AddToPage(Document.Pages[Options.PageIndex], new PdfRectangle(Options.Rectangle)); + }); + }); } PdfSignature2 GetSignatureDictionary(PdfSignaturePlaceholderItem contents, PdfArray byteRange) @@ -230,7 +203,6 @@ PdfSignature2 GetSignatureDictionary(PdfSignaturePlaceholderItem contents, PdfAr propertyItems.Elements.Add("/Name", String.IsNullOrWhiteSpace(Options.AppName) ? new PdfName("/PDFsharp http://www.pdfsharp.net") : - //new PdfName($"/{Options.AppName}")); // #DELETE PdfName.FromString(Options.AppName)); Document.Internals.AddObject(signatureDic); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/EntryInfoAttribute.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/EntryInfoAttribute.cs index 10a7d979..78dcf9b6 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/EntryInfoAttribute.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/EntryInfoAttribute.cs @@ -66,14 +66,19 @@ public KeyInfoAttribute(string version, KeyType keyType) KeyType = keyType; } - public KeyInfoAttribute(KeyType keyType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type objectType) + public KeyInfoAttribute(KeyType keyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type objectType) { //_version = version; KeyType = keyType; _objectType = objectType; } - public KeyInfoAttribute(string version, KeyType keyType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type objectType) + public KeyInfoAttribute(string version, + KeyType keyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type objectType) { //_version = version; KeyType = keyType; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysBase.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysBase.cs index af635e01..ed48d925 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysBase.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysBase.cs @@ -10,7 +10,10 @@ namespace PdfSharp.Pdf ///
public class KeysBase { - internal static DictionaryMeta CreateMeta([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type) => new(type); + internal static DictionaryMeta CreateMeta( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] + Type type) + => new(type); /// /// Creates the DictionaryMeta with the specified default type to return in DictionaryElements.GetValue @@ -19,7 +22,12 @@ public class KeysBase /// The type. /// Default type of the content key. /// Default type of the content. - internal static DictionaryMeta CreateMeta([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type, KeyType defaultContentKeyType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type defaultContentType) - => new(type, defaultContentKeyType, defaultContentType); + internal static DictionaryMeta CreateMeta( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] + Type type, + KeyType defaultContentKeyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type defaultContentType) + => new(type, defaultContentKeyType, defaultContentType); } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysMeta.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysMeta.cs index 650512d3..1c38d1ed 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysMeta.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/KeysMeta.cs @@ -162,7 +162,10 @@ public DictionaryMeta([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes /// The type. /// Default type of the content key. /// Default type of the content. - public DictionaryMeta([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type, KeyType defaultContentKeyType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type defaultContentType) : this(type) + public DictionaryMeta([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type, + KeyType defaultContentKeyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type defaultContentType) : this(type) { _defaultContentKeyDescriptor = new KeyDescriptor(new KeyInfoAttribute(defaultContentKeyType, defaultContentType)); } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs index 273e9f2f..afc150a5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs @@ -334,8 +334,8 @@ public string GetName(int index) } /// - /// Gets the PdfArray with the specified index, or null if no such object exists. If the index refers to - /// a reference, the referenced PdfArray is returned. + /// Gets the PdfDictionary with the specified index, or null if no such object exists. If the index refers to + /// a reference, the referenced PdfDictionary is returned. /// public PdfDictionary? GetDictionary(int index) => GetObject(index) as PdfDictionary; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs index 01996bad..d2acc070 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs @@ -582,26 +582,27 @@ public void SetName(string key, string value) /// public PdfRectangle GetRectangle(string key, bool create) { - var value = new PdfRectangle(); var obj = this[key]; if (obj == null) { if (create) - this[key] = value = new PdfRectangle(); - return value; + return (PdfRectangle)(this[key] = new PdfRectangle()); + return new(); } if (obj is PdfReference reference) obj = reference.Value; if (obj is PdfArray { Elements.Count: 4 } array) { - value = new PdfRectangle(array.Elements.GetReal(0), array.Elements.GetReal(1), - array.Elements.GetReal(2), array.Elements.GetReal(3)); - this[key] = value; + return (PdfRectangle)(this[key] = + new PdfRectangle(array.Elements.GetReal(0), array.Elements.GetReal(1), + array.Elements.GetReal(2), array.Elements.GetReal(3))); } - else - value = (PdfRectangle)obj; - return value; + + if (obj is PdfRectangle rectangle) + return rectangle; + + throw new InvalidOperationException($"PDF item is '{obj.GetType().FullName}', but PdfRectangle expected."); } /// @@ -623,28 +624,26 @@ public void SetRectangle(string key, PdfRectangle rect) /// If the value is not convertible, the function throws an InvalidCastException. public XMatrix GetMatrix(string key, bool create) { - var value = new XMatrix(); var obj = this[key]; if (obj == null) { if (create) - this[key] = new PdfLiteral("[1 0 0 1 0 0]"); // cannot be parsed, implement a PdfMatrix... - return value; + this[key] = new PdfLiteral("[1 0 0 1 0 0]"); // cannot be parsed, implement a PdfMatrix... + return XMatrix.Identity; } if (obj is PdfReference reference) obj = reference.Value; - value = obj switch + return obj switch { - PdfArray { Elements.Count: 6 } array - => new XMatrix(array.Elements.GetReal(0), - array.Elements.GetReal(1), array.Elements.GetReal(2), array.Elements.GetReal(3), - array.Elements.GetReal(4), array.Elements.GetReal(5)), + PdfArray { Elements.Count: 6 } array => + new(array.Elements.GetReal(0), array.Elements.GetReal(1), + array.Elements.GetReal(2), array.Elements.GetReal(3), + array.Elements.GetReal(4), array.Elements.GetReal(5)), PdfLiteral => throw new NotImplementedException("Parsing matrix from literal."), _ => throw new InvalidCastException("Element is not an array with 6 values.") }; - return value; } /// Converts the specified value to XMatrix. @@ -670,7 +669,7 @@ public DateTime GetDateTime(string key, DateTime defaultValue) if (obj == null) return defaultValue; - // TODO_OLD obj = PdfReference.GetValueIfReference(obj) + //PdfReference.Dereference(ref obj); if (obj is PdfReference reference) obj = reference.Value; @@ -739,12 +738,7 @@ internal void SetEnumAsName(string key, object value) /// public PdfItem? GetValue(string key, VCF options) { - // PdfDictionary? dict; - // PdfArray? array; var value = this[key]; - //if (value == null || - // value is PdfNull || - // value is PdfReference && ((PdfReference)value).Value is PdfNullObject) if (value is null or PdfNull or PdfReference { Value: PdfNullObject }) { if (options != VCF.None) @@ -891,7 +885,10 @@ internal void SetEnumAsName(string key, object value) return type; } - PdfArray CreateArray([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, PdfArray? oldArray) + PdfArray CreateArray( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type, + PdfArray? oldArray) { #if true PdfArray? array; @@ -953,7 +950,10 @@ PdfArray CreateArray([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes. #endif } - PdfDictionary CreateDictionary([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, PdfDictionary? oldDictionary) + PdfDictionary CreateDictionary( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type, + PdfDictionary? oldDictionary) { #if true ConstructorInfo? ctorInfo; @@ -1012,7 +1012,10 @@ PdfDictionary CreateDictionary([DynamicallyAccessedMembers(DynamicallyAccessedMe #endif } - PdfItem CreateValue([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, PdfDictionary? oldValue) + PdfItem CreateValue( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type, + PdfDictionary? oldValue) { #if true var ctorInfo = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, @@ -1100,10 +1103,7 @@ public void SetValue(string key, PdfItem value) /// Gets the PdfReference with the specified key, or null if no such object exists. ///
public PdfReference? GetReference(string key) - { - var item = this[key]; - return item as PdfReference; - } + => this[key] as PdfReference; /// /// Sets the entry to the specified object. The object must not be an indirect object, @@ -1111,7 +1111,8 @@ public void SetValue(string key, PdfItem value) /// public void SetObject(string key, PdfObject obj) { - if (obj.Reference is not null) + //if (obj.Reference is not null) + if (obj.IsIndirect) throw new ArgumentException("PdfObject must not be an indirect object.", nameof(obj)); this[key] = obj; } @@ -1122,7 +1123,8 @@ public void SetObject(string key, PdfObject obj) ///
public void SetReference(string key, PdfObject obj) { - if (obj.Reference is null) + //if (obj.Reference is null) + if (obj.IsIndirect is false) throw new ArgumentException("PdfObject must be an indirect object.", nameof(obj)); this[key] = obj.Reference; } @@ -1130,7 +1132,7 @@ public void SetReference(string key, PdfObject obj) /// /// Sets the entry as a reference to the specified iref. /// - public void SetReference(string key, PdfReference? iref) + public void SetReference(string key, PdfReference iref) { if (iref is null) throw new ArgumentNullException(nameof(iref)); @@ -1233,15 +1235,6 @@ public PdfItem? this[PdfName key] public bool Remove(KeyValuePair item) => throw new NotImplementedException(); - ///// - ///// Determines whether the dictionary contains the specified name. - ///// - //[Obsolete("Use ContainsKey.")] - //public bool Contains(string key) - //{ - // return _elements.ContainsKey(key); - //} - /// /// Determines whether the dictionary contains the specified name. /// @@ -1527,7 +1520,7 @@ public byte[] UnfilteredValue /// Otherwise, the content remains untouched and the function returns false. /// The function is useful for analyzing existing PDF files. ///
- [Obsolete("Not implemented. Use the function TryUncompress.")] + [Obsolete("Not correctly implemented. Use the function TryUncompress.")] public bool TryUnfilter() { // Keep old code for not break existing code. @@ -1633,18 +1626,10 @@ public override string ToString() var filter = _ownerDictionary.Elements["/Filter"]; if (filter != null) { -#if true var decodeParms = _ownerDictionary.Elements[Keys.DecodeParms]; var bytes = Filtering.Decode(_value, filter, decodeParms); - if (bytes != null) + if (bytes != null!) stream = PdfEncoders.RawEncoding.GetString(bytes, 0, bytes.Length); -#else - - if (_owner.Elements.GetString("/Filter") == "/FlateDecode") - { - stream = Filtering.FlateDecode.DecodeToString(_value); - } -#endif else throw new NotImplementedException("Unknown filter"); } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index 7e9db2ec..4cc9e740 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -484,6 +484,9 @@ internal override void PrepareForSave() } info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); + // Prepare AcroForm. Must occur BEFORE preparing the Fonts ! + Catalog.AcroForm?.PrepareForSave(); + // Prepare used fonts. _fontTable?.PrepareForSave(); @@ -502,7 +505,8 @@ internal override void PrepareForSave() // #PDF-UA // Create PdfMetadata now to include the final document information in XMP generation. - Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); + if (Options.CreateMetadata) + Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); } /// @@ -711,7 +715,52 @@ public PdfPageMode PageMode /// /// Get the AcroForm dictionary. /// - public PdfAcroForm AcroForm => Catalog.AcroForm; + public PdfAcroForm? AcroForm => Catalog.AcroForm; + + /// + /// Imports the fields from the specified into the current document.

+ /// If the current document does not contain an AcroForm, a new one is created automatically.

+ /// This method should be called after importing pages into the current document. + ///
+ /// The to import + /// A method that allows filtering the fields to import.

+ /// It receives the field to import from the remote AcroForm as a parameter.

+ /// When the method returns true, the field is imported, otherwise the field (and all childs of that field) is skipped + /// A method that allows modifying a field after it was imported.

+ /// It receives the original (remote) field and the imported (local) field as parameters. + /// + /// While importing, the new fields may be renamed, if a field with the same name is already present.

+ /// The new field receives a number-suffix in this case, starting at 2.

+ /// i.e. if the new field has the name myField and there is already a field myField present, + /// the new field is renamed to myField2.

+ /// If more fields with the same name are imported, the number-suffix will increase automatically.

+ /// This is useful when merging multiple versions of the same document. (e.g.with different field-values) + ///
+ public void ImportAcroForm(PdfAcroForm remoteForm, + Func? fieldFilter = null, + Action? fieldHandler = null) + { + var importer = new PdfAcroFormImporter(this); + importer.ImportAcroForm(remoteForm, fieldFilter, fieldHandler); + } + + /// + /// Gets the existing or creates a new one, if there is no in the current document + /// + /// The associated with this document + public PdfAcroForm GetOrCreateAcroForm() + { + var form = AcroForm; + if (form == null) + { + form = new PdfAcroForm(this); + IrefTable.Add(form); + if (form.Reference != null) + form.Reference.Document = this; + Catalog.AcroForm = form; + } + return form; + } /// /// Gets or sets the default language of the document. @@ -884,14 +933,12 @@ public void AddEmbeddedFile(string name, Stream stream) => Internals.Catalog.Names.AddEmbeddedFile(name, stream); /// - /// Flattens a document (make the fields non-editable). + /// Flattens the AcroField's widget annotations of this document.

+ /// Other annotations are unaffected. ///
- public void Flatten() + public void FlattenAcroForm() { - for (int idx = 0; idx < AcroForm.Fields.Count; idx++) - { - AcroForm.Fields[idx].ReadOnly = true; - } + AcroForm?.Flatten(); } /// @@ -978,9 +1025,9 @@ internal void EnsureNotYetSaved() return; var message = "The document was already saved and cannot be modified anymore. " + - "Saving a document converts its in memory representation into a PDF file or stream. " + + "Saving a document converts its in-memory representation into a PDF file or stream. " + "This can only be done once. " + - "After that process the in memory representation is outdated and protected against further modification."; + "After that process the in-memory representation is outdated and protected against further modification."; throw new InvalidOperationException(message); } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocumentOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocumentOptions.cs index fcc8df71..2708c1e4 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocumentOptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocumentOptions.cs @@ -81,5 +81,14 @@ public PdfUseFlateDecoderForJpegImages UseFlateDecoderForJpegImages set => _useFlateDecoderForJpegImages = value; } PdfUseFlateDecoderForJpegImages _useFlateDecoderForJpegImages = PdfUseFlateDecoderForJpegImages.Never; + + /// + /// Gets or sets a value indicating whether a metadata-stream in XMP-format should be created when saving the document.

+ /// The default value is true.

+ ///
+ /// + /// Should only be set to false if you prefer small files over PDF/A compliance. + /// + public bool CreateMetadata { get; set; } = true; } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs index be4a1d70..a7e80ba6 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs @@ -110,9 +110,10 @@ internal void SetObjectID(int objectNumber, int generationNumber) { // ReSharper disable once ObjectCreationAsStatement because the new object is set to this object // in the constructor of PdfReference. - new PdfReference(this); + //new PdfReference(this); + PdfReference.CreateFromObject(this, objectID, 0); Debug.Assert(_iref != null); - _iref.ObjectID = objectID; + //_iref.ObjectID = objectID; } _iref.Value = this; _iref.Document = _document; @@ -146,7 +147,7 @@ internal virtual PdfDocument Document /// Gets or sets the comment for debugging purposes. ///
public string Comment { get; set; } = ""; - + /// /// Indicates whether the object is an indirect object. /// @@ -169,7 +170,7 @@ internal virtual void PrepareForSave() /// /// Saves the stream position. 2nd Edition. /// - internal override void WriteObject(PdfWriter writer) + internal override void WriteObject(PdfWriter writer) => Debug.Assert(false, "Must not come here, WriteObject must be overridden in derived class."); /// @@ -364,7 +365,7 @@ static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject PdfDictionary? dict; PdfArray? array; - if ((dict = value as PdfDictionary) != null) + if ((dict = value as PdfDictionary) is not null) { // Case: The object is a dictionary. // Set document for cloned direct objects. @@ -418,13 +419,13 @@ static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject // The item is something else, e.g. a name. // Nothing to do. - // ...but let’s double check this case in DEBUG build. + // ...but let’s double-check this case in DEBUG build. DebugCheckNonObjects(item); } } } } - else if ((array = value as PdfArray) != null) + else if ((array = value as PdfArray) is not null) { // Case: The object is an array. // Set document for cloned direct objects. @@ -478,7 +479,7 @@ static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject // The item is something else, e.g. a name. // Nothing to do. - // ...but let’s double check this case in DEBUG build. + // ...but let’s double-check this case in DEBUG build. DebugCheckNonObjects(item); } } @@ -490,7 +491,7 @@ static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject // Indirect integers, booleans, etc. are allowed, but PDFsharp do not create them. // If such objects occur in imported PDF files from other producers, nothing more is to do. // The owner was already set, which is double-checked by the assertions below. - if (value is PdfNameObject or PdfStringObject or PdfBooleanObject /*or PdfIntegerObject*/ or PdfNumberObject) + if (value is PdfNameObject or PdfStringObject or PdfBooleanObject or PdfNumberObject) { Debug.Assert(value.IsIndirect); Debug.Assert(value.Owner == owner); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObjectID.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObjectID.cs index d4094da7..a932d743 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObjectID.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObjectID.cs @@ -1,6 +1,9 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using Microsoft.Extensions.Logging; +using PdfSharp.Logging; + namespace PdfSharp.Pdf { /// @@ -10,40 +13,56 @@ namespace PdfSharp.Pdf // ReSharper disable once InconsistentNaming public readonly struct PdfObjectID : IComparable { - /// - /// Initializes a new instance of the class. - /// - /// The object number. - public PdfObjectID(int objectNumber) - { - Debug.Assert(objectNumber >= 1, "Object number out of range."); - _objectNumber = objectNumber; - _generationNumber = 0; -#if DEBUG_ - // Just a place for a breakpoint during debugging. - if (objectNumber == 5894) - _ = typeof(int); -#endif - } + //// /// + //// /// Initializes a new instance of the class. + //// /// + //// /// The object number. + //// public PdfObjectID(int objectNumber) + //// { + //// Debug.Assert(objectNumber >= 1, "Object number out of range."); + //// _objectNumber = objectNumber; + //// _generationNumber = 0; + ////#if DEBUG_ + //// // Just a place for a breakpoint during debugging. + //// if (objectNumber == 5894) + //// _ = typeof(int); + ////#endif + //// } /// /// Initializes a new instance of the class. /// /// The object number. /// The generation number. - public PdfObjectID(int objectNumber, int generationNumber) + public PdfObjectID(int objectNumber, int generationNumber = 0) { Debug.Assert(objectNumber >= 1, "Object number out of range."); //Debug.Assert(generationNumber >= 0 && generationNumber <= 65535, "Generation number out of range."); -#if DEBUG_ - // iText creates generation numbers with a value of 65536... - if (generationNumber > 65535) - Debug.WriteLine(String.Format("Generation number: {0}", generationNumber)); -#endif + + if (objectNumber is < 1 or > 0x_7F_FF_FF) + { + // We do not break existing code. + PdfSharpLogHost.PdfReadingLogger.LogError("Object number '{ObjectNumber}' is out of range [1..8388608].", objectNumber); + // No high-performance logging because it is a rare case. + } + + if (generationNumber is <0 or > 0x_FF_FF) + { + // We do not break existing code. + // We found an iText document with generation numbers with a value of 65536... + PdfSharpLogHost.PdfReadingLogger.LogError("Generation number '{GenerationNumber}' is out of range [0..65535}.", generationNumber); + // No high-performance logging because it is a rare case. + } + _objectNumber = objectNumber; _generationNumber = (ushort)generationNumber; } + /// + /// Calculates a 64-bit unsigned integer from object and generation number. + /// + internal ulong UniqueNumber => ((ulong)_objectNumber << 32) + _generationNumber; + /// /// Gets or sets the object number. /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs index a1f4c8dc..9d451f45 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs @@ -53,7 +53,7 @@ internal void Initialize(bool setupSizeFromMediaBox) { // Setup page size from MediaBox. var rectangle = Elements.GetRectangle(InheritablePageKeys.MediaBox, false); - if (rectangle.IsEmpty) + if (rectangle.IsZero) throw new InvalidOperationException("Page has no MediaBox."); _width = XUnit.FromPoint(rectangle.X2 - rectangle.X1); @@ -180,7 +180,7 @@ public PageSize Size throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(PageSize)); var size = PageSizeConverter.ToSize(value); - MediaBox = new PdfRectangle(0, 0, size.Width, size.Height); + MediaBox = new(0, 0, size.Width, size.Height); } } @@ -191,14 +191,12 @@ public TrimMargins TrimMargins { get { - if (_trimMargins == default!) - _trimMargins = new TrimMargins(); + _trimMargins ??= new TrimMargins(); return _trimMargins; } set { - if (_trimMargins == default!) - _trimMargins = new TrimMargins(); + _trimMargins ??= new TrimMargins(); if (value != null!) { _trimMargins.Left = value.Left; @@ -210,7 +208,7 @@ public TrimMargins TrimMargins _trimMargins.All = XUnit.Zero; } } - TrimMargins _trimMargins = new TrimMargins(); + TrimMargins? _trimMargins; /// /// Gets or sets the media box directly. XGraphics is not prepared to work with a media box @@ -232,6 +230,16 @@ public PdfRectangle MediaBox } } + /// + /// Gets a value indicating whether a media box is set. + /// + public bool HasMediaBox => Elements[InheritablePageKeys.MediaBox] != null; + + /// + /// Gets a copy of the media box if it exists, or PdfRectangle.Empty if no media box is set. + /// + public PdfRectangle MediaBoxReadOnly => Elements.GetRectangle(InheritablePageKeys.MediaBox, false); + /// /// Gets or sets the crop box. /// @@ -241,6 +249,29 @@ public PdfRectangle CropBox set => Elements.SetRectangle(InheritablePageKeys.CropBox, value); } + /// + /// Gets a value indicating whether a crop box is set. + /// + public bool HasCropBox => Elements[InheritablePageKeys.CropBox] != null; + + /// + /// Gets a copy of the crop box if it exists, or PdfRectangle.Empty if no crop box is set. + /// + public PdfRectangle CropBoxReadOnly => Elements.GetRectangle(InheritablePageKeys.CropBox, false); + + /// + /// Gets a copy of the effective crop box if it exists, or PdfRectangle.Empty if neither crop box nor media box are set. + /// + public PdfRectangle EffectiveCropBoxReadOnly + { + get + { + if (HasCropBox) + return CropBox; + return MediaBoxReadOnly; + } + } + /// /// Gets or sets the bleed box. /// @@ -250,6 +281,29 @@ public PdfRectangle BleedBox set => Elements.SetRectangle(Keys.BleedBox, value); } + /// + /// Gets a value indicating whether a bleed box is set. + /// + public bool HasBleedBox => Elements[Keys.BleedBox] != null; + + /// + /// Gets a copy of the bleed box if it exists, or PdfRectangle.Empty if no bleed box is set. + /// + public PdfRectangle BleedBoxReadOnly => Elements.GetRectangle(Keys.BleedBox, false); + + /// + /// Gets a copy of the effective bleed box if it exists, or PdfRectangle.Empty if neither bleed box nor crop box nor media box are set. + /// + public PdfRectangle EffectiveBleedBoxReadOnly + { + get + { + if (HasBleedBox) + return BleedBox; + return EffectiveCropBoxReadOnly; + } + } + /// /// Gets or sets the art box. /// @@ -259,6 +313,29 @@ public PdfRectangle ArtBox set => Elements.SetRectangle(Keys.ArtBox, value); } + /// + /// Gets a value indicating whether an art box is set. + /// + public bool HasArtBox => Elements[Keys.ArtBox] != null; + + /// + /// Gets a copy of the art box if it exists, or PdfRectangle.Empty if no art box is set. + /// + public PdfRectangle ArtBoxReadOnly => Elements.GetRectangle(Keys.ArtBox, false); + + /// + /// Gets a copy of the effective art box if it exists, or PdfRectangle.Empty if neither art box nor crop box nor media box are set. + /// + public PdfRectangle EffectiveArtBoxReadOnly + { + get + { + if (HasArtBox) + return ArtBox; + return EffectiveCropBoxReadOnly; + } + } + /// /// Gets or sets the trim box. /// @@ -268,6 +345,29 @@ public PdfRectangle TrimBox set => Elements.SetRectangle(Keys.TrimBox, value); } + /// + /// Gets a value indicating whether a trim box is set. + /// + public bool HasTrimBox => Elements[Keys.TrimBox] != null; + + /// + /// Gets a copy of the trim box if it exists, or PdfRectangle.Empty if no trim box is set. + /// + public PdfRectangle TrimBoxReadOnly => Elements.GetRectangle(Keys.TrimBox, false); + + /// + /// Gets a copy of the effective trim box if it exists, or PdfRectangle.Empty if neither trim box nor crop box nor media box are set. + /// + public PdfRectangle EffectiveTrimBoxReadOnly + { + get + { + if (HasTrimBox) + return TrimBox; + return EffectiveCropBoxReadOnly; + } + } + /// /// Gets or sets the height of the page. /// If the page width is less than or equal to page height, the orientation is Portrait; @@ -820,15 +920,17 @@ internal static void InheritValues(PdfDictionary page, ref InheritedValues value internal override void PrepareForSave() { - if (_trimMargins.AreSet) + if (_trimMargins?.AreSet ?? false) { // These are the values InDesign set for an A4 page with 3mm crop margin at each edge. - // (recall that PDF rect are two points and NOT a point and a width) - // /MediaBox[0.0 0.0 612.283 858.898] 216 302.7 - // /CropBox[0.0 0.0 612.283 858.898] - // /BleedBox[0.0 0.0 612.283 858.898] - // /ArtBox[8.50394 8.50394 603.78 850.394] 3 3 213 300 - // /TrimBox[8.50394 8.50394 603.78 850.394] + // (recall that PDF rectangles are two points and NOT a point and width and height) + // /MediaBox[0.0 0.0 612.283 858.898] # in millimeter: (0 0) (216 302.7) + // /CropBox[0.0 0.0 612.283 858.898] # -------------- " --------------- + // /BleedBox[0.0 0.0 612.283 858.898] # -------------- " --------------- + // /ArtBox[8.50394 8.50394 603.78 850.394] # in millimeter: (3 3) (213 300) + // /TrimBox[8.50394 8.50394 603.78 850.394] # ------------- " -------------- + // + // An A4 page has a size of 210 x 297 mm² double width = _trimMargins.Left.Point + Width.Point + _trimMargins.Right.Point; double height = _trimMargins.Top.Point + Height.Point + _trimMargins.Bottom.Point; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfRectangle.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfRectangle.cs index da5019c2..1e6ef8fd 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfRectangle.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfRectangle.cs @@ -5,9 +5,9 @@ using System.Drawing; #endif #if WPF -using System.Windows.Media; +using System.Windows; #endif -using PdfSharp.Internal; +using System.CodeDom; using PdfSharp.Drawing; using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.IO; @@ -16,15 +16,16 @@ namespace PdfSharp.Pdf { /// - /// Represents a PDF rectangle value, that is internally an array with 4 real values. + /// Represents an immutable PDF rectangle value. + /// In a PDF file it is represented by an array of four real values. /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] public sealed class PdfRectangle : PdfItem { - // This class must behave like a value type. Therefore it cannot be changed (like System.String). + // This reference type must behave like a value type. Therefore, it cannot be changed (like System.String). /// - /// Initializes a new instance of the PdfRectangle class. + /// Initializes a new instance of the PdfRectangle class with all values set to zero. /// public PdfRectangle() { } @@ -37,10 +38,10 @@ public PdfRectangle() /// internal PdfRectangle(double x1, double y1, double x2, double y2) { - _x1 = x1; - _y1 = y1; - _x2 = x2; - _y2 = y2; + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; } #if GDI @@ -50,10 +51,24 @@ internal PdfRectangle(double x1, double y1, double x2, double y2) /// public PdfRectangle(PointF pt1, PointF pt2) { - _x1 = pt1.X; - _y1 = pt1.Y; - _x2 = pt2.X; - _y2 = pt2.Y; + X1 = pt1.X; + Y1 = pt1.Y; + X2 = pt2.X; + Y2 = pt2.Y; + } +#endif + +#if WPF + /// + /// Initializes a new instance of the PdfRectangle class with two points specifying + /// two diagonally opposite corners. + /// + public PdfRectangle(Point pt1, Point pt2) + { + X1 = pt1.X; + Y1 = pt1.Y; + X2 = pt2.X; + Y2 = pt2.Y; } #endif @@ -63,10 +78,10 @@ public PdfRectangle(PointF pt1, PointF pt2) /// public PdfRectangle(XPoint pt1, XPoint pt2) { - _x1 = pt1.X; - _y1 = pt1.Y; - _x2 = pt2.X; - _y2 = pt2.Y; + X1 = pt1.X; + Y1 = pt1.Y; + X2 = pt2.X; + Y2 = pt2.Y; } #if GDI @@ -75,10 +90,10 @@ public PdfRectangle(XPoint pt1, XPoint pt2) /// public PdfRectangle(PointF pt, SizeF size) { - _x1 = pt.X; - _y1 = pt.Y; - _x2 = pt.X + size.Width; - _y2 = pt.Y + size.Height; + X1 = pt.X; + Y1 = pt.Y; + X2 = pt.X + size.Width; + Y2 = pt.Y + size.Height; } #endif @@ -87,10 +102,10 @@ public PdfRectangle(PointF pt, SizeF size) ///
public PdfRectangle(XPoint pt, XSize size) { - _x1 = pt.X; - _y1 = pt.Y; - _x2 = pt.X + size.Width; - _y2 = pt.Y + size.Height; + X1 = pt.X; + Y1 = pt.Y; + X2 = pt.X + size.Width; + Y2 = pt.Y + size.Height; } /// @@ -98,10 +113,13 @@ public PdfRectangle(XPoint pt, XSize size) /// public PdfRectangle(XRect rect) { - _x1 = rect.X; - _y1 = rect.Y; - _x2 = rect.X + rect.Width; - _y2 = rect.Y + rect.Height; + if (rect.IsEmpty) + throw new InvalidOperationException("Cannot create PdfRectangle from an empty XRect."); + + X1 = rect.X; + Y1 = rect.Y; + X2 = rect.X + rect.Width; + Y2 = rect.Y + rect.Height; } /// @@ -119,46 +137,40 @@ internal PdfRectangle(PdfItem item) if (array == null) throw new InvalidOperationException(PsMsgs.UnexpectedTokenInPdfFile); - _x1 = array.Elements.GetReal(0); - _y1 = array.Elements.GetReal(1); - _x2 = array.Elements.GetReal(2); - _y2 = array.Elements.GetReal(3); + X1 = array.Elements.GetReal(0); + Y1 = array.Elements.GetReal(1); + X2 = array.Elements.GetReal(2); + Y2 = array.Elements.GetReal(3); } /// /// Clones this instance. /// - public new PdfRectangle Clone() - => (PdfRectangle)Copy(); + public new PdfRectangle Clone() => (PdfRectangle)Copy(); /// /// Implements cloning this instance. /// - protected override object Copy() - { - PdfRectangle rect = (PdfRectangle)base.Copy(); - return rect; - } + protected override object Copy() => (PdfRectangle)base.Copy(); /// /// Tests whether all coordinates are zero. /// - public bool IsEmpty => _x1 == 0 && _y1 == 0 && _x2 == 0 && _y2 == 0; + [Obsolete("Use 'IsZero' instead.")] + public bool IsEmpty => IsZero; + + /// + /// Tests whether all coordinates are zero. + /// + public bool IsZero => X1 == 0 && Y1 == 0 && X2 == 0 && Y2 == 0; /// /// Tests whether the specified object is a PdfRectangle and has equal coordinates. /// - public override bool Equals(object? obj) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - if (obj is PdfRectangle rectangle) - { - var rect = rectangle; - return rect._x1 == _x1 && rect._y1 == _y1 && rect._x2 == _x2 && rect._y2 == _y2; - } - return false; - // ReSharper restore CompareOfFloatsByEqualityOperator - } + // ReSharper disable CompareOfFloatsByEqualityOperator + public override bool Equals(object? obj) => + obj is PdfRectangle rect && rect.X1 == X1 && rect.Y1 == Y1 && rect.X2 == X2 && rect.Y2 == Y2; + // ReSharper restore CompareOfFloatsByEqualityOperator /// /// Serves as a hash function for a particular type. @@ -166,10 +178,10 @@ public override bool Equals(object? obj) public override int GetHashCode() { // This code is from System.Drawing... - return (int)(((((uint)_x1) ^ ((((uint)_y1) << 13) | - (((uint)_y1) >> 0x13))) ^ ((((uint)_x2) << 0x1a) | - (((uint)_x2) >> 6))) ^ ((((uint)_y2) << 7) | - (((uint)_y2) >> 0x19))); + return (int)(((((uint)X1) ^ ((((uint)Y1) << 13) | + (((uint)Y1) >> 0x13))) ^ ((((uint)X2) << 0x1a) | + (((uint)X2) >> 6))) ^ ((((uint)Y2) << 7) | + (((uint)Y2) >> 0x19))); } /// @@ -182,7 +194,7 @@ public override int GetHashCode() if ((object?)left != null) { if ((object?)right != null) - return left._x1 == right._x1 && left._y1 == right._y1 && left._x2 == right._x2 && left._y2 == right._y2; + return left.X1 == right.X1 && left.Y1 == right.Y1 && left.X2 == right.X2 && left.Y2 == right.Y2; return false; } return (object?)right == null; @@ -192,122 +204,94 @@ public override int GetHashCode() /// /// Tests whether two structures differ in one or more coordinates. /// - public static bool operator !=(PdfRectangle? left, PdfRectangle? right) - { - return !(left == right); - } + public static bool operator !=(PdfRectangle? left, PdfRectangle? right) => !(left == right); /// /// Gets or sets the x-coordinate of the first corner of this PdfRectangle. /// - public double X1 => _x1; - - readonly double _x1; + public double X1 { get; } /// /// Gets or sets the y-coordinate of the first corner of this PdfRectangle. /// - public double Y1 => _y1; - - readonly double _y1; + public double Y1 { get; } /// /// Gets or sets the x-coordinate of the second corner of this PdfRectangle. /// - public double X2 => _x2; - - readonly double _x2; + public double X2 { get; } /// /// Gets or sets the y-coordinate of the second corner of this PdfRectangle. /// - public double Y2 => _y2; - - readonly double _y2; + public double Y2 { get; } /// /// Gets X2 - X1. /// - public double Width => _x2 - _x1; + public double Width => X2 - X1; /// /// Gets Y2 - Y1. /// - public double Height => _y2 - _y1; + public double Height => Y2 - Y1; /// /// Gets or sets the coordinates of the first point of this PdfRectangle. /// - public XPoint Location => new XPoint(_x1, _y1); + public XPoint Location => new(X1, Y1); /// /// Gets or sets the size of this PdfRectangle. /// - public XSize Size => new XSize(_x2 - _x1, _y2 - _y1); + public XSize Size => new(X2 - X1, Y2 - Y1); #if GDI /// /// Determines if the specified point is contained within this PdfRectangle. /// - public bool Contains(PointF pt) - { - return Contains(pt.X, pt.Y); - } + public bool Contains(PointF pt) => Contains(pt.X, pt.Y); #endif /// /// Determines if the specified point is contained within this PdfRectangle. /// - public bool Contains(XPoint pt) - { - return Contains(pt.X, pt.Y); - } + public bool Contains(XPoint pt) => Contains(pt.X, pt.Y); /// /// Determines if the specified point is contained within this PdfRectangle. /// - public bool Contains(double x, double y) - { + public bool Contains(double x, double y) => // Treat rectangle inclusive/inclusive. - return _x1 <= x && x <= _x2 && _y1 <= y && y <= _y2; - } + X1 <= x && x <= X2 && Y1 <= y && y <= Y2; #if GDI /// /// Determines if the rectangular region represented by rect is entirely contained within this PdfRectangle. /// - public bool Contains(RectangleF rect) - { - return _x1 <= rect.X && (rect.X + rect.Width) <= _x2 && - _y1 <= rect.Y && (rect.Y + rect.Height) <= _y2; - } + public bool Contains(RectangleF rect) => + X1 <= rect.X && (rect.X + rect.Width) <= X2 && + Y1 <= rect.Y && (rect.Y + rect.Height) <= Y2; #endif /// /// Determines if the rectangular region represented by rect is entirely contained within this PdfRectangle. /// - public bool Contains(XRect rect) - { - return _x1 <= rect.X && (rect.X + rect.Width) <= _x2 && - _y1 <= rect.Y && (rect.Y + rect.Height) <= _y2; - } + public bool Contains(XRect rect) => + X1 <= rect.X && (rect.X + rect.Width) <= X2 && + Y1 <= rect.Y && (rect.Y + rect.Height) <= Y2; /// /// Determines if the rectangular region represented by rect is entirely contained within this PdfRectangle. /// - public bool Contains(PdfRectangle rect) - { - return _x1 <= rect._x1 && rect._x2 <= _x2 && - _y1 <= rect._y1 && rect._y2 <= _y2; - } + public bool Contains(PdfRectangle rect) => + X1 <= rect.X1 && rect.X2 <= X2 && + Y1 <= rect.Y1 && rect.Y2 <= Y2; /// /// Returns the rectangle as an XRect object. /// - public XRect ToXRect() - { - return new XRect(_x1, _y1, Width, Height); - } + public XRect ToXRect() => new(X1, Y1, Width, Height); /// /// Returns the rectangle as a string in the form «[x1 y1 x2 y2]». @@ -315,16 +299,19 @@ public XRect ToXRect() public override string ToString() { const string format = Config.SignificantDecimalPlaces3; - return PdfEncoders.Format("[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "}]", _x1, _y1, _x2, _y2); + return PdfEncoders.Format("[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "}]", X1, Y1, X2, Y2); } /// /// Writes the rectangle. /// - internal override void WriteObject(PdfWriter writer) - { - writer.Write(this); - } + internal override void WriteObject(PdfWriter writer) => writer.Write(this); + + /// + /// Represents an empty PdfRectangle. + /// + [Obsolete("A rectangle defined by two points cannot be meaningfully defined as empty. Do not use this property.")] + public static PdfRectangle Empty => throw new InvalidOperationException("Use 'new PdfRectangle()' instead of 'PdfRectangle.Empty'"); /// /// Gets the DebuggerDisplayAttribute text. @@ -337,107 +324,8 @@ string DebuggerDisplay { const string format = Config.SignificantDecimalPlaces10; return String.Format(CultureInfo.InvariantCulture, - "X1={0:" + format + "}, Y1={1:" + format + "}, X2={2:" + format + "}, Y2={3:" + format + "}", _x1, _y1, _x2, _y2); + "X1={0:" + format + "}, Y1={1:" + format + "}, X2={2:" + format + "}, Y2={3:" + format + "}", X1, Y1, X2, Y2); } } - -#if false // This object is considered as immutable. - // /// - // /// Adjusts the location of this PdfRectangle by the specified amount. - // /// - // public void Offset(PointF pos) - // { - // Offset(pos.X, pos.Y); - // } - // - // /// - // /// Adjusts the location of this PdfRectangle by the specified amount. - // /// - // public void Offset(double x, double y) - // { - // _x1 += x; - // _y1 += y; - // _x2 += x; - // _y2 += y; - // } - // - // /// - // /// Inflates this PdfRectangle by the specified amount. - // /// - // public void Inflate(double x, double y) - // { - // _x1 -= x; - // _y1 -= y; - // _x2 += x; - // _y2 += y; - // } - // - // /// - // /// Inflates this PdfRectangle by the specified amount. - // /// - // public void Inflate(SizeF size) - // { - // Inflate(size.Width, size.Height); - // } - // - // /// - // /// Creates and returns an inflated copy of the specified PdfRectangle. - // /// - // public static PdfRectangle Inflate(PdfRectangle rect, double x, double y) - // { - // rect.Inflate(x, y); - // return rect; - // } - // - // /// - // /// Replaces this PdfRectangle with the intersection of itself and the specified PdfRectangle. - // /// - // public void Intersect(PdfRectangle rect) - // { - // PdfRectangle rect2 = PdfRectangle.Intersect(rect, this); - // _x1 = rect2.x1; - // _y1 = rect2.y1; - // _x2 = rect2.x2; - // _y2 = rect2.y2; - // } - // - // /// - // /// Returns a PdfRectangle that represents the intersection of two rectangles. If there is no intersection, - // /// an empty PdfRectangle is returned. - // /// - // public static PdfRectangle Intersect(PdfRectangle rect1, PdfRectangle rect2) - // { - // double xx1 = Math.Max(rect1.x1, rect2.x1); - // double xx2 = Math.Min(rect1.x2, rect2.x2); - // double yy1 = Math.Max(rect1.y1, rect2.y1); - // double yy2 = Math.Min(rect1.y2, rect2.y2); - // if (xx2 >= xx1 && yy2 >= yy1) - // return new PdfRectangle(xx1, yy1, xx2, yy2); - // return PdfRectangle.Empty; - // } - // - // /// - // /// Determines if this rectangle intersects with the specified PdfRectangle. - // /// - // public bool IntersectsWith(PdfRectangle rect) - // { - // return rect.x1 < _x2 && _x1 < rect.x2 && rect.y1 < _y2 && _y1 < rect.y2; - // } - // - // /// - // /// Creates the smallest rectangle that can contain both of two specified rectangles. - // /// - // public static PdfRectangle Union(PdfRectangle rect1, PdfRectangle rect2) - // { - // return new PdfRectangle( - // Math.Min(rect1.x1, rect2.x1), Math.Max(rect1.x2, rect2.x2), - // Math.Min(rect1.y1, rect2.y1), Math.Max(rect1.y2, rect2.y2)); - // } -#endif - - /// - /// Represents an empty PdfRectangle. - /// - public static readonly PdfRectangle Empty = new PdfRectangle(); } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs index d5654aed..a05fbdff 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs @@ -47,7 +47,7 @@ public enum PdfStringEncoding /// /// Not yet used by PDFsharp. /// - MacRomanEncoding = PdfStringFlags.MacExpertEncoding, + MacRomanEncoding = PdfStringFlags.MacRomanEncoding, /// /// Not yet used by PDFsharp. @@ -68,12 +68,12 @@ public enum PdfStringEncoding enum PdfStringFlags { // ReSharper disable InconsistentNaming - RawEncoding = 0x00, - StandardEncoding = 0x01, // not used by PDFsharp + RawEncoding = 0x00, // Each char maps to a byte. + StandardEncoding = 0x01, // Not used by PDFsharp. PDFDocEncoding = 0x02, WinAnsiEncoding = 0x03, - MacRomanEncoding = 0x04, // not used by PDFsharp - MacExpertEncoding = 0x05, // not used by PDFsharp + MacRomanEncoding = 0x04, // Not used by PDFsharp. + MacExpertEncoding = 0x05, // Not used by PDFsharp. Unicode = 0x06, EncodingMask = 0x0F, diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs index dff4e2fe..86dfcc40 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs @@ -1,6 +1,8 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if false // DELETE 2025-12-31 - PDF has no explicit unsigned number type. + using PdfSharp.Pdf.IO; namespace PdfSharp.Pdf @@ -160,3 +162,4 @@ public uint ToUInt32(IFormatProvider? provider) #endregion } } +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUIntegerObject.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUIntegerObject.cs index 28964439..19e18aeb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUIntegerObject.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUIntegerObject.cs @@ -1,6 +1,7 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if false // DELETE 2025-12-31 - PDF has no explicit unsigned number type. using PdfSharp.Pdf.IO; namespace PdfSharp.Pdf @@ -61,3 +62,4 @@ internal override void WriteObject(PdfWriter writer) } } } +#endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/enums/PdfFontEmbedding.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/enums/PdfFontEmbedding.cs index 0253612f..1f3aeea7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/enums/PdfFontEmbedding.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/enums/PdfFontEmbedding.cs @@ -1,6 +1,9 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Fonts.StandardFonts; + namespace PdfSharp.Pdf { /// @@ -34,6 +37,16 @@ public enum PdfFontEmbedding [Obsolete("Renamed to EmbedCompleteFontFile.")] Always = 1, + /// + /// No embedding if the font is one of the 14 standard-fonts defined in the PDF-specification. (PDF 1.0 to PDF 1.7)

+ /// If the font is not one of the standard-fonts, an exception is raised when creating an and specifying this value.

+ /// See also + ///
+ /// + /// This value should not be used, unless you prefer small files over PDF/A compliance. + /// + OmitStandardFont = 2, + /// /// Fonts are not embedded. This is not an option anymore. /// Treated as Automatic. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj index 6fb9e5af..9de9991c 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj @@ -67,4 +67,19 @@ + + + True + True + FontResources.resx + + + + + + ResXFileCodeGenerator + FontResources.Designer.cs + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Properties/GlobalDeclarations.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Properties/GlobalDeclarations.cs index 6d44c245..3b6d8a7d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Properties/GlobalDeclarations.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Properties/GlobalDeclarations.cs @@ -19,7 +19,7 @@ [assembly: ComVisible(false)] [assembly: SuppressMessage("LoggingGenerator", "SYSLIB1006:Multiple logging methods cannot use the same event ID within a class", - Justification = "We use logging event ids as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] + Justification = "We use logging event IDs as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] // TODO_OLD We should add a WPF Preview panel //#if WPF diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Properties/PdfSharpProductVersionInformation.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Properties/PdfSharpProductVersionInformation.cs index ab3cbcb2..d989e384 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Properties/PdfSharpProductVersionInformation.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Properties/PdfSharpProductVersionInformation.cs @@ -1,7 +1,7 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -#pragma warning disable 0436 +//#pragma warning disable 0436 namespace PdfSharp { @@ -27,43 +27,43 @@ public static class PdfSharpProductVersionInformation /// /// The major version number of the product. /// - public static readonly string VersionMajor = GitVersionInformation.Major; + public static readonly string VersionMajor = PdfSharpGitVersionInformation.Major; /// /// The minor version number of the product. /// - public static readonly string VersionMinor = GitVersionInformation.Minor; + public static readonly string VersionMinor = PdfSharpGitVersionInformation.Minor; /// /// The patch number of the product. /// - public static readonly string VersionPatch = GitVersionInformation.Patch; + public static readonly string VersionPatch = PdfSharpGitVersionInformation.Patch; /// /// The Version PreRelease string for NuGet. /// - public static readonly string VersionPreRelease = GitVersionInformation.NuGetPreReleaseTagV2; - + public static readonly string VersionPreRelease = PdfSharpGitVersionInformation.PreReleaseLabel; + /// /// The PDF creator application information string. /// - public static readonly string Creator = $"{Title} {GitVersionInformation.NuGetVersion}{Technology}"; + public static readonly string Creator = $"{Title} {PdfSharpGitVersionInformation.InformationalVersion}{Technology}"; /// /// The PDF producer (created by) information string. /// TODO_OLD: Called Creator in MigraDoc??? /// - public static readonly string Producer = $"{Title} {GitVersionInformation.NuGetVersion} ({Url})"; + public static readonly string Producer = $"{Title} {PdfSharpGitVersionInformation.InformationalVersion} ({Url})"; /// /// The full version number. /// - public static readonly string Version = GitVersionInformation.MajorMinorPatch; + public static readonly string Version = PdfSharpGitVersionInformation.MajorMinorPatch; /// /// The full semantic version number created by GitVersion. /// - public static readonly string SemanticVersion = GitVersionInformation.SemVer; + public static readonly string SemanticVersion = PdfSharpGitVersionInformation.SemVer; /// /// The home page of this product. @@ -88,7 +88,7 @@ public static class PdfSharpProductVersionInformation /// /// The copyright information. /// - public const string Copyright = "Copyright © 2005-2024 empira Software GmbH."; + public const string Copyright = "Copyright © 2005-2025 empira Software GmbH."; /// /// The trademark of the product. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/D050000L.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/D050000L.ttf new file mode 100644 index 00000000..64afd29e Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/D050000L.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Bold.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Bold.ttf new file mode 100644 index 00000000..63b641cb Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Bold.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-BoldItalic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-BoldItalic.ttf new file mode 100644 index 00000000..77fc730d Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-BoldItalic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Italic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Italic.ttf new file mode 100644 index 00000000..350a02aa Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Italic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Regular.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Regular.ttf new file mode 100644 index 00000000..a5c7ef63 Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusMonoPS-Regular.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Bold.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Bold.ttf new file mode 100644 index 00000000..e6f28674 Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Bold.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-BoldItalic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-BoldItalic.ttf new file mode 100644 index 00000000..2251994c Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-BoldItalic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Italic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Italic.ttf new file mode 100644 index 00000000..b31bf3c4 Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Italic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Regular.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Regular.ttf new file mode 100644 index 00000000..807d6e4f Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusRoman-Regular.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Bold.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Bold.ttf new file mode 100644 index 00000000..d3645376 Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Bold.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-BoldItalic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-BoldItalic.ttf new file mode 100644 index 00000000..1058bee5 Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-BoldItalic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Italic.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Italic.ttf new file mode 100644 index 00000000..b09a597d Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Italic.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Regular.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Regular.ttf new file mode 100644 index 00000000..34945b7a Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/NimbusSans-Regular.ttf differ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Resources/StandardSymbolsPS.ttf b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/StandardSymbolsPS.ttf new file mode 100644 index 00000000..c3e0738e Binary files /dev/null and b/src/foundation/src/PDFsharp/src/PdfSharp/Resources/StandardSymbolsPS.ttf differ diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests-gdi/PdfSharp.Tests-gdi.csproj b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests-gdi/PdfSharp.Tests-gdi.csproj index cb0eeb3f..a6b39f99 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests-gdi/PdfSharp.Tests-gdi.csproj +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests-gdi/PdfSharp.Tests-gdi.csproj @@ -29,6 +29,7 @@ + diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFieldTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFieldTests.cs new file mode 100644 index 00000000..99171d10 --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFieldTests.cs @@ -0,0 +1,434 @@ +using FluentAssertions; +using PdfSharp.Drawing; +using PdfSharp.Fonts; +using PdfSharp.Fonts.StandardFonts; +using PdfSharp.Pdf; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Annotations.enums; +using Xunit; + +namespace PdfSharp.Tests.AcroForms +{ + public class AcroFieldTests + { + [Fact] + public void Added_Fields_Are_Present() + { + var doc = new PdfDocument(); + var acroForm = doc.GetOrCreateAcroForm(); + + acroForm.Should().NotBeNull(); + acroForm.Fields.Elements.Count.Should().Be(0); + acroForm.AddTextField(field => + { + field.Name = "text"; + }); + acroForm.Fields.Elements.Count.Should().Be(1); + } + + [Fact] + public void TextFieldValue() + { + var doc = CreateTestDocument(); + var field = GetAllFields(doc).FirstOrDefault(f => f.FullyQualifiedName == "FirstName"); + field.Should().NotBeNull(); + field?.GetType().Should().Be(); + var textField = field as PdfTextField; + textField.Should().NotBeNull(); + if (textField != null) + { + textField.Value.Should().BeEmpty(); + textField.Text.Should().BeEmpty(); + + const string theValue = "TestText"; + textField.Text = theValue; + + textField.Text.Should().Be(theValue); + textField.Value.ToString().Should().Be(theValue); + + // reset to null + textField.Text = null!; + + textField.Text?.Should().BeEmpty(); + textField.Value.Should().BeEmpty(); + } + } + + [Fact] + public void CheckBoxValue() + { + var doc = CreateTestDocument(); + var field = GetAllFields(doc).FirstOrDefault(f => f.FullyQualifiedName == "Interest_cooking"); + field.Should().NotBeNull(); + field?.GetType().Should().Be(); + var checkBoxField = field as PdfCheckBoxField; + checkBoxField.Should().NotBeNull(); + if (checkBoxField != null) + { + checkBoxField.Checked.Should().BeFalse(); + checkBoxField.Value.Should().Be("Off"); + + checkBoxField.Checked = true; + + checkBoxField.Checked.Should().BeTrue(); + checkBoxField.Value.Should().Be("Yes"); + + checkBoxField.Checked = false; + + checkBoxField.Checked.Should().BeFalse(); + checkBoxField.Value.Should().Be("Off"); + + checkBoxField.Value = "Yes"; + checkBoxField.Checked.Should().BeTrue(); + + checkBoxField.Value = "Off"; + checkBoxField.Checked.Should().BeFalse(); + + Action act = () => checkBoxField.Value = "Nope"; // invalid value + act.Should().Throw(); + } + } + + [Fact] + public void RadioButtonValue() + { + var doc = CreateTestDocument(); + var field = GetAllFields(doc).FirstOrDefault(f => f.FullyQualifiedName == "Group_Gender.Gender"); + field.Should().NotBeNull(); + field?.GetType().Should().Be(); + var radioButtonField = field as PdfRadioButtonField; + radioButtonField.Should().NotBeNull(); + if (radioButtonField != null) + { + radioButtonField.Value.Should().Be("Off"); + radioButtonField.SelectedIndex.Should().Be(-1); + radioButtonField.Options.Should().Equal(new[] { "male", "female", "unspecified" }); + radioButtonField.ExportValues.Should().Equal(new[] { "male", "female", "unspecified" }); + + radioButtonField.SelectedIndex = 0; + radioButtonField.SelectedIndex.Should().Be(0); + radioButtonField.Value?.Should().Be("male"); + + radioButtonField.SelectedIndex = 1; + radioButtonField.SelectedIndex.Should().Be(1); + radioButtonField.Value?.Should().Be("female"); + + radioButtonField.SelectedIndex = 2; + radioButtonField.SelectedIndex.Should().Be(2); + radioButtonField.Value?.Should().Be("unspecified"); + + radioButtonField.SelectedIndex = -1; + radioButtonField.SelectedIndex.Should().Be(-1); + radioButtonField.Value?.Should().Be("Off"); + + radioButtonField.Value = "male"; + radioButtonField.SelectedIndex.Should().Be(0); + radioButtonField.Value?.Should().Be("male"); + + radioButtonField.Value = "female"; + radioButtonField.SelectedIndex.Should().Be(1); + radioButtonField.Value?.Should().Be("female"); + + radioButtonField.Value = "unspecified"; + radioButtonField.SelectedIndex.Should().Be(2); + radioButtonField.Value?.Should().Be("unspecified"); + + radioButtonField.Value = null!; + radioButtonField.SelectedIndex.Should().Be(-1); + radioButtonField.Value?.Should().Be("Off"); + + Action act = () => radioButtonField.Value = "Nope"; // invalid value + act.Should().Throw(); + + act = () => radioButtonField.SelectedIndex = 10; + act.Should().Throw(); + } + } + + [Fact] + public void ComboBoxValue() + { + var doc = CreateTestDocument(); + var field = GetAllFields(doc).FirstOrDefault(f => f.FullyQualifiedName == "SelectedNumber"); + field.Should().NotBeNull(); + field?.GetType().Should().Be(); + var comboBoxField = field as PdfComboBoxField; + comboBoxField.Should().NotBeNull(); + if (comboBoxField != null) + { + comboBoxField.SelectedIndex.Should().Be(-1); + comboBoxField.Value.Should().BeEmpty(); + comboBoxField.Options.Should().Equal(new[] { "One", "Two", "Three", "Four", "Five" }); + + comboBoxField.SelectedIndex = 0; + comboBoxField.SelectedIndex.Should().Be(0); + comboBoxField.Value.Should().Be("One"); + + comboBoxField.SelectedIndex = 4; + comboBoxField.SelectedIndex.Should().Be(4); + comboBoxField.Value.Should().Be("Five"); + + comboBoxField.Value = "One"; + comboBoxField.SelectedIndex.Should().Be(0); + comboBoxField.Value.Should().Be("One"); + + comboBoxField.Value = "Five"; + comboBoxField.SelectedIndex.Should().Be(4); + comboBoxField.Value.Should().Be("Five"); + + comboBoxField.SelectedIndex = -1; + comboBoxField.SelectedIndex.Should().Be(-1); + comboBoxField.Value.Should().BeEmpty(); + + // invalid value + Action act = () => comboBoxField.Value = "Ten"; + act.Should().Throw(); + + act = () => comboBoxField.SelectedIndex = 10; + act.Should().Throw(); + } + } + + [Fact] + public void ListBoxValue() + { + var doc = CreateTestDocument(); + var field = GetAllFields(doc).FirstOrDefault(f => f.FullyQualifiedName == "SelectedColor"); + field.Should().NotBeNull(); + field?.GetType().Should().Be(); + var listBoxField = field as PdfListBoxField; + listBoxField.Should().NotBeNull(); + if (listBoxField != null) + { + listBoxField.SelectedIndices.Should().BeEmpty(); + listBoxField.Value.Should().BeEmpty(); + listBoxField.Options.Should().Equal(new[] { "Blue", "Red", "Green", "Black", "White" }); + + listBoxField.SelectedIndices = new[] { 0 }; + listBoxField.SelectedIndices.Should().Equal(new[] { 0 }); + listBoxField.Value.Should().Equal(new[] { "Blue" }); + + listBoxField.SelectedIndices = new[] { 0, 4 }; + listBoxField.SelectedIndices.Should().Equal(new[] { 0, 4 }); + listBoxField.Value.Should().Equal(new[] { "Blue", "White" }); + + listBoxField.SelectedIndices = new[] { 1, 3 }; + listBoxField.SelectedIndices.Should().Equal(new[] { 1, 3 }); + listBoxField.Value.Should().Equal(new[] { "Red", "Black" }); + + listBoxField.Value = Array.Empty(); + listBoxField.SelectedIndices.Should().BeEmpty(); + listBoxField.Value.Should().BeEmpty(); + + listBoxField.Value = new[] { "Green", "Blue" }; + listBoxField.SelectedIndices.Should().Equal(new[] { 0, 2 }); + listBoxField.Value.Should().Equal(new[] { "Blue", "Green" }); + + listBoxField.Value = null!; + listBoxField.SelectedIndices.Should().BeEmpty(); + listBoxField.Value.Should().BeEmpty(); + + listBoxField.Value = new[] { "Green" }; + listBoxField.SelectedIndices.Should().Equal(new[] { 2 }); + listBoxField.Value.Should().Equal(new[] { "Green" }); + + listBoxField.SelectedIndices = null!; + listBoxField.SelectedIndices.Should().BeEmpty(); + listBoxField.Value.Should().BeEmpty(); + + Action act = () => listBoxField.SelectedIndices = new[] { 1, 10 }; // invalid index + act.Should().Throw(); + + act = () => listBoxField.Value = new[] { "Red", "Orange" }; // Orange does not exist + act.Should().Throw(); + } + } + + private static PdfDocument CreateTestDocument() + { + // code copied from AcroFormsTests with minimal modifications + + // we use one of the 14 standard-fonts here (Helvetica) + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var document = new PdfDocument(); + var page1 = document.AddPage(); + var page2 = document.AddPage(); + var acroForm = document.GetOrCreateAcroForm(); + var textFont = new XFont(StandardFontNames.Helvetica, 12, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.EmbedCompleteFontFile)); + + double x = 40, y = 80; + var page1Renderer = XGraphics.FromPdfPage(page1); + var page2Renderer = XGraphics.FromPdfPage(page2); + page1Renderer.DrawString("Name of Subject", textFont, XBrushes.Black, x, y); + page2Renderer.DrawString("For Demo purposes. Modify the fields and observe the field on the first page is modified as well.", + textFont, XBrushes.Black, x, y); + + y += 10; + // Text fields + var firstNameField = acroForm.AddTextField(field => + { + // Note: Chromium-based browsers (ie. Edge/Chrome) do not render fields without a name + field.Name = "FirstName"; + field.Font = textFont; + field.ForeColor = XColors.DarkRed; + //field.Text = string.Empty; + // place annotation on both pages + // if the document is opened in a PdfReader and one of the Annotations is changed (e.g. by typing inside it), + // the other Annotation will be changed as well (as they belong to the same field) + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 100, 20))); + }); + field.AddAnnotation(annot => + { + // Note: The border is currently always solid and 1 unit wide + annot.BorderColor = XColors.Green; + annot.BackColor = XColors.DarkGray; + annot.AddToPage(page2, new PdfRectangle(new XRect(x, y, 100, 20))); + }); + }); + var lastNameField = acroForm.AddTextField(field => + { + field.Name = "LastName"; + field.Font = textFont; + //field.Text = string.Empty; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 10 + 100, y, 100, 20))); + }); + field.AddAnnotation(annot => + { + annot.AddToPage(page2, new PdfRectangle(new XRect(x + 10 + 100, y, 100, 20))); + }); + }); + + y += 40; + // Checkbox fields + page1Renderer.DrawString("Subject's interests", textFont, XBrushes.Black, x, y); + y += 10; + var cbx1 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_cooking"; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Cooking", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + var cbx2 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_coding"; + //field.Checked = true; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Coding", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + var cbx3 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_cycling"; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Cycling", textFont, XBrushes.Black, x + 20, y + 10); + + y += 40; + // RadioButton fields + page1Renderer.DrawString("Subject's gender", textFont, XBrushes.Black, x, y); + y += 10; + // used as parent-field for the radio-button (testing field-nesting) + var groupGender = acroForm.AddGenericField(field => + { + field.Name = "Group_Gender"; + }); + acroForm.AddRadioButtonField(field => + { + field.Name = "Gender"; + // add individual buttons + field.AddAnnotation("male", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Male", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + field.AddAnnotation("female", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Female", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + field.AddAnnotation("unspecified", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Unspecified", textFont, XBrushes.Black, x + 20, y + 10); + // as an alternative, you can also use field.SelectedIndex + //field.Value = "male"; + groupGender.AddChild(field); + }); + + y += 40; + // ComboBox fields + page1Renderer.DrawString("Select a number:", textFont, XBrushes.Black, x, y + 10); + acroForm.AddComboBoxField(field => + { + field.Name = "SelectedNumber"; + field.Options = new[] { "One", "Two", "Three", "Four", "Five" }; + //field.SelectedIndex = 2; // select "Three" + field.Font = textFont; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 100, y, 100, 20))); + }); + }); + + y += 40; + // ListBox fields + page1Renderer.DrawString("Select a color:", textFont, XBrushes.Black, x, y + 10); + acroForm.AddListBoxField(field => + { + field.Name = "SelectedColor"; + field.Options = new[] { "Blue", "Red", "Green", "Black", "White" }; + //field.SelectedIndices = new[] { 1 }; // select "Red" + field.Font = textFont; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 100, y, 100, 5 * textFont.Height))); + }); + }); + + y += 100; + acroForm.AddPushButtonField(button => + { + button.Name = "SubmitButton"; + button.Caption = "Submit"; + button.Font = textFont; + button.AddAnnotation(annot => + { + // TODO: these properties should be part of the field and propagated down to the annotations + annot.Highlighting = PdfAnnotationHighlightingMode.Invert; + annot.BorderColor = XColors.Gray; + annot.BackColor = XColors.LightBlue; + annot.Border.Width = 2; + annot.Border.BorderStyle = PdfAnnotationBorderStyle.Solid; + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 100, 20))); + }); + }); + // TODO: Signature fields + + return document; + } + + private static IEnumerable GetAllFields(PdfDocument doc) + { + return doc.AcroForm?.GetAllFields() ?? Array.Empty(); + } + } +} \ No newline at end of file diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFormsTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFormsTests.cs new file mode 100644 index 00000000..d168949d --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/AcroForms/AcroFormsTests.cs @@ -0,0 +1,929 @@ +using FluentAssertions; +using PdfSharp.Drawing; +using PdfSharp.Fonts; +using PdfSharp.Fonts.StandardFonts; +using PdfSharp.Pdf; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Annotations.enums; +using PdfSharp.Pdf.IO; +using PdfSharp.Quality; +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace PdfSharp.Tests.AcroForms +{ + public class AcroFormsTests + { + private readonly ITestOutputHelper output; + + public AcroFormsTests(ITestOutputHelper outputHelper) + { + output = outputHelper; + + var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + Directory.SetCurrentDirectory(Path.Combine(baseDir!, "..", "..", "..")); + } + + [Fact] + public void CanImportForm() + { + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf"); + var pdfPath = IOUtility.GetAssetsPath("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf")!; + using var fs = File.OpenRead(pdfPath); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Import); + + PdfPage? lastPage = null; + // import into new document + var copiedDocument = new PdfDocument(); + foreach (var page in inputDocument.Pages) + { + copiedDocument.AddPage(page); + lastPage = page; + } + // import AcroForm + copiedDocument.ImportAcroForm(inputDocument.AcroForm!); + + copiedDocument.AcroForm.Should().NotBeNull(); + + var fieldsInInputDocument = GetAllFields(inputDocument); + var fieldsInCopiedDocument = GetAllFields(copiedDocument); + fieldsInCopiedDocument.Count.Should().Be(fieldsInInputDocument.Count); + + // fill all fields + FillFields(fieldsInCopiedDocument); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/FilledForm.pdf"); + using var fsOut = File.Create(outFileName); + copiedDocument.Save(fsOut); + fsOut.Close(); + + VerifyFieldsFilledWithDefaultValues(outFileName); + } + + [Fact] + public void CanImportMultipleForms() + { + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf"); + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DemoFormWithCombs.pdf"); + var pdfPath = IOUtility.GetAssetsPath("pdfsharp-6.x\\pdfs")!; + + GlobalFontSettings.ResetAll(); + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var files = new[] { "DocumentWithAcroForm.pdf", "DemoFormWithCombs.pdf" }; + var copiedDocument = new PdfDocument(); + var importedFields = new List(); + foreach (var file in files) + { + using var fs = File.OpenRead(Path.Combine(pdfPath, file)); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Import); + foreach (var page in inputDocument.Pages) + copiedDocument.AddPage(page); + copiedDocument.ImportAcroForm(inputDocument.AcroForm!); + importedFields.AddRange(GetAllFields(inputDocument)); + } + var fieldsInCopiedDocument = GetAllFields(copiedDocument); + fieldsInCopiedDocument.Count.Should().Be(importedFields.Count); + + FillFields(fieldsInCopiedDocument); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/FilledForm_multiple.pdf"); + using var fsOut = File.Create(outFileName); + copiedDocument.Save(fsOut); + fsOut.Close(); + + VerifyFieldsFilledWithDefaultValues(outFileName); + } + + [Fact] + public void CanImportSameFormMultipleTimes() + { + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf"); + var pdfPath = IOUtility.GetAssetsPath("pdfsharp-6.x\\pdfs")!; + + GlobalFontSettings.ResetAll(); + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var files = new[] { "DocumentWithAcroForm.pdf", "DocumentWithAcroForm.pdf" }; + var copiedDocument = new PdfDocument(); + var importedFields = new List(); + foreach (var file in files) + { + using var fs = File.OpenRead(Path.Combine(pdfPath, file)); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Import); + foreach (var page in inputDocument.Pages) + copiedDocument.AddPage(page); + copiedDocument.ImportAcroForm(inputDocument.AcroForm!, null, (remoteField, localField) => + { + output.WriteLine("Field import: {0} -> {1}", remoteField.FullyQualifiedName, localField.FullyQualifiedName); + }); + importedFields.AddRange(GetAllFields(inputDocument)); + } + var fieldsInCopiedDocument = GetAllFields(copiedDocument); + fieldsInCopiedDocument.Count.Should().Be(importedFields.Count); + // root field names should be distinct + var rootNames = copiedDocument.AcroForm!.Fields.Names; + rootNames.Distinct().Count().Should().Be(rootNames.Length); + + FillFields(fieldsInCopiedDocument); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/FilledForm_same_multiple.pdf"); + using var fsOut = File.Create(outFileName); + copiedDocument.Save(fsOut); + fsOut.Close(); + + VerifyFieldsFilledWithDefaultValues(outFileName); + } + + [Fact] + public void CanFilterFieldsDuringImport() + { + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf"); + var pdfPath = IOUtility.GetAssetsPath("pdfsharp-6.x\\pdfs")!; + + GlobalFontSettings.ResetAll(); + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + using var fs = File.OpenRead(Path.Combine(pdfPath, "DocumentWithAcroForm.pdf")); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Import); + + PdfPage? lastPage = null; + // import into new document + var copiedDocument = new PdfDocument(); + foreach (var page in inputDocument.Pages) + { + copiedDocument.AddPage(page); + lastPage = page; + } + // import AcroForm + copiedDocument.ImportAcroForm(inputDocument.AcroForm!, + remoteField => + { + // only import TextFields + return remoteField is PdfTextField; + }, + (remoteField, localField) => + { + output.WriteLine("Field import: {0} -> {1}", remoteField.FullyQualifiedName, localField.FullyQualifiedName); + } + ); + + copiedDocument.AcroForm.Should().NotBeNull(); + + var fieldsInInputDocument = GetAllFields(inputDocument).Where(f => f is PdfTextField).ToList(); + var fieldsInCopiedDocument = GetAllFields(copiedDocument); + fieldsInCopiedDocument.Count.Should().Be(fieldsInInputDocument.Count); + + // fill all fields + FillFields(fieldsInCopiedDocument); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/FilledForm_filtered.pdf"); + using var fsOut = File.Create(outFileName); + copiedDocument.Save(fsOut); + fsOut.Close(); + + VerifyFieldsFilledWithDefaultValues(outFileName); + } + + [Fact] + public void ReturnsCorrectChildFields() + { + GlobalFontSettings.ResetAll(); + // we use one of the 14 standard-fonts here (Helvetica) + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var document = new PdfDocument(); + var page1 = document.AddPage(); + var acroForm = document.GetOrCreateAcroForm(); + var textFont = new XFont(StandardFontNames.Helvetica, 12, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.EmbedCompleteFontFile)); + + var personField = acroForm.AddGenericField(field => + { + field.Name = "Person"; + field.Flags = PdfAcroFieldFlags.DoNotSpellCheckTextField; + }); + var nameField = acroForm.AddGenericField(field => + { + field.Name = "Name"; + field.Flags = PdfAcroFieldFlags.Required; + field.DefaultValue = new PdfString("Please enter"); + personField.AddChild(field); + }); + var addressField = acroForm.AddGenericField(field => + { + field.Name = "Address"; + personField.AddChild(field); + }); + var titleField = acroForm.AddTextField(field => + { + field.Name = "Title"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var firstNameField = acroForm.AddTextField(field => + { + field.Name = "FirstName"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var lastNameField = acroForm.AddTextField(field => + { + field.Name = "LastName"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var zipCodeField = acroForm.AddTextField(field => + { + field.Name = "ZipCode"; + field.MaxLength = 10; + field.Font = textFont; + addressField.AddChild(field); + }); + var cityField = acroForm.AddTextField(field => + { + field.Name = "City"; + field.MaxLength = 100; + field.Font = textFont; + addressField.AddChild(field); + }); + var streetField = acroForm.AddTextField(field => + { + field.Name = "Street"; + field.MaxLength = 100; + field.Font = textFont; + addressField.AddChild(field); + }); + var streetNumberField = acroForm.AddTextField(field => + { + field.Name = "StreetNumber"; + field.MaxLength = 10; + field.Font = textFont; + addressField.AddChild(field); + }); + + var personFieldFromForm = acroForm.Fields["Person"]!; + personFieldFromForm.Should().NotBeNull(); + + var nameFieldFromForm = personFieldFromForm.Fields["Name"]!; + nameFieldFromForm.Should().NotBeNull(); + nameFieldFromForm.FullyQualifiedName.Should().Be("Person.Name"); + var firstNameFromForm = nameFieldFromForm["FirstName"]!; + firstNameFromForm.Should().NotBeNull(); + firstNameFromForm.FullyQualifiedName.Should().Be("Person.Name.FirstName"); + var lastNameFromForm = nameFieldFromForm["LastName"]!; + lastNameFromForm.Should().NotBeNull(); + lastNameFromForm.FullyQualifiedName.Should().Be("Person.Name.LastName"); + + var addressFieldFromForm = personFieldFromForm["Address"]!; + addressFieldFromForm.Should().NotBeNull(); + addressFieldFromForm.FullyQualifiedName.Should().Be("Person.Address"); + var streetFieldFromForm = addressFieldFromForm["Street"]!; + streetFieldFromForm.Should().NotBeNull(); + streetFieldFromForm.FullyQualifiedName.Should().Be("Person.Address.Street"); + var cityFieldFromForm = addressFieldFromForm["City"]!; + cityFieldFromForm.Should().NotBeNull(); + cityFieldFromForm.FullyQualifiedName.Should().Be("Person.Address.City"); + } + + [Fact] + public void PropertiesAreInheritedFromParent() + { + GlobalFontSettings.ResetAll(); + // we use one of the 14 standard-fonts here (Helvetica) + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var document = new PdfDocument(); + var page1 = document.AddPage(); + var acroForm = document.GetOrCreateAcroForm(); + var textFont = new XFont(StandardFontNames.Helvetica, 12, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.EmbedCompleteFontFile)); + + //double x = 40, y = 40; + //var page1Renderer = XGraphics.FromPdfPage(page1); + + var personField = acroForm.AddGenericField(field => + { + field.Name = "Person"; + field.Flags = PdfAcroFieldFlags.DoNotSpellCheckTextField; + }); + var nameField = acroForm.AddGenericField(field => + { + field.Name = "Name"; + field.Flags = PdfAcroFieldFlags.Required; + field.DefaultValue = new PdfString("Please enter"); + personField.AddChild(field); + }); + var addressField = acroForm.AddGenericField(field => + { + field.Name = "Address"; + personField.AddChild(field); + }); + var titleField = acroForm.AddTextField(field => + { + field.Name = "Title"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var firstNameField = acroForm.AddTextField(field => + { + field.Name = "FirstName"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var lastNameField = acroForm.AddTextField(field => + { + field.Name = "LastName"; + field.MaxLength = 100; + field.Font = textFont; + nameField.AddChild(field); + }); + var zipCodeField = acroForm.AddTextField(field => + { + field.Name = "ZipCode"; + field.MaxLength = 10; + field.Font = textFont; + addressField.AddChild(field); + }); + var cityField = acroForm.AddTextField(field => + { + field.Name = "City"; + field.MaxLength = 100; + field.Font = textFont; + addressField.AddChild(field); + }); + var streetField = acroForm.AddTextField(field => + { + field.Name = "Street"; + field.MaxLength = 100; + field.Font = textFont; + addressField.AddChild(field); + }); + var streetNumberField = acroForm.AddTextField(field => + { + field.Name = "StreetNumber"; + field.MaxLength = 10; + field.Font = textFont; + addressField.AddChild(field); + }); + + personField.Fields.Names.Length.Should().Be(2); + personField.Fields.Names.Should().Equal(["Name", "Address"]); + nameField.Fields.Names.Length.Should().Be(3); + nameField.Fields.Names.Should().Equal(["Title", "FirstName", "LastName"]); + addressField.Fields.Names.Length.Should().Be(4); + addressField.Fields.Names.Should().Equal(["ZipCode", "City", "Street", "StreetNumber"]); + + var allFields = acroForm.GetAllFields(); + allFields.Count().Should().Be(10); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Name"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Address"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Name.Title"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Name.FirstName"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Name.LastName"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Address.ZipCode"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Address.City"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Address.Street"); + allFields.Should().Contain(f => f.FullyQualifiedName == "Person.Address.StreetNumber"); + + zipCodeField.Flags.Should().Be(PdfAcroFieldFlags.DoNotSpellCheckTextField); + cityField.Flags.Should().Be(PdfAcroFieldFlags.DoNotSpellCheckTextField); + streetField.Flags.Should().Be(PdfAcroFieldFlags.DoNotSpellCheckTextField); + streetNumberField.Flags.Should().Be(PdfAcroFieldFlags.DoNotSpellCheckTextField); + + titleField.Flags.Should().Be(PdfAcroFieldFlags.Required); + firstNameField.Flags.Should().Be(PdfAcroFieldFlags.Required); + lastNameField.Flags.Should().Be(PdfAcroFieldFlags.Required); + titleField.DefaultValue.Should().BeOfType(); + ((PdfString)titleField.DefaultValue!).Value.Should().Be("Please enter"); + firstNameField.DefaultValue.Should().BeOfType(); + ((PdfString)firstNameField.DefaultValue!).Value.Should().Be("Please enter"); + lastNameField.DefaultValue.Should().BeOfType(); + ((PdfString)lastNameField.DefaultValue!).Value.Should().Be("Please enter"); + } + + [Fact] + public void CanFlattenForm() + { + IOUtility.EnsureAssets("pdfsharp-6.x\\pdfs\\DocumentWithAcroForm.pdf"); + var pdfPath = IOUtility.GetAssetsPath("pdfsharp-6.x\\pdfs")!; + + GlobalFontSettings.ResetAll(); + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var sourceFile = Path.Combine(pdfPath, "DocumentWithAcroForm.pdf"); + var targetFile = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/Flattened.pdf"); + File.Copy(sourceFile, targetFile, true); + + var copiedDocument = PdfReader.Open(targetFile, PdfDocumentOpenMode.Modify); + + var fieldsInCopiedDocument = GetAllFields(copiedDocument); + // fill all fields + FillFields(fieldsInCopiedDocument); + + // flatten the form. after that, AcroForm should be null and all annotations should be removed + // (this is true for the tested document, other documents may contain annotations not related to Form-Fields) + copiedDocument.FlattenAcroForm(); + copiedDocument.AcroForm.Should().BeNull(); + copiedDocument.Pages[0].Annotations.Count.Should().Be(0); + copiedDocument.Save(targetFile); + } + + [Fact] + public void CanFlattenCreatedForm() + { + var filePath = CanCreateNewForm(); // create the form + + GlobalFontSettings.ResetAll(); + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + using var fs = File.OpenRead(filePath); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Import); + + // import into new document + var copiedDocument = new PdfDocument(); + foreach (var page in inputDocument.Pages) + copiedDocument.AddPage(page); + copiedDocument.ImportAcroForm(inputDocument.AcroForm!); + + copiedDocument.FlattenAcroForm(); + copiedDocument.AcroForm.Should().BeNull(); + copiedDocument.Pages[0].Annotations.Count.Should().Be(0); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/CreatedForm_flattened.pdf"); + using var fsOut = File.Create(outFileName); + copiedDocument.Save(fsOut); + + // don't know what to assert here, have to check with "real" eyes + // (we mainly want to check the behavior of the AcroFieldRenderers) + } + + [Fact] + public string CanCreateNewForm() + { + // test some special characters + // Note: Current limitations regarding CJK/Arabic, etc. still applies + // make sure you use a font that supports the used characters ! + const string firstNameValue = "Sebastién"; + const string lastNameValue = "Süßölgefäß"; // yep, that's a valid german word + + GlobalFontSettings.ResetAll(); + // we use one of the 14 standard-fonts here (Helvetica) + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var document = new PdfDocument(); + var page1 = document.AddPage(); + var page2 = document.AddPage(); + var acroForm = document.GetOrCreateAcroForm(); + var textFont = new XFont(StandardFontNames.Helvetica, 12, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEncoding.Automatic, PdfFontEmbedding.OmitStandardFont)); + // use same font with different size + var textFontBig = XFont.FromExisting(textFont, 24); + + acroForm.SetDefaultAppearance(textFont, 12, XColors.Black); + + double x = 40, y = 80; + var page1Renderer = XGraphics.FromPdfPage(page1); + var page2Renderer = XGraphics.FromPdfPage(page2); + page1Renderer.DrawString("Name of Subject", textFont, XBrushes.Black, x, y); + page2Renderer.DrawString("For Demo purposes. Modify the fields and observe the field on the first page is modified as well.", + textFont, XBrushes.Black, x, y); + + y += 10; + // Text fields + var firstNameField = acroForm.AddTextField(field => + { + // Note: Chromium-based browsers (ie. Edge/Chrome) do not render fields without a name + field.Name = "FirstName"; + field.Font = textFontBig; + field.ForeColor = XColors.DarkRed; + field.Text = firstNameValue; + // place annotation on both pages + // if the document is opened in a PdfReader and one of the Annotations is changed (e.g. by typing inside it), + // the other Annotation will be changed as well (as they belong to the same field) + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 200, 40))); + }); + field.AddAnnotation(annot => + { + // Note: The border is currently always solid and 1 unit wide + annot.BorderColor = XColors.Green; + annot.BackColor = XColors.DarkGray; + // testing dynamic font-size by doubling the height of the second widget + annot.AddToPage(page2, new PdfRectangle(new XRect(x, y, 200, 40))); + }); + }); + var lastNameField = acroForm.AddTextField(field => + { + field.Name = "LastName"; + field.Font = textFont; + field.Text = lastNameValue; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 10 + 200, y, 100, 20))); + }); + field.AddAnnotation(annot => + { + annot.AddToPage(page2, new PdfRectangle(new XRect(x + 10 + 200, y, 100, 20))); + }); + }); + + y += 60; + // Checkbox fields + page1Renderer.DrawString("Subject's interests", textFont, XBrushes.Black, x, y); + y += 10; + var cbx1 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_cooking"; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Cooking", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + var cbx2 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_coding"; + field.Checked = true; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Coding", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + var cbx3 = acroForm.AddCheckBoxField(field => + { + field.Name = "Interest_cycling"; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + }); + page1Renderer.DrawString("Cycling", textFont, XBrushes.Black, x + 20, y + 10); + + y += 40; + // RadioButton fields + page1Renderer.DrawString("Subject's gender", textFont, XBrushes.Black, x, y); + y += 10; + // used as parent-field for the radio-button (testing field-nesting) + var groupGender = acroForm.AddGenericField(field => + { + field.Name = "Group_Gender"; + }); + acroForm.AddRadioButtonField(field => + { + field.Name = "Gender"; + // add individual buttons + field.AddAnnotation("male", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Male", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + field.AddAnnotation("female", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Female", textFont, XBrushes.Black, x + 20, y + 10); + y += 20; + field.AddAnnotation("unspecified", annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 12, 12))); + }); + page1Renderer.DrawString("Unspecified", textFont, XBrushes.Black, x + 20, y + 10); + // as an alternative, you can also use field.SelectedIndex + field.Value = "male"; + groupGender.AddChild(field); + }); + + y += 40; + // ComboBox fields + page1Renderer.DrawString("Select a number:", textFont, XBrushes.Black, x, y + 10); + acroForm.AddComboBoxField(field => + { + field.Name = "SelectedNumber"; + field.Options = ["One", "Two", "Three", "Four", "Five"]; + field.SelectedIndex = 2; // select "Three" + field.Font = textFont; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 100, y, 100, 20))); + }); + }); + + y += 40; + // ListBox fields + page1Renderer.DrawString("Select a color:", textFont, XBrushes.Black, x, y + 10); + acroForm.AddListBoxField(field => + { + field.Name = "SelectedColor"; + field.Options = ["Blue", "Red", "Green", "Black", "White"]; + field.SelectedIndices = [1]; // select "Red" + field.Font = textFont; + field.AddAnnotation(annot => + { + annot.AddToPage(page1, new PdfRectangle(new XRect(x + 100, y, 100, 5 * textFont.Height))); + }); + }); + + y += 100; + // PushButton fields + acroForm.AddPushButtonField(button => + { + button.Name = "SubmitButton"; + button.Caption = "Submit"; + button.Font = textFont; + button.AddAnnotation(annot => + { + // TODO: these properties should be part of the field and propagated down to the annotations + annot.Highlighting = PdfAnnotationHighlightingMode.Invert; + annot.BorderColor = XColors.Gray; + annot.BackColor = XColors.LightBlue; + annot.Border.Width = 2; + annot.Border.BorderStyle = PdfAnnotationBorderStyle.Solid; + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 100, 20))); + // Note: setting icons for push-buttons is somewhat involved at the moment, but boils down to: + // - create an XForm + // - create XGraphics from the XForm + // - draw something using the graphics (like an image but it can be anything) + // - set the annotation's icon to the XForm's PdfForm, e.g. annot.NormalIcon = xform.PdfForm + // maybe we simplify this in the future if there is strong denand for it + }); + }); + + y += 40; + // Signature fields + acroForm.AddSignatureField(field => + { + field.Name = "Signature"; + field.AddAnnotation(annot => + { + annot.BackColor = XColors.White; + annot.BorderColor = XColors.Gray; + annot.Border.Width = 1; + annot.Border.BorderStyle = PdfAnnotationBorderStyle.Solid; + annot.AddToPage(page1, new PdfRectangle(new XRect(x, y, 160, 60))); + }); + }); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/CreatedForm.pdf"); + using var fsOut = File.Create(outFileName); + document.Save(fsOut, true); + fsOut.Close(); + + // read back and validate + document = PdfReader.Open(outFileName, PdfDocumentOpenMode.Modify); + var fields = GetAllFields(document); + + fields.Count.Should().Be(11); + + var field = fields.First(f => f.FullyQualifiedName == "FirstName"); + field.GetType().Should().Be(typeof(PdfTextField)); + ((PdfTextField)field).Text.Should().Be(firstNameValue); + field.ForeColor.Should().Be(XColors.DarkRed); + field.Font.Should().NotBeNull(); + field.Font!.Size.Should().Be(textFontBig.Size); + field.Annotations.Elements.Count.Should().Be(2); + field.Annotations.Elements[1].BorderColor.Should().Be(XColors.Green); + field.Annotations.Elements[1].BackColor.Should().Be(XColors.DarkGray); + + field = fields.First(f => f.FullyQualifiedName == "LastName"); + field.GetType().Should().Be(typeof(PdfTextField)); + field.Font.Should().NotBeNull(); + field.Font!.Size.Should().Be(textFont.Size); + ((PdfTextField)field).Text.Should().Be(lastNameValue); + field.Annotations.Elements.Count.Should().Be(2); + + field = fields.First(f => f.FullyQualifiedName == "Interest_cooking"); + field.GetType().Should().Be(typeof(PdfCheckBoxField)); + ((PdfCheckBoxField)field).Checked.Should().Be(false); + field.Annotations.Elements.Count.Should().Be(1); + + field = fields.First(f => f.FullyQualifiedName == "Interest_coding"); + field.GetType().Should().Be(typeof(PdfCheckBoxField)); + ((PdfCheckBoxField)field).Checked.Should().Be(true); + field.Annotations.Elements.Count.Should().Be(1); + + field = fields.First(f => f.FullyQualifiedName == "Interest_cycling"); + field.GetType().Should().Be(typeof(PdfCheckBoxField)); + ((PdfCheckBoxField)field).Checked.Should().Be(false); + field.Annotations.Elements.Count.Should().Be(1); + + field = fields.First(f => f.FullyQualifiedName == "Group_Gender"); + field.GetType().Should().Be(typeof(PdfGenericField)); + field.HasChildFields.Should().Be(true); + field.Annotations.Elements.Count.Should().Be(0); + field.Fields.Elements.Count.Should().Be(1); + + field = fields.First(f => f.FullyQualifiedName == "Group_Gender.Gender"); + field.GetType().Should().Be(typeof(PdfRadioButtonField)); + field.Annotations.Elements.Count.Should().Be(3); + ((PdfRadioButtonField)field).SelectedIndex.Should().Be(0); + ((PdfRadioButtonField)field).Options.Should().Equal(new[] { "male", "female", "unspecified" }); + ((PdfRadioButtonField)field).Value.Should().Be("male"); + + field = fields.First(f => f.FullyQualifiedName == "SelectedNumber"); + field.GetType().Should().Be(typeof(PdfComboBoxField)); + field.Annotations.Elements.Count.Should().Be(1); + ((PdfComboBoxField)field).SelectedIndex.Should().Be(2); + ((PdfComboBoxField)field).Options.Should().Equal(new[] { "One", "Two", "Three", "Four", "Five" }); + ((PdfComboBoxField)field).Value.Should().Be("Three"); + + field = fields.First(f => f.FullyQualifiedName == "SelectedColor"); + field.GetType().Should().Be(typeof(PdfListBoxField)); + field.Annotations.Elements.Count.Should().Be(1); + ((PdfListBoxField)field).SelectedIndices.Count().Should().Be(1); + ((PdfListBoxField)field).SelectedIndices.Should().Contain(1); + ((PdfListBoxField)field).Options.Should().Equal(new[] { "Blue", "Red", "Green", "Black", "White" }); + ((PdfListBoxField)field).Value.Count().Should().Be(1); + ((PdfListBoxField)field).Value.Should().Contain("Red"); + + field = fields.First(f => f.FullyQualifiedName == "SubmitButton"); + field.GetType().Should().Be(typeof(PdfPushButtonField)); + field.Annotations.Elements.Count.Should().Be(1); + ((PdfPushButtonField)field).Caption.Should().Be("Submit"); + field.Annotations.Elements.Count.Should().Be(1); + field.Annotations.Elements[0].Border.Width.Should().Be(2); + field.Annotations.Elements[0].Border.BorderStyle.Should().Be(PdfAnnotationBorderStyle.Solid); + field.Annotations.Elements[0].Highlighting.Should().Be(PdfAnnotationHighlightingMode.Invert); + field.Annotations.Elements[0].BorderColor.Should().Be(XColors.Gray); + field.Annotations.Elements[0].BackColor.Should().Be(XColors.LightBlue); + + return outFileName; + } + + [Fact] + public void CanDeleteFields() + { + var filePath = CanCreateNewForm(); + + var document = PdfReader.Open(filePath, PdfDocumentOpenMode.Modify); + var startFields = document.AcroForm!.GetAllFields().ToList(); + var startAnnotsCount = document.Pages[0].Annotations.Elements.Count; + foreach (var field in startFields) + { + if (field is PdfPushButtonField) + field.Remove(); + if (field is PdfSignatureField) + field.Remove(); + } + var endFields = document.AcroForm!.GetAllFields().ToList(); + var endAnnotsCount = document.Pages[0].Annotations.Elements.Count; + endFields.Count.Should().Be(startFields.Count - 2); + endAnnotsCount.Should().Be(startAnnotsCount - 2); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/AcroForms/CreatedForm_removed.pdf"); + document.Save(outFileName); + } + + //[Fact(Skip = "Only run when we want it to run")] + [Fact] + public void RenderGlyphsOfStandardFonts() + { + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + foreach (var fontName in StandardFontData.FontNames) + { + using var document = new PdfDocument(); + + var renderFont = new XFont(fontName, 16); + var helveticaFont = new XFont("Helvetica", 12); + var headerFont = new XFont("Helvetica", 36); + var brush = new XSolidBrush(XColors.Black); + var left = 60.0; + var top = 60.0; + var bottom = 60.0; + var gapX = 120.0; + var gapY = 20.0; + var x = left; + var y = top; + var page = document.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + var fullFontName = renderFont.OpenTypeDescriptor.FontFace.FullFaceName; + gfx.DrawString(fullFontName, headerFont, brush, x, y); + y += 50; + + // test rendering a specific character + //gfx.DrawString(char.ConvertFromUtf32(0x29C9C), renderFont, brush, x, y); + //y += 40; + + var characterList = renderFont.GetSupportedCharacters(); + if (characterList.Count > 0) + { + foreach (var c in characterList) + { + gfx.DrawString(c.ToString("X4"), helveticaFont, brush, x, y); + var s = char.ConvertFromUtf32(c); + gfx.DrawString(s, renderFont, brush, x + 80, y); + x += gapX; + if (x + gapX >= page.Width.Point) + { + x = left; + y += gapY; + if (y >= page.Height.Point - bottom) + { + gfx.Dispose(); + page = document.AddPage(); + gfx = XGraphics.FromPdfPage(page); + x = left; + y = top; + } + } + } + gfx.Dispose(); + } + else + output.WriteLine($"Font {fontName} has no glyphs"); + + var outFileName = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/Fonts/StandardFonts/" + fontName); + document.Save(outFileName); + } + } + + private static void FillFields(IList fields) + { + foreach (var field in fields) + { + if (field.ReadOnly) + continue; + // Values for the fields: + // - TextFields: name of field + // - CheckBoxes: checked + // - RadioButtons: second option is checked (if there is only one option, this is checked) + // - ChoiceFields (List, Combo): second option is selected (if there is only one option, this is selected) + if (field is PdfTextField textField) + textField.Text = field.Name; + else if (field is PdfComboBoxField comboBoxField) + comboBoxField.SelectedIndex = Math.Min(1, comboBoxField.Options.Count - 1); + else if (field is PdfCheckBoxField checkboxField) + checkboxField.Checked = true; + else if (field is PdfRadioButtonField radioButtonField) + radioButtonField.SelectedIndex = Math.Min(1, radioButtonField.Options.Count - 1); + else if (field is PdfListBoxField listBoxField) + listBoxField.SelectedIndices = [Math.Min(1, listBoxField.Options.Count - 1)]; + } + } + + private static List GetAllFields(PdfDocument doc) + { + return (doc.AcroForm?.GetAllFields() ?? []).ToList(); + } + + /// + /// Verifies, that the specified document has the expected field-values as set by + /// + /// + private static void VerifyFieldsFilledWithDefaultValues(string documentFilePath) + { + VerifyFilledFields(documentFilePath, field => + { + if (field is PdfTextField textField) + { + textField.Text.Should().Be(textField.Name); + } + else if (field is PdfCheckBoxField checkBox) + { + checkBox.Checked.Should().BeTrue(); + } + else if (field is PdfRadioButtonField radioButton) + { + radioButton.Options.Count.Should().BeGreaterThan(0); + radioButton.SelectedIndex.Should().Be(Math.Min(1, radioButton.Options.Count - 1)); + } + else if (field is PdfComboBoxField comboBox) + { + comboBox.SelectedIndex.Should().Be(Math.Min(1, comboBox.Options.Count - 1)); + } + else if (field is PdfListBoxField listBox) + { + listBox.Options.Count.Should().BeGreaterThan(0); + listBox.SelectedIndices.Count().Should().BeGreaterThan(0); + listBox.SelectedIndices.First().Should().Be(Math.Min(1, listBox.Options.Count - 1)); + } + }); + } + + private static void VerifyFilledFields(string documentFilePath, Action fieldVerifier) + { + using var fs = File.OpenRead(documentFilePath); + var inputDocument = PdfReader.Open(fs, PdfDocumentOpenMode.Modify); + var allFields = GetAllFields(inputDocument); + foreach (var field in allFields) + { + if (field.ReadOnly) + continue; + fieldVerifier(field); + } + } + } +} diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Build/ReleaseBuildTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Build/ReleaseBuildTests.cs index 1cd610d7..6c379b69 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Build/ReleaseBuildTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Build/ReleaseBuildTests.cs @@ -29,7 +29,7 @@ public void Check_renamed_identifiers() // Check to undo some temporary renames. const string automatic = nameof(PdfFontEmbedding.TryComputeSubset); (!automatic.EndsWith("_")).Should().BeTrue("some identifiers must be re-renamed before release."); - _ = automatic; + _ = automatic; } #endif @@ -200,28 +200,30 @@ static void Resave(byte[] bytes, bool isAnsi, string fileName, bool withBom) } } + /// + /// Writes the file. + /// Must be called only if the file requires an update (add a non-existing BOM or remove an existing BOM). + /// static void WriteFile(String fileName, Byte[] bytes, Boolean addBom) { bool utf8Bom = bytes is [0xEF, 0xBB, 0xBF, ..]; + (addBom == utf8Bom).Should().BeFalse("Should not come here."); + using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { if (addBom && !utf8Bom) { // Write new BOM. fs.Write([0xEF, 0xBB, 0xBF], 0, 3); - } - if (!addBom && utf8Bom) - { - // Write bytes without existing BOM. - fs.Write(bytes, 3, bytes.Length - 3); + // Write bytes as they are. + fs.Write(bytes, 0, bytes.Length); } else { - Debug.Assert(addBom && !utf8Bom, "Should not come here for other cases."); - // Write bytes as they are. - fs.Write(bytes, 0, bytes.Length); + // Write bytes without existing BOM. + fs.Write(bytes, 3, bytes.Length - 3); } } } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/images/ImageTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/images/ImageTests.cs index 8a81f4d4..a1c25ec8 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/images/ImageTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/images/ImageTests.cs @@ -248,7 +248,7 @@ void ExportJpeg(PdfDictionary image) } [Fact] - void PDF_with_Image_from_stream() + public void PDF_with_Image_from_stream() { // Attempt to avoid "image file locked" under .NET 4.6.2. GC.Collect(); @@ -261,7 +261,73 @@ void PDF_with_Image_from_stream() var imagePath = IOUtility.GetAssetsPath("PDFsharp/images/samples/jpeg/truecolorA.jpg")!; - var stream = new FileStream(imagePath, FileMode.Open); + var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); + using var xImage = XImage.FromStream(stream); + + gfx.DrawImage(xImage, 100, 100, 100, 100); + + // Save the document... + var filename = PdfFileUtility.GetTempPdfFileName("ImageFromStream"); + document.Save(filename); + // ...and start a viewer. + PdfFileUtility.ShowDocumentIfDebugging(filename); + } + + // Attempt to avoid "image file locked" under .NET 4.6.2. + GC.Collect(); + GC.WaitForFullGCComplete(); + } + + [Fact] + public void PDF_with_Image_from_private_memorystream() + { + // Attempt to avoid "image file locked" under .NET 4.6.2. + GC.Collect(); + GC.WaitForFullGCComplete(); + + { + var document = new PdfDocument(); + var page = document.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + + var imagePath = IOUtility.GetAssetsPath("PDFsharp/images/samples/jpeg/truecolorA.jpg")!; + var pngBytes = File.ReadAllBytes(imagePath); + + // Create a MemoryStream that does not allow GetBuffer. + var stream = new MemoryStream(pngBytes); + using var xImage = XImage.FromStream(stream); + + gfx.DrawImage(xImage, 100, 100, 100, 100); + + // Save the document... + var filename = PdfFileUtility.GetTempPdfFileName("ImageFromStream"); + document.Save(filename); + // ...and start a viewer. + PdfFileUtility.ShowDocumentIfDebugging(filename); + } + + // Attempt to avoid "image file locked" under .NET 4.6.2. + GC.Collect(); + GC.WaitForFullGCComplete(); + } + + [Fact] + public void PDF_with_Image_from_public_memorystream() + { + // Attempt to avoid "image file locked" under .NET 4.6.2. + GC.Collect(); + GC.WaitForFullGCComplete(); + + { + var document = new PdfDocument(); + var page = document.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + + var imagePath = IOUtility.GetAssetsPath("PDFsharp/images/samples/jpeg/truecolorA.jpg")!; + var pngBytes = File.ReadAllBytes(imagePath); + + // Create a MemoryStream that allows GetBuffer. + var stream = new MemoryStream(pngBytes, 0, pngBytes.Length, false, true); using var xImage = XImage.FromStream(stream); gfx.DrawImage(xImage, 100, 100, 100, 100); diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/text/GlyphHelperTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/text/GlyphHelperTests.cs index cec5fa96..c43a2b05 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/text/GlyphHelperTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Drawing/text/GlyphHelperTests.cs @@ -66,7 +66,7 @@ public void GlyphIndicesFromString_Test() [Fact] public void Glyphs_from_invalid_ANSI_codes() { - // Ensure no glyph ids for non ANSI characters. + // Ensure no glyph IDs for non ANSI characters. var font = new XFont("Arial", 10); diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Fonts/FontEmbeddingTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Fonts/FontEmbeddingTests.cs index 4eaebb3a..10101c90 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Fonts/FontEmbeddingTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Fonts/FontEmbeddingTests.cs @@ -7,6 +7,7 @@ using FluentAssertions; using PdfSharp.Drawing; using PdfSharp.Fonts; +using PdfSharp.Fonts.StandardFonts; using PdfSharp.Pdf; using PdfSharp.Quality; using Xunit; @@ -95,5 +96,49 @@ public void Embed_Segoe_UI_Emoji_file() size.Should().BeGreaterThan(1_000_000); } + + [Fact] + public void Omit_Standard_Fonts() + { + GlobalFontSettings.FontResolver = new StandardFontResolver(); + + var doc = new PdfDocument(); + + // squeeze out an additional KB of data by NOT creating a metadata-stream + doc.Options.CreateMetadata = false; + + var page = doc.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + + double x = 50, y = 50; + foreach (var fontName in StandardFontData.FontNames) + { + if (fontName == StandardFontNames.Symbol || fontName == StandardFontNames.ZapfDingbats) + continue; + + var font = new XFont(fontName, 12, XFontStyleEx.Regular, + new XPdfFontOptions(PdfFontEmbedding.OmitStandardFont)); + + gfx.DrawString(fontName, font, XBrushes.Black, x, y); + y += 20; + // english pangram + //gfx.DrawString("The quick brown fox jumps over the lazy dog.", font, XBrushes.Black, x, y); + // german pangram + gfx.DrawString("Typograf Jakob zürnt schweißgequält vom öden Text.", font, XBrushes.Black, x, y); + + y += 40; + } + + var fileName = PdfFileUtility.GetTempPdfFullFileName( + "PDFsharp/UnitTest/Fonts/FontEmbeddingTests/" + nameof(Omit_Standard_Fonts)); + doc.Save(fileName); + var info = new FileInfo(fileName); + var size = info.Length; +#if DEBUG + size.Should().BeLessThan(10_000); +#else + size.Should().BeLessThan(5_000); +#endif + } } } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/LexerTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/LexerTests.cs index e9d9e937..99bba3c0 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/LexerTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/LexerTests.cs @@ -5,20 +5,18 @@ using System.IO; #endif using FluentAssertions; -using PdfSharp.Diagnostics; -using PdfSharp.Drawing; -using PdfSharp.Fonts; using PdfSharp.Pdf; using PdfSharp.Pdf.Internal; using PdfSharp.Pdf.IO; -using PdfSharp.Quality; -using PdfSharp.Snippets; -using PdfSharp.Snippets.Font; -using PdfSharp.TestHelper; +using System.Numerics; using Xunit; namespace PdfSharp.Tests.IO { + /// + /// Most Lexer function are tested implicitly by correctly parsing PDF files. + /// We only test the non-trivial functions. + /// [Collection("PDFsharp")] public class LexerTests { @@ -85,5 +83,187 @@ public void ReverseSolidusTests() modifiedDocument.Info.Creator.Should().Be(creatorExpected); } + + [Fact] + public void ScanNumberTests() + { + var lexer = CreateLexer("123"); + var symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(123); + + lexer = CreateLexer("5000000000"); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(5_000_000_000); + + // Int32.MaxValue must be Integer + lexer = CreateLexer(Invariant($"{Int32.MaxValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(Int32.MaxValue); + + // Int32.MaxValue must be Integer, even with leading zeros. + lexer = CreateLexer(Invariant($"000000{Int32.MaxValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(Int32.MaxValue); + + // Int32.MaxValue + 1 must be LongInteger + lexer = CreateLexer(Invariant($"{Int32.MaxValue + 1L}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(Int32.MaxValue + 1L); + + // Int32.MinValue must be Integer + lexer = CreateLexer(Invariant($"{Int32.MinValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(Int32.MinValue); + + // Int32.MinValue - 1 must be LongInteger + lexer = CreateLexer(Invariant($"{Int32.MinValue - 1L}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(Int32.MinValue - 1L); + + // Int64.MaxValue must be LongInteger + lexer = CreateLexer(Invariant($"{Int64.MaxValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(Int64.MaxValue); + + // Int64.MaxValue + 1 must be Real + lexer = CreateLexer(Invariant($"{(BigInteger)1 + Int64.MaxValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be((double)((BigInteger)1 + Int64.MaxValue)); + + // Int64.MinValue must be LongInteger + lexer = CreateLexer(Invariant($"{Int64.MinValue}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(Int64.MinValue); + + // Int64.MinValue - 1 must be Real + lexer = CreateLexer(Invariant($"{(BigInteger)Int64.MinValue - 1}")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be((double)((BigInteger)Int64.MinValue - 1)); + + // From https://github.com/empira/PDFsharp/issues/223 + lexer = CreateLexer("00000000000001588776"); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(00000000000001588776); + + // Real literals + + lexer = CreateLexer("100.124"); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(100.124); + + lexer = CreateLexer("123."); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(123); + + lexer = CreateLexer("123.00000000000000000000000000000"); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(123); + + + lexer = CreateLexer(Invariant($"{Int64.MaxValue - 42}.")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(Int64.MaxValue - 42); + + lexer = CreateLexer(Invariant($"{Int64.MaxValue - 42}.00")); + symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(Int64.MaxValue - 42); + } + + [Theory] + [InlineData("42", 42, true, true)] + [InlineData("2147483647", 2147483647, true, true)] // int32.MaxValue is 2147483647. + [InlineData("-2147483648", -2147483648, true, true)] + public void Scan_Integer_Tests(string text, int value, bool testAsLong, bool testAsReal) + { + var lexer = CreateLexer(text); + var symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Integer); + lexer.TokenToInteger.Should().Be(value); + if (testAsLong) + lexer.TokenToLongInteger.Should().Be(value); + if (testAsReal) + lexer.TokenToReal.Should().Be(value); + } + + [Theory] + [InlineData("9223372036854775807", 9223372036854775807L, true, true)] // UInt64.MaxValue is 9223372036854775807L. + [InlineData("-9223372036854775808", -9223372036854775808L, true, true)] + [InlineData("2147483648", 2147483648, true, true)] // int32.MaxValue is 2147483647. + [InlineData("-2147483649", -2147483649, true, true)] + public void Scan_LongInteger_Tests(string text, long value, bool testAsInteger, bool testAsReal) + { + var lexer = CreateLexer(text); + var symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.LongInteger); + lexer.TokenToLongInteger.Should().Be(value); + if (testAsInteger) + { + var getInt = () => lexer.TokenToInteger; + getInt.Should().Throw(); + } + if (testAsReal) + lexer.TokenToReal.Should().Be(value); + } + + [Theory] + [InlineData("42.0", 42, true, true)] + [InlineData("42.17", 42.17, true, true)] + [InlineData("42.12345678", 42.12345678, true, true)] + [InlineData("-42.0", -42, true, true)] + [InlineData("-42.17", -42.17, true, true)] + [InlineData("-42.12345678", -42.12345678, true, true)] + [InlineData("9223372036854775808", 9223372036854775808, true, true)] // UInt64.MaxValue is 9223372036854775807L. + [InlineData("-9223372036854775809", -9223372036854775809d, true, true)] + [InlineData("9223372036854775807.0", 9223372036854775807L, true, true)] // UInt64.MaxValue is 9223372036854775807L. + [InlineData("-9223372036854775808.0", -9223372036854775808L, true, true)] + [InlineData("2147483648.0", 2147483648, true, true)] // int32.MaxValue is 2147483647. + [InlineData("-2147483649.0", -2147483649, true, true)] + public void Scan_Real_Tests(string text, double value, bool testAsInteger, bool testAsLong) + { + var lexer = CreateLexer(text); + var symbol = lexer.ScanNumber(false); + symbol.Should().Be(Symbol.Real); + lexer.TokenToReal.Should().Be(value); + if (testAsInteger) + { + var getInt = () => lexer.TokenToInteger; + getInt.Should().Throw(); + } + if (testAsLong) + { + var getInt = () => lexer.TokenToLongInteger; + getInt.Should().Throw(); + } + } + + public void Scan_ObjRef_Tests(string text, (int, int) objID/*, bool testAsLong, bool testAsReal*/) + { + + } + + Lexer CreateLexer(string text) + { + var pdfString = new PdfString(text, PdfStringEncoding.RawEncoding); + var bytes = pdfString.GetRawBytes(); + var stream = new MemoryStream(bytes); + return new(stream, null); + } } } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs index 6fe6f893..41d2f455 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs @@ -1,6 +1,7 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using FluentAssertions; using PdfSharp.Diagnostics; using PdfSharp.Drawing; using PdfSharp.Fonts; @@ -66,5 +67,310 @@ public void Create_Hello_World_BasicTests() // ...and start a viewer. PdfFileUtility.ShowDocumentIfDebugging(filename); } + + [Fact] + public void Create_CropBox_BasicTests() + { + // Create a new PDF document. + var document = new PdfDocument(); + document.Info.Title = "Created with PDFsharp"; + + // Create an empty page in this document. + var page = document.AddPage(); + + var mediaBox = page.MediaBoxReadOnly; + mediaBox.Should().NotBeNull(); + mediaBox.Should().NotBe(XRect.Empty); + mediaBox.IsZero.Should().BeFalse(); + + // CropBox does not exist by default. + var cropBox = page.CropBoxReadOnly; + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeFalse(); + + page.Width = XUnit.FromCentimeter(5); + page.Height = XUnit.FromCentimeter(2); + + mediaBox = page.MediaBoxReadOnly; + mediaBox.Should().NotBeNull(); + mediaBox.Should().NotBe(XRect.Empty); + mediaBox.IsZero.Should().BeFalse(); + + cropBox = page.CropBoxReadOnly; + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeFalse(); + + // Calling page.CropBox sets "new PdfRectangle()" as CropBox. + // For "new PdfRectangle()", IsZero is true. + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.CropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + page.CropBox = new PdfRectangle(XUnitPt.FromMillimeter(1), XUnitPt.FromMillimeter(1), + XUnitPt.FromMillimeter(49), XUnitPt.FromMillimeter(19)); + + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeFalse(); + + // Get an XGraphics object for drawing on this page. + var gfx = XGraphics.FromPdfPage(page); + + // Draw two lines with a red default pen. + var width = page.Width.Point; + var height = page.Height.Point; + gfx.DrawLine(XPens.Red, 0, 0, width, height); + gfx.DrawLine(XPens.Red, width, 0, 0, height); + + // Draw a circle with a red pen which is 1.5 point thick. + var r = width / 6; + gfx.DrawEllipse(new XPen(XColors.Red, 1.5), XBrushes.White, new XRect(width / 2 - r, height / 2 - r, 2 * r, 2 * r)); + + // Create a font. + var font = new XFont("Times New Roman", 10, XFontStyleEx.BoldItalic); + + // Draw the text. + gfx.DrawString("Hello, dotnet 6.0!", font, XBrushes.Black, + new XRect(0, 0, width, height), XStringFormats.Center); + + // Save the document... + string filename = PdfFileUtility.GetTempPdfFileName("BasicMediaBoxTest"); + document.Save(filename); + // ...and start a viewer. + PdfFileUtility.ShowDocumentIfDebugging(filename); + } + + [Fact] + public void Create_all_boxes_BasicTests() + { + // Create a new PDF document. + var document = new PdfDocument(); + document.Info.Title = "Created with PDFsharp"; + + // Create an empty page in this document. + var page = document.AddPage(); + var pageWidth = page.Width.Point; + var pageHeight = page.Height.Point; + var pageRectangle = new PdfRectangle(0, 0, pageWidth, pageHeight); + var defaultRectangle = new PdfRectangle(); + + var mediaBox = page.MediaBoxReadOnly; + mediaBox.Should().NotBeNull(); + mediaBox.Should().Be(pageRectangle); + + // CropBox does not exist by default. + var cropBox = page.CropBoxReadOnly; + cropBox.Should().Be(defaultRectangle); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().Be(pageRectangle); + + page.Width = XUnit.FromCentimeter(5); + page.Height = XUnit.FromCentimeter(2); + + mediaBox = page.MediaBoxReadOnly; + mediaBox.Should().NotBeNull(); + mediaBox.Should().NotBe(XRect.Empty); + mediaBox.IsZero.Should().BeFalse(); + + // ----- CropBox ----- + cropBox = page.CropBoxReadOnly; + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeFalse(); + + // Calling page.CropBox sets "new PdfRectangle()" as CropBox. + // For "new PdfRectangle()", IsZero is true. + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.EffectiveCropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.CropBoxReadOnly; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeTrue(); + + page.CropBox = page.MediaBox; + + cropBox = page.CropBox; + cropBox.Should().NotBeNull(); + cropBox.Should().NotBe(XRect.Empty); + cropBox.IsZero.Should().BeFalse(); + + // ----- ArtBox ----- + var artBox = page.ArtBoxReadOnly; + artBox.IsZero.Should().BeTrue(); + + artBox = page.EffectiveArtBoxReadOnly; + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeFalse(); + + // Calling page.ArtBox sets "new PdfRectangle()" as ArtBox. + // For "new PdfRectangle()", IsZero is true. + artBox = page.ArtBox; + artBox.Should().NotBeNull(); + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeTrue(); + + artBox = page.EffectiveArtBoxReadOnly; + artBox.Should().NotBeNull(); + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeTrue(); + + artBox = page.ArtBoxReadOnly; + artBox.Should().NotBeNull(); + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeTrue(); + + artBox = page.ArtBox; + artBox.Should().NotBeNull(); + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeTrue(); + + page.ArtBox = page.MediaBox; + + artBox = page.ArtBox; + artBox.Should().NotBeNull(); + artBox.Should().NotBe(XRect.Empty); + artBox.IsZero.Should().BeFalse(); + + // ----- BleedBox ----- + var bleedBox = page.BleedBoxReadOnly; + bleedBox.IsZero.Should().BeTrue(); + + bleedBox = page.EffectiveBleedBoxReadOnly; + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeFalse(); + + // Calling page.BleedBox sets "new PdfRectangle()" as BleedBox. + // For "new PdfRectangle()", IsZero is true. + bleedBox = page.BleedBox; + bleedBox.Should().NotBeNull(); + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeTrue(); + + bleedBox = page.EffectiveBleedBoxReadOnly; + bleedBox.Should().NotBeNull(); + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeTrue(); + + bleedBox = page.BleedBoxReadOnly; + bleedBox.Should().NotBeNull(); + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeTrue(); + + bleedBox = page.BleedBox; + bleedBox.Should().NotBeNull(); + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeTrue(); + + page.BleedBox = page.MediaBox; + + bleedBox = page.BleedBox; + bleedBox.Should().NotBeNull(); + bleedBox.Should().NotBe(XRect.Empty); + bleedBox.IsZero.Should().BeFalse(); + + // ----- TrimBox ----- + var trimBox = page.TrimBoxReadOnly; + trimBox.IsZero.Should().BeTrue(); + + trimBox = page.EffectiveTrimBoxReadOnly; + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeFalse(); + + // Calling page.TrimBox sets "new PdfRectangle()" as TrimBox. + // For "new PdfRectangle()", IsZero is true. + trimBox = page.TrimBox; + trimBox.Should().NotBeNull(); + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeTrue(); + + trimBox = page.EffectiveTrimBoxReadOnly; + trimBox.Should().NotBeNull(); + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeTrue(); + + trimBox = page.TrimBoxReadOnly; + trimBox.Should().NotBeNull(); + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeTrue(); + + trimBox = page.TrimBox; + trimBox.Should().NotBeNull(); + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeTrue(); + + page.TrimBox = page.MediaBox; + + trimBox = page.TrimBox; + trimBox.Should().NotBeNull(); + trimBox.Should().NotBe(XRect.Empty); + trimBox.IsZero.Should().BeFalse(); + + // Get an XGraphics object for drawing on this page. + var gfx = XGraphics.FromPdfPage(page); + + // Draw two lines with a red default pen. + var width = page.Width.Point; + var height = page.Height.Point; + gfx.DrawLine(XPens.Red, 0, 0, width, height); + gfx.DrawLine(XPens.Red, width, 0, 0, height); + + // Draw a circle with a red pen which is 1.5 point thick. + var r = width / 6; + gfx.DrawEllipse(new XPen(XColors.Red, 1.5), XBrushes.White, new XRect(width / 2 - r, height / 2 - r, 2 * r, 2 * r)); + + // Create a font. + var font = new XFont("Times New Roman", 10, XFontStyleEx.BoldItalic); + + // Draw the text. + gfx.DrawString("Hello, dotnet 6.0!", font, XBrushes.Black, + new XRect(0, 0, width, height), XStringFormats.Center); + + // Save the document... + string filename = PdfFileUtility.GetTempPdfFileName("BasicAllBoxesTest"); + document.Save(filename); + // ...and start a viewer. + PdfFileUtility.ShowDocumentIfDebugging(filename); + } } } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Properties/GlobalDeclarations.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Properties/GlobalDeclarations.cs new file mode 100644 index 00000000..e4265a5c --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Properties/GlobalDeclarations.cs @@ -0,0 +1,22 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +global using System.IO; +global using PdfSharp.Internal; + + +#if USE_LONG_SIZE +global using SizeType = System.Int64; +#else +global using SizeType = System.Int32; +#endif + +global using static System.FormattableString; + +using System.Diagnostics.CodeAnalysis; +//using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: SuppressMessage("LoggingGenerator", "SYSLIB1006:Multiple logging methods cannot use the same event ID within a class", + Justification = "We use logging event IDs as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Security/SecurityTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Security/SecurityTests.cs index 7243ab69..a784cf32 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Security/SecurityTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Security/SecurityTests.cs @@ -11,7 +11,7 @@ namespace PdfSharp.Tests.Security [Collection("PDFsharp")] public class SecurityTests { - //[Fact] // TODO: Skip when finally tested. + //[Fact] // The tests succeeds and is now skipped. [Fact(Skip = "Test only once, because it is slow and does not depend on PDFsharp source code.")] public void MD5_creation() { diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Structs/XUnitTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Structs/XUnitTests.cs index 4ee3f090..aca5dcdd 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Structs/XUnitTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Structs/XUnitTests.cs @@ -318,7 +318,7 @@ public void XUnit_vs_XUnitPt_Test() const double d2 = 2; const double d2Cm = d2; - + XUnit xUnit2Cm = XUnit.FromCentimeter(d2Cm); XUnitPt xUnitPt2Cm = XUnitPt.FromCentimeter(d2Cm); @@ -334,5 +334,15 @@ public void XUnit_vs_XUnitPt_Test() xUnitPt2CmAsXUnit.Point.Should().Be(xUnit2Cm.Point); xUnitPt2CmAsXUnit.Centimeter.Should().BeApproximately(xUnit2Cm.Centimeter, acceptedRoundingError); } + + [Fact] + public void XUnit_No_commas_allowed_anymore_Test() + { + XUnit unit = "10000.0"; + unit.Value.Should().Be(10_000); + + Func createUnit = () => unit = "10000,0"; + createUnit.Should().Throw(); + } } } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.tests-wpf/PdfSharp.tests-wpf.csproj b/src/foundation/src/PDFsharp/tests/PdfSharp.tests-wpf/PdfSharp.tests-wpf.csproj index 8de33d02..b355f268 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.tests-wpf/PdfSharp.tests-wpf.csproj +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.tests-wpf/PdfSharp.tests-wpf.csproj @@ -29,6 +29,7 @@ + diff --git a/src/foundation/src/shared/src/PdfSharp.Quality/IOUtility.cs b/src/foundation/src/shared/src/PdfSharp.Quality/IOUtility.cs index 33a0ce0e..b6ad0a7d 100644 --- a/src/foundation/src/shared/src/PdfSharp.Quality/IOUtility.cs +++ b/src/foundation/src/shared/src/PdfSharp.Quality/IOUtility.cs @@ -44,12 +44,15 @@ public static void NormalizeDirectorySeparators(ref string? path) /// Gets the root path of the current solution, or null, if no parent /// directory with a solution file exists. /// - public static string? GetSolutionRoot() + public static string? GetSolutionRoot([CallerFilePath] string? callerFilePath = null) { if (_solutionRoot is not null) return _solutionRoot; - var path = Directory.GetCurrentDirectory(); + // when running tests from within VS, Directory.GetCurrentDirectory may return a path below %temp% + if (callerFilePath != null) + callerFilePath = Path.GetDirectoryName(callerFilePath); + var path = callerFilePath ?? Directory.GetCurrentDirectory(); while (true) { string[] files = Directory.GetFiles(path, "*.sln", SearchOption.TopDirectoryOnly); diff --git a/src/foundation/src/shared/src/PdfSharp.Shared/Internal/PdfSharpGitVersionInformation.cs b/src/foundation/src/shared/src/PdfSharp.Shared/Internal/PdfSharpGitVersionInformation.cs new file mode 100644 index 00000000..4a52820a --- /dev/null +++ b/src/foundation/src/shared/src/PdfSharp.Shared/Internal/PdfSharpGitVersionInformation.cs @@ -0,0 +1,56 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +namespace PdfSharp.Internal +{ + /// + /// Product version information from git version tool. + /// + public static class PdfSharpGitVersionInformation + { + /// + /// The major version number of the product. + /// + public static string Major = global::GitVersionInformation.Major; + + /// + /// The minor version number of the product. + /// + public static string Minor = global::GitVersionInformation.Minor; + + /// + /// The patch number of the product. + /// + public static string Patch = global::GitVersionInformation.Patch; + + /// + /// The Version pre-release string for NuGet. + /// + public static string PreReleaseLabel = global::GitVersionInformation.PreReleaseLabel; + + /// + /// The full version number. + /// + public static string MajorMinorPatch = global::GitVersionInformation.MajorMinorPatch; + + /// + /// The full semantic version number created by GitVersion. + /// + public static string SemVer = global::GitVersionInformation.SemVer; + + /// + /// The full informational version number created by GitVersion. + /// + public static string InformationalVersion = global::GitVersionInformation.InformationalVersion; + + /// + /// The branch name of the product. + /// + public static string BranchName = global::GitVersionInformation.BranchName; + + /// + /// The commit date of the product. + /// + public static string CommitDate = global::GitVersionInformation.CommitDate; + } +} diff --git a/src/foundation/src/shared/src/PdfSharp.Shared/Properties/GlobalDeclarations.cs b/src/foundation/src/shared/src/PdfSharp.Shared/Properties/GlobalDeclarations.cs index 9569c891..b3191fe0 100644 --- a/src/foundation/src/shared/src/PdfSharp.Shared/Properties/GlobalDeclarations.cs +++ b/src/foundation/src/shared/src/PdfSharp.Shared/Properties/GlobalDeclarations.cs @@ -17,4 +17,4 @@ [assembly: ComVisible(false)] //[assembly: SuppressMessage("LoggingGenerator", "SYSLIB1006:Multiple logging methods cannot use the same event ID within a class", -// Justification = "We use logging event ids as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] +// Justification = "We use logging event IDs as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] diff --git a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/selection/DefaultConstructionSnippets.cs b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/selection/DefaultConstructionSnippets.cs index a7fd6204..52ea9e2d 100644 --- a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/selection/DefaultConstructionSnippets.cs +++ b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/selection/DefaultConstructionSnippets.cs @@ -54,7 +54,7 @@ public class DefaultWindowsFontsSnippet : Snippet #endif #if TIMES_NEW_ROMAN - private const string FamilyName = "Times New Roman"; + const string FamilyName = "Times New Roman"; #elif SEGOE_UI const string FamilyName = "Segoe UI"; #endif @@ -442,7 +442,7 @@ public class PlatformFontConstructionSnippet : Snippet #endif #if TIMES_NEW_ROMAN - private const string FamilyName = "Times New Roman"; + const string FamilyName = "Times New Roman"; #elif SEGOE_UI const string FamilyName = "Segoe UI"; #endif diff --git a/src/foundation/src/shared/src/PdfSharp.System/Properties/GlobalDeclarations.cs b/src/foundation/src/shared/src/PdfSharp.System/Properties/GlobalDeclarations.cs index b05fbb8f..1f8cc8db 100644 --- a/src/foundation/src/shared/src/PdfSharp.System/Properties/GlobalDeclarations.cs +++ b/src/foundation/src/shared/src/PdfSharp.System/Properties/GlobalDeclarations.cs @@ -18,4 +18,4 @@ [assembly: ComVisible(false)] //[assembly: SuppressMessage("LoggingGenerator", "SYSLIB1006:Multiple logging methods cannot use the same event ID within a class", -// Justification = "We use logging event ids as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] +// Justification = "We use logging event IDs as documented, i.e. multiple times", Scope = "member"/*, Target = "~M:PdfSharp.Internal.Logging.LogMessages.XGraphicsCreated(Microsoft.Extensions.Logging.ILogger,System.String)"*/)] diff --git a/src/foundation/src/shared/testapps/Shared.TestApp/Program.cs b/src/foundation/src/shared/testapps/Shared.TestApp/Program.cs index e49e7bf8..9f16c927 100644 --- a/src/foundation/src/shared/testapps/Shared.TestApp/Program.cs +++ b/src/foundation/src/shared/testapps/Shared.TestApp/Program.cs @@ -24,21 +24,23 @@ static void Main( /*string[] args*/) //.AddFilter("System", LogLevel.Warning) //.AddFilter("LoggingConsoleApp.Program", LogLevel.Debug) .AddFilter("", LogLevel.Debug) + .AddFilter(PdfSharpLogCategory.DocumentProcessing, LogLevel.Debug) + .AddFilter(PdfSharpLogCategory.FontManagement, LogLevel.Debug) + .AddFilter(PdfSharpLogCategory.PdfReading, LogLevel.Debug) + .AddFilter(PdfSharpLogCategory.ImageProcessing, LogLevel.Debug) //.AddFilter("PDFsharp", LogLevel.Critical) .AddConsole(); }); + LogHost.Factory = loggerFactory; ILogger logger = loggerFactory.CreateLogger(); - //logger.LogInformation("Example log message 1"); - - //LogHost.Logger.LogInformation("Example log message 2"); + - LogHost.Factory = loggerFactory; //LogHost.Logger.LogError("Something went wrong."); //LogHost.Logger.TestMessage(LogLevel.Critical, "blah"); //LogHost.Logger.TestMessage("di-blub"); - //LogHost.Logger.TestMessage("------------------------------------------------------------------------------"); + LogHost.Logger.TestMessage("------------------------------------------------------------------------------"); @@ -48,7 +50,7 @@ static void Main( /*string[] args*/) // Call some developer specific test code from a file not in the repo. // Implement your code in ProgramEx.cs in partial class Program. - var test = typeof(Program).GetMethod("Test", BindingFlags.Static| BindingFlags.NonPublic); + var test = typeof(Program).GetMethod("Test", BindingFlags.Static | BindingFlags.NonPublic); if (test != null) { test.Invoke(null, null);