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.
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 0x4BGZIP0x1F 0x8BA 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 printedfiles/read back on importdocs/read back on importnotes/read back on importimages/written, never read backfiles/snapshots/read back on importThree differences from the folder on disk, and all three matter:
- The manifest is called
project.json, not<id>.tintero. A scanner looking for*.tinterowill not find it. - Content files have no extension. Inside
files/an entry is named by its bare id, with no.jsonon 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.