|
1 | | -using QRCoder; |
2 | | -using Shouldly; |
3 | | -using Xunit; |
| 1 | +using System.IO; |
4 | 2 |
|
5 | 3 | namespace QRCoderTests; |
6 | 4 |
|
@@ -54,4 +52,97 @@ public void can_render_pdfbyte_qrcode_from_helper_2() |
54 | 52 | var pdfCodeGfx = PdfByteQRCodeHelper.GetQRCode("This is a quick test! 123#?", 5, "#FF0000", "#0000FF", QRCodeGenerator.ECCLevel.L); |
55 | 53 | pdfCodeGfx.ShouldMatchApproved("pdf"); |
56 | 54 | } |
| 55 | + |
| 56 | + private static readonly char[] _lineEndChars = { '\r', '\n' }; |
| 57 | + |
| 58 | + [Fact] |
| 59 | + public void pdf_xref_table_is_valid() |
| 60 | + { |
| 61 | + var gen = new QRCodeGenerator(); |
| 62 | + var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.L); |
| 63 | + var pdfBytes = new PdfByteQRCode(data).GetGraphic(5); |
| 64 | + |
| 65 | + // Parse from the end to find startxref |
| 66 | + var pdfText = Encoding.ASCII.GetString(pdfBytes); |
| 67 | + |
| 68 | + // Verify no \n line breaks; only \r\n should be used (this test file has no binary image data) |
| 69 | + pdfText.Replace("\r\n", "CRLF").ShouldNotContain('\n', "PDF should not contain LF line breaks; only CRLF should be used"); |
| 70 | + pdfText.Replace("\r\n", "CRLF").ShouldNotContain('\r', "PDF should not contain CR line breaks; only CRLF should be used"); |
| 71 | + |
| 72 | + // Find %%EOF at the end, then work backward to find startxref |
| 73 | + var eofIndex = pdfText.LastIndexOf("%%EOF", StringComparison.Ordinal); |
| 74 | + eofIndex.ShouldBeGreaterThan(0, "%%EOF not found"); |
| 75 | + |
| 76 | + var startxrefIndex = pdfText.LastIndexOf("startxref\r\n", eofIndex, StringComparison.Ordinal); |
| 77 | + startxrefIndex.ShouldBeGreaterThan(0, "startxref not found"); |
| 78 | + |
| 79 | + // Read the xref byte offset (the number on the line after "startxref") |
| 80 | + var afterStartxref = startxrefIndex + "startxref\r\n".Length; |
| 81 | + var endOfOffset = pdfText.IndexOf("\r\n", afterStartxref, StringComparison.Ordinal); |
| 82 | + var xrefOffsetStr = pdfText.Substring(afterStartxref, endOfOffset - afterStartxref); |
| 83 | + var xrefOffset = int.Parse(xrefOffsetStr, NumberStyles.None, CultureInfo.InvariantCulture); |
| 84 | + xrefOffset.ShouldBeGreaterThan(0, "xref byte offset should be positive"); |
| 85 | + |
| 86 | + // Seek to xref table and parse it |
| 87 | + using var stream = new MemoryStream(pdfBytes); |
| 88 | + stream.Position = xrefOffset; |
| 89 | + var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true); |
| 90 | + |
| 91 | + // First line must be "xref" |
| 92 | + var xrefLine = reader.ReadLine(); |
| 93 | + xrefLine.ShouldBe("xref", "xref keyword not found at expected offset"); |
| 94 | + |
| 95 | + // Parse subsections: "firstObjNum count" |
| 96 | + var objectOffsets = new Dictionary<int, long>(); |
| 97 | + string? subsectionLine; |
| 98 | + while ((subsectionLine = reader.ReadLine()) != null && subsectionLine != "trailer") |
| 99 | + { |
| 100 | + var parts = subsectionLine.Split(' '); |
| 101 | + parts.Length.ShouldBe(2, $"Expected 'firstObj count' but got: {subsectionLine}"); |
| 102 | + var firstObj = int.Parse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture); |
| 103 | + firstObj.ShouldBe(0); |
| 104 | + var count = int.Parse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture); |
| 105 | + |
| 106 | + for (int i = 0; i < count; i++) |
| 107 | + { |
| 108 | + // Each entry: "NNNNNNNNNN GGGGG f\r\n" or "NNNNNNNNNN GGGGG n\r\n" |
| 109 | + var entry = reader.ReadLine(); |
| 110 | + entry.ShouldNotBeNull(); |
| 111 | + entry.Length.ShouldBe(18); |
| 112 | + var entryParts = entry.Split(' '); |
| 113 | + entryParts.Length.ShouldBe(3, $"Expected 'offset gen type' but got: {entry}"); |
| 114 | + var offset = long.Parse(entryParts[0], NumberStyles.None, CultureInfo.InvariantCulture); |
| 115 | + var generation = int.Parse(entryParts[1], NumberStyles.None, CultureInfo.InvariantCulture); |
| 116 | + var type = entryParts[2]; |
| 117 | + type.ShouldBeOneOf("n", "f"); |
| 118 | + |
| 119 | + if (type == "n") |
| 120 | + { |
| 121 | + generation.ShouldBe(0, $"Expected generation 0 for in-use object but got {generation}"); |
| 122 | + objectOffsets[i] = offset; |
| 123 | + } |
| 124 | + else |
| 125 | + { |
| 126 | + // Free objects should only be listed for the first object in the subsection |
| 127 | + i.ShouldBe(0); |
| 128 | + offset.ShouldBe(0); |
| 129 | + generation.ShouldBe(65535, $"Expected generation 65535 for free object but got {generation}"); |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + objectOffsets.Count.ShouldBeGreaterThan(0, "No in-use objects found in xref table"); |
| 135 | + |
| 136 | + // Verify each object: seek to its offset and confirm "N 0 obj" is present |
| 137 | + foreach (var kvp in objectOffsets) |
| 138 | + { |
| 139 | + stream.Position = kvp.Value; |
| 140 | + var objNum = kvp.Key; |
| 141 | + var offset = kvp.Value; |
| 142 | + var objReader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true); |
| 143 | + var objLine = objReader.ReadLine(); |
| 144 | + objLine.ShouldNotBeNull($"No content at offset {offset} for object {objNum}"); |
| 145 | + objLine.ShouldBe($"{objNum} 0 obj", $"Object {objNum} at offset {offset} did not start with '{objNum} 0 obj'"); |
| 146 | + } |
| 147 | + } |
57 | 148 | } |
0 commit comments