diff --git a/PdfSharpCore.Test/Assets/Pdf20.pdf b/PdfSharpCore.Test/Assets/Pdf20.pdf new file mode 100644 index 00000000..d26da331 Binary files /dev/null and b/PdfSharpCore.Test/Assets/Pdf20.pdf differ diff --git a/PdfSharpCore.Test/IO/PdfVersionTests.cs b/PdfSharpCore.Test/IO/PdfVersionTests.cs new file mode 100644 index 00000000..5d7ef28f --- /dev/null +++ b/PdfSharpCore.Test/IO/PdfVersionTests.cs @@ -0,0 +1,65 @@ +using FluentAssertions; +using PdfSharpCore.Pdf; +using PdfSharpCore.Pdf.IO; +using PdfSharpCore.Test.Helpers; +using System.IO; +using System.Text; +using Xunit; + +namespace PdfSharpCore.Test.IO +{ + public class PdfVersionTests + { + [Theory] + [InlineData("%PDF-1.0\n", 10)] + [InlineData("%PDF-1.4\n", 14)] + [InlineData("%PDF-1.7\n", 17)] + [InlineData("%PDF-2.0\n", 20)] + // Acrobat also accepts a PostScript style header with the PDF version embedded. + [InlineData("%!PS-Adobe-3.0 PDF-2.0\n", 20)] + public void TestPdfFile_returnsTheVersionOfTheHeader(string header, int expected) + { + Pdf.IO.PdfReader.TestPdfFile(Encoding.ASCII.GetBytes(header)).Should().Be(expected); + } + + [Theory] + [InlineData("Definitely not a PDF")] + [InlineData("%PDF-0.9\n")] + [InlineData("%PDF-1.A\n")] + [InlineData("%PDF\n")] + public void TestPdfFile_returnsZeroForANonPdfHeader(string header) + { + Pdf.IO.PdfReader.TestPdfFile(Encoding.ASCII.GetBytes(header)).Should().Be(0); + } + + [Fact] + public void Should_beAbleToReadAPdf20Document() + { + using var fs = File.OpenRead(PathHelper.GetInstance().GetAssetPath("Pdf20.pdf")); + var inputDocument = Pdf.IO.PdfReader.Open(fs, PdfDocumentOpenMode.Import); + + inputDocument.Should().NotBeNull(); + inputDocument.Version.Should().Be(20); + inputDocument.PageCount.Should().BeGreaterThan(0); + } + + [Fact] + public void Should_preserveThePdf20HeaderWhenSavingADocument() + { + using var fs = File.OpenRead(PathHelper.GetInstance().GetAssetPath("Pdf20.pdf")); + var inputDocument = Pdf.IO.PdfReader.Open(fs, PdfDocumentOpenMode.Modify); + + using var ms = new MemoryStream(); + inputDocument.Save(ms, false); + + Encoding.ASCII.GetString(ms.ToArray(), 0, 8).Should().Be("%PDF-2.0"); + } + + [Fact] + public void Version_acceptsPdf20() + { + var document = new PdfDocument { Version = 20 }; + document.Version.Should().Be(20); + } + } +} diff --git a/PdfSharpCore/Pdf.IO/PdfReader.cs b/PdfSharpCore/Pdf.IO/PdfReader.cs index 2cdb0b90..8e41f7cd 100644 --- a/PdfSharpCore/Pdf.IO/PdfReader.cs +++ b/PdfSharpCore/Pdf.IO/PdfReader.cs @@ -153,19 +153,37 @@ public static int TestPdfFile(byte[] data) /// /// internal static int GetPdfFileVersion(byte[] bytes) + { + int version = ScanFileVersion(PdfEncoders.RawEncoding, bytes); + + // If it doesn't work with the specified encoding the file might be incorrectly encoded as ASCII. + if (version == 0) + version = ScanFileVersion(System.Text.Encoding.ASCII, bytes); + + return version; + } + + /// + /// Scans the file header for «%PDF-x.y» using the specified encoding and returns the version + /// as an integer (e.g. 14 for PDF 1.4, 20 for PDF 2.0), or 0 if no version was found. + /// + static int ScanFileVersion(System.Text.Encoding encoding, byte[] bytes) { try { // Acrobat accepts headers like «%!PS-Adobe-N.n PDF-M.m»... - string header = PdfEncoders.RawEncoding.GetString(bytes, 0, bytes.Length); // Encoding.ASCII.GetString(bytes); + string header = encoding.GetString(bytes, 0, bytes.Length); + if (header.Length == 0) + return 0; if (header[0] == '%' || header.IndexOf("%PDF", StringComparison.Ordinal) >= 0) { int ich = header.IndexOf("PDF-", StringComparison.Ordinal); - if (ich > 0 && header[ich + 5] == '.') + if (ich > 0 && ich + 6 < header.Length && header[ich + 5] == '.') { char major = header[ich + 4]; char minor = header[ich + 6]; - if (major >= '1' && major < '2' && minor >= '0' && minor <= '9') + // PDF 1.0 to 1.7 and PDF 2.0 are the versions defined so far. + if (major >= '1' && major <= '2' && minor >= '0' && minor <= '9') return (major - '0') * 10 + (minor - '0'); } } @@ -174,26 +192,6 @@ internal static int GetPdfFileVersion(byte[] bytes) catch { } - // If it doesn't work with the specified encoding ... - try - { - // The file might be incorrectly encoded as ASCII - string header = System.Text.Encoding.ASCII.GetString(bytes); - if (header[0] == '%' || header.IndexOf("%PDF", StringComparison.Ordinal) >= 0) - { - int ich = header.IndexOf("PDF-", StringComparison.Ordinal); - if (ich > 0 && header[ich + 5] == '.') - { - char major = header[ich + 4]; - char minor = header[ich + 6]; - if (major >= '1' && major < '2' && minor >= '0' && minor <= '9') - return (major - '0') * 10 + (minor - '0'); - } - } - } - // ReSharper disable once EmptyGeneralCatchClause - catch { } - return 0; } diff --git a/PdfSharpCore/Pdf/PdfDocument.cs b/PdfSharpCore/Pdf/PdfDocument.cs index 674ef4b0..fb7e6f68 100644 --- a/PdfSharpCore/Pdf/PdfDocument.cs +++ b/PdfSharpCore/Pdf/PdfDocument.cs @@ -483,6 +483,7 @@ internal bool EarlyWrite /// /// Gets or sets the PDF version number. Return value 14 e.g. means PDF 1.4 / Acrobat 5 etc. + /// Return value 20 means PDF 2.0. /// public int Version { @@ -491,7 +492,7 @@ public int Version { if (!CanModify) throw new InvalidOperationException(PSSR.CannotModify); - if (value < 12 || value > 17) // TODO not really implemented + if ((value < 12 || value > 17) && value != 20) // TODO not really implemented throw new ArgumentException(PSSR.InvalidVersionNumber, "value"); _version = value; } diff --git a/PdfSharpCore/root/PSSR.cs b/PdfSharpCore/root/PSSR.cs index f047cbfd..62c7f376 100644 --- a/PdfSharpCore/root/PSSR.cs +++ b/PdfSharpCore/root/PSSR.cs @@ -217,7 +217,7 @@ public static string InvalidPdf public static string InvalidVersionNumber { - get { return "Invalid version number. Valid values are 12, 13, and 14."; } + get { return "Invalid version number. Valid values are 12 to 17 and 20."; } } public static string CannotHandleXRefStreams