Идея превратить многолетний архив переписки из мессенджера в настоящую бумажную книгу в твердом переплете с красивой типографикой и иллюстрациями выглядит романтично ровно до тех пор, пока инженер не сталкивается с суровой реальностью браузерной платформы. Личные диалоги содержат сотни тысяч сообщений, приватные фотографии и конфиденциальные сведения, поэтому отправка таких архивов на удаленный сервер категорически недопустима по соображениям безопасности и требованиям регуляторов (GDPR, 152-ФЗ).
Разработка клиентского приложения «Чатограмма» от независимого инженера m1221 стала ярким примером решения этой задачи: верстка и компиляция полноценного 80-страничного полиграфического PDF-буклета формата A5 происходят целиком в браузере пользователя, включая мобильные устройства на iOS и Android. Чтобы веб-страница не вылетала с ошибкой нехватки памяти (Out of Memory), автору пришлось построить филигранную архитектуру управления ресурсами движка JavaScript.
Изоляция парсинга в Web Worker и сабсеттинг шрифтов
Первым вызовом стал разбор исходных дампов переписки. Экспорт чата в Telegram представляет собой JSON-файл объемом до 150 МБ, а архив сообщений ВКонтакте выгружается в виде набора HTML-документов в устаревшей кодировке windows-1251.
Если выполнять парсинг такого объема данных в основном потоке интерфейса, браузер немедленно зависнет, а сборщик мусора V8 начнет судорожно замораживать интерфейс. Решением стал одноразовый фоновый Web Worker:
- Основной поток передает файл архива в воркер.
- Фоновый поток нормализует сообщения, фильтрует служебные события и формирует компактный массив структур.
- Данные возвращаются в основной поток через
postMessage, после чего воркер немедленно уничтожается принудительным вызовомworker.terminate().
Такой паттерн гарантирует, что десятки мегабайт временных строковых объектов не останутся в куче памяти браузера, а сборщик мусора очистит оперативную память до начала следующего этапа.
Вторым препятствием стал размер типографических шрифтов. Библиотека jsPDF требует внедрения шрифтов формата TrueType (TTF), закодированных в строку base64. Подключение полных гарнитур Lora и Montserrat для заголовков и основного текста раздуло бы объем документа более чем на 15 мегабайт. Приложение динамически анализирует весь текст переписки, собирает сабсет (только реально встречающиеся глифы алфавита и символы пунктуации) и кодирует в PDF исключительно задействованные символы, сокращая размер шрифтового блока до нескольких сотен килобайт.
Векторная верстка на Canvas и утилизация холстов
Полиграфическая верстка требует строгой привязки к миллиметрам: перенос строк, выравнивание пузырей сообщений, плашки с датами и цитаты должны выглядеть безупречно. Для расчета типографики используется контекст HTML5 Canvas 2D и метод ctx.measureText(). Движок предварительно рассчитывает координаты всех текстовых блоков и делит книгу на главы и страницы.

Однако мобильный браузер Safari накладывает жесткое системное ограничение на совокупный объем выделенной видеопамяти под элементы Canvas (Canvas Allocation Limit). Попытка создать отдельный элемент <canvas> для каждой из 80 страниц книги неизбежно приводила к мгновенному падению вкладки браузера.
Архитектурным спасением стал паттерн единственного переиспользуемого холста:
- В памяти создается ровно один холст с физическим разрешением целевой страницы.
- На нем отрисовываются векторные элементы страницы.
- Изображение переносится в формируемый документ
jsPDF. - Холст очищается методом
ctx.clearRect(), а его буфер сбрасывается обнулением размеров (canvas.width = 0).
Ниже представлен пример расчетного цикла подготовки текстового блока в Canvas:
interface MessageBlock {
author: string;
text: string;
timestamp: string;
}
// Расчет переносов строк и габаритов пузыря сообщения на холсте
function layoutMessage(ctx: CanvasRenderingContext2D, msg: MessageBlock, maxWidth: number) {
ctx.font = '14px Lora';
const words = msg.text.split(' ');
const lines: string[] = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) lines.push(currentLine);
return { lines, height: lines.length * 20 + 16 };
}
Постраничная сборка и спуск полос для брошюры
После того как макет страниц рассчитан, подключается генератор документа jsPDF. Чтобы избежать перегрузки памяти, экспорт страниц выполняется последовательно через порционные задержки, дающие браузеру возможность вернуть память сборщику мусора:
import { jsPDF } from 'jspdf';
// Постраничный экспорт с утилизацией холста и сбросом памяти
async function generateBookPdf(pagesData: MessageBlock[][], canvas: HTMLCanvasElement) {
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a5'
});
const ctx = canvas.getContext('2d')!;
for (let i = 0; i < pagesData.length; i++) {
if (i > 0) pdf.addPage('a5', 'portrait');
// Настройка размеров под A5 (148 x 210 мм при 300 DPI)
canvas.width = 1748;
canvas.height = 2480;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Отрисовка элементов текущей полосы
renderPageToCanvas(ctx, pagesData[i]);
// Перенос содержимого в PDF без промежуточных утечек памяти
const pageImage = canvas.toDataURL('image/jpeg', 0.85);
pdf.addImage(pageImage, 'JPEG', 0, 0, 148, 210, undefined, 'FAST');
// Принудительное обнуление для сброса видеопамяти в WebKit
canvas.width = 0;
canvas.height = 0;
await new Promise(resolve => setTimeout(resolve, 10));
}
return pdf.output('blob');
}
function renderPageToCanvas(ctx: CanvasRenderingContext2D, blocks: MessageBlock[]) {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 1748, 2480);
ctx.fillStyle = '#1a1a1a';
ctx.font = '24px Montserrat';
ctx.fillText('Глава переписки', 120, 150);
}
Финальным штрихом стал алгоритм спуска полос (Booklet Imposition). Если пользователь хочет распечатать книгу на домашнем принтере в виде брошюры на листах формата A4 со сгибом пополам, страницы нельзя выводить по порядку. Алгоритм переставляет полосы по математическому правилу: первая страница печатается рядом с последней (N), вторая — рядом с предпоследней (N-1), формируя правильные развороты.
Кейс «Чатограммы» доказывает: возможности современной веб-платформы вплотную приблизились к специализированным настольным издательским системам. Грамотное управление жизненным циклом объектов, Web Workers и бережное отношение к памяти позволяют реализовывать сложные полиграфические пайплайны прямо в окне браузера с сохранением безупречной приватности пользователя.
