From a255e82e034a593b5812b8b4732064856081219a Mon Sep 17 00:00:00 2001 From: Hatton Date: Fri, 9 Jan 2026 11:59:16 -0700 Subject: [PATCH 1/3] Update placeholder images - Extract placeholder color and opacity as LESS variables for easier customization - Change placeholder color from #ECECEC to #4f4f4f with 0.2 opacity for less visual distraction - Add new video placeholder icon for empty video containers - Apply consistent styling to all placeholder types (image, canvas, video) --- src/BloomBrowserUI/images/placeHolder.png | Bin 13344 -> 11531 bytes src/BloomBrowserUI/placeHolderImages.less | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/BloomBrowserUI/images/placeHolder.png b/src/BloomBrowserUI/images/placeHolder.png index 5075adae58cce14dcc6dfbfb70231e317421775a..5cefd047999449103822a73595d24e639b7729e2 100644 GIT binary patch literal 11531 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'); +// bloom blue looks better on white, but conflicts with some page colors #3994a5; +@placeholder-color-encoded: "%234f4f4f"; // %23 instead of # for URL encoding +@placeholder-opacity: 0.2; -@image-placeholder: url('data:image/svg+xml,'); +@image-placeholder: url('data:image/svg+xml,'); -@canvas-placeholder: url('data:image/svg+xml,'); +@canvas-placeholder: url('data:image/svg+xml,'); + +@video-placeholder: url('data:image/svg+xml,'); .image-placeholder-background() { background-image: @image-placeholder; @@ -20,6 +24,11 @@ .image-placeholder-background(); } +.customPage .bloom-videoContainer.bloom-noVideoSelected { + background: @video-placeholder no-repeat center; + background-size: contain; +} + // When previewing templates in the Collection Tab, and also when in Change Layout mode in the Edit tab, // img elements may not be wrapped in canvas-element divs (see BL-15514). Also below. // Real books always have a data-l1 attribute added From f4e2e2815b7113da30f15e42b3950d029ed82131 Mon Sep 17 00:00:00 2001 From: Hatton Date: Fri, 16 Jan 2026 10:09:38 -0700 Subject: [PATCH 2/3] More placeholder work prevent flash of broken image: when asked for placeholder we don't want to use, return a different http code instead of empty data Show image placeholder in template, even when in bloom-player. --- .vscode/settings.json | 10 ++- .../bookEdit/toolbox/canvas/image-overlay.svg | 9 ++- src/BloomBrowserUI/placeHolderImages.less | 7 +- .../icons/ImagePlaceholderIcon.tsx | 68 ++++++++++++++++--- src/BloomExe/Book/AccessibilityCheckers.cs | 2 +- src/BloomExe/Book/Book.cs | 10 ++- src/BloomExe/web/BloomServer.cs | 25 +++++-- src/BloomExe/web/IRequestInfo.cs | 1 + src/BloomExe/web/RequestInfo.cs | 14 ++++ src/BloomTests/PretendRequestInfo.cs | 12 +++- .../Spreadsheet/SpreadsheetImporterTests.cs | 2 +- .../templates/template books/Games/Games.html | 4 +- 12 files changed, 134 insertions(+), 30 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 07a34c23f284..8665ed1fc82f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -68,5 +68,13 @@ "Winforms", "xmatter" ], - "chat.useNestedAgentsMdFiles": true + "chat.useNestedAgentsMdFiles": true, + "workbench.colorCustomizations": { + "statusBar.background": "#96668f", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#ab84a5", + "statusBarItem.remoteBackground": "#96668f", + "statusBarItem.remoteForeground": "#e7e7e7" + }, + "peacock.color": "#96668f" } diff --git a/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg b/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg index edda0a2d1977..e2b395aa3b80 100644 --- a/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg +++ b/src/BloomBrowserUI/bookEdit/toolbox/canvas/image-overlay.svg @@ -1,3 +1,8 @@ - - + + + + + + + diff --git a/src/BloomBrowserUI/placeHolderImages.less b/src/BloomBrowserUI/placeHolderImages.less index 7f173aa9cf74..f613ea66bafb 100644 --- a/src/BloomBrowserUI/placeHolderImages.less +++ b/src/BloomBrowserUI/placeHolderImages.less @@ -32,6 +32,7 @@ // When previewing templates in the Collection Tab, and also when in Change Layout mode in the Edit tab, // img elements may not be wrapped in canvas-element divs (see BL-15514). Also below. // Real books always have a data-l1 attribute added +.preview.template .bloom-canvas:has(img[src*="placeHolder.png"]), .preview:not([data-l1]) .bloom-canvas:has(img[src*="placeHolder.png"]), .origami-layout-mode .bloom-canvas:not(:has(.bloom-canvas-element)):has( @@ -69,7 +70,11 @@ // Hopefully we don't in any way put the string "placeHolder.png" in the inline style of canvases which should not get it! // We need !important here to override the inline style attribute background-image which we normally use to put images in bloompubs .bloomPlayer-page.template - .bloom-canvas[style*="background-image"][style*="placeHolder.png"] { + .bloom-canvas:is( + [style*="background-image"][style*="placeHolder.png"], + /* BloomPlayer moves background-image URLs to data-background for lazy loading, so include that too. */ + [data-background*="placeHolder.png"] + ) { background-image: @image-placeholder !important; &[data-tool-id="canvas"] { diff --git a/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx b/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx index 3af45838594a..7fde956d4dcd 100644 --- a/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx +++ b/src/BloomBrowserUI/react_components/icons/ImagePlaceholderIcon.tsx @@ -12,15 +12,40 @@ export const ImagePlaceholderIcon: React.FunctionComponent< const { color, strokeColor, ...rest } = props; return ( + + + + + @@ -34,23 +59,44 @@ export const WrongImagePlaceholderIcon: React.FunctionComponent< const { color, strokeColor, ...rest } = props; return ( + + + + + ); }; diff --git a/src/BloomExe/Book/AccessibilityCheckers.cs b/src/BloomExe/Book/AccessibilityCheckers.cs index 48d4230cd5e5..1f91538f9622 100644 --- a/src/BloomExe/Book/AccessibilityCheckers.cs +++ b/src/BloomExe/Book/AccessibilityCheckers.cs @@ -34,7 +34,7 @@ public static IEnumerable CheckDescriptionsForAllImages(Book.Book book) "The {0} is where the page number will be inserted." ); - // Note in BL-6089 we may decide to except placeholder.png from these complaints, if + // Note in BL-6089 we may decide to except placeHolder.png from these complaints, if // if we are going to trim them out of epub and bloom reader publishing. // Note that we intentionally are not dealing with unusual hypothetical situations like where diff --git a/src/BloomExe/Book/Book.cs b/src/BloomExe/Book/Book.cs index 1ac1a2025e8f..92bb22aef88f 100644 --- a/src/BloomExe/Book/Book.cs +++ b/src/BloomExe/Book/Book.cs @@ -2361,8 +2361,12 @@ internal static void RemoveImgTagInDataDiv(HtmlDom bookDom) } var imgNode = imgNodes[0]; var src = imgNode.GetAttribute("src"); - coverImageElement.InnerText = - (string.IsNullOrEmpty(src)) ? string.Empty : HttpUtility.UrlDecode(src); + // If malformed markup omits a src, treat it as the legacy placeholder marker + // so downstream code sees an intentional placeholder rather than a missing value. + // This preserves expectations in BringBookUpToDate_EmbeddedEmptyImgTagRemoved. + if (string.IsNullOrWhiteSpace(src)) + src = "placeHolder.png"; + coverImageElement.InnerText = HttpUtility.UrlDecode(src); } } @@ -5189,7 +5193,7 @@ public string GetCoverImagePathAndElt(out SafeXmlElement coverImgElt) ref coverImageFileName ); // We no longer put placeHolder.png files in books (BL-15441) but we still need to detect when the placeholder - // is called for, so here we return placeHolder.png instead of null. Callers of this method should handle this special case. + // is called for, so return coverImagePath unchanged when it is a placeholder. Callers of this method should handle this special-case value. if (ImageUtils.IsPlaceholderImageFilename(coverImagePath)) { return coverImagePath; diff --git a/src/BloomExe/web/BloomServer.cs b/src/BloomExe/web/BloomServer.cs index 94daca5322cd..b52092373422 100644 --- a/src/BloomExe/web/BloomServer.cs +++ b/src/BloomExe/web/BloomServer.cs @@ -674,6 +674,18 @@ bool IsInBookFolder(string path) return path.Replace("\\", "/").StartsWith(CurrentBook.FolderPath.Replace("\\", "/")); } + private bool TryHandlePlaceholderImageRequest(IRequestInfo info, string imageFile) + { + if (!ImageUtils.IsPlaceholderImageFilename(imageFile)) + return false; + + // We now use css to put in the placeholder images, but still use "placeHolder.png" to mark them. + // So we actually don't want to provide an image file for this placeholder marker. + // Return 204 No Content to avoid browser showing broken image icon. + info.WriteNoContent(); + return true; + } + // Handle requests for image files, that is, URLs that end in one of our image extensions. // Returns true if this is, in fact, a request for an image, in which case it will have // been handled; any reporting of problems will have been done, and a response generated. @@ -686,6 +698,9 @@ private bool ProcessImageFileRequest(IRequestInfo info) if (!IsImageTypeThatCanBeDegraded(imageFile) && !isSvg) return false; + if (TryHandlePlaceholderImageRequest(info, imageFile)) + return true; + // This can't be right. At some point it may have had something to do with // images in page thumbnails, but that is now handled by a param. // But we definitely don't want Bloom to fail to find any picture of a thumbnail! @@ -736,6 +751,9 @@ private bool ProcessImageFileRequest(IRequestInfo info) imageFile = Path.Combine(sourceDir, imageFile); + if (TryHandlePlaceholderImageRequest(info, imageFile)) + return true; + if (!RobustFileExistsWithCaseCheck(imageFile)) { // There are a few special cases where it's not desirable to change the source of the image @@ -746,13 +764,6 @@ private bool ProcessImageFileRequest(IRequestInfo info) { imageFile = imageFile.Replace("flat", "icy_orange"); } - else if (ImageUtils.IsPlaceholderImageFilename(imageFile)) - { - // We now use css to put in the placeholder images, but still use "placeHolder.png" to mark them - // So we actually don't want to provide an image file for placeHolder.png. - info.WriteCompleteOutput(""); - return true; - } // If the user does add a video or widget, these placeholder .svgs will get copied to the // book folder and used from there. But we don't copy to the book folder while the user // is still in origami in case the user doesn't actually add the video or widget. diff --git a/src/BloomExe/web/IRequestInfo.cs b/src/BloomExe/web/IRequestInfo.cs index 9025f4fadccb..3513014ea12d 100644 --- a/src/BloomExe/web/IRequestInfo.cs +++ b/src/BloomExe/web/IRequestInfo.cs @@ -34,6 +34,7 @@ public interface IRequestInfo string GetPostString(bool unescape = true); HttpMethods HttpMethod { get; } void ExternalLinkSucceeded(); + void WriteNoContent(); string DoNotCacheFolder { set; } byte[] GetRawPostData(); Stream GetRawPostStream(); diff --git a/src/BloomExe/web/RequestInfo.cs b/src/BloomExe/web/RequestInfo.cs index b88fa0e772aa..a8f247cdaf11 100644 --- a/src/BloomExe/web/RequestInfo.cs +++ b/src/BloomExe/web/RequestInfo.cs @@ -76,6 +76,20 @@ public RequestInfo(IHttpListenerContext context) public void ExternalLinkSucceeded() { _actualContext.Response.StatusCode = 200; //Completed + } + + public void WriteNoContent() + { + try + { + _actualContext.Response.StatusCode = 204; // No Content + _actualContext.Response.ContentLength64 = 0; + _actualContext.Response.Close(); + } + catch (HttpListenerException e) + { + ReportHttpListenerProblem(e); + } HaveOutput = true; } diff --git a/src/BloomTests/PretendRequestInfo.cs b/src/BloomTests/PretendRequestInfo.cs index f1031b23139e..2532517af0b1 100644 --- a/src/BloomTests/PretendRequestInfo.cs +++ b/src/BloomTests/PretendRequestInfo.cs @@ -127,7 +127,17 @@ public string GetPostString(bool unescape = true) return ""; } - public void ExternalLinkSucceeded() { } + public void ExternalLinkSucceeded() + { + StatusCode = 200; + HaveOutput = true; + } + + public void WriteNoContent() + { + StatusCode = 204; + HaveOutput = true; + } public string DoNotCacheFolder { get; set; } diff --git a/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs b/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs index 6abed15dd9c0..96f98308e07b 100644 --- a/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs +++ b/src/BloomTests/Spreadsheet/SpreadsheetImporterTests.cs @@ -679,7 +679,7 @@ public static string PageWithJustImage(int pageNumber, int icNumber)
- +
diff --git a/src/content/templates/template books/Games/Games.html b/src/content/templates/template books/Games/Games.html index b3907022b185..6f189814d0b2 100644 --- a/src/content/templates/template books/Games/Games.html +++ b/src/content/templates/template books/Games/Games.html @@ -1,4 +1,4 @@ - + @@ -5178,7 +5178,7 @@ >
From 1e60a95e36a99e75d4f5aa58e2621e8332cecf61 Mon Sep 17 00:00:00 2001 From: Hatton Date: Wed, 21 Jan 2026 15:19:18 -0700 Subject: [PATCH 3/3] HaveOutput --> HaveFullyProcessedRequest, Fix ExternalLinkSucceeded --- src/BloomExe/web/IRequestInfo.cs | 2 +- src/BloomExe/web/RequestInfo.cs | 18 ++++++++++-------- src/BloomExe/web/controllers/ApiRequest.cs | 2 +- src/BloomTests/PretendRequestInfo.cs | 12 ++++++------ 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/BloomExe/web/IRequestInfo.cs b/src/BloomExe/web/IRequestInfo.cs index 3513014ea12d..5f08d86f8330 100644 --- a/src/BloomExe/web/IRequestInfo.cs +++ b/src/BloomExe/web/IRequestInfo.cs @@ -20,7 +20,7 @@ public interface IRequestInfo string RequestContentType { get; } string ResponseContentType { set; } string RawUrl { get; } - bool HaveOutput { get; } + bool HaveFullyProcessedRequest { get; } void WriteCompleteOutput(string s); void ReplyWithFileContent(string path, string originalPath = null); void ReplyWithStreamContent(Stream input, string responseType); diff --git a/src/BloomExe/web/RequestInfo.cs b/src/BloomExe/web/RequestInfo.cs index a8f247cdaf11..6c4c130c20a3 100644 --- a/src/BloomExe/web/RequestInfo.cs +++ b/src/BloomExe/web/RequestInfo.cs @@ -76,6 +76,7 @@ public RequestInfo(IHttpListenerContext context) public void ExternalLinkSucceeded() { _actualContext.Response.StatusCode = 200; //Completed + HaveFullyProcessedRequest = true; } public void WriteNoContent() @@ -90,7 +91,7 @@ public void WriteNoContent() { ReportHttpListenerProblem(e); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } public string DoNotCacheFolder { get; set; } @@ -119,7 +120,7 @@ private void WriteOutput(byte[] buffer, HttpListenerResponse response) { ReportHttpListenerProblem(e); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } private static void ReportHttpListenerProblem(HttpListenerException e) @@ -133,7 +134,7 @@ private static void ReportHttpListenerProblem(HttpListenerException e) Debug.WriteLine(e.Message); } - public bool HaveOutput { get; private set; } + public bool HaveFullyProcessedRequest { get; private set; } public void ReplyWithFileContent(string path, string originalPath = null) { @@ -143,7 +144,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) { // Earlier there was concern that we were coming here to look for .wav file existence, but that task // is now handled in the "/bloom/api/audio" endpoint. So if we get here, we're looking for a different file. - // Besides, if we don't set HaveOutput to true (w/WriteError), we'll have other problems. + // Besides, if we don't set HaveFullyProcessedRequest to true (w/WriteError), we'll have other problems. Logger.WriteError("Server could not find" + path, new FileNotFoundException()); WriteError(404, "Server could not find " + path); return; @@ -160,7 +161,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) // BL-12237 actually had a FileNotFoundException here, in a Team Collection setting, which should // have been caught by the RobustFile.Exists() above. So we'll just log it and continue. // The important thing for avoiding a big ugly EndpointHandler error (in the case of BL-12237) is to - // set HaveOutput to true, which WriteError() does. + // set HaveFullyProcessedRequest to true, which WriteError() does. Logger.WriteError("Server could not read " + path, error); WriteError(500, "Server could not read " + path + ": " + error.Message); return; @@ -282,7 +283,7 @@ public void ReplyWithFileContent(string path, string originalPath = null) fs.Dispose(); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithStreamContent(Stream input, string responseType) @@ -318,7 +319,7 @@ public void ReplyWithStreamContent(Stream input, string responseType) _actualContext.Response.Close(buffer, false); } - HaveOutput = true; + HaveFullyProcessedRequest = true; } readonly HashSet _cacheableExtensions = new HashSet( @@ -400,7 +401,7 @@ public void WriteError(int errorCode, string errorDescription) if (LocalPathWithoutQuery.ToLowerInvariant().EndsWith(".json")) _actualContext.Response.ContentType = "application/json"; _actualContext.Response.Close(); - HaveOutput = true; + HaveFullyProcessedRequest = true; } private string SanitizeForAscii(string errorDescription) @@ -612,6 +613,7 @@ public void WriteRedirect(string url, bool permanent) // This supports Bloom Player Storybook's "Live from Bloom Editor" feature, preventing CORS errors on the redirect. _actualContext.Response.AppendHeader("Access-Control-Allow-Origin", "*"); _actualContext.Response.Close(); + HaveFullyProcessedRequest = true; } } } diff --git a/src/BloomExe/web/controllers/ApiRequest.cs b/src/BloomExe/web/controllers/ApiRequest.cs index 9ffb772bb71a..da5b7abcdc6a 100644 --- a/src/BloomExe/web/controllers/ApiRequest.cs +++ b/src/BloomExe/web/controllers/ApiRequest.cs @@ -272,7 +272,7 @@ await InvokeWithErrorHandling( } } } - if (!info.HaveOutput) + if (!info.HaveFullyProcessedRequest) { throw new ApplicationException( $"The EndpointHandler for {info.RawUrl} never called a Succeeded(), Failed(), or ReplyWith() Function." diff --git a/src/BloomTests/PretendRequestInfo.cs b/src/BloomTests/PretendRequestInfo.cs index 2532517af0b1..35ad93125ed8 100644 --- a/src/BloomTests/PretendRequestInfo.cs +++ b/src/BloomTests/PretendRequestInfo.cs @@ -60,20 +60,20 @@ public string ReplyContentsAsXml // get is required to fulfil interface contract. Not currently used in tests. // set is required to satisfy (Team City version of) compiler for a valid auto-implemented property. - public bool HaveOutput { get; set; } + public bool HaveFullyProcessedRequest { get; set; } public void WriteCompleteOutput(string s) { var buffer = Encoding.UTF8.GetBytes(s); ReplyContents = Encoding.UTF8.GetString(buffer); - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithFileContent(string path, string originalPath = null) { ReplyImagePath = path; WriteCompleteOutput(RobustFile.ReadAllText(path)); - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void ReplyWithStreamContent(Stream input, string responseType) @@ -90,7 +90,7 @@ public void WriteError(int errorCode, string errorDescription) { StatusCode = errorCode; StatusDescription = errorDescription; - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void WriteError(int errorCode) @@ -130,13 +130,13 @@ public string GetPostString(bool unescape = true) public void ExternalLinkSucceeded() { StatusCode = 200; - HaveOutput = true; + HaveFullyProcessedRequest = true; } public void WriteNoContent() { StatusCode = 204; - HaveOutput = true; + HaveFullyProcessedRequest = true; } public string DoNotCacheFolder { get; set; }