From 48eb8f2a4ba01c5ce8a512e49d8f187bfb2cf2bb Mon Sep 17 00:00:00 2001 From: John Thomson Date: Thu, 2 Apr 2026 17:43:14 -0500 Subject: [PATCH] Save the best possible img from custom layout to standard (BL-16086) --- src/BloomExe/Book/Book.cs | 50 ++++++++++------ src/BloomExe/Book/BookData.cs | 86 ++++++++++++++++++++++++++++ src/BloomTests/Book/BookDataTests.cs | 47 +++++++++++++++ 3 files changed, 167 insertions(+), 16 deletions(-) diff --git a/src/BloomExe/Book/Book.cs b/src/BloomExe/Book/Book.cs index cccb5afed776..41635d1aa571 100644 --- a/src/BloomExe/Book/Book.cs +++ b/src/BloomExe/Book/Book.cs @@ -5228,6 +5228,18 @@ public string GetCoverImagePathAndElt(out SafeXmlElement coverImgElt) if (outsideFrontCover == null) return null; + coverImgElt = GetCoverImageElt(outsideFrontCover); + if (coverImgElt == null) + return null; + + return GetImagePath(coverImgElt); + } + + internal static SafeXmlElement GetCoverImageElt(SafeXmlElement outsideFrontCover) + { + if (outsideFrontCover == null) + return null; + // Prefer the designated cover image if it is present and not placeholder. var designatedCoverImage = outsideFrontCover .SafeSelectNodes( @@ -5237,21 +5249,20 @@ public string GetCoverImagePathAndElt(out SafeXmlElement coverImgElt) .FirstOrDefault(); SafeXmlElement bestPlaceholderElt = null; - string bestPlaceholderPath = null; + string bestPlaceholderSource = null; if (designatedCoverImage != null) { - var designatedPath = GetImagePath(designatedCoverImage); - if (designatedPath != null) + var designatedSource = GetImageSourceForCoverSelection(designatedCoverImage); + if (!string.IsNullOrWhiteSpace(designatedSource)) { - if (!ImageUtils.IsPlaceholderImageFilename(designatedPath)) + if (!ImageUtils.IsPlaceholderImageFilename(designatedSource)) { - coverImgElt = designatedCoverImage; - return designatedPath; + return designatedCoverImage; } bestPlaceholderElt = designatedCoverImage; - bestPlaceholderPath = designatedPath; + bestPlaceholderSource = designatedSource; } } @@ -5271,25 +5282,32 @@ var candidate in outsideFrontCover if (candidate == designatedCoverImage) continue; - var candidatePath = GetImagePath(candidate); - if (candidatePath == null) + var candidateSource = GetImageSourceForCoverSelection(candidate); + if (string.IsNullOrWhiteSpace(candidateSource)) continue; - if (!ImageUtils.IsPlaceholderImageFilename(candidatePath)) + if (!ImageUtils.IsPlaceholderImageFilename(candidateSource)) { - coverImgElt = candidate; - return candidatePath; + return candidate; } - if (bestPlaceholderPath == null) + if (bestPlaceholderSource == null) { bestPlaceholderElt = candidate; - bestPlaceholderPath = candidatePath; + bestPlaceholderSource = candidateSource; } } - coverImgElt = bestPlaceholderElt; - return bestPlaceholderPath; + return bestPlaceholderElt; + } + + private static string GetImageSourceForCoverSelection(SafeXmlElement imageElement) + { + var imageUrl = HtmlDom.GetImageElementUrl(imageElement); + if (!string.IsNullOrWhiteSpace(imageUrl.PathOnly.NotEncoded)) + return imageUrl.PathOnly.NotEncoded; + + return imageUrl.UrlEncoded; } private string GetImagePath(SafeXmlElement imageElement) diff --git a/src/BloomExe/Book/BookData.cs b/src/BloomExe/Book/BookData.cs index 65615771cf7e..ec3a2d8f8975 100644 --- a/src/BloomExe/Book/BookData.cs +++ b/src/BloomExe/Book/BookData.cs @@ -1658,6 +1658,8 @@ public void GatherDataItemsFromXElement( ); } } + + AddFallbackCoverImageFromCustomOutsideFrontCover(data, sourceElement); } catch (Exception error) { @@ -1671,6 +1673,90 @@ public void GatherDataItemsFromXElement( } } + private void AddFallbackCoverImageFromCustomOutsideFrontCover( + DataSet data, + SafeXmlNode sourceElement + ) + { + var customOutsideFrontCover = sourceElement + .SafeSelectNodes( + "self::div[contains(concat(' ', normalize-space(@class), ' '), ' outsideFrontCover ') and contains(concat(' ', normalize-space(@class), ' '), ' bloom-customLayout ')] | .//div[contains(concat(' ', normalize-space(@class), ' '), ' outsideFrontCover ') and contains(concat(' ', normalize-space(@class), ' '), ' bloom-customLayout ')]" + ) + .Cast() + .FirstOrDefault(); + + if (customOutsideFrontCover == null) + return; + + var fallbackImageNode = Book.GetCoverImageElt(customOutsideFrontCover); + if (fallbackImageNode == null) + return; + + var imageUrl = HtmlDom.GetImageElementUrl(fallbackImageNode); + var encodedImagePath = imageUrl.UrlEncoded; + if ( + string.IsNullOrWhiteSpace(encodedImagePath) + && !string.IsNullOrWhiteSpace(imageUrl.PathOnly.NotEncoded) + ) + { + encodedImagePath = UrlPathString + .CreateFromUnencodedString(imageUrl.PathOnly.NotEncoded) + .UrlEncoded; + } + if (string.IsNullOrWhiteSpace(encodedImagePath) && fallbackImageNode.Name == "img") + { + var src = fallbackImageNode.GetAttribute("src"); + if (!string.IsNullOrWhiteSpace(src)) + { + encodedImagePath = UrlPathString.CreateFromUrlEncodedString(src).UrlEncoded; + } + } + if (string.IsNullOrWhiteSpace(encodedImagePath)) + return; + + AddOrUpdateTextVariableFromNode( + data, + "coverImage", + "*", + encodedImagePath, + false, + true, + fallbackImageNode + ); + } + + private void AddOrUpdateTextVariableFromNode( + DataSet data, + string key, + string lang, + string value, + bool isCollectionValue, + bool isVariableUrlEncoded, + SafeXmlElement nodeForAttributes + ) + { + if (!data.TextVariables.TryGetValue(key, out var dataSetValue)) + { + var textAlternatives = new MultiTextBase(); + textAlternatives.SetAlternative(lang, value); + dataSetValue = new DataSetElementValue(textAlternatives, isCollectionValue); + data.TextVariables.Add(key, dataSetValue); + } + else if (!dataSetValue.TextAlternatives.ContainsAlternative(lang)) + { + dataSetValue.TextAlternatives.SetAlternative(lang, value); + } + else + { + return; + } + + if (isVariableUrlEncoded) + KeysOfVariablesThatAreUrlEncoded.Add(key); + + dataSetValue.SetAttributeList(lang, GetAttributesToSave(nodeForAttributes)); + } + // Attributes not to copy when saving element attribute data in a DataSetElementValue. static HashSet _attributesNotToCopy = new HashSet( new[] diff --git a/src/BloomTests/Book/BookDataTests.cs b/src/BloomTests/Book/BookDataTests.cs index 7ee9cf99d10a..ffeb69bf405a 100644 --- a/src/BloomTests/Book/BookDataTests.cs +++ b/src/BloomTests/Book/BookDataTests.cs @@ -3457,6 +3457,53 @@ public void SuckInDataFromEditedDom_CustomLayoutPage_InactivatesNestedDataAndCla ); } + [Test] + public void SuckInDataFromEditedDom_CustomOutsideFrontCoverWithoutCoverImageDataBook_UsesFallbackImageForCoverImage() + { + var bookDom = new HtmlDom( + @" +
+
+
+
+ +
+
+
+ " + ); + var bookData = new BookData(bookDom, _collectionSettings, null); + + var editedPageDom = new HtmlDom( + @" +
+
+
+ +
+
+
+ " + ); + + bookData.SuckInDataFromEditedDom(editedPageDom); + + var coverImageDataDivElement = bookDom.SelectSingleNodeHonoringDefaultNS( + "//div[@id='bloomDataDiv']/div[@data-book='coverImage']" + ); + Assert.That(coverImageDataDivElement, Is.Not.Null); + Assert.That(coverImageDataDivElement.InnerText, Is.EqualTo("fallbackCoverImage.png")); + + var standardCoverImageElement = (SafeXmlElement) + bookDom.SelectSingleNodeHonoringDefaultNS( + "//div[contains(@class,'outsideFrontCover')]//img[@data-book='coverImage']" + ); + Assert.That( + standardCoverImageElement.GetAttribute("src"), + Is.EqualTo("fallbackCoverImage.png") + ); + } + [Test] public void GatherDataItemsFromXElement_CustomLayoutPageWithXmatterPage_GathersXmatterAttributes() {