diff --git a/Epub/KoeBook.Epub/Contracts/Services/IEpubCreateService.cs b/Epub/KoeBook.Epub/Contracts/Services/IEpubCreateService.cs new file mode 100644 index 0000000..6b9df0a --- /dev/null +++ b/Epub/KoeBook.Epub/Contracts/Services/IEpubCreateService.cs @@ -0,0 +1,8 @@ +using KoeBook.Epub.Models; + +namespace KoeBook.Epub.Contracts.Services; + +public interface IEpubCreateService +{ + ValueTask TryCreateEpubAsync(EpubDocument epubDocument, string tmpDirectory, CancellationToken ct); +} diff --git a/Epub/KoeBook.Epub/EpubCreateHelper.cs b/Epub/KoeBook.Epub/EpubCreateHelper.cs deleted file mode 100644 index 936a546..0000000 --- a/Epub/KoeBook.Epub/EpubCreateHelper.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace KoeBook.Epub; - -internal static class EpubCreateHelper -{ - internal static string GetImagesMediaType(string path) - { - return Path.GetExtension(path) switch - { - ".gif" => "image/gif", - ".jpg" or ".jpeg" => "image/jpeg", - ".png" => "image/png", - ".svg" => "image/svg+xml", - ".webp" => "image/webp", - _ => "" - }; - } -} diff --git a/Epub/KoeBook.Epub/Models/EpubDocument.cs b/Epub/KoeBook.Epub/Models/EpubDocument.cs index 583d0ce..e32192f 100644 --- a/Epub/KoeBook.Epub/Models/EpubDocument.cs +++ b/Epub/KoeBook.Epub/Models/EpubDocument.cs @@ -6,15 +6,6 @@ namespace KoeBook.Epub.Models; public class EpubDocument(string title, string author, string coverFilePath, Guid id) { - readonly string _containerXml = """ - - - - - - - """; - public string Title { get; set; } = title; public string Author { get; set; } = author; @@ -23,17 +14,17 @@ public class EpubDocument(string title, string author, string coverFilePath, Gui public Guid Id { get; } = id; public List CssClasses { get; set; } = [ - new CssClass("-epub-media-overlay-active", """ - .-epub-media-overlay-active *{ - background-color: yellow; - color: black !important; - } - """), + new CssClass("-epub-media-overlay-active", """ + .-epub-media-overlay-active *{ + background-color: yellow; + color: black !important; + } + """), new CssClass("-epub-media-overlay-unactive", """ - .-epub-media-overlay-unactive * { - color: gray; - } - """), + .-epub-media-overlay-unactive * { + color: gray; + } + """), ]; public List Chapters { get; set; } = []; @@ -63,248 +54,4 @@ internal void EnsureParagraph(int chapterIndex, int sectionIndex) if (Chapters[chapterIndex].Sections[sectionIndex].Elements.Count == 0) Chapters[chapterIndex].Sections[sectionIndex].Elements.Add(new Paragraph()); } - - public string CreateNavXhtml() - { - var builder = new StringBuilder($""" - - - - - {Title} - - - - - - """); - return builder.ToString(); - } - - public string CreateCssText() - { - var builder = new StringBuilder(); - foreach (var cssClass in CssClasses) - { - builder.AppendLine(cssClass.Text); - } - return builder.ToString(); - } - - public string CreateOpf() - { - var builder = new StringBuilder($""" - - - {Title} - {Author} - aut - urn:uuid:{Guid.NewGuid()} - ja - {DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)} - {DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)} - -epub-media-overlay-active - -epub-media-overlay-unactive - - """); - - var totalTime = TimeSpan.Zero; - for (var i = 0; i < Chapters.Count; i++) - { - for (var j = 0; j < Chapters[i].Sections.Count; j++) - { - var time = Chapters[i].Sections[j].GetTotalTime(); - totalTime += time; - builder.AppendLine($""" - {time} - """); - } - } - builder.AppendLine($""" - {totalTime} - - - - - - """); - - for (var i = 0; i < Chapters.Count; i++) - { - for (var j = 0; j < Chapters[i].Sections.Count; j++) - { - builder.AppendLine($""" - - - """); - for (var k = 0; k < Chapters[i].Sections[j].Elements.Count; k++) - { - var element = Chapters[i].Sections[j].Elements[k]; - if (element is Paragraph para && para.Audio != null) - { - builder.AppendLine(@$" "); - } - else if (element is Picture pic && File.Exists(pic.PictureFilePath)) - { - builder.AppendLine(@$" "); - } - } - } - } - - builder.AppendLine($""" - - - """); - - for (var i = 0; i < Chapters.Count; i++) - { - for (var j = 0; j < Chapters[i].Sections.Count; j++) - { - builder.AppendLine($""" - - """); - } - } - - builder.AppendLine($""" - - - """); - return builder.ToString(); - } - - public async Task TryCreateEpubAsync(string tmpDirectory, string name, CancellationToken ct) - { - if (!File.Exists(CoverFilePath)) - { - throw new FileNotFoundException("指定されたカバーファイルが存在しません", CoverFilePath); - } - try - { - using var fs = File.Create(Path.Combine(tmpDirectory, $"{name}.epub")); - using var archive = new ZipArchive(fs, ZipArchiveMode.Create); - - var mimeTypeEntry = archive.CreateEntry("mimetype", CompressionLevel.NoCompression); - using (var mimeTypeStream = new StreamWriter(mimeTypeEntry.Open())) - { - await mimeTypeStream.WriteAsync("application/epub+zip").ConfigureAwait(false); - await mimeTypeStream.FlushAsync(ct).ConfigureAwait(false); - } - - var containerEntry = archive.CreateEntry("META-INF/container.xml"); - using (var containerStream = new StreamWriter(containerEntry.Open())) - { - await containerStream.WriteLineAsync(_containerXml).ConfigureAwait(false); - await containerStream.FlushAsync(ct).ConfigureAwait(false); - } - - archive.CreateEntryFromFile(CoverFilePath, $"OEBPS/{Path.GetFileName(CoverFilePath)}"); - - var cssEntry = archive.CreateEntry("OEBPS/style.css"); - using (var cssStream = new StreamWriter(cssEntry.Open())) - { - await cssStream.WriteLineAsync(CreateCssText()).ConfigureAwait(false); - await cssStream.FlushAsync(ct).ConfigureAwait(false); - } - - var navEntry = archive.CreateEntry("OEBPS/nav.xhtml"); - using (var navStream = new StreamWriter(navEntry.Open())) - { - await navStream.WriteLineAsync(CreateNavXhtml()).ConfigureAwait(false); - await navStream.FlushAsync(ct).ConfigureAwait(false); - } - - var opfEntry = archive.CreateEntry("OEBPS/book.opf"); - using (var opfStream = new StreamWriter(opfEntry.Open())) - { - await opfStream.WriteLineAsync(CreateOpf()).ConfigureAwait(false); - await opfStream.FlushAsync(ct).ConfigureAwait(false); - } - - for (var i = 0; i < Chapters.Count; i++) - { - for (var j = 0; j < Chapters[i].Sections.Count; j++) - { - var sectionXhtmlEntry = archive.CreateEntry($"OEBPS/{Chapters[i].Sections[j].Id}.xhtml"); - using (var sectionXhtmlStream = new StreamWriter(sectionXhtmlEntry.Open())) - { - await sectionXhtmlStream.WriteLineAsync(Chapters[i].Sections[j].CreateSectionXhtml()).ConfigureAwait(false); - await sectionXhtmlStream.FlushAsync(ct).ConfigureAwait(false); - } - var sectionSmilEntry = archive.CreateEntry($"OEBPS/{Chapters[i].Sections[j].Id}_audio.smil"); - using (var sectionSmilStream = new StreamWriter(sectionSmilEntry.Open())) - { - await sectionSmilStream.WriteLineAsync(Chapters[i].Sections[j].CreateSectionSmil()).ConfigureAwait(false); - await sectionSmilStream.FlushAsync(ct).ConfigureAwait(false); - } - for (var k = 0; k < Chapters[i].Sections[j].Elements.Count; k++) - { - var element = Chapters[i].Sections[j].Elements[k]; - if (element is Paragraph para && para.Audio != null) - { - var audioEntry = archive.CreateEntry($"OEBPS/{Chapters[i].Sections[j].Id}_p{k}.mp3"); - using var audioStream = para.Audio.GetStream(); - using var audioEntryStream = audioEntry.Open(); - await audioStream.CopyToAsync(audioEntryStream, ct).ConfigureAwait(false); - await audioEntryStream.FlushAsync(ct).ConfigureAwait(false); - } - else if (element is Picture pic && File.Exists(pic.PictureFilePath)) - { - archive.CreateEntryFromFile(pic.PictureFilePath, $"OEBPS/{Chapters[i].Sections[j].Id}_p{k}{Path.GetExtension(pic.PictureFilePath)}"); - } - } - } - } - return true; - } - catch (OperationCanceledException) - { - throw; - } - catch - { - return false; - } - } } diff --git a/Epub/KoeBook.Epub/Models/Section.cs b/Epub/KoeBook.Epub/Models/Section.cs index 81da7ec..bd283d9 100644 --- a/Epub/KoeBook.Epub/Models/Section.cs +++ b/Epub/KoeBook.Epub/Models/Section.cs @@ -8,73 +8,6 @@ public sealed class Section(string title) public string Title { get; set; } = title; public List Elements { get; set; } = []; - public string CreateSectionXhtml() - { - var builder = new StringBuilder($""" - - - - - {Title} - - - """); - - for (var i = 0; i < Elements.Count; i++) - { - if (Elements[i] is Paragraph para) - { - builder.AppendLine($""" -

- {para.Text} -

- """); - } - else if (Elements[i] is Picture pic && File.Exists(pic.PictureFilePath)) - { - builder.AppendLine($""" -

- - """); - } - } - - builder.AppendLine(""" - - - """); - return builder.ToString(); - } - - public string CreateSectionSmil() - { - var builder = new StringBuilder($""" - - - - """); - - for (var i = 0; i < Elements.Count; i++) - { - if (Elements[i] is Paragraph para && para.Audio != null) - { - builder.AppendLine($""" - - - - """); - } - } - - builder.AppendLine(""" - - - """); - return builder.ToString(); - } - public TimeSpan GetTotalTime() { var time = TimeSpan.Zero; diff --git a/Epub/KoeBook.Epub/Services/EpubCreateService.cs b/Epub/KoeBook.Epub/Services/EpubCreateService.cs new file mode 100644 index 0000000..b34d590 --- /dev/null +++ b/Epub/KoeBook.Epub/Services/EpubCreateService.cs @@ -0,0 +1,332 @@ +using KoeBook.Epub.Contracts.Services; +using KoeBook.Epub.Models; +using System.Globalization; +using System.IO.Compression; +using System.Text; + +namespace KoeBook.Epub.Services; +public class EpubCreateService(IFileExtensionService fileExtensionService) : IEpubCreateService +{ + private readonly IFileExtensionService _fileExtensionService = fileExtensionService; + + internal const string ContainerXml = """ + + + + + + + """; + + public async ValueTask TryCreateEpubAsync(EpubDocument epubDocument, string tmpDirectory, CancellationToken ct) + { + if (!File.Exists(epubDocument.CoverFilePath)) + { + throw new FileNotFoundException("指定されたカバーファイルが存在しません", epubDocument.CoverFilePath); + } + try + { + using var fs = File.Create(Path.Combine(tmpDirectory, $"{epubDocument.Id}.epub")); + using var archive = new ZipArchive(fs, ZipArchiveMode.Create); + + var mimeTypeEntry = archive.CreateEntry("mimetype", CompressionLevel.NoCompression); + using (var mimeTypeStream = new StreamWriter(mimeTypeEntry.Open())) + { + await mimeTypeStream.WriteAsync("application/epub+zip").ConfigureAwait(false); + await mimeTypeStream.FlushAsync(ct).ConfigureAwait(false); + } + + var containerEntry = archive.CreateEntry("META-INF/container.xml"); + using (var containerStream = new StreamWriter(containerEntry.Open())) + { + await containerStream.WriteLineAsync(ContainerXml).ConfigureAwait(false); + await containerStream.FlushAsync(ct).ConfigureAwait(false); + } + + archive.CreateEntryFromFile(epubDocument.CoverFilePath, $"OEBPS/{Path.GetFileName(epubDocument.CoverFilePath)}"); + + var cssEntry = archive.CreateEntry("OEBPS/style.css"); + using (var cssStream = new StreamWriter(cssEntry.Open())) + { + await cssStream.WriteLineAsync(CreateCssText(epubDocument)).ConfigureAwait(false); + await cssStream.FlushAsync(ct).ConfigureAwait(false); + } + + var navEntry = archive.CreateEntry("OEBPS/nav.xhtml"); + using (var navStream = new StreamWriter(navEntry.Open())) + { + await navStream.WriteLineAsync(CreateNavXhtml(epubDocument)).ConfigureAwait(false); + await navStream.FlushAsync(ct).ConfigureAwait(false); + } + + var opfEntry = archive.CreateEntry("OEBPS/book.opf"); + using (var opfStream = new StreamWriter(opfEntry.Open())) + { + await opfStream.WriteLineAsync(CreateOpf(epubDocument)).ConfigureAwait(false); + await opfStream.FlushAsync(ct).ConfigureAwait(false); + } + + for (var i = 0; i < epubDocument.Chapters.Count; i++) + { + for (var j = 0; j < epubDocument.Chapters[i].Sections.Count; j++) + { + var sectionXhtmlEntry = archive.CreateEntry($"OEBPS/{epubDocument.Chapters[i].Sections[j].Id}.xhtml"); + using (var sectionXhtmlStream = new StreamWriter(sectionXhtmlEntry.Open())) + { + await sectionXhtmlStream.WriteLineAsync(CreateSectionXhtml(epubDocument.Chapters[i].Sections[j])).ConfigureAwait(false); + await sectionXhtmlStream.FlushAsync(ct).ConfigureAwait(false); + } + var sectionSmilEntry = archive.CreateEntry($"OEBPS/{epubDocument.Chapters[i].Sections[j].Id}_audio.smil"); + using (var sectionSmilStream = new StreamWriter(sectionSmilEntry.Open())) + { + await sectionSmilStream.WriteLineAsync(CreateSectionSmil(epubDocument.Chapters[i].Sections[j])).ConfigureAwait(false); + await sectionSmilStream.FlushAsync(ct).ConfigureAwait(false); + } + for (var k = 0; k < epubDocument.Chapters[i].Sections[j].Elements.Count; k++) + { + var element = epubDocument.Chapters[i].Sections[j].Elements[k]; + if (element is Paragraph para && para.Audio != null) + { + var audioEntry = archive.CreateEntry($"OEBPS/{epubDocument.Chapters[i].Sections[j].Id}_p{k}.mp3"); + using var audioStream = para.Audio.GetStream(); + using var audioEntryStream = audioEntry.Open(); + await audioStream.CopyToAsync(audioEntryStream, ct).ConfigureAwait(false); + await audioEntryStream.FlushAsync(ct).ConfigureAwait(false); + } + else if (element is Picture pic && File.Exists(pic.PictureFilePath)) + { + archive.CreateEntryFromFile(pic.PictureFilePath, $"OEBPS/{epubDocument.Chapters[i].Sections[j].Id}_p{k}{Path.GetExtension(pic.PictureFilePath)}"); + } + } + } + } + return true; + } + catch (OperationCanceledException) + { + throw; + } + catch + { + return false; + } + } + + internal static string CreateNavXhtml(EpubDocument epubDocument) + { + var builder = new StringBuilder($""" + + + + + {epubDocument.Title} + + +

+ + + """); + return builder.ToString(); + } + + internal static string CreateCssText(EpubDocument epubDocument) + { + var builder = new StringBuilder(); + foreach (var cssClass in epubDocument.CssClasses) + { + builder.AppendLine(cssClass.Text); + } + return builder.ToString(); + } + + internal string CreateOpf(EpubDocument epubDocument) + { + var builder = new StringBuilder($""" + + + {epubDocument.Title} + {epubDocument.Author} + aut + urn:uuid:{Guid.NewGuid()} + ja + {DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)} + {DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)} + -epub-media-overlay-active + -epub-media-overlay-unactive + + """); + + var totalTime = TimeSpan.Zero; + for (var i = 0; i < epubDocument.Chapters.Count; i++) + { + for (var j = 0; j < epubDocument.Chapters[i].Sections.Count; j++) + { + var time = epubDocument.Chapters[i].Sections[j].GetTotalTime(); + totalTime += time; + builder.AppendLine($""" + {time} + """); + } + } + builder.AppendLine($""" + {totalTime} + + + + + + """); + + for (var i = 0; i < epubDocument.Chapters.Count; i++) + { + for (var j = 0; j < epubDocument.Chapters[i].Sections.Count; j++) + { + builder.AppendLine($""" + + + """); + for (var k = 0; k < epubDocument.Chapters[i].Sections[j].Elements.Count; k++) + { + var element = epubDocument.Chapters[i].Sections[j].Elements[k]; + if (element is Paragraph para && para.Audio != null) + { + builder.AppendLine(@$" "); + } + else if (element is Picture pic && File.Exists(pic.PictureFilePath)) + { + builder.AppendLine(@$" "); + } + } + } + } + + builder.AppendLine($""" + + + """); + + for (var i = 0; i < epubDocument.Chapters.Count; i++) + { + for (var j = 0; j < epubDocument.Chapters[i].Sections.Count; j++) + { + builder.AppendLine($""" + + """); + } + } + + builder.AppendLine($""" + + + """); + return builder.ToString(); + } + + + internal static string CreateSectionXhtml(Section section) + { + var builder = new StringBuilder($""" + + + + + {section.Title} + + + """); + + for (var i = 0; i < section.Elements.Count; i++) + { + if (section.Elements[i] is Paragraph para) + { + builder.AppendLine($""" +

+ {para.Text} +

+ """); + } + else if (section.Elements[i] is Picture pic && File.Exists(pic.PictureFilePath)) + { + builder.AppendLine($""" +

+ + """); + } + } + + builder.AppendLine(""" + + + """); + return builder.ToString(); + } + + internal static string CreateSectionSmil(Section section) + { + var builder = new StringBuilder($""" + + + + """); + + for (var i = 0; i < section.Elements.Count; i++) + { + if (section.Elements[i] is Paragraph para && para.Audio != null) + { + builder.AppendLine($""" + + + + """); + } + } + + builder.AppendLine(""" + + + """); + return builder.ToString(); + } +} diff --git a/KoeBook.Core/Services/EpubGenerateService.cs b/KoeBook.Core/Services/EpubGenerateService.cs index 6052d8c..2810e8d 100644 --- a/KoeBook.Core/Services/EpubGenerateService.cs +++ b/KoeBook.Core/Services/EpubGenerateService.cs @@ -1,14 +1,16 @@ using KoeBook.Core.Contracts.Services; using KoeBook.Core.Models; using KoeBook.Epub; +using KoeBook.Epub.Contracts.Services; using KoeBook.Epub.Models; namespace KoeBook.Core.Services; -public class EpubGenerateService(ISoundGenerationService soundGenerationService, IEpubDocumentStoreService epubDocumentStoreService) : IEpubGenerateService +public class EpubGenerateService(ISoundGenerationService soundGenerationService, IEpubDocumentStoreService epubDocumentStoreService, IEpubCreateService epubCreateService) : IEpubGenerateService { private ISoundGenerationService _soundGenerationService = soundGenerationService; private IEpubDocumentStoreService _documentStoreService = epubDocumentStoreService; + private IEpubCreateService _createService = epubCreateService; public async ValueTask GenerateEpubAsync(BookScripts bookScripts, string tempDirectory, CancellationToken cancellationToken) { @@ -22,7 +24,7 @@ public async ValueTask GenerateEpubAsync(BookScripts bookScripts, string scriptLine.Paragraph.Audio = new Audio(await _soundGenerationService.GenerateLineSoundAsync(scriptLine, bookScripts.Options, cancellationToken).ConfigureAwait(false)); } - if (await document.TryCreateEpubAsync(tempDirectory, bookScripts.BookProperties.Id.ToString(), cancellationToken).ConfigureAwait(false)) + if (await _createService.TryCreateEpubAsync(document, tempDirectory, cancellationToken).ConfigureAwait(false)) { _documentStoreService.Unregister(bookScripts.BookProperties.Id); return Path.Combine(tempDirectory, $"{bookScripts.BookProperties.Id}.epub"); diff --git a/KoeBook/App.xaml.cs b/KoeBook/App.xaml.cs index ddde655..3b1e62f 100644 --- a/KoeBook/App.xaml.cs +++ b/KoeBook/App.xaml.cs @@ -102,6 +102,7 @@ public App() services.AddSingleton() .AddSingleton() .AddSingleton(); + services.AddSingleton(); // Views and ViewModels services.AddTransient();