requireLogin();
$user_id = $_SESSION['user_id'];
$bookModel = new Book($this->pdo);
$chapterModel = new Chapter($this->pdo);
$book = $bookModel->findById($book_id);
if (!$book || $book['user_id'] != $user_id) {
$_SESSION['error'] = "Доступ запрещен";
$this->redirect('/books');
}
// Для автора - все главы
$chapters = $chapterModel->findByBook($book_id);
// Получаем информацию об авторе
$author_name = $this->getAuthorName($book['user_id']);
$this->handleExport($book, $chapters, false, $author_name, $format);
}
public function exportShared($share_token, $format = 'pdf') {
$bookModel = new Book($this->pdo);
$chapterModel = new Chapter($this->pdo);
$book = $bookModel->findByShareToken($share_token);
if (!$book) {
$_SESSION['error'] = "Книга не найдена";
$this->redirect('/');
}
// Для публичного доступа - только опубликованные главы
$chapters = $bookModel->getPublishedChapters($book['id']);
// Получаем информацию об авторе
$author_name = $this->getAuthorName($book['user_id']);
$this->handleExport($book, $chapters, true, $author_name, $format);
}
private function getAuthorName($user_id) {
$stmt = $this->pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
if ($author_info && $author_info['display_name'] != "") {
return $author_info['display_name'];
} elseif ($author_info) {
return $author_info['username'];
}
return "Неизвестный автор";
}
private function handleExport($book, $chapters, $is_public, $author_name, $format) {
$Parsedown = new ParsedownExtra();
switch ($format) {
case 'pdf':
$this->exportPDF($book, $chapters, $is_public, $author_name, $Parsedown);
break;
case 'docx':
$this->exportDOCX($book, $chapters, $is_public, $author_name, $Parsedown);
break;
case 'html':
$this->exportHTML($book, $chapters, $is_public, $author_name, $Parsedown);
break;
case 'txt':
$this->exportTXT($book, $chapters, $is_public, $author_name, $Parsedown);
break;
default:
$_SESSION['error'] = "Неверный формат экспорта";
$redirect_url = $is_public ?
"/book/{$book['share_token']}" :
"/books/{$book['id']}/edit";
$this->redirect($redirect_url);
}
}
function exportPDF($book, $chapters, $is_public, $author_name) {
global $Parsedown;
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// Устанавливаем метаданные документа
$pdf->SetCreator(APP_NAME);
$pdf->SetAuthor($author_name);
$pdf->SetTitle($book['title']);
$pdf->SetSubject($book['genre'] ?? '');
// Устанавливаем margins
$pdf->SetMargins(15, 25, 15);
$pdf->SetHeaderMargin(10);
$pdf->SetFooterMargin(10);
// Устанавливаем авто разрыв страниц
$pdf->SetAutoPageBreak(TRUE, 15);
// Добавляем страницу
$pdf->AddPage();
// Устанавливаем шрифт с поддержкой кириллицы
$pdf->SetFont('dejavusans', '', 12);
// Заголовок книги
$pdf->SetFont('dejavusans', 'B', 18);
$pdf->Cell(0, 10, $book['title'], 0, 1, 'C');
$pdf->Ln(2);
// Автор
$pdf->SetFont('dejavusans', 'I', 14);
$pdf->Cell(0, 10, $author_name, 0, 1, 'C');
$pdf->Ln(5);
// Обложка книги
if (!empty($book['cover_image'])) {
$cover_path = COVERS_PATH . $book['cover_image'];
if (file_exists($cover_path)) {
list($width, $height) = getimagesize($cover_path);
$max_width = 80;
$ratio = $width / $height;
$new_height = $max_width / $ratio;
$x = (210 - $max_width) / 2;
$pdf->Image($cover_path, $x, $pdf->GetY(), $max_width, $new_height, '', '', 'N', false, 300, '', false, false, 0, false, false, false);
$pdf->Ln($new_height + 5);
}
}
// Жанр
if (!empty($book['genre'])) {
$pdf->SetFont('dejavusans', 'I', 12);
$pdf->Cell(0, 10, 'Жанр: ' . $book['genre'], 0, 1, 'C');
$pdf->Ln(5);
}
// Описание
if (!empty($book['description'])) {
$pdf->SetFont('dejavusans', '', 11);
$pdf->MultiCell(0, 6, $book['description'], 0, 'J');
$pdf->Ln(10);
}
// Интерактивное оглавление
$chapterLinks = [];
if (!empty($chapters)) {
$pdf->SetFont('dejavusans', 'B', 14);
$pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C');
$pdf->Ln(5);
$toc_page = $pdf->getPage();
$pdf->SetFont('dejavusans', '', 11);
foreach ($chapters as $index => $chapter) {
$chapter_number = $index + 1;
$link = $pdf->AddLink();
$chapterLinks[$chapter['id']] = $link; // Сохраняем ссылку для этой главы
$pdf->Cell(0, 6, "{$chapter_number}. {$chapter['title']}", 0, 1, 'L', false, $link);
}
$pdf->Ln(10);
}
// Разделитель
$pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY());
$pdf->Ln(10);
// Главы с закладками и правильными ссылками
foreach ($chapters as $index => $chapter) {
// Добавляем новую страницу для каждой главы
$pdf->AddPage();
// УСТАНАВЛИВАЕМ ЯКОРЬ ДЛЯ ССЫЛКИ
if (isset($chapterLinks[$chapter['id']])) {
$pdf->SetLink($chapterLinks[$chapter['id']]);
}
// Устанавливаем закладку для этой главы
$pdf->Bookmark($chapter['title'], 0, 0, '', 'B', array(0, 0, 0));
// Название главы
$pdf->SetFont('dejavusans', 'B', 14);
$pdf->Cell(0, 8, $chapter['title'], 0, 1);
$pdf->Ln(2);
// Контент главы
$pdf->SetFont('dejavusans', '', 11);
if ($book['editor_type'] == 'markdown') {
$htmlContent = $Parsedown->text($chapter['content']);
} else {
$htmlContent = $chapter['content'];
}
$pdf->writeHTML($htmlContent, true, false, true, false, '');
$pdf->Ln(8);
}
// Футер с информацией
$pdf->SetY(-25);
$pdf->SetFont('dejavusans', 'I', 8);
$pdf->Cell(0, 6, 'Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), 0, 1, 'C');
$pdf->Cell(0, 6, 'Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), 0, 1, 'C');
// Отправляем файл
$filename = cleanFilename($book['title']) . '.pdf';
$pdf->Output($filename, 'D');
exit;
}
function exportDOCX($book, $chapters, $is_public, $author_name) {
global $Parsedown;
$phpWord = new PhpWord();
// Стили документа
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(12);
// Секция документа
$section = $phpWord->addSection();
// Заголовок книги
$section->addText($book['title'], ['bold' => true, 'size' => 16], ['alignment' => 'center']);
$section->addTextBreak(1);
// Автор
$section->addText($author_name, ['italic' => true, 'size' => 14], ['alignment' => 'center']);
$section->addTextBreak(2);
// Обложка книги
if (!empty($book['cover_image'])) {
$cover_path = COVERS_PATH . $book['cover_image'];
if (file_exists($cover_path)) {
$section->addImage($cover_path, [
'width' => 150,
'height' => 225,
'alignment' => 'center'
]);
$section->addTextBreak(2);
}
}
// Жанр
if (!empty($book['genre'])) {
$section->addText('Жанр: ' . $book['genre'], ['italic' => true], ['alignment' => 'center']);
$section->addTextBreak(1);
}
// Описание
if (!empty($book['description'])) {
if ($book['editor_type'] == 'markdown') {
$descriptionParagraphs = $this->markdownToParagraphs($book['description']);
} else {
$descriptionParagraphs = $this->htmlToParagraphs($book['description']);
}
foreach ($descriptionParagraphs as $paragraph) {
if (!empty(trim($paragraph))) {
$section->addText($paragraph);
}
}
$section->addTextBreak(2);
}
// Интерактивное оглавление
if (!empty($chapters)) {
$section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']);
$section->addTextBreak(1);
foreach ($chapters as $index => $chapter) {
$chapter_number = $index + 1;
// Создаем гиперссылку на заголовок главы
$section->addLink("chapter_{$chapter['id']}", "{$chapter_number}. {$chapter['title']}", null, null, true);
$section->addTextBreak(1);
}
$section->addTextBreak(2);
}
// Разделитель
$section->addPageBreak();
// Главы с закладками
foreach ($chapters as $index => $chapter) {
// Добавляем закладку для главы
$section->addBookmark("chapter_{$chapter['id']}");
// Заголовок главы
$section->addText($chapter['title'], ['bold' => true, 'size' => 14]);
$section->addTextBreak(1);
// Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора
if ($book['editor_type'] == 'markdown') {
$cleanContent = $this->cleanMarkdown($chapter['content']);
$paragraphs = $this->markdownToParagraphs($cleanContent);
} else {
$cleanContent = strip_tags($chapter['content']);
$paragraphs = $this->htmlToParagraphs($chapter['content']);
}
// Добавляем каждый абзац
foreach ($paragraphs as $paragraph) {
if (!empty(trim($paragraph))) {
$section->addText($paragraph);
$section->addTextBreak(1);
}
}
// Добавляем разрыв страницы между главами (кроме последней)
if ($index < count($chapters) - 1) {
$section->addPageBreak();
}
}
// Футер
$section->addTextBreak(2);
$section->addText('Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), ['italic' => true, 'size' => 9]);
$section->addText('Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), ['italic' => true, 'size' => 9]);
// Сохраняем и отправляем
$filename = cleanFilename($book['title']) . '.docx';
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$objWriter = IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save('php://output');
exit;
}
function exportHTML($book, $chapters, $is_public, $author_name) {
global $Parsedown;
$html = '
' . htmlspecialchars($book['title']) . '
' . htmlspecialchars($book['title']) . '
' . htmlspecialchars($author_name) . '
';
if (!empty($book['genre'])) {
$html .= 'Жанр: ' . htmlspecialchars($book['genre']) . '
';
}
// Обложка книги
if (!empty($book['cover_image'])) {
$cover_url = COVERS_URL . $book['cover_image'];
$html .= '';
$html .= '
![' . htmlspecialchars($book['title']) . '](' . $cover_url . ')
';
$html .= '
';
}
if (!empty($book['description'])) {
$html .= '';
if ($book['editor_type'] == 'markdown') {
$html .= nl2br(htmlspecialchars($book['description']));
} else {
$html .= $book['description'];
}
$html .= '
';
}
// Интерактивное оглавление
if (!empty($chapters)) {
$html .= '';
$html .= '
Оглавление
';
$html .= '
';
$html .= '
';
}
$html .= '
';
foreach ($chapters as $index => $chapter) {
$html .= '';
$html .= '
' . htmlspecialchars($chapter['title']) . '
';
// Обрабатываем контент в зависимости от типа редактора
if ($book['editor_type'] == 'markdown') {
$htmlContent = $Parsedown->text($chapter['content']);
} else {
$htmlContent = $chapter['content'];
}
$html .= '
' . $htmlContent . '
';
$html .= '
';
if ($index < count($chapters) - 1) {
$html .= '
';
}
}
$html .= '
';
$filename = cleanFilename($book['title']) . '.html';
header('Content-Type: text/html; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo $html;
exit;
}
function exportTXT($book, $chapters, $is_public, $author_name) {
$content = "=" . str_repeat("=", 80) . "=\n";
$content .= str_pad($book['title'], 80, " ", STR_PAD_BOTH) . "\n";
$content .= str_pad($author_name, 80, " ", STR_PAD_BOTH) . "\n";
$content .= "=" . str_repeat("=", 80) . "=\n\n";
if (!empty($book['genre'])) {
$content .= "Жанр: " . $book['genre'] . "\n\n";
}
if (!empty($book['description'])) {
$content .= "ОПИСАНИЕ:\n";
// Обрабатываем описание в зависимости от типа редактора
if ($book['editor_type'] == 'markdown') {
$descriptionText = $this->cleanMarkdown($book['description']);
} else {
$descriptionText = strip_tags($book['description']);
}
$content .= wordwrap($descriptionText, 144) . "\n\n";
}
// Оглавление
if (!empty($chapters)) {
$content .= "ОГЛАВЛЕНИЕ:\n";
$content .= str_repeat("-", 60) . "\n";
foreach ($chapters as $index => $chapter) {
$chapter_number = $index + 1;
$content .= "{$chapter_number}. {$chapter['title']}\n";
}
$content .= "\n";
}
$content .= str_repeat("-", 144) . "\n\n";
foreach ($chapters as $index => $chapter) {
$content .= $chapter['title'] . "\n";
$content .= str_repeat("-", 60) . "\n\n";
// Получаем очищенный текст в зависимости от типа редактора
if ($book['editor_type'] == 'markdown') {
$cleanContent = $this->cleanMarkdown($chapter['content']);
$paragraphs = $this->markdownToParagraphs($cleanContent);
} else {
$cleanContent = strip_tags($chapter['content']);
$paragraphs = $this->htmlToPlainTextParagraphs($cleanContent);
}
foreach ($paragraphs as $paragraph) {
if (!empty(trim($paragraph))) {
$content .= wordwrap($paragraph, 144) . "\n\n";
}
}
if ($index < count($chapters) - 1) {
$content .= str_repeat("-", 144) . "\n\n";
}
}
$content .= "\n" . str_repeat("=", 144) . "\n";
$content .= "Экспортировано из " . APP_NAME . " - " . date('d.m.Y H:i') . "\n";
$content .= "Автор: " . $author_name . " | Всего глав: " . count($chapters) . " | Всего слов: " . array_sum(array_column($chapters, 'word_count')) . "\n";
$content .= str_repeat("=", 144) . "\n";
$filename = cleanFilename($book['title']) . '.txt';
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo $content;
exit;
}
// Функция для преобразования Markdown в чистый текст с форматированием абзацев
function markdownToPlainText($markdown) {
// Обрабатываем диалоги (заменяем - на —)
$markdown = preg_replace('/^- (.+)$/m', "— $1", $markdown);
// Убираем Markdown разметку, но сохраняем переносы строк
$text = $markdown;
// Убираем заголовки
$text = preg_replace('/^#+\s+/m', '', $text);
// Убираем жирный и курсив
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
$text = preg_replace('/__(.*?)__/', '$1', $text);
$text = preg_replace('/_(.*?)_/', '$1', $text);
// Убираем зачеркивание
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
// Убираем код (встроенный)
$text = preg_replace('/`(.*?)`/', '$1', $text);
// Убираем блоки кода (сохраняем содержимое)
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
// Убираем ссылки
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
// Обрабатываем списки - заменяем маркеры на *
$text = preg_replace('/^[\*\-+]\s+/m', '* ', $text);
$text = preg_replace('/^\d+\.\s+/m', '* ', $text);
// Обрабатываем цитаты
$text = preg_replace('/^>\s+/m', '', $text);
return $text;
}
// Функция для разбивки Markdown на абзацы с сохранением структуры
function markdownToParagraphs($markdown) {
// Нормализуем переносы строк
$text = str_replace(["\r\n", "\r"], "\n", $markdown);
// Обрабатываем диалоги (заменяем - на —)
$text = preg_replace('/^- (.+)$/m', "— $1", $text);
// Разбиваем на строки
$lines = explode("\n", $text);
$paragraphs = [];
$currentParagraph = '';
foreach ($lines as $line) {
$trimmedLine = trim($line);
// Пустая строка - конец абзаца
if (empty($trimmedLine)) {
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
$currentParagraph = '';
}
continue;
}
// Диалог (начинается с —) всегда начинает новый абзац
if (str_starts_with($trimmedLine, '—')) {
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
}
$currentParagraph = $trimmedLine;
$paragraphs[] = $currentParagraph;
$currentParagraph = '';
continue;
}
// Заголовки (начинаются с #) всегда начинают новый абзац
if (str_starts_with($trimmedLine, '#')) {
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
}
$currentParagraph = preg_replace('/^#+\s+/', '', $trimmedLine);
$paragraphs[] = $currentParagraph;
$currentParagraph = '';
continue;
}
// Обычный текст - добавляем к текущему абзацу
if (!empty($currentParagraph)) {
$currentParagraph .= ' ' . $trimmedLine;
} else {
$currentParagraph = $trimmedLine;
}
}
// Добавляем последний абзац
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
}
return $paragraphs;
}
// Функция для очистки Markdown разметки
function cleanMarkdown($markdown) {
$text = $markdown;
// Убираем заголовки
$text = preg_replace('/^#+\s+/m', '', $text);
// Убираем жирный и курсив
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
$text = preg_replace('/__(.*?)__/', '$1', $text);
$text = preg_replace('/_(.*?)_/', '$1', $text);
// Убираем зачеркивание
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
// Убираем код
$text = preg_replace('/`(.*?)`/', '$1', $text);
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
// Убираем ссылки
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
// Обрабатываем списки - убираем маркеры
$text = preg_replace('/^[\*\-+]\s+/m', '', $text);
$text = preg_replace('/^\d+\.\s+/m', '', $text);
// Обрабатываем цитаты
$text = preg_replace('/^>\s+/m', '', $text);
return $text;
}
// Функция для форматирования текста с сохранением абзацев и диалогов
function formatPlainText($text) {
$lines = explode("\n", $text);
$formatted = [];
$in_paragraph = false;
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
if ($in_paragraph) {
$formatted[] = ''; // Пустая строка для разделения абзацев
$in_paragraph = false;
}
continue;
}
// Диалоги начинаются с —
if (str_starts_with($line, '—')) {
if ($in_paragraph) {
$formatted[] = ''; // Разделяем абзацы перед диалогом
}
$formatted[] = $line;
$formatted[] = ''; // Пустая строка после диалога
$in_paragraph = false;
} else {
// Обычный текст
$formatted[] = $line;
$in_paragraph = true;
}
}
return implode("\n", array_filter($formatted, function($line) {
return $line !== '' || !empty($line);
}));
}
// // Новая функция для разбивки HTML на абзацы
function htmlToParagraphs($html) {
// Убираем HTML теги и нормализуем пробелы
$text = strip_tags($html);
$text = preg_replace('/\s+/', ' ', $text);
// Разбиваем на абзацы по точкам и переносам строк
$paragraphs = preg_split('/(?<=[.!?])\s+/', $text);
// Фильтруем пустые абзацы
$paragraphs = array_filter($paragraphs, function($paragraph) {
return !empty(trim($paragraph));
});
return $paragraphs;
}
function htmlToPlainTextParagraphs($html) {
// Убираем HTML теги
$text = strip_tags($html);
// Заменяем HTML entities
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Нормализуем переносы строк
$text = str_replace(["\r\n", "\r"], "\n", $text);
// Разбиваем на строки
$lines = explode("\n", $text);
$paragraphs = [];
$currentParagraph = '';
foreach ($lines as $line) {
$trimmedLine = trim($line);
// Пустая строка - конец абзаца
if (empty($trimmedLine)) {
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
$currentParagraph = '';
}
continue;
}
// Добавляем к текущему абзацу
if (!empty($currentParagraph)) {
$currentParagraph .= ' ' . $trimmedLine;
} else {
$currentParagraph = $trimmedLine;
}
}
// Добавляем последний абзац
if (!empty($currentParagraph)) {
$paragraphs[] = $currentParagraph;
}
return $paragraphs;
}
}
?>