diff --git a/.gitattributes b/.gitattributes index 412eeda7..8b42d130 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,7 +16,7 @@ *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain +*.pdf binary +*.PDF binary *.rtf diff=astextplain *.RTF diff=astextplain diff --git a/QRCoder/PdfByteQRCode.cs b/QRCoder/PdfByteQRCode.cs index dd80fc84..ae994813 100644 --- a/QRCoder/PdfByteQRCode.cs +++ b/QRCoder/PdfByteQRCode.cs @@ -72,7 +72,7 @@ public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string li // Binary comment - ensures PDF is treated as binary file (prevents text mode corruption) stream.Write(_pdfBinaryComment, 0, _pdfBinaryComment.Length); - writer.WriteLine(); + writer.Write("\r\n"); writer.Flush(); xrefs.Add(stream.Position); @@ -92,18 +92,12 @@ public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string li // Object 2: Pages - defines page tree structure writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<<\r\n" + // Begin dictionary - "/Count 1\r\n" + // Number of pages in document - "/Kids [ <<\r\n" + // Array of page objects - begin inline page dictionary - "/Type /Page\r\n" + // Declares this as a page - "/Parent 2 0 R\r\n" + // References parent Pages object - "/MediaBox [0 0 " + pdfMediaSize + " " + pdfMediaSize + "]\r\n" + // Page dimensions [x1 y1 x2 y2] - "/Resources << /ProcSet [ /PDF ] >>\r\n" + // Required resources: PDF operations only (no images) - "/Contents 3 0 R\r\n" + // References content stream (object 3) - ">> ]\r\n" + // End inline page dictionary and Kids array - ">>\r\n" + // End dictionary - "endobj\r\n" // End object + ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) + "<<\r\n" + // Begin dictionary + "/Count 1\r\n" + // Number of pages in document + "/Kids [ 3 0 R ]\r\n" + // Kids must contain indirect references to Page objects + ">>\r\n" + // End dictionary + "endobj\r\n" // End object ); // Content stream - PDF drawing instructions @@ -122,13 +116,29 @@ public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string li writer.Flush(); xrefs.Add(stream.Position); - // Object 3: Content stream - contains the drawing instructions + // Object 3: Page - indirect page object (Kids array must reference pages indirectly) + writer.Write( + ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) + "<<\r\n" + // Begin dictionary + "/Type /Page\r\n" + // Declares this as a page + "/Parent 2 0 R\r\n" + // References parent Pages object + "/MediaBox [0 0 " + pdfMediaSize + " " + pdfMediaSize + "]\r\n" + // Page dimensions [x1 y1 x2 y2] + "/Resources << /ProcSet [ /PDF ] >>\r\n" + // Required resources: PDF operations only (no images) + "/Contents 4 0 R\r\n" + // References content stream (object 4) + ">>\r\n" + // End dictionary + "endobj\r\n" // End object + ); + + writer.Flush(); + xrefs.Add(stream.Position); + + // Object 4: Content stream - contains the drawing instructions writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<< /Length " + ToStr(content.Length) + " >>\r\n" + // Dictionary with stream length in bytes - "stream\r\n" + // Begin stream data - content + "endstream\r\n" + // Stream content followed by end stream marker - "endobj\r\n" // End object + ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) + "<< /Length " + ToStr(System.Text.Encoding.ASCII.GetByteCount(content)) + " >>\r\n" + // Dictionary with stream length in bytes + "stream\r\n" + // Begin stream data + content + "endstream\r\n" + // Stream content followed by end stream marker + "endobj\r\n" // End object ); writer.Flush(); diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_blackwhite.approved.pdf b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_blackwhite.approved.pdf index fde095c8..ca00f1f9 100644 Binary files a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_blackwhite.approved.pdf and b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_blackwhite.approved.pdf differ diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_color.approved.pdf b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_color.approved.pdf index 54874db3..12bb9212 100644 Binary files a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_color.approved.pdf and b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_color.approved.pdf differ diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_custom_dpi.approved.pdf b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_custom_dpi.approved.pdf index 1012c199..bf62cadb 100644 Binary files a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_custom_dpi.approved.pdf and b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_custom_dpi.approved.pdf differ diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper.approved.pdf b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper.approved.pdf index f70a6e13..ea8d941a 100644 Binary files a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper.approved.pdf and b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper.approved.pdf differ diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper_2.approved.pdf b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper_2.approved.pdf index 54874db3..12bb9212 100644 Binary files a/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper_2.approved.pdf and b/QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper_2.approved.pdf differ diff --git a/QRCoderTests/PdfByteQRCodeRendererTests.cs b/QRCoderTests/PdfByteQRCodeRendererTests.cs index d41fc82a..ad02b27c 100644 --- a/QRCoderTests/PdfByteQRCodeRendererTests.cs +++ b/QRCoderTests/PdfByteQRCodeRendererTests.cs @@ -1,6 +1,4 @@ -using QRCoder; -using Shouldly; -using Xunit; +using System.IO; namespace QRCoderTests; @@ -54,4 +52,97 @@ public void can_render_pdfbyte_qrcode_from_helper_2() var pdfCodeGfx = PdfByteQRCodeHelper.GetQRCode("This is a quick test! 123#?", 5, "#FF0000", "#0000FF", QRCodeGenerator.ECCLevel.L); pdfCodeGfx.ShouldMatchApproved("pdf"); } + + private static readonly char[] _lineEndChars = { '\r', '\n' }; + + [Fact] + public void pdf_xref_table_is_valid() + { + var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.L); + var pdfBytes = new PdfByteQRCode(data).GetGraphic(5); + + // Parse from the end to find startxref + var pdfText = Encoding.ASCII.GetString(pdfBytes); + + // Verify no \n line breaks; only \r\n should be used (this test file has no binary image data) + pdfText.Replace("\r\n", "CRLF").ShouldNotContain('\n', "PDF should not contain LF line breaks; only CRLF should be used"); + pdfText.Replace("\r\n", "CRLF").ShouldNotContain('\r', "PDF should not contain CR line breaks; only CRLF should be used"); + + // Find %%EOF at the end, then work backward to find startxref + var eofIndex = pdfText.LastIndexOf("%%EOF", StringComparison.Ordinal); + eofIndex.ShouldBeGreaterThan(0, "%%EOF not found"); + + var startxrefIndex = pdfText.LastIndexOf("startxref\r\n", eofIndex, StringComparison.Ordinal); + startxrefIndex.ShouldBeGreaterThan(0, "startxref not found"); + + // Read the xref byte offset (the number on the line after "startxref") + var afterStartxref = startxrefIndex + "startxref\r\n".Length; + var endOfOffset = pdfText.IndexOf("\r\n", afterStartxref, StringComparison.Ordinal); + var xrefOffsetStr = pdfText.Substring(afterStartxref, endOfOffset - afterStartxref); + var xrefOffset = int.Parse(xrefOffsetStr, NumberStyles.None, CultureInfo.InvariantCulture); + xrefOffset.ShouldBeGreaterThan(0, "xref byte offset should be positive"); + + // Seek to xref table and parse it + using var stream = new MemoryStream(pdfBytes); + stream.Position = xrefOffset; + var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true); + + // First line must be "xref" + var xrefLine = reader.ReadLine(); + xrefLine.ShouldBe("xref", "xref keyword not found at expected offset"); + + // Parse subsections: "firstObjNum count" + var objectOffsets = new Dictionary(); + string? subsectionLine; + while ((subsectionLine = reader.ReadLine()) != null && subsectionLine != "trailer") + { + var parts = subsectionLine.Split(' '); + parts.Length.ShouldBe(2, $"Expected 'firstObj count' but got: {subsectionLine}"); + var firstObj = int.Parse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture); + firstObj.ShouldBe(0); + var count = int.Parse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture); + + for (int i = 0; i < count; i++) + { + // Each entry: "NNNNNNNNNN GGGGG f\r\n" or "NNNNNNNNNN GGGGG n\r\n" + var entry = reader.ReadLine(); + entry.ShouldNotBeNull(); + entry.Length.ShouldBe(18); + var entryParts = entry.Split(' '); + entryParts.Length.ShouldBe(3, $"Expected 'offset gen type' but got: {entry}"); + var offset = long.Parse(entryParts[0], NumberStyles.None, CultureInfo.InvariantCulture); + var generation = int.Parse(entryParts[1], NumberStyles.None, CultureInfo.InvariantCulture); + var type = entryParts[2]; + type.ShouldBeOneOf("n", "f"); + + if (type == "n") + { + generation.ShouldBe(0, $"Expected generation 0 for in-use object but got {generation}"); + objectOffsets[i] = offset; + } + else + { + // Free objects should only be listed for the first object in the subsection + i.ShouldBe(0); + offset.ShouldBe(0); + generation.ShouldBe(65535, $"Expected generation 65535 for free object but got {generation}"); + } + } + } + + objectOffsets.Count.ShouldBeGreaterThan(0, "No in-use objects found in xref table"); + + // Verify each object: seek to its offset and confirm "N 0 obj" is present + foreach (var kvp in objectOffsets) + { + stream.Position = kvp.Value; + var objNum = kvp.Key; + var offset = kvp.Value; + var objReader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true); + var objLine = objReader.ReadLine(); + objLine.ShouldNotBeNull($"No content at offset {offset} for object {objNum}"); + objLine.ShouldBe($"{objNum} 0 obj", $"Object {objNum} at offset {offset} did not start with '{objNum} 0 obj'"); + } + } } diff --git a/QRCoderTests/TransposeVerificationTests.pdf_renderer.approved.pdf b/QRCoderTests/TransposeVerificationTests.pdf_renderer.approved.pdf index 085c1a5b..6f70b668 100644 Binary files a/QRCoderTests/TransposeVerificationTests.pdf_renderer.approved.pdf and b/QRCoderTests/TransposeVerificationTests.pdf_renderer.approved.pdf differ