/// <summary> /// Creates a thumbnail of just the cover image (no title, language name, etc.) /// </summary> /// <returns>Returns true if successful; false otherwise. </returns> internal static bool CreateThumbnailOfCoverImage(Book.Book book, HtmlThumbNailer.ThumbnailOptions options, Action <Image> callback = null) { var imageSrc = book.GetCoverImagePath(); if (!IsCoverImageSrcValid(imageSrc, options)) { Debug.WriteLine(book.StoragePageFolder + " does not have a cover image."); return(false); } var size = Math.Max(options.Width, options.Height); var destFilePath = Path.Combine(book.StoragePageFolder, options.FileName); // Writing a transparent image to a file, then reading it in again appears to be the only // way to get the thumbnail image to draw with the book's cover color background reliably. var transparentImageFile = Path.Combine(Path.GetTempPath(), "Bloom", "Transparent", Path.GetFileName(imageSrc)); Directory.CreateDirectory(Path.GetDirectoryName(transparentImageFile)); try { if (RuntimeImageProcessor.MakePngBackgroundTransparent(imageSrc, transparentImageFile)) { imageSrc = transparentImageFile; } using (var coverImage = PalasoImage.FromFile(imageSrc)) { if (imageSrc == transparentImageFile) { coverImage.Image = MakeImageOpaque(coverImage.Image, book.GetCoverColor()); } var shouldAddDashedBorder = options.BorderStyle == HtmlThumbNailer.ThumbnailOptions.BorderStyles.Dashed; coverImage.Image = options.CenterImageUsingTransparentPadding ? ImageUtils.CenterImageIfNecessary(new Size(size, size), coverImage.Image, shouldAddDashedBorder) : ImageUtils.ResizeImageIfNecessary(new Size(size, size), coverImage.Image, shouldAddDashedBorder); switch (Path.GetExtension(destFilePath).ToLowerInvariant()) { case ".jpg": case ".jpeg": ImageUtils.SaveAsTopQualityJpeg(coverImage.Image, destFilePath); break; default: PalasoImage.SaveImageRobustly(coverImage, destFilePath); break; } if (callback != null) { callback(coverImage.Image.Clone() as Image); // don't leave GC to chance } } } finally { if (File.Exists(transparentImageFile)) { SIL.IO.RobustFile.Delete(transparentImageFile); } } return(true); }
private static void MakeSizedThumbnail(Book book, Color backColor, string destinationFolder, int heightAndWidth) { var coverImagePath = book.GetCoverImagePath(); if (coverImagePath != null) { var thumbPath = Path.Combine(destinationFolder, "thumbnail.png"); RuntimeImageProcessor.GenerateEBookThumbnail(coverImagePath, thumbPath, heightAndWidth, heightAndWidth, backColor); } // else, BR shows a default thumbnail for the book }
public void GetTinyImage_DoesNotChangeSize() { using (var cache = new RuntimeImageProcessor(new BookRenamedEvent()) { TargetDimension = 100 }) using (var file = MakeTempPNGImage(10, 10)) { using (var img = ImageUtils.GetImageFromFile(cache.GetPathToResizedImage(file.Path))) { Assert.AreEqual(10, img.Width); } } }
public PublishToAndroidApi(BloomWebSocketServer bloomWebSocketServer, BookServer bookServer, RuntimeImageProcessor imageProcessor) { _webSocketServer = bloomWebSocketServer; _bookServer = bookServer; _imageProcessor = imageProcessor; _progress = new WebSocketProgress(_webSocketServer, kWebSocketContext); _wifiPublisher = new WiFiPublisher(_progress, _bookServer); #if !__MonoCS__ _usbPublisher = new UsbPublisher(_progress, _bookServer) { Stopped = () => SetState("stopped") }; #endif }
public void GetWideImage_ReturnsShrunkImageWithCorrectProportions() { using (var cache = new RuntimeImageProcessor(new BookRenamedEvent()) { TargetDimension = 100 }) using (var file = MakeTempPNGImage(200, 80)) { using (var img = ImageUtils.GetImageFromFile(cache.GetPathToResizedImage(file.Path))) { Assert.AreEqual(100, img.Width); Assert.AreEqual(40, img.Height); } } }
public void GetJPG_ReturnsShrunkJPG() { using (var cache = new RuntimeImageProcessor(new BookRenamedEvent()) { TargetDimension = 100 }) using (var file = MakeTempJPGImage(200, 80)) { var pathToResizedImage = cache.GetPathToResizedImage(file.Path); using (var img = ImageUtils.GetImageFromFile(pathToResizedImage)) { Assert.AreEqual(".jpg", Path.GetExtension(pathToResizedImage)); //TODO: why does this always report PNG format? Checks by hand of the file show it as jpg //Assert.AreEqual(ImageFormat.Jpeg.Guid, img.RawFormat.Guid); Assert.AreEqual(100, img.Width); Assert.AreEqual(40, img.Height); } } }
// We are trying our best to end up with a thumbnail whose height/width ratio // is the same as the original image. This allows the Uploading and Already in Bloom Library // thumbs to top-align. private string ChooseBestUploadingThumbnailPath(Book.Book book) { // If this exists, it will have the original image's ratio of height to width. var thumb70Path = Path.Combine(book.FolderPath, "thumbnail-70.png"); if (RobustFile.Exists(thumb70Path)) { return(thumb70Path); } var coverImagePath = book.GetCoverImagePath(); if (coverImagePath == null) { return(book.ThumbnailPath); } else { RuntimeImageProcessor.GenerateThumbnail(book.GetCoverImagePath(), book.NonPaddedThumbnailPath, 70, ColorTranslator.FromHtml(book.GetCoverColor())); return(book.NonPaddedThumbnailPath); } }
internal static void MakeSizedThumbnail(Book book, Color backColor, string destinationFolder, int heightAndWidth) { // If this fails to create a 'coverImage200.jpg', either the cover image is missing or it's only a placeholder. // If this is a new book, the file may exist already, but we want to make sure it's up-to-date. // If this is an older book, we need the .bloomd to have it so that Harvester will be able to access it. BookThumbNailer.GenerateImageForWeb(book); var coverImagePath = book.GetCoverImagePath(); if (coverImagePath == null) { var blankImage = Path.Combine(FileLocationUtilities.DirectoryOfApplicationOrSolution, "DistFiles", "Blank.png"); if (RobustFile.Exists(blankImage)) { coverImagePath = blankImage; } } if (coverImagePath != null) { var thumbPath = Path.Combine(destinationFolder, "thumbnail.png"); RuntimeImageProcessor.GenerateEBookThumbnail(coverImagePath, thumbPath, heightAndWidth, heightAndWidth, backColor); } }
public void RegisterWithApiHandler(BloomApiHandler apiHandler) { // This is just for storing the user preference of method // If we had a couple of these, we could just have a generic preferences api // that browser-side code could use. apiHandler.RegisterEndpointLegacy(kApiUrlPart + "method", request => { if (request.HttpMethod == HttpMethods.Get) { var method = Settings.Default.PublishAndroidMethod; if (!new string[] { "wifi", "usb", "file" }.Contains(method)) { method = "wifi"; } request.ReplyWithText(method); } else // post { Settings.Default.PublishAndroidMethod = request.RequiredPostString(); #if __MonoCS__ if (Settings.Default.PublishAndroidMethod == "usb") { _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet."); } #endif request.PostSucceeded(); } }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "backColor", request => { if (request.HttpMethod == HttpMethods.Get) { if (request.CurrentBook != _coverColorSourceBook) { _coverColorSourceBook = request.CurrentBook; ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor); } request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor)); } else // post { // ignore invalid colors (very common while user is editing hex) Color newColor; var newColorAsString = request.RequiredPostString(); if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor)) { _thumbnailBackgroundColor = newColor; request.CurrentBook.SetCoverColor(newColorAsString); } request.PostSucceeded(); } }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode", readRequest => { // If the user has taken off all possible motion, force not having motion in the // Bloom Reader book. See https://issues.bloomlibrary.org/youtrack/issue/BL-7680. if (!readRequest.CurrentBook.HasMotionPages) { readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = false; } return(readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion); }, (writeRequest, value) => { writeRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = value; writeRequest.CurrentBook.BookInfo.SavePublishSettings(); _webSocketServer.SendEvent("publish", "motionChanged"); } , true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request => { MakeBloompubPreview(request, false); }, false); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "thumbnail", request => { var coverImage = request.CurrentBook.GetCoverImagePath(); if (coverImage == null) { request.Failed("no cover image"); } else { // We don't care as much about making it resized as making its background transparent. using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile()) { if (_thumbnailBackgroundColor == Color.Transparent) { ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor); } RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor); request.ReplyWithImage(thumbnail.Path); } } }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request => { #if !__MonoCS__ SetState("UsbStarted"); UpdatePreviewIfNeeded(request); _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings()); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request => { #if !__MonoCS__ _usbPublisher.Stop(disposing: false); SetState("stopped"); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/start", request => { SetState("ServingOnWifi"); UpdatePreviewIfNeeded(request); _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings()); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/stop", request => { _wifiPublisher.Stop(); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/save", request => { UpdatePreviewIfNeeded(request); FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings()); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubsParams", request => { request.ReplyWithJson(JsonConvert.SerializeObject(_collectionSettings.BulkPublishBloomPubSettings)); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubs", request => { // update what's in the collection so that we remember for next time _collectionSettings.BulkPublishBloomPubSettings = request.RequiredPostObject <BulkBloomPubPublishSettings>(); _collectionSettings.Save(); _bulkBloomPubCreator.PublishAllBooks(_collectionSettings.BulkPublishBloomPubSettings); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointLegacy(kApiUrlPart + "textToClipboard", request => { PortableClipboard.SetText(request.RequiredPostString()); request.PostSucceeded(); }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode", request => { return(request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate", request => { return(request.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion && request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape", request => { return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterEndpointLegacy(kApiUrlPart + "languagesInBook", request => { try { InitializeLanguagesInBook(request); Dictionary <string, InclusionSetting> textLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.TextLangs; Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.AudioLangs; var result = "[" + string.Join(",", _allLanguages.Select(kvp => { string langCode = kvp.Key; bool includeText = false; if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting)) { includeText = includeTextSetting.IsIncluded(); } bool includeAudio = false; if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting)) { includeAudio = includeAudioSetting.IsIncluded(); } var value = new LanguagePublishInfo() { code = kvp.Key, name = request.CurrentBook.PrettyPrintLanguage(langCode), complete = kvp.Value, includeText = includeText, containsAnyAudio = _languagesWithAudio.Contains(langCode), includeAudio = includeAudio }; var json = JsonConvert.SerializeObject(value); return(json); })) + "]"; request.ReplyWithText(result); }
public EnhancedImageServer(RuntimeImageProcessor cache, BookThumbNailer thumbNailer, BookSelection bookSelection, BloomFileLocator fileLocator = null) : base(cache) { _thumbNailer = thumbNailer; _bookSelection = bookSelection; _fileLocator = fileLocator; }
public void RegisterWithApiHandler(BloomApiHandler apiHandler) { // This is just for storing the user preference of method // If we had a couple of these, we could just have a generic preferences api // that browser-side code could use. apiHandler.RegisterEndpointHandler(kApiUrlPart + "method", request => { if (request.HttpMethod == HttpMethods.Get) { var method = Settings.Default.PublishAndroidMethod; if (!new string[] { "wifi", "usb", "file" }.Contains(method)) { method = "wifi"; } request.ReplyWithText(method); } else // post { Settings.Default.PublishAndroidMethod = request.RequiredPostString(); #if __MonoCS__ if (Settings.Default.PublishAndroidMethod == "usb") { _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet."); } #endif request.PostSucceeded(); } }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "backColor", request => { if (request.HttpMethod == HttpMethods.Get) { if (request.CurrentBook != _coverColorSourceBook) { _coverColorSourceBook = request.CurrentBook; ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor); } request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor)); } else // post { // ignore invalid colors (very common while user is editing hex) Color newColor; var newColorAsString = request.RequiredPostString(); if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor)) { _thumbnailBackgroundColor = newColor; request.CurrentBook.SetCoverColor(newColorAsString); } request.PostSucceeded(); } }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode", readRequest => { // If the user has taken off all possible motion, force not having motion in the // Bloom Reader book. See https://issues.bloomlibrary.org/youtrack/issue/BL-7680. if (!readRequest.CurrentBook.HasMotionPages) { readRequest.CurrentBook.MotionMode = false; } return(readRequest.CurrentBook.MotionMode); }, (writeRequest, value) => { writeRequest.CurrentBook.MotionMode = value; } , true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request => { if (request.HttpMethod == HttpMethods.Post) { // This is already running on a server thread, so there doesn't seem to be any need to kick off // another background one and return before the preview is ready. But in case something in C# // might one day kick of a new preview, or we find we do need a background thread, // I've made it a websocket broadcast when it is ready. // If we've already left the publish tab...we can get a few of these requests queued up when // a tester rapidly toggles between views...abandon the attempt if (!PublishHelper.InPublishTab) { request.Failed("aborted, no longer in publish tab"); return; } try { UpdatePreview(request); request.PostSucceeded(); } catch (Exception e) { request.Failed("Error while updating preview. Message: " + e.Message); NonFatalProblem.Report(ModalIf.Alpha, PassiveIf.All, "Error while updating preview.", null, e, true); } } }, false); apiHandler.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request => { var coverImage = request.CurrentBook.GetCoverImagePath(); if (coverImage == null) { request.Failed("no cover image"); } else { // We don't care as much about making it resized as making its background transparent. using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile()) { if (_thumbnailBackgroundColor == Color.Transparent) { ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor); } RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor); request.ReplyWithImage(thumbnail.Path); } } }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request => { #if !__MonoCS__ SetState("UsbStarted"); UpdatePreviewIfNeeded(request); _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings()); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request => { #if !__MonoCS__ _usbPublisher.Stop(); SetState("stopped"); #endif request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request => { SetState("ServingOnWifi"); UpdatePreviewIfNeeded(request); _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings()); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request => { _wifiPublisher.Stop(); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "file/save", request => { UpdatePreviewIfNeeded(request); FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings()); SetState("stopped"); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "cleanup", request => { Stop(); request.PostSucceeded(); }, true); apiHandler.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request => { PortableClipboard.SetText(request.RequiredPostString()); request.PostSucceeded(); }, true); apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode", request => { return(request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate", request => { return(request.CurrentBook.MotionMode && request.CurrentBook.HasMotionPages); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape", request => { return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape); }, null, // no write action false, true); // we don't really know, just safe default apiHandler.RegisterEndpointHandler(kApiUrlPart + "languagesInBook", request => { try { InitializeLanguagesInBook(request); Dictionary <string, InclusionSetting> textLangsToPublish = request.CurrentBook.BookInfo.MetaData.TextLangsToPublish.ForBloomPUB; Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.MetaData.AudioLangsToPublish.ForBloomPUB; var result = "[" + string.Join(",", _allLanguages.Select(kvp => { string langCode = kvp.Key; bool includeText = false; if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting)) { includeText = includeTextSetting.IsIncluded(); } bool includeAudio = false; if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting)) { includeAudio = includeAudioSetting.IsIncluded(); } var value = new LanguagePublishInfo() { code = kvp.Key, name = request.CurrentBook.PrettyPrintLanguage(langCode), complete = kvp.Value, includeText = includeText, containsAnyAudio = _languagesWithAudio.Contains(langCode), includeAudio = includeAudio }; var json = JsonConvert.SerializeObject(value); return(json); })) + "]"; request.ReplyWithText(result); }
public void RegisterWithServer(EnhancedImageServer server) { // This is just for storing the user preference of method // If we had a couple of these, we could just have a generic preferences api // that browser-side code could use. server.RegisterEndpointHandler(kApiUrlPart + "method", request => { if (request.HttpMethod == HttpMethods.Get) { var method = Settings.Default.PublishAndroidMethod; if (!new string[] { "wifi", "usb", "file" }.Contains(method)) { method = "wifi"; } request.ReplyWithText(method); } else // post { Settings.Default.PublishAndroidMethod = request.RequiredPostString(); #if __MonoCS__ if (Settings.Default.PublishAndroidMethod == "usb") { _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet."); } #endif request.PostSucceeded(); } }, true); server.RegisterEndpointHandler(kApiUrlPart + "backColor", request => { if (request.HttpMethod == HttpMethods.Get) { if (request.CurrentBook != _coverColorSourceBook) { _coverColorSourceBook = request.CurrentBook; TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor); } request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor)); } else // post { // ignore invalid colors (very common while user is editing hex) Color newColor; var newColorAsString = request.RequiredPostString(); if (TryCssColorFromString(newColorAsString, out newColor)) { _thumbnailBackgroundColor = newColor; request.CurrentBook.SetCoverColor(newColorAsString); } request.PostSucceeded(); } }, true); server.RegisterEndpointHandler(kApiUrlPart + "photoStoryMode", request => { if (request.HttpMethod == HttpMethods.Get) { // this is temporary, just trying to get support for full screen pan & zoom out quickly in 4.2 request.ReplyWithText(request.CurrentBook.UsePhotoStoryModeInBloomReader.ToString() .ToLowerInvariant()); // "false", not "False" } else // post { request.CurrentBook.UsePhotoStoryModeInBloomReader = bool.Parse(request.RequiredPostString()); request.PostSucceeded(); } }, true); server.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request => { var coverImage = request.CurrentBook.GetCoverImagePath(); if (coverImage == null) { request.Failed("no cover image"); } else { // We don't care as much about making it resized as making its background transparent. using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile()) { if (_thumbnailBackgroundColor == Color.Transparent) { TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor); } RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor); request.ReplyWithImage(thumbnail.Path); } } }, true); server.RegisterEndpointHandler(kApiUrlPart + "usb/start", request => { #if !__MonoCS__ SetState("UsbStarted"); _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor); #endif request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request => { #if !__MonoCS__ _usbPublisher.Stop(); SetState("stopped"); #endif request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request => { _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor); SetState("ServingOnWifi"); request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request => { _wifiPublisher.Stop(); SetState("stopped"); request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "file/save", request => { FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress); SetState("stopped"); request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "cleanup", request => { Stop(); request.PostSucceeded(); }, true); server.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request => { PortableClipboard.SetText(request.RequiredPostString()); request.PostSucceeded(); }, true); }