24
$\begingroup$

I want to export a figure from Mathematica in PDF format. Moreover, I want to fill Author and Title fields of the PDF. For example, to fill Author field, I use the following code:

pp = Plot[{x^2}, {x, -1, 1}]
Export["plot-example.pdf", pp, "Author" -> "Dmitry"]

However, when I open generated PDF file in a PDF viewer, the "Author" field is not filled.

How should I specify Export call to include Author and Title fields in the generated PDF file?

$\endgroup$
  • 1
    $\begingroup$ @rhermans The attachments work here (V9), but not the Author, Creator, etc $\endgroup$ – Dr. belisarius Sep 9 '14 at 13:54
  • 1
    $\begingroup$ I didn't see any attachments in the pdf when I tried v10. Anybody else can confirm? $\endgroup$ – rhermans Sep 9 '14 at 14:30
  • 1
    $\begingroup$ @rhermans Indeed, the attachment example for PDF in the help browser doesn't work for me in version 10.1 on OS X. So I agree that PDF export is suffering from multiple bugs. $\endgroup$ – Jens Sep 14 '15 at 22:39
  • 1
    $\begingroup$ I've added the bugs tag because the comments confirm that this should work and does not. I have reported this as [CASE:3422830]. $\endgroup$ – Jens Sep 14 '15 at 22:47
  • 1
    $\begingroup$ @Mr.Wizard PDF supports Incremental Update and random access, so we don't need to read large files complete, and only need to append the modifications. In this case we would only need to append a new version of the info and metadata sections. See my answer. $\endgroup$ – rhermans Sep 18 '15 at 15:04
22
+50
$\begingroup$

The package and all code of this answer can be found on my GitHub account.


A solution that takes only small amount of time is to follow this route:

  • take the first usable java library for accessing and changing PDF files you find
  • do one of the following:
    • write a small amount of Java code to create a simple interface to the functionality (if you are interested, this design pattern is call Facade)
    • use the Mathematica functions of JLink` directly to access the PDF library. This is often harder to read IMO and I would recommend this only in rare cases
  • write a small Mathematica package that can be loaded and provides you with a high-level function to call the Java library

The structure of the finished package layout can be as easy as this

PDFMetaData
├── Java
│   └── PDFMetaData.jar
└── PDFMetaData.m

In this simple case PDFMetaData.m contains a function like this

SetPDFMetaData[file_String][meta : {(_String -> _String)..}] := JavaBlock[
  InstallJava[];
  LoadJavaClass["de.halirutan.pdfmetadata.PDFMetaDataSetter",
    StaticsVisible -> True];
  PDFMetaDataSetter`setMetaData[file, Flatten[List @@@ meta]]
];

The static Java function that is called at the end, is rather simple too. I should note, that I have compiled my source and packed the created class file together with the PDF library itself into PDFMetaData.jar. This jar is now selfcontained.

The package needs to be put into a place where Mathematica can find it, like

FileNameJoin[{$UserBaseDirectory, "Applications"}]

This is especially important, because then, the Java folder is automatically added to the java CLASSPATH.

Well, that's basically it. Go and use it

<< PDFMetaData`

SetPDFMetaData[
  Export["tmp/meta.pdf", "Hello"]][{"Author" -> "Xavier Breath", 
  "Title" -> "Stop Arguing!", "Creator" -> "Funny Inc.", 
  "Producer" -> "Nick R. Elastic",
  "Keywords" -> "Mathematica PDF"}]

enter image description here


Final note, please just change the call pattern of SetPDFMetaData[file][metalist] to SetPDFMetaData[file, metalist] if you feel more comfortable with it. I had no specific reason to use SubValues.

$\endgroup$
  • $\begingroup$ Thank you, Patrick. This is wonderfully easy to install and use. If this were my Question I would Accept this. $\endgroup$ – Mr.Wizard Sep 23 '15 at 1:10
  • $\begingroup$ Incidentally do you know of an easy way to "lock" a PDF file to prevent modification but still allow printing? (I know such measures are defeatable but it still lets responsible people know it's not supposed to be modified.) $\endgroup$ – Mr.Wizard Sep 23 '15 at 1:19
  • $\begingroup$ @Mr.Wizard Yes, in theory it's pretty simple and a basic example can be found here. The problem is, it doesn't work on my machine. What you want is to set certain permissions and then encrypt the pdf without a user password. With this, the user can open the pdf, but is only allowed to do certain operations. To gain complete access, you would need to give the owner password which is 1234 in the linked example. I already added this to the package, but as I said, for me it doesn't work. If you want I can still push it so you can testIt $\endgroup$ – halirutan Sep 23 '15 at 5:51
  • $\begingroup$ If you feel like providing a wrapper for it I am curious to test it. $\endgroup$ – Mr.Wizard Sep 23 '15 at 10:38
  • $\begingroup$ @Mr.Wizard I have pushed the version to the repository but I'm pretty sure it won't work. I have created a unit test where I lock a test-pdf and verify that afterwards the permission are still not set correctly. For the Mathematica package, I have added a PDFLock function. $\endgroup$ – halirutan Sep 23 '15 at 13:22
13
+100
$\begingroup$

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 metadata

PDF code

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

enter image description here

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.

$\endgroup$
  • 3
    $\begingroup$ Thanks for taking the time to write such an extensive method! I'll be checking this later. $\endgroup$ – Mr.Wizard Sep 18 '15 at 21:29
  • $\begingroup$ Well, I do have a personal interest on learning how to do this. Suggestions on how to do it better are most welcome. Particularly the parsing and the writing. $\endgroup$ – rhermans Sep 19 '15 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.