private bool SaveMediaFiles(string baseDir, Func <int, string, bool> progressDelegate = null) { if (progressDelegate == null) { progressDelegate = (p, m) => false; } int index = 0; var entries = MergeEntries.SelectMany(me => me.DescententsAndSelf).ToList(); foreach (var entry in entries) { if (progressDelegate((100 * index) / entries.Count, "Saving media files")) { return(false); } foreach (var imgUri in entry.TextElements .SelectMany(e => e.DescendantsAndSelf(e.Name.Namespace + "img").Select(img => img.Attribute("src"))) .Select(Utils.GetUri) .Distinct()) { File.Copy( Uri.UnescapeDataString(imgUri.LocalPath), Path.Combine(baseDir, GetImageFileName(imgUri, index))); } index++; } return(true); }
/// <summary> /// Builds the merged DTB /// </summary> /// <param name="progressDelegate"> /// A progress delegate, receiving progress percentage and message. /// When the delegate returns <c>true</c>, it is a signal to cancel the save process</param> /// <returns><c>true</c> if the DTB was successfully saved, <c>false</c> if the save process was cancelled via the <paramref name="progressDelegate"/></returns> public bool BuildDtb(Func <int, string, bool> progressDelegate = null) { if (progressDelegate == null) { progressDelegate = (p, m) => false; } if (!MergeEntries.Any()) { throw new InvalidOperationException("No merge entries added to builder"); } ResetBuilder(); var generator = $"{Assembly.GetExecutingAssembly().GetName().Name} v{Assembly.GetExecutingAssembly().GetName().Version}"; var entries = MergeEntries.SelectMany(entry => entry.DescententsAndSelf).ToList(); if (entries.Any(me => me.TextElements.Any())) { ContentDocument = Utils.GenerateSkeletonXhtmlDocument(); } var identifier = Utils.CreateOrGetMeta(entries.First().Ncc, "dc:identifier")?.Attribute("content")?.Value ?? Guid.NewGuid().ToString(); for (int i = 0; i < entries.Count; i++) { if (progressDelegate(100 * i / entries.Count, $"Building merge entry {i+1} of {entries.Count}")) { ResetBuilder(); return(false); } var me = entries[i]; var smilFile = Utils.GenerateSkeletonSmilDocument(); var smilElements = me.SmilElements.Select(Utils.CloneWithBaseUri).ToList(); NccDocument.Root?.Element(NccDocument?.Root.Name.Namespace + "body") ?.Add(GetNccElements(me, smilElements)); var contentElements = GetContentElements(me, smilElements); if (contentElements.Any()) { var firstHeading = contentElements.FirstOrDefault(Utils.IsHeading); if (firstHeading != null) { firstHeading.Name = firstHeading.Name.Namespace + $"h{Math.Min(me.Depth, 6)}"; } ContentDocument.Root?.Element(ContentDocument?.Root.Name.Namespace + "body")?.Add(contentElements); } audioFileSegments.Add(me.AudioSegments.ToList()); var elapsedInThisSmil = TimeSpan.Zero; foreach (var audio in smilElements.Descendants("audio")) { var clipBegin = Utils.ParseSmilClip(audio.Attribute("clip-begin")?.Value); var clipEnd = Utils.ParseSmilClip(audio.Attribute("clip-end")?.Value); audio.SetAttributeValue("src", GetAudioFileName(entryIndex)); audio.SetAttributeValue( "clip-begin", $"npt={elapsedInThisSmil.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)}s"); elapsedInThisSmil += clipEnd.Subtract(clipBegin); audio.SetAttributeValue( "clip-end", $"npt={elapsedInThisSmil.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)}s"); } var timeInThisSmil = TimeSpan.FromSeconds(smilElements .SelectMany(e => e.Descendants("audio")) .Select(audio => Utils .ParseSmilClip(audio.Attribute("clip-end")?.Value) .Subtract(Utils.ParseSmilClip(audio.Attribute("clip-begin")?.Value)).TotalSeconds) .Sum()); Utils.CreateOrGetMeta(smilFile, "ncc:totalElapsedTime")?.SetAttributeValue( "content", Utils.GetHHMMSSFromTimeSpan(totalElapsedTime)); Utils.CreateOrGetMeta(smilFile, "ncc:timeInThisSmil")?.SetAttributeValue( "content", Utils.GetHHMMSSFromTimeSpan(timeInThisSmil)); var seq = smilFile.Root?.Element("body")?.Element("seq"); if (seq == null) { throw new ApplicationException("Generated smil document contains no main seq"); } seq.SetAttributeValue( "dur", $"{timeInThisSmil.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)}s"); Utils.CreateOrGetMeta(smilFile, "ncc:generator")?.SetAttributeValue("content", generator); Utils.CreateOrGetMeta(smilFile, "dc:identifier")?.SetAttributeValue("content", identifier); seq.Add(smilElements); smilFiles.Add(smilFile); totalElapsedTime += timeInThisSmil; foreach (var imgSrc in contentElements.SelectMany(ce => ce.DescendantsAndSelf(Utils.XhtmlNs + "img").Select(img => img.Attribute("src")) .Where(src => src != null))) { imgSrc.Value = GetImageFileName(Utils.GetUri(imgSrc), entryIndex); } entryIndex++; } NccDocument.Root?.Element(Utils.XhtmlNs + "head")?.Add( entries.First().Ncc.Root?.Element(Utils.XhtmlNs + "head")?.Elements(Utils.XhtmlNs + "meta")); NccDocument.Root?.Element(Utils.XhtmlNs + "head")?.Add( entries.First().Ncc.Root?.Element(Utils.XhtmlNs + "head")?.Elements(Utils.XhtmlNs + "title")); ContentDocument?.Root?.Element(Utils.XhtmlNs + "head")?.Add( NccDocument.Root?.Element(Utils.XhtmlNs + "head")?.Element(Utils.XhtmlNs + "title")); ContentDocument?.Root?.Element(Utils.XhtmlNs + "head")?.Add( NccDocument.Root?.Element(Utils.XhtmlNs + "head")?.Elements(Utils.XhtmlNs + "meta").Where(meta => meta.Attribute("name")?.Value == "dc:identifier")); var firstContentDocHead = MergeEntries .First() .ContentDocuments .FirstOrDefault() .Value?.Root ?.Element(Utils.XhtmlNs + "head"); ContentDocument?.Root?.Element(Utils.XhtmlNs + "head")?.Add( firstContentDocHead?.Elements(Utils.XhtmlNs + "style")); Utils.CreateOrGetMeta(NccDocument, "dc:format") ?.SetAttributeValue("content", "Daisy 2.02"); Utils.CreateOrGetMeta(NccDocument, "ncc:totalTime") ?.SetAttributeValue("content", Utils.GetHHMMSSFromTimeSpan(totalElapsedTime)); var fileCount = 1 + 2 * smilFiles.Count + (ContentDocument == null ? 0 : 1) + ContentDocument ?.Descendants(Utils.XhtmlNs + "img") .Select(img => img.Attribute("src")?.Value) .Distinct().Count(src => !String.IsNullOrWhiteSpace(src)); Utils.CreateOrGetMeta(NccDocument, "ncc:files")?.SetAttributeValue("content", fileCount); Utils.CreateOrGetMeta(NccDocument, "ncc:depth")?.SetAttributeValue( "content", entries.Select(me => me.Depth).Max()); Utils.CreateOrGetMeta(NccDocument, "ncc:tocItems")?.SetAttributeValue( "content", entries.SelectMany(me => me.NccElements).Count()); foreach (var pt in new[] { "Front", "Normal", "Special" }) { Utils.CreateOrGetMeta(NccDocument, $"ncc:page{pt}")?.SetAttributeValue( "content", entries.SelectMany(me => me.NccElements .Where(e => e.Name == Utils.XhtmlNs + "span" && e.Attribute("class")?.Value == $"page-{pt.ToLowerInvariant()}")) .Count()); } Utils.CreateOrGetMeta(NccDocument, "ncc:multimediaType")?.SetAttributeValue( "content", ContentDocument == null ? "audioNCC" : "audioFullText"); Utils.CreateOrGetMeta(NccDocument, "ncc:generator")?.SetAttributeValue("content", generator); //Remove whitespace only text nodes in smil files foreach (var whiteSpace in SmilFiles.Values .SelectMany(doc => doc.DescendantNodes().OfType <XText>()) .Where(text => String.IsNullOrWhiteSpace(text.Value)) .ToList()) { whiteSpace.Remove(); } //Remove whitespace at the start or end of h1-6 and p elements foreach (var elem in new[] { NccDocument, ContentDocument }.Where(doc => doc != null) .SelectMany(doc => doc.Descendants()) .Where(Utils.IsHeadingOrParagraph) .ToList()) { foreach (var text in elem.DescendantNodes().OfType <XText>()) { text.Value = Regex.Replace(text.Value, @"\s+", " "); } TrimWhitespace(elem); } return(true); }