From e6b18457f240cf2253a7cfeaecd573297d51b93b Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sun, 11 Dec 2022 08:54:34 -0600 Subject: [PATCH] File Dimension API (#1690) * Implemented an api for getting file dimensions for a given chapter. This is for CDisplayEx integration. This might be usable in Double Renderer. * Added the cached filename for new API --- API.Tests/Services/CacheServiceTests.cs | 8 ++-- API.Tests/Services/ReaderServiceTests.cs | 8 ++-- API/Controllers/OPDSController.cs | 2 +- API/Controllers/ReaderController.cs | 27 ++++++++++--- API/DTOs/Reader/ChapterInfoDto.cs | 3 +- API/DTOs/Reader/FileDimensionDto.cs | 13 ++++++ API/Helpers/ReadingListHelper.cs | 29 +------------- API/Services/CacheService.cs | 40 +++++++++++++++++-- API/Services/ReaderService.cs | 3 +- UI/Web/src/app/_services/reader.service.ts | 5 +++ .../manga-reader/_models/file-dimension.ts | 5 +++ openapi.json | 28 ++++++++++++- 12 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 API/DTOs/Reader/FileDimensionDto.cs create mode 100644 UI/Web/src/app/manga-reader/_models/file-dimension.ts diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index d05cc9113..6d973aecf 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -325,7 +325,7 @@ public class CacheServiceTests // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); - var path = cs.GetCachedPagePath(c, 11); + var path = cs.GetCachedPagePath(c.Id, 11); Assert.Equal(string.Empty, path); } @@ -377,7 +377,7 @@ public class CacheServiceTests // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); - Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/000_001.jpg"), ds.FileSystem.Path.GetFullPath(cs.GetCachedPagePath(c, 0))); + Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/000_001.jpg"), ds.FileSystem.Path.GetFullPath(cs.GetCachedPagePath(c.Id, 0))); } @@ -425,7 +425,7 @@ public class CacheServiceTests ds.Flatten($"{CacheDirectory}1/"); // Remember that we start at 0, so this is the 10th file - var path = cs.GetCachedPagePath(c, c.Pages); + var path = cs.GetCachedPagePath(c.Id, c.Pages); Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/000_0{c.Pages}.jpg"), ds.FileSystem.Path.GetFullPath(path)); } @@ -478,7 +478,7 @@ public class CacheServiceTests ds.Flatten($"{CacheDirectory}1/"); // Remember that we start at 0, so this is the page + 1 file - var path = cs.GetCachedPagePath(c, 10); + var path = cs.GetCachedPagePath(c.Id, 10); Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/001_001.jpg"), ds.FileSystem.Path.GetFullPath(path)); } diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 7c1011a69..6d4a3b359 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -2357,7 +2357,7 @@ public class ReaderServiceTests public void FormatChapterName_Manga_Chapter() { var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); - var actual = readerService.FormatChapterName(LibraryType.Manga, false, false); + var actual = ReaderService.FormatChapterName(LibraryType.Manga, false, false); Assert.Equal("Chapter", actual); } @@ -2365,7 +2365,7 @@ public class ReaderServiceTests public void FormatChapterName_Book_Chapter_WithTitle() { var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); - var actual = readerService.FormatChapterName(LibraryType.Book, false, false); + var actual = ReaderService.FormatChapterName(LibraryType.Book, false, false); Assert.Equal("Book", actual); } @@ -2373,7 +2373,7 @@ public class ReaderServiceTests public void FormatChapterName_Comic() { var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); - var actual = readerService.FormatChapterName(LibraryType.Comic, false, false); + var actual = ReaderService.FormatChapterName(LibraryType.Comic, false, false); Assert.Equal("Issue", actual); } @@ -2381,7 +2381,7 @@ public class ReaderServiceTests public void FormatChapterName_Comic_WithHash() { var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); - var actual = readerService.FormatChapterName(LibraryType.Comic, true, true); + var actual = ReaderService.FormatChapterName(LibraryType.Comic, true, true); Assert.Equal("Issue #", actual); } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 3c3277dc2..9159e763c 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -824,7 +824,7 @@ public class OpdsController : BaseApiController try { - var path = _cacheService.GetCachedPagePath(chapter, pageNumber); + var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}"); var content = await _directoryService.ReadFileAsync(path); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index c92f4398d..b020a3450 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -102,7 +102,7 @@ public class ReaderController : BaseApiController try { - var path = _cacheService.GetCachedPagePath(chapter, page); + var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache."); var format = Path.GetExtension(path).Replace(".", ""); @@ -152,6 +152,23 @@ public class ReaderController : BaseApiController } } + /// + /// Returns the file dimensions for all pages in a chapter. If the underlying chapter is PDF, use extractPDF to unpack as images. + /// + /// This has a side effect of caching the images. + /// This will only be populated on archive filetypes and not in bookmark mode + /// + /// + /// + [HttpGet("file-dimensions")] + public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) + { + if (chapterId <= 0) return null; + var chapter = await _cacheService.Ensure(chapterId, extractPdf); + if (chapter == null) return BadRequest("Could not find Chapter"); + return Ok(_cacheService.GetCachedFileDimensions(chapterId)); + } + /// /// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading. /// @@ -183,7 +200,7 @@ public class ReaderController : BaseApiController Pages = dto.Pages, ChapterTitle = dto.ChapterTitle ?? string.Empty, Subtitle = string.Empty, - Title = dto.SeriesName + Title = dto.SeriesName, }; if (info.ChapterTitle is {Length: > 0}) { @@ -195,14 +212,14 @@ public class ReaderController : BaseApiController info.Subtitle = info.FileName; } else if (!info.IsSpecial && info.VolumeNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume)) { - info.Subtitle = _readerService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber; + info.Subtitle = ReaderService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber; } else { info.Subtitle = "Volume " + info.VolumeNumber; if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter)) { - info.Subtitle += " " + _readerService.FormatChapterName(info.LibraryType, true, true) + + info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber; } } @@ -673,7 +690,7 @@ public class ReaderController : BaseApiController if (chapter == null) return BadRequest("Could not find cached image. Reload and try again."); bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); - var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page); + var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest("Could not save bookmark"); diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 7f4079910..842add713 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -1,4 +1,5 @@ -using API.Entities.Enums; +using System.Collections.Generic; +using API.Entities.Enums; namespace API.DTOs.Reader; diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/API/DTOs/Reader/FileDimensionDto.cs new file mode 100644 index 000000000..33abd99be --- /dev/null +++ b/API/DTOs/Reader/FileDimensionDto.cs @@ -0,0 +1,13 @@ +namespace API.DTOs.Reader; + +public class FileDimensionDto +{ + public int Width { get; set; } + public int Height { get; set; } + public int PageNumber { get; set; } + /// + /// The filename of the cached file. If this was nested in a subfolder, the foldername will be appended with _ + /// + /// chapter01_page01.png + public string FileName { get; set; } = default!; +} diff --git a/API/Helpers/ReadingListHelper.cs b/API/Helpers/ReadingListHelper.cs index c8911ab3d..e5a3a6524 100644 --- a/API/Helpers/ReadingListHelper.cs +++ b/API/Helpers/ReadingListHelper.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using API.DTOs.ReadingLists; using API.Entities; using API.Entities.Enums; +using API.Services; namespace API.Helpers; @@ -40,35 +41,9 @@ public static class ReadingListHelper } if (title == string.Empty) { - title = FormatChapterName(item.LibraryType, true, true) + chapterNum; + title = ReaderService.FormatChapterName(item.LibraryType, true, true) + chapterNum; } return title; } - /// - /// Formats a Chapter name based on the library it's in - /// - /// - /// For comics only, includes a # which is used for numbering on cards - /// Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end. - /// - private static string FormatChapterName(LibraryType libraryType, bool includeHash = false, - bool includeSpace = false) - { - switch (libraryType) - { - case LibraryType.Manga: - return "Chapter" + (includeSpace ? " " : string.Empty); - case LibraryType.Comic: - if (includeHash) { - return "Issue #"; - } - return "Issue" + (includeSpace ? " " : string.Empty); - case LibraryType.Book: - return "Book" + (includeSpace ? " " : string.Empty); - default: - throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null); - } - } - } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 62607f1aa..5ef4184d3 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -5,11 +5,13 @@ using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Data; +using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; using API.Extensions; using Kavita.Common; using Microsoft.Extensions.Logging; +using NetVips; namespace API.Services; @@ -29,7 +31,8 @@ public interface ICacheService /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(Chapter chapter, int page); + string GetCachedPagePath(int chapterId, int page); + IEnumerable GetCachedFileDimensions(int chapterId); string GetCachedBookmarkPagePath(int seriesId, int page); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); @@ -55,6 +58,35 @@ public class CacheService : ICacheService _bookmarkService = bookmarkService; } + public IEnumerable GetCachedFileDimensions(int chapterId) + { + var path = GetCachePath(chapterId); + var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + .OrderByNatural(Path.GetFileNameWithoutExtension) + .ToArray(); + + if (files.Length == 0) + { + return ArraySegment.Empty; + } + + var dimensions = new List(); + for (var i = 0; i < files.Length; i++) + { + var file = files[i]; + using var image = Image.NewFromStream(File.OpenRead(file), access: Enums.Access.SequentialUnbuffered); + dimensions.Add(new FileDimensionDto() + { + PageNumber = i, + Height = image.Height, + Width = image.Width, + FileName = file + }); + } + + return dimensions; + } + public string GetCachedBookmarkPagePath(int seriesId, int page) { // Calculate what chapter the page belongs to @@ -208,13 +240,13 @@ public class CacheService : ICacheService /// /// Returns the absolute path of a cached page. /// - /// Chapter entity with Files populated. + /// Chapter id with Files populated. /// Page number to look for /// Page filepath or empty if no files found. - public string GetCachedPagePath(Chapter chapter, int page) + public string GetCachedPagePath(int chapterId, int page) { // Calculate what chapter the page belongs to - var path = GetCachePath(chapter.Id); + var path = GetCachePath(chapterId); // TODO: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 04257b5f9..c14a0425e 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -32,7 +32,6 @@ public interface IReaderService Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub); - string FormatChapterName(LibraryType libraryType, bool includeHash = false, bool includeSpace = false); } public class ReaderService : IReaderService @@ -612,7 +611,7 @@ public class ReaderService : IReaderService /// For comics only, includes a # which is used for numbering on cards /// Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end. /// - public string FormatChapterName(LibraryType libraryType, bool includeHash = false, bool includeSpace = false) + public static string FormatChapterName(LibraryType libraryType, bool includeHash = false, bool includeSpace = false) { switch(libraryType) { diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index c62e55910..a3a5ed8d2 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -13,6 +13,7 @@ import { ProgressBookmark } from '../_models/readers/progress-bookmark'; import { SeriesFilter } from '../_models/metadata/series-filter'; import { UtilityService } from '../shared/_services/utility.service'; import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service'; +import { FileDimension } from '../manga-reader/_models/file-dimension'; export const CHAPTER_ID_DOESNT_EXIST = -1; export const CHAPTER_ID_NOT_FETCHED = -2; @@ -106,6 +107,10 @@ export class ReaderService { return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId); } + getFileDimensions(chapterId: number) { + return this.httpClient.get>(this.baseUrl + 'reader/file-dimensions?chapterId=' + chapterId); + } + saveProgress(libraryId: number, seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) { return this.httpClient.post(this.baseUrl + 'reader/progress', {libraryId, seriesId, volumeId, chapterId, pageNum: page, bookScrollId}); } diff --git a/UI/Web/src/app/manga-reader/_models/file-dimension.ts b/UI/Web/src/app/manga-reader/_models/file-dimension.ts new file mode 100644 index 000000000..47fb0a64d --- /dev/null +++ b/UI/Web/src/app/manga-reader/_models/file-dimension.ts @@ -0,0 +1,5 @@ +export interface FileDimension { + pageNumber: number; + width: number; + height: number; +} \ No newline at end of file diff --git a/openapi.json b/openapi.json index 41aa353b1..82a478ce6 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.6.1.9" + "version": "0.6.1.10" }, "servers": [ { @@ -9829,6 +9829,14 @@ "type": "string", "description": "Series Title", "nullable": true + }, + "fileDimensions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDimensionDto" + }, + "description": "A list of images and their dimensions to be used with double page readers", + "nullable": true } }, "additionalProperties": false, @@ -10231,6 +10239,24 @@ "additionalProperties": false, "description": "Represents if Test Email Service URL was successful or not and if any error occured" }, + "FileDimensionDto": { + "type": "object", + "properties": { + "width": { + "type": "integer", + "format": "int32" + }, + "height": { + "type": "integer", + "format": "int32" + }, + "pageNumber": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "FileExtensionBreakdownDto": { "type": "object", "properties": {