Seshat
Fonts and templates
Put missing fonts into a print file, change words and fonts, fill one template with many names, and find where a text sits.
What this is for
A print file that uses a font without carrying it prints in whatever font the printer has. A certificate needs a different name on every copy. A flyer needs one word changed the day before it prints. Seshat does all three on the PDF itself, and says exactly which font it used, so a font is never swapped without a word.
Seshat's fonts come from a library of open-licence fonts (OFL, Apache or UFL), free to use and embed.
Missing fonts
The check reports fonts_not_embedded and names which of the fonts are in the library (in_library) and which are not (not_in_library). It decides fix only when the library has every font.
The fix embed_fonts puts in the same font from the library. With the option lookalike, it also puts in a look-alike for the fonts the library does not have:
$fixed = Rekhu::seshat()->fix($fileId, [['fix' => 'embed_fonts']], $order);
$fixed = Rekhu::seshat()->fix($fileId, [['fix' => 'embed_fonts', 'option' => 'lookalike']], $order);
let fixed = await seshat.fix(fileId, [{ fix: 'embed_fonts' }], order);
fixed = await seshat.fix(fileId, [{ fix: 'embed_fonts', option: 'lookalike' }], order);
applied says per font what happened:
| Result | Meaning |
|---|---|
same |
the real font was put in |
lookalike |
a look-alike was put in, named in as; only with the option lookalike |
not_available |
the library does not have it; the file keeps the font as it was |
not_possible |
a Type 3 font, which the file draws itself |
A look-alike carries its own name inside the file, and the file's metadata records the substitution, so anyone who opens the file later can see it.
Your own font picker
fonts() lists the library: families, styles, PostScript names and licences. Offer them in your own text editor when a customer changes a word. fontFile() gives one font as TrueType with its licence, for an app that sets text with its own tool. Both are free.
$library = Rekhu::seshat()->fonts();
$font = Rekhu::seshat()->fontFile('Montserrat-Bold'); // ['name' => …, 'licence' => …, 'bytes' => …]
Rekhu::seshat()->fontFileTo('Montserrat-Bold', storage_path('fonts/Montserrat-Bold.ttf'));
const library = await seshat.fonts();
const font = await seshat.fontFile('Montserrat-Bold');
await seshat.fontFileTo('Montserrat-Bold', 'storage/fonts/Montserrat-Bold.ttf');
Change words and fonts
replace_text changes words at the same place and size. replace_font sets all text of one font in a library font. Both can come several times in one call; they are applied in order, as one charge.
$fixed = Rekhu::seshat()->fix($fileId, [
['fix' => 'replace_text', 'find' => 'Summer sale', 'replace' => 'Winter sale'],
['fix' => 'replace_text', 'find' => 'DRAFT', 'replace' => ''],
['fix' => 'replace_font', 'from' => 'ArialMT', 'to' => 'Montserrat-Bold'],
], $order);
const fixed = await seshat.fix(fileId, [
{ fix: 'replace_text', find: 'Summer sale', replace: 'Winter sale' },
{ fix: 'replace_text', find: 'DRAFT', replace: '' },
{ fix: 'replace_font', from: 'ArialMT', to: 'Montserrat-Bold' },
], order);
An empty replace deletes the text; nothing is drawn in its place. A placeholder that an editor split into several pieces inside the PDF is found whole.
Which font the new text gets
The new text may hold letters the file's font does not have. Seshat tries, in this order:
- The file's own font, when it has every letter.
- The same font from the library, when the library has it with those letters.
- The font in
to, when you gave one. - The closest free font with those letters: the same kind (sans, serif or mono), weight and slant; for another script, such as Arabic, a font of that script.
- Nothing. The text stays as it was, and
appliedreports it asnot_setwith the missing letters.
applied names per font where the text was set (set_in), so a choice is never hidden. A to that is not in the library is refused with InvalidRequest, naming the closest fonts.
Note: a PDF has no paragraphs, so a longer text does not re-wrap. Words after it on the same line move along. For a new paragraph, change the design before the PDF is made.
Many copies of one template
fillTemplate makes a copy of a PDF template per row: certificates, name badges, grade lists. Each row maps placeholders to values, such as ['[name]' => 'Sara', '[grade]' => 'A'], or is a list of find and replace.
- Make the template. A PDF with placeholders such as
[name]and[grade]where the values go. - Start the job. Call
fillTemplatewith the template (an upload, a path, bytes with a name, or afile_id), the rows and the options. The template is uploaded once. - Wait for it.
waitForpolls the job until it is done or failed, every 2 seconds for up to 10 minutes. Or give awebhook_url. - Download the files. The result lists the files: a ZIP of one PDF per row, or one merged PDF, per 250 copies.
- Read the report. Each row says
done,partly(with what was not found or the missing letters) orfailed.
$rows = [
['[name]' => 'Sara Ahmed', '[grade]' => 'A'],
['[name]' => 'Tom Berg', '[grade]' => 'B'],
];
$job = Rekhu::seshat()->fillTemplate($request->file('template'), $rows, ['name_by' => '[name]', 'align' => 'center']);
$job = Rekhu::seshat()->waitFor($job['job_id']);
foreach ($job['result']['files'] as $file) {
Rekhu::seshat()->downloadTo($file['file_id'], storage_path('app/certificates/'.$file['name']));
}
const rows = [
{ '[name]': 'Sara Ahmed', '[grade]': 'A' },
{ '[name]': 'Tom Berg', '[grade]': 'B' },
];
let job = await seshat.fillTemplate('/data/certificate.pdf', rows, { nameBy: '[name]', align: 'center' });
job = await seshat.waitFor(job.job_id);
for (const file of job.result.files) {
await seshat.downloadTo(file.file_id, `storage/certificates/${file.name}`);
}
| Option (Laravel / Node.js) | |
|---|---|
output |
zip (one PDF per row, the default) or merged (one PDF) |
name_by / nameBy |
the placeholder whose value names each file, in plain Latin letters |
align |
left, center or right: keep the start, middle or end of each filled text, for a centred certificate |
webhook_url / webhookUrl |
where the result is posted, signed with the job's secret |
name |
the template's name, when you send bytes |
A job takes up to 5,000 rows; the package sends them in steps of 1,000. A ZIP is closed sooner when it reaches about 50 MB. A name is set in the template's own font when it has the letters, else as described under Which font the new text gets.
What it costs. A job is charged per copy that came out right. A copy only partly set (a name with a letter the font lacks) is free up to a tenth of the job. A copy where nothing could be set is never charged. Polling the job shows what it cost in meta.cost_usd.
Where a text sits
textPositions finds a text, or a list of texts, in a file. Per match it gives the page, the box in points from the bottom left corner, the font and the size the text prints at. Use it to place your own layer next to a placeholder.
$positions = Rekhu::seshat()->textPositions($fileId, ['[name]', '[grade]']);
const positions = await seshat.textPositions(fileId, ['[name]', '[grade]']);
The search matches case; pass matchCase: false (Laravel) or { matchCase: false } (Node.js) to ignore it.
Next
- Fixing files: every other fix, and what each call returns.
- Every task, with examples: profiles, PDF/X, pages and pictures.