Tintero Developers

The .tint package

What is inside the file someone sends you when they share a project. The archive layout, how to recognise it, and the one thing a round trip through it loses.

On this page

A project on disk is a folder. A .tint is that same folder packed into a single file, which is what people actually send each other when they share a manuscript. It is also what third party cloud sync moves around, so a project living in Drive or OneDrive is a .tint going up and down whole.

The good news, if you have one in front of you: it is a zip. Rename it, unzip it, and everything inside is plain JSON except the images.

Recognising one

The importer sniffs the first two bytes and accepts two containers, which is not obvious and is the sort of thing you get wrong once:

ZIP0x50 0x4B
GZIP0x1F 0x8B

A plain zip, or a zip that has been gzipped. If neither signature matches, the importer still tries both before giving up, so a .tint with the wrong bytes at the front can still open. Anything Tintero itself writes is a plain zip, deflated at level 9.

What is inside

project.jsonthe whole manifest, pretty printed
files/read back on import
docs/read back on import
notes/read back on import
images/written, never read back
files/snapshots/read back on import

Three differences from the folder on disk, and all three matter:

  • The manifest is called project.json, not <id>.tintero. A scanner looking for *.tintero will not find it.
  • Content files have no extension. Inside files/ an entry is named by its bare id, with no .json on the end, although the contents are the same ProseMirror JSON.
  • Chapters, docs and notes are separated into folders of their own, where on disk they all share one flat files/. On import they are flattened back together.

Audio and PDFs are not in a .tint at all. Send someone a project with narrations attached and they get the project without them.

The round trip is not lossless

That list is not written by hand. It is read out of the exporter and the importer on every build, so the day someone fixes it, this page stops saying it.

Reading one yourself

Nothing here needs Tintero. Unzip it, parse project.json against the project format reference, and read each entry’s text from files/<location>, docs/<location> or notes/<location> as ProseMirror JSON.

import { readFile } from 'node:fs/promises';
import AdmZip from 'adm-zip';

const zip = new AdmZip(await readFile('Manuscript.tint'));
const project = JSON.parse(zip.readAsText('project.json'));

for (const file of project.files) {
  const entry = zip.getEntry(`files/${file.location}`);
  const document = entry ? JSON.parse(zip.readAsText(entry)) : null;

  console.log(file.name, document?.content?.length ?? 0, 'top-level nodes');
}

The one thing to be careful about is what you write back. A project rebuilt by hand and imported goes through the app’s allow list, and anything not on it is dropped without a word.