Export image and annotation to PDF using a template

I have a project that includes document images, and the corresponding transcriptions are in annotations.

I’d like to export the image and the annotation (and also, if possible, tags and other metadata) to a PDF, following a template like this:

It’s probably possible to achieve this with a script, but my technical resources are limited.

Could someone please help or point me in the right direction?

Thank you very much.

This is not a trivial thing to accomplish. May I ask why this specific layout?

Yes, I understand that this might not be easy to implement.

This layout serves an academic purpose, as it allows the original document and its corresponding transcription to be displayed side by side for comparison and study.

Won’t the original document be too small to read with this layout?

Opening the image in a document window, with the transcription in the annotations inspector—like in your first screenshot—seems much more functional to me. Why the PDF? (If it’s for sharing, why include the file path and item link?)

Could you provide some context and share a bit of your reasoning?

…like what?


Even though I’m not convinced of the utility of your desired output, I found the technical aspects interesting enough to give it a go. Here is a proof of concept. We can generate the PDF from a markdown document with a style sheet tailored for the purpose.

Markdown template

I guess there’s more raw HTML than markdown, but using markdown does make it easy to add the annotation via transclusion. It also simplifies writing tables, which I chose to do for the metadata.

css: x-devonthink-item://[Style Sheet]

<p class=title>[Image Name]</p>
<div class=infosheet>
<div class=image>
<img src="x-devonthink-item://[Image]">

|----|
| Path:      |    |
| Item Link: |    |
| Tags:      |    |

</div>
<div class=annotation>

{{x-devonthink-item://[Annotation File]}}

</div>
</div>

Style sheet

Save this as a CSS file and put it in your database. Copy the item link and paste it into the script that follows.

/*–––––––––––––––––––––
    Image Info Sheet
––––––––––––––––––––––*/
p.title {
	font-size: 1.1em;
	font-family: -apple-system, sans-serif;
	margin-block-end: .5rem;
}
/* Container */
.infosheet {
	display: grid;
	border: 1px solid black;
	grid-template-columns: 60% 40%;
	grid-auto-flow: column;
	grid-gap: 0;
	
	/* Right column */
	& .annotation {
		border-left: 1px solid black;
		padding: .5em;
	}
	
	/* Left column */
	& .image {
		font-family: -apple-system, sans-serif;
		& :is(table, tr, td) {
			border: none;
		}
		& table {
			margin-inline: .5em;
			margin-block-start: .5em;
			font-size: .95em;
			font-family: inherit;
		/* 	Remove alternate row background (DT default) */
			& tr:nth-child(2n) {
				background-color: inherit;
			}
			& td {
				padding: .25em .5em;
				overflow-wrap: anywhere;
				&:nth-of-type(1) {
					overflow-wrap: break-word;
					font-weight: 500;
				}
			}
		}
	}
	
}
@media print {
	body {
		font-size: 12px;
	}
}

Script (JXA)

I wrote this for DT3. For DT4 it needs a few adjustments, mainly swapping type for recordType. (There might be more I’m not aware of.)

(() => {
	const app = Application("DEVONthink 3"); // For DT4 use `Application("DEVONthink")`
	app.includeStandardAdditions = true;
	const stylesheet = "x-devonthink-item://..." // <-- Paste item link to the style sheet here

	const r = app.contentRecord();
	if ( !r || r.type() !== "picture" ) { // For DT4 use `r.recordType()`
		app.displayAlert("Please open an image");
		return;
	} else if ( !r.annotation() ) {
		app.displayAlert("Image has no annotation file");
		return;
	}
	const imgName = r.nameWithoutExtension();
	
	// Markdown template
	const md =
`css: ${stylesheet}

<p class=title>${imgName}</p>
<div class=infosheet>
<div class=image>
<img src="${r.referenceURL()}">

|----|
| Path:      | ${r.path()}            |
| Item Link: | ${r.referenceURL()}    |
| Tags:      | ${r.tags().join(', ')} |

</div>
<div class=annotation>

{{${r.annotation.referenceURL()}}}

</div>
</div>`

  const mdRec = app.createRecordWith(
    { name: `${imgName} (infosheet)`,
      type: "markdown", // For DT4 use `"record type"`
      "plain text": md },
    { in: r.locationGroup() });
  const pdfRec = app.createRecordWith(
	{ name: `${imgName} (infosheet)`,
	  type: "PDF document", // For DT4 use `"record type"`
	  data: mdRec.paginatedPDF() },
    { in: r.locationGroup() });
  
  // Cleanup: Move markdown file to trash (or delete it)
  app.move({record: mdRec,
	  to: mdRec.database.trashGroup() });
  // app.delete({record: mdRec});
})()

Example output:


Bonus

If you want the list of tags in the PDF to be clickable links, we can add something like this:

let linkedTags = [];
r.parents.whose({_not: [
	{_match: [ObjectSpecifier().tagType, "no tag"] }
]})()
.forEach(tag => 
	linkedTags.push(`[${tag.name()}](${tag.referenceURL()})`)
);

…and adjust the markdown template accordingly:

const md = 
// ...
`| Tags:   | ${linkedTags.join(', ')} |`
1 Like

Thank you so much troejgaard for taking the time to create this script!

The layout works exactly the way I wanted.

I agree that some of the metadata might not be that useful when sharing the PDF, but thanks to the base you’ve provided, I can now try making a few tweaks myself.

The script works perfectly for a single image. How could I apply it to multiple selected images?

Looks like it :wink: Thanks for the bait.

You could use `# {imgName}` to get rid of the <p class=title> (which is not very semantical, either).
In the CSS, most of the &, perhaps even all of them, are not needed (anymore). nth-of-type(1) looks more complicated to me than first-of-type.

And I suggest a grid for the body element with two columns. Something like

body {
  display: grid;
  grid-template-areas: "headline headline " 
  "image annotation" 
  "metadata annotation";
  grid-template-columns: 60% 40%;
}
h1 {
  grid-area: headline;
}
figure {
  grid-area: image;
}
table {
  grid-area: metadata;
} 
div {
  grid-area: annotation;
}

(plus all the other styling, of course)
Then the MD part could be simplified to

# ${imgName}

![](${r.referenceURL()})

| ---- |
| Path : | ${r.path} |
...

<div>
{{${r.annotation.referenceURL()}}
</div>

With this code, the image gets put in a figure element.

If DT rendered a transcluded document in a section or so, the div in the MD wouldn’t be needed. Alas… as it stands, one has no chance to see that content was transcluded in the final HTML. Something like <section data-url="url of transcluded file">the file's content</section>, and then one could write section {grid-area: annotation;}

Scrap that: The transcluded text will be inserted first, and then the whole MD document is processed. Just read it up in the MMD6 documentation.

1 Like

Why not just create an HTML file instead of Markdown?

1 Like

The main reason for using markdown was transcluding the annotation. Is there a way to do something similar in an HTML file?

I guess you could convert the annotation file first. Or get its source property, but that returns a full HTML document.

I was just curious.

Me too, I thought there might be something I wasn’t aware of when you asked :smiley:

You’re welcome. I’m glad you find it useful!
I’m still curious for some context, I would like to understand why it is useful.

It was a proof of concept, so I just wrote it to work with the currently displayed document (contentRecord). You could loop through all selected images like this:

(() => {
	const app = Application("DEVONthink 3"); // For DT4 use `Application("DEVONthink")`
	app.includeStandardAdditions = true;
	const stylesheet = "x-devonthink-item://..."; // <-- Paste item link to the style sheet here
	
	const records = app.selectedRecords.whose({
		_match: [ObjectSpecifier().type, "picture"] // For DT4 use `recordType`
	});
	records().forEach(r => {
	
		if (!r.annotation()) {
			app.logMessage("Missing annotation file", {record: r});
		} else {
			const imgName = r.nameWithoutExtension();
			// Markdown template
			const md =
`css: ${stylesheet}

<p class=title>${imgName}</p>
<div class=infosheet>
<div class=image>
<img src="${r.referenceURL()}">

|----|
| Path:      | ${r.path()}            |
| Item Link: | ${r.referenceURL()}    |
| Tags:      | ${r.tags().join(', ')} |

</div>
<div class=annotation>

{{${r.annotation.referenceURL()}}}

</div>
</div>`

			const mdRec = app.createRecordWith(
			  { name: `${imgName} (infosheet)`,
				type: "markdown", // For DT4 use `"record type"`
				"plain text": md },
			  { in: r.locationGroup() });
			const pdfRec = app.createRecordWith(
			  { name: `${imgName} (infosheet)`,
				type: "PDF document",
				data: mdRec.paginatedPDF() },
			  { in: r.locationGroup() });

			// Cleanup: Move markdown file to trash (or delete it)
			app.move({record: mdRec,
			  to: r.database.trashGroup() });
			// app.delete({record: mdRec});
		}

	})
})()

(I didn’t incorporate chrilleks suggestions here, it’s been a long day)

Once again, thank you so much!

As for the layout, it might be debatable, but I find it useful. I do genealogical research and use a genealogy program where I now can upload these PDFs and have a compact information about the document with both the transcription and the image, even if just a kind of a large thumbnail version.

Interesting. And all those folks above are why the DT community kicks ass.

2 Likes

Ah, that makes more sense to me. Thank you for adding some context! I didn’t doubt that you found it useful—I just got a bit confused when you said the purpose was to display the original and the transcription side-by-side “for comparison and study”, academically. The PDF output seemed suboptimal for that, so I assumed I was missing something.

Another reason I asked is that DEVONthink can be used in so many ways. Examples like yours can serve as inspiration and give other people ideas :smiley:

I actually thought it was an interesting idea. I can see the utility if one wants to just “look” at something without dealing with menus, sidebars, etc. Not of use to me but I get it. And I don’t even think OP intended it that way.

Good on you to tackle the thing. That’s why I said this space is awesome.