From the PDF file definitions (7.5.4 - 7.5.6) you don't need to modify the inner structure of a PDF file to make changes, its enough to append the new definitions of the components (New or old) and suitable cross-reference section with pointers to the shift positions of some relevant components for random access.
Here I attempt some code to do all the work in Mathematica.
It still needs some polishing, probably with some help I could write a reasonable package.
needed improvements
- Currently it is also only modifying the "/Info" section, not the "Metadata" but that is doable in the same way.
- I'm sure most of the parsing, writing and strings operations can be done in a more clever and compact way too.
- It assumes a single xref subsection, which would be true for fresh Mathematica generated files, but not universal.
Edits
- Now it quits gracefully if the file isn't writable.
- The "ModDate" for last modified date is updated
Usage:
fn = "test.PDF";
Export[fn, α];
pdfWriteInfo[fn, {
"Title" -> "Alpha"
, "Producer" -> "Mod by RH"
, "Author" -> $UserName
, "Subject" -> "mathematica.stackexchange.com/questions/59270"
, "Keywords" -> "PDF,Mathematica"
}]

PDF code
we are dealing whith this portion of plaintext at the end of the PDF

Define PDF Readout functions
Open the file to read the last 30 bytes, finds the "startxref" and reads the shift position of "xref". Checks that the files ends in "%%EOF".
Read position of cross reference
We need this to avoid a long search in a potentially huge file.
pdfReadStartXref::notPDF = "`1` does not comply with the PDF format";
pdfReadStartXref[file_String] := If[
FileExistsQ[file],
Block[{fs, sxref} (* File Stream ; Start of Cross refference*),
fs = OpenRead[file, BinaryFormat -> True];
SetStreamPosition[fs, -30];
If[Find[fs, "startxref"] === EndOfFile, Close[fs];
Message[pdfReadStartXref::notPDF, file]; Return[Null]];
sxref = Read[fs, Number];
If[Find[fs, "%%EOF"] === EndOfFile, Close[fs];
Message[pdfReadStartXref::notPDF, file]; Return[Null]];
Close[fs];
sxref
]]
test
pdfReadStartXref[fn]
4041
That number we use to SetStreamPosition
next to the xref and trailer section.
Read trailer
pdfReadTrailer[file_String, esxref_: 0] := If[
FileExistsQ[file],
Block[{filestream, startxref, trailer, starttrailer} (*
File Stream *),
filestream = OpenRead[file, BinaryFormat -> True];
startxref = If[
esxref === 0,
SetStreamPosition[filestream, -30];
Read[
StringToStream[
Read[filestream, Record,
RecordSeparators -> {{"startxref"}, {"%%EOF"}}]], Number],
esxref
];
SetStreamPosition[filestream, startxref];
Find[filestream, "trailer"];
starttrailer = StreamPosition[filestream];
trailer = Association@Flatten@StringSplit[
ReadList[
StringToStream[
Read[filestream, Record, RecordSeparators -> {{"<<"}, {">>"}}]
], Record],
Shortest[___ ~~ "/" ~~ key__ ~~ Whitespace] ~~
Longest[value___ ~~ EndOfString] :> key -> value];
Close[filestream];
{
starttrailer,
Map[If[StringMatchQ[#, NumberString],
Interpreter["Number"][#], #] &, Map[StringSplit, trailer], {2}]
}
]
]
test
pdfReadTrailer[fn]
{4358, <|"Size" -> {15}, "Root" -> {14, 0, "R"}, "Info" -> {13, 0, "R"}|>}
Now we know that the Info section is labeled "13 0".
Read crossreference
pdfReadXRef[file_String, esxref_: 0] := If[
FileExistsQ[file],
Block[{filestream, startxref, xref} (* File Stream *),
startxref = If[esxref === 0, pdfReadStartXref[file], esxref];
filestream = OpenRead[file, BinaryFormat -> True];
SetStreamPosition[filestream, startxref];
Find[filestream, "xref"];
xref = Association@Table[
{i -> Read[filestream, {Number, Number, String}]}
, {i, #1, #1 + #2 - 1}] & @@ Read[filestream, {Number, Number}];
Close[filestream];
{startxref, xref}
]
]
test
pdfReadXRef[fn]
{4041, <|0 -> {0, 65535, " f "}, 1 -> {3721, 0, " n "}, 2 -> {182, 0, " n "}, 3 -> {15, 0, " n "}, 4 -> {161, 0, " n "}, 5 -> {3557, 0, " n "}, 6 -> {291, 0, " n "}, 7 -> {502, 0, " n "}, 8 -> {2690, 0, " n "}, 9 -> {2713, 0, " n "}, 10 -> {3015, 0, " n "}, 11 -> {3038, 0, " n "}, 12 -> {3317, 0, " n "}, 13 -> {3786, 0, " n "}, 14 -> {3988, 0, " n "}|>}
Now we know where in the file is defined each section. We care only for "13 0". The number we were looking for was "3786".
Read Info
Now we can SetStreamPosition
to "3786" where Info is defined and read its contents.
pdfReadInfo::xref = "Failed parsing xref in file `1` .";
pdfReadInfo[file_String] := If[
FileExistsQ[file],
Block[{startxref, xref, trailer, trailerinfo, startInfo, filestream,
info, infohead, inforef} ,
{startxref, xref} = pdfReadXRef[file];
trailer = Last@pdfReadTrailer[file, startxref];
trailerinfo = trailer["Info"];
inforef = xref[First[trailerinfo]];
If[Head[inforef] === Missing, Message[pdfReadInfo::xref, file];,
startInfo = First[inforef];
infohead = Flatten[{Most@trailerinfo, " obj"}];
filestream = OpenRead[file, BinaryFormat -> True];
SetStreamPosition[filestream, startInfo];
If[Read[filestream, {Number, Number, String}] =!= infohead,
Abort[]];
info = Association@Flatten@StringSplit[
ReadList[
StringToStream[
Read[filestream, Record,
RecordSeparators -> {{"<<"}, {">>"}}]], Record],
Shortest[___ ~~ "/" ~~ key__ ~~ Whitespace] ~~
Longest["(" ~~ value___ ~~ ")"] :> key -> value];
Close[filestream];
{infohead, info}
]
]
]
test
pdfReadInfo[fn]
{{13, 0, " obj"}, <|"Creator" -> "Wolfram Mathematica 10.2.0.0 for Microsoft Windows (64-bit) (July 7, 2015)", "Producer" -> "", "CreationDate" -> "20150918133940+01'00", "ModDate" -> "20150918133940+01'00"|>}
That's the old Info we will keep that as long as the user doesn't define the same key.
Write new Info
pdfWriteInfo::nowrite = " `1` file its not writable.";
pdfWriteInfo[file_String, options_: {"Producer" -> "Code by RH"}] :=
If[
FileExistsQ[file],
Block[{rinfo, infohead, info, newinfo, filestream, newstartxref,
oldstartxref, startinfo, trailer, moddatestr} ,
startinfo = FileByteCount[file];
If[startinfo > 100,
oldstartxref = pdfReadStartXref[file];
rinfo = pdfReadInfo[file];
If[rinfo =!= Null,
{infohead, info} = rinfo;
moddatestr =
"D:" <> StringReplace[
DateString[{"Year", "Month", "Day", "Hour", "Minute",
"Second", "ISOTimeZone"}], ":" -> "'"] <> "'";
newinfo =
Join[info, Association@options, <|"ModDate" -> moddatestr |>];
trailer = Last@pdfReadTrailer[file];
filestream = Check[OpenAppend[file, BinaryFormat -> True], Null];
If[filestream =!= Null,
WriteString[filestream, infohead[[1]], " ", infohead[[2]],
" obj\r\n<<\r\n"];
Table[
WriteString[filestream, " /", First[item], " (", Last[item],
")\r\n"]
, {item, Normal[newinfo]}
];
WriteString[filestream, ">>\r\n", "endobj", "\r\n"];
newstartxref = StreamPosition[filestream];
WriteString[filestream, "xref\r\n", "0 1\r\n",
"0000000000 65535 f\r\n", infohead[[1]], " ", infohead[[2]],
"\r\n",
IntegerString[startinfo, 10, 10], " ",
IntegerString[infohead[[2]], 10, 5], " n\r\n",
"trailer\r\n<<"];
Table[
WriteString[filestream, " /", First[item2], " ",
Sequence @@ Riffle[Last[item2], " "], "\r\n"]
, {item2, Normal[trailer]}];
WriteString[filestream, " /Prev ", oldstartxref, "\r\n",
">>\r\n", "startxref\r\n", newstartxref, "\r\n", "%%EOF"];
]
Close[filestream];
, Message[pdfWriteInfo::nowrite, file]
]]]]
test
pdfWriteInfo[fn, {
"Title" -> "Alpha"
, "Producer" -> "Mod by RH"
, "Author" -> $UserName}
]
Voila.