Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 29 additions & 19 deletions QRCoder/PdfByteQRCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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();
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
97 changes: 94 additions & 3 deletions QRCoderTests/PdfByteQRCodeRendererTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using QRCoder;
using Shouldly;
using Xunit;
using System.IO;

namespace QRCoderTests;

Expand Down Expand Up @@ -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<int, long>();
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'");
}
}
}
Binary file modified QRCoderTests/TransposeVerificationTests.pdf_renderer.approved.pdf
Binary file not shown.