shoko 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../core/services/base_service.rb'
|
|
4
|
+
require_relative '../../../core/services/pagination/internal/chapter_cache.rb'
|
|
5
|
+
require_relative '../terminal/text_metrics.rb'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters
|
|
9
|
+
module Output
|
|
10
|
+
module Formatting
|
|
11
|
+
# Service responsible for wrapping chapter lines to a column width.
|
|
12
|
+
# Uses the shared ChapterCache to avoid recomputation across frames.
|
|
13
|
+
class WrappingService < BaseService
|
|
14
|
+
WINDOW_CACHE_LIMIT = 200
|
|
15
|
+
|
|
16
|
+
def initialize(dependencies)
|
|
17
|
+
super
|
|
18
|
+
@chapter_cache = Shoko::Core::Services::Pagination::Internal::ChapterCache.new
|
|
19
|
+
@window_cache = Hash.new { |h, k| h[k] = { store: {}, order: [] } }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Wrap raw lines for a chapter to the given width.
|
|
23
|
+
# Falls back to a local wrapper if cache is unavailable.
|
|
24
|
+
#
|
|
25
|
+
# @param lines [Array<String>] raw chapter lines
|
|
26
|
+
# @param chapter_index [Integer] chapter index for caching keying
|
|
27
|
+
# @param width [Integer] column width
|
|
28
|
+
# @return [Array<String>] wrapped lines
|
|
29
|
+
def wrap_lines(lines, chapter_index, width)
|
|
30
|
+
return [] if lines.nil? || width.to_i < 10
|
|
31
|
+
|
|
32
|
+
formatted = fetch_formatted_lines(chapter_index, width, 0, lines.length)
|
|
33
|
+
return formatted if formatted
|
|
34
|
+
|
|
35
|
+
cache = begin
|
|
36
|
+
registered?(:chapter_cache) ? resolve(:chapter_cache) : @chapter_cache
|
|
37
|
+
rescue StandardError
|
|
38
|
+
@chapter_cache
|
|
39
|
+
end
|
|
40
|
+
cache.get_wrapped_lines(chapter_index, lines, width)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Wrap only a window of text sufficient for immediate display.
|
|
44
|
+
# This avoids wrapping the entire chapter on first render.
|
|
45
|
+
#
|
|
46
|
+
# @param lines [Array<String>] raw chapter lines
|
|
47
|
+
# @param chapter_index [Integer] chapter index (for caching semantics if needed)
|
|
48
|
+
# @param width [Integer] column width
|
|
49
|
+
# @param start [Integer] wrapped-lines start offset
|
|
50
|
+
# @param length [Integer] number of wrapped lines to return
|
|
51
|
+
# @return [Array<String>] slice of wrapped lines covering the requested window
|
|
52
|
+
def wrap_window(lines, chapter_index, width, start, length)
|
|
53
|
+
width_i = width.to_i
|
|
54
|
+
length_i = length.to_i
|
|
55
|
+
start_i = start.to_i
|
|
56
|
+
return [] if lines.nil? || width_i <= 0 || length_i <= 0
|
|
57
|
+
|
|
58
|
+
formatted = fetch_formatted_lines(chapter_index, width_i, start_i, length_i)
|
|
59
|
+
return formatted if formatted
|
|
60
|
+
|
|
61
|
+
target_end = [start_i, 0].max + length_i - 1
|
|
62
|
+
key = [lines.object_id, chapter_index, width_i]
|
|
63
|
+
cached = @window_cache[key][:store][[start_i, length_i]]
|
|
64
|
+
return cached if cached
|
|
65
|
+
|
|
66
|
+
wrapped = []
|
|
67
|
+
|
|
68
|
+
lines.each do |line|
|
|
69
|
+
break if wrapped.length >= (target_end + 1)
|
|
70
|
+
|
|
71
|
+
next if line.nil?
|
|
72
|
+
|
|
73
|
+
if line.strip.empty?
|
|
74
|
+
wrapped << ''
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
segments = Shoko::Adapters::Output::Terminal::TextMetrics.wrap_plain_text(line, width_i)
|
|
79
|
+
wrapped.concat(segments)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
start_index = [start_i, 0].max
|
|
83
|
+
return [] if start_index >= wrapped.length
|
|
84
|
+
|
|
85
|
+
slice = wrapped[start_index, length_i] || []
|
|
86
|
+
cache_put(key, [start_i, length_i], slice)
|
|
87
|
+
slice
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def prefetch_windows(lines, chapter_index, width, start, length)
|
|
91
|
+
wrap_window(lines, chapter_index, width, start, length)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Wrap the visible window and prefetch ±N pages around it in the background.
|
|
95
|
+
# This centralizes the behavior that was previously embedded in ReaderController.
|
|
96
|
+
#
|
|
97
|
+
# @param doc [Object] document responding to #get_chapter(index)
|
|
98
|
+
# @param chapter_index [Integer]
|
|
99
|
+
# @param col_width [Integer]
|
|
100
|
+
# @param offset [Integer] wrapped-line offset
|
|
101
|
+
# @param display_height [Integer] lines per page
|
|
102
|
+
# @param pre_pages [Integer,nil] optional number of pages to prefetch; defaults from config
|
|
103
|
+
# @return [Array<String>] visible wrapped lines for the requested window
|
|
104
|
+
def fetch_window_and_prefetch(doc, chapter_index, col_width, offset, display_height,
|
|
105
|
+
pre_pages = nil)
|
|
106
|
+
return [] unless doc && display_height.to_i.positive?
|
|
107
|
+
|
|
108
|
+
chapter = doc.get_chapter(chapter_index)
|
|
109
|
+
return [] unless chapter
|
|
110
|
+
|
|
111
|
+
lines = chapter.lines || []
|
|
112
|
+
start_i = [offset.to_i, 0].max
|
|
113
|
+
length_i = display_height.to_i
|
|
114
|
+
|
|
115
|
+
visible = wrap_window(lines, chapter_index, col_width, start_i, length_i)
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
pages = pre_pages
|
|
119
|
+
if pages.nil?
|
|
120
|
+
st = resolve(:state_store) if registered?(:state_store)
|
|
121
|
+
pages = begin
|
|
122
|
+
st.get(%i[config prefetch_pages]) if st.respond_to?(:get)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
pages = pages.nil? ? 20 : pages.to_i
|
|
128
|
+
pages = pages.clamp(0, 200)
|
|
129
|
+
window = pages * length_i
|
|
130
|
+
prefetch_start = [start_i - window, 0].max
|
|
131
|
+
prefetch_end = start_i + window + (length_i - 1)
|
|
132
|
+
prefetch_len = prefetch_end - prefetch_start + 1
|
|
133
|
+
enqueue_prefetch(chapter_index, col_width, prefetch_start, prefetch_len, lines)
|
|
134
|
+
rescue StandardError
|
|
135
|
+
# best-effort prefetch
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
visible
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Clear all cached wrapped lines
|
|
142
|
+
def clear_cache
|
|
143
|
+
@chapter_cache = Shoko::Core::Services::Pagination::Internal::ChapterCache.new
|
|
144
|
+
@window_cache.clear
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Clear cache entries for a given width
|
|
148
|
+
def clear_cache_for_width(width)
|
|
149
|
+
width_i = width.to_i
|
|
150
|
+
@chapter_cache.clear_cache_for_width(width_i)
|
|
151
|
+
@window_cache.delete_if do |key, _|
|
|
152
|
+
key.is_a?(Array) ? key[2] == width_i : key.to_s.end_with?("_#{width_i}")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
protected
|
|
157
|
+
|
|
158
|
+
def required_dependencies
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# No-op private helpers retained for compatibility-free interface.
|
|
165
|
+
|
|
166
|
+
def cache_put(key, subkey, value)
|
|
167
|
+
entry = @window_cache[key]
|
|
168
|
+
store = entry[:store]
|
|
169
|
+
order = entry[:order]
|
|
170
|
+
unless store.key?(subkey)
|
|
171
|
+
order << subkey
|
|
172
|
+
if order.length > WINDOW_CACHE_LIMIT
|
|
173
|
+
oldest = order.shift
|
|
174
|
+
store.delete(oldest)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
store[subkey] = value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def fetch_formatted_lines(chapter_index, width, offset, length)
|
|
181
|
+
return unless registered?(:formatting_service)
|
|
182
|
+
|
|
183
|
+
document = begin
|
|
184
|
+
resolve(:document)
|
|
185
|
+
rescue StandardError
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
return unless document
|
|
189
|
+
|
|
190
|
+
formatting = resolve(:formatting_service)
|
|
191
|
+
lines = formatting.wrap_window(document, chapter_index, width, offset: offset, length: length)
|
|
192
|
+
return unless lines && !lines.empty?
|
|
193
|
+
|
|
194
|
+
lines.map { |line| line.respond_to?(:text) ? line.text : line }
|
|
195
|
+
rescue StandardError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def enqueue_prefetch(chapter_index, col_width, prefetch_start, prefetch_len, lines)
|
|
200
|
+
worker = background_worker
|
|
201
|
+
job = lambda do
|
|
202
|
+
prefetch_windows(lines, chapter_index, col_width, prefetch_start, prefetch_len)
|
|
203
|
+
end
|
|
204
|
+
if worker
|
|
205
|
+
worker.submit(&job)
|
|
206
|
+
else
|
|
207
|
+
Thread.new do
|
|
208
|
+
job.call
|
|
209
|
+
rescue StandardError
|
|
210
|
+
# ignore background failures
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
rescue StandardError
|
|
214
|
+
# ignore background failures
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def background_worker
|
|
218
|
+
return nil unless registered?(:background_worker)
|
|
219
|
+
|
|
220
|
+
resolve(:background_worker)
|
|
221
|
+
rescue StandardError
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/services/base_service.rb'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output
|
|
7
|
+
# Provides a single facade for performance monitoring and tracing so that
|
|
8
|
+
# higher layers do not talk to infrastructure modules directly.
|
|
9
|
+
class InstrumentationService < BaseService
|
|
10
|
+
def initialize(dependencies)
|
|
11
|
+
super
|
|
12
|
+
@monitor = resolve_optional(:performance_monitor)
|
|
13
|
+
@tracer = resolve_optional(:perf_tracer)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def time(metric, &)
|
|
17
|
+
raise ArgumentError, 'block required for #time' unless block_given?
|
|
18
|
+
return yield unless @monitor.respond_to?(:time)
|
|
19
|
+
|
|
20
|
+
@monitor.time(metric, &)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record_metric(name, value, count = 1)
|
|
24
|
+
@monitor&.record_metric(name, value, count)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_trace(metric, value)
|
|
28
|
+
@tracer&.record(metric, value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def complete_trace(**payload)
|
|
32
|
+
@tracer&.complete(**payload)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cancel_trace
|
|
36
|
+
@tracer&.cancel
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def start_trace(path)
|
|
40
|
+
@tracer.respond_to?(:start_open) ? @tracer.start_open(path) : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def resolve_optional(name)
|
|
46
|
+
resolve(name)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output::Kitty
|
|
7
|
+
# Converts common raster image formats to PNG bytes so they can be rendered
|
|
8
|
+
# by the Kitty graphics protocol.
|
|
9
|
+
#
|
|
10
|
+
# This is invoked only when Kitty images are enabled and an image is about
|
|
11
|
+
# to be rendered; it is not part of the default import pipeline.
|
|
12
|
+
class ImageTranscoder
|
|
13
|
+
PNG_HEADER = "\x89PNG\r\n\x1a\n".b
|
|
14
|
+
|
|
15
|
+
def initialize(command: nil)
|
|
16
|
+
@command = command || default_command
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def available?
|
|
20
|
+
!@command.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param bytes [String] original image bytes
|
|
24
|
+
# @return [String,nil] PNG bytes
|
|
25
|
+
def to_png(bytes)
|
|
26
|
+
data = String(bytes).dup
|
|
27
|
+
data.force_encoding(Encoding::BINARY)
|
|
28
|
+
return nil if data.empty?
|
|
29
|
+
return data if png?(data)
|
|
30
|
+
return nil unless available?
|
|
31
|
+
|
|
32
|
+
stdout, _stderr, status = Open3.capture3(*@command, stdin_data: data)
|
|
33
|
+
return nil unless status.success?
|
|
34
|
+
|
|
35
|
+
out = String(stdout).dup
|
|
36
|
+
out.force_encoding(Encoding::BINARY)
|
|
37
|
+
return nil unless png?(out)
|
|
38
|
+
|
|
39
|
+
out
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def png?(bytes)
|
|
47
|
+
bytes.to_s.b.start_with?(PNG_HEADER)
|
|
48
|
+
rescue StandardError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def default_command
|
|
53
|
+
return ['magick', '-', 'png:-'] if executable_in_path?('magick')
|
|
54
|
+
return ['convert', '-', 'png:-'] if executable_in_path?('convert')
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def executable_in_path?(name)
|
|
60
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
61
|
+
next false if dir.to_s.empty?
|
|
62
|
+
|
|
63
|
+
path = File.join(dir, name)
|
|
64
|
+
File.file?(path) && File.executable?(path)
|
|
65
|
+
end
|
|
66
|
+
rescue StandardError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output::Kitty
|
|
7
|
+
# Minimal support for the Kitty graphics protocol.
|
|
8
|
+
#
|
|
9
|
+
# This module intentionally keeps all logic in-process (no external commands)
|
|
10
|
+
# and suppresses terminal responses (q=2) to avoid corrupting raw input reads.
|
|
11
|
+
module KittyGraphics
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
ESC = "\e"
|
|
15
|
+
APC_START = "#{ESC}_G".freeze
|
|
16
|
+
APC_END = "#{ESC}\\".freeze
|
|
17
|
+
MAX_CHUNK_BYTES = 4096
|
|
18
|
+
|
|
19
|
+
def supported?
|
|
20
|
+
return true if env_present?('KITTY_WINDOW_ID')
|
|
21
|
+
return true if ENV.fetch('TERM', '').include?('kitty')
|
|
22
|
+
return true if ENV.fetch('TERM_PROGRAM', '') == 'kitty'
|
|
23
|
+
|
|
24
|
+
false
|
|
25
|
+
rescue StandardError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enabled_for?(config_store)
|
|
30
|
+
return false unless supported?
|
|
31
|
+
return false unless config_store.respond_to?(:get)
|
|
32
|
+
|
|
33
|
+
!!config_store.get(%i[config kitty_images])
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def transmit_png(image_id, png_bytes, quiet: true)
|
|
39
|
+
bytes = String(png_bytes).dup
|
|
40
|
+
bytes.force_encoding(Encoding::BINARY)
|
|
41
|
+
payload = Base64.strict_encode64(bytes)
|
|
42
|
+
chunks = chunk_payload(payload)
|
|
43
|
+
chunks.map.with_index do |chunk, index|
|
|
44
|
+
more = index < chunks.length - 1 ? 1 : 0
|
|
45
|
+
keys = if index.zero?
|
|
46
|
+
{ a: 't', f: 100, t: 'd', i: image_id.to_i, m: more }
|
|
47
|
+
else
|
|
48
|
+
{ m: more }
|
|
49
|
+
end
|
|
50
|
+
keys[:q] = 2 if quiet
|
|
51
|
+
control = serialize_keys(keys)
|
|
52
|
+
"#{APC_START}#{control};#{chunk}#{APC_END}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def place(image_id, placement_id:, cols:, rows:, quiet: true, **options)
|
|
57
|
+
z = options.fetch(:z, nil)
|
|
58
|
+
keys = {
|
|
59
|
+
a: 'p',
|
|
60
|
+
i: image_id.to_i,
|
|
61
|
+
p: placement_id.to_i,
|
|
62
|
+
c: cols.to_i,
|
|
63
|
+
r: rows.to_i,
|
|
64
|
+
C: 1,
|
|
65
|
+
}
|
|
66
|
+
keys[:q] = 2 if quiet
|
|
67
|
+
keys[:z] = z.to_i if z
|
|
68
|
+
"#{APC_START}#{serialize_keys(keys)}#{APC_END}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def virtual_place(image_id, cols:, rows:, placement_id: nil, quiet: true, **options)
|
|
72
|
+
z = options.fetch(:z, nil)
|
|
73
|
+
keys = {
|
|
74
|
+
a: 'p',
|
|
75
|
+
U: 1,
|
|
76
|
+
i: image_id.to_i,
|
|
77
|
+
p: placement_id.to_i,
|
|
78
|
+
c: cols.to_i,
|
|
79
|
+
r: rows.to_i,
|
|
80
|
+
C: 1,
|
|
81
|
+
}
|
|
82
|
+
keys.delete(:p) if placement_id.to_i <= 0
|
|
83
|
+
keys[:q] = 2 if quiet
|
|
84
|
+
keys[:z] = z.to_i if z
|
|
85
|
+
"#{APC_START}#{serialize_keys(keys)}#{APC_END}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_visible(quiet: true)
|
|
89
|
+
keys = { a: 'd' }
|
|
90
|
+
keys[:q] = 2 if quiet
|
|
91
|
+
"#{APC_START}#{serialize_keys(keys)}#{APC_END}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def chunk_payload(payload)
|
|
95
|
+
return [] if payload.nil? || payload.empty?
|
|
96
|
+
|
|
97
|
+
max = MAX_CHUNK_BYTES
|
|
98
|
+
payload.scan(/.{1,#{max}}/m)
|
|
99
|
+
end
|
|
100
|
+
private_class_method :chunk_payload
|
|
101
|
+
|
|
102
|
+
def serialize_keys(hash)
|
|
103
|
+
hash.map { |k, v| "#{k}=#{v}" }.join(',')
|
|
104
|
+
end
|
|
105
|
+
private_class_method :serialize_keys
|
|
106
|
+
|
|
107
|
+
def env_present?(key)
|
|
108
|
+
value = ENV[key].to_s
|
|
109
|
+
!value.empty?
|
|
110
|
+
end
|
|
111
|
+
private_class_method :env_present?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha1'
|
|
4
|
+
|
|
5
|
+
require_relative '../../book_sources/epub/epub_resource_loader.rb'
|
|
6
|
+
require_relative 'image_transcoder'
|
|
7
|
+
require_relative 'kitty_graphics'
|
|
8
|
+
|
|
9
|
+
module Shoko
|
|
10
|
+
module Adapters::Output::Kitty
|
|
11
|
+
# Stateful renderer that transmits images once per session and then places
|
|
12
|
+
# them on screen using the Kitty graphics protocol.
|
|
13
|
+
class KittyImageRenderer
|
|
14
|
+
MAX_ID = 4_294_967_295
|
|
15
|
+
PNG_SIGNATURE = "\x89PNG\r\n\x1a\n".b
|
|
16
|
+
DEFAULT_CELL_ASPECT = 0.5 # width/height ratio for typical terminal cells
|
|
17
|
+
|
|
18
|
+
def initialize(resource_loader: EpubResourceLoader.new, transcoder: ImageTranscoder.new)
|
|
19
|
+
@resource_loader = resource_loader
|
|
20
|
+
@transcoder = transcoder
|
|
21
|
+
@transmitted = {}
|
|
22
|
+
@dimensions = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enabled?(config_store)
|
|
26
|
+
KittyGraphics.enabled_for?(config_store)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(output:, book_sha:, epub_path:, chapter_entry_path:, src:, row:, col:, cols:, rows:,
|
|
32
|
+
placement_id:, **options)
|
|
33
|
+
z = options.fetch(:z, nil)
|
|
34
|
+
return false unless output
|
|
35
|
+
return false unless epub_path && File.file?(epub_path)
|
|
36
|
+
|
|
37
|
+
entry_path = EpubResourceLoader.resolve_chapter_relative(chapter_entry_path, src)
|
|
38
|
+
return false unless entry_path
|
|
39
|
+
|
|
40
|
+
image_id = image_id_for(book_sha, epub_path, entry_path)
|
|
41
|
+
return false unless ensure_transmitted(output, image_id, book_sha, epub_path, entry_path)
|
|
42
|
+
|
|
43
|
+
fit = fit_geometry(image_id, cols.to_i, rows.to_i)
|
|
44
|
+
|
|
45
|
+
place_seq = KittyGraphics.place(image_id,
|
|
46
|
+
placement_id: clamp_id(placement_id),
|
|
47
|
+
cols: fit[:cols],
|
|
48
|
+
rows: fit[:rows],
|
|
49
|
+
quiet: true,
|
|
50
|
+
z: z)
|
|
51
|
+
abs_row = row.to_i + fit[:row_offset]
|
|
52
|
+
abs_col = col.to_i + fit[:col_offset]
|
|
53
|
+
if output.respond_to?(:raw)
|
|
54
|
+
output.raw(TerminalOutput::ANSI.move(abs_row, abs_col) + place_seq)
|
|
55
|
+
elsif output.respond_to?(:print)
|
|
56
|
+
output.print(TerminalOutput::ANSI.move(abs_row, abs_col) + place_seq)
|
|
57
|
+
elsif output.respond_to?(:write)
|
|
58
|
+
output.write(abs_row, abs_col, place_seq)
|
|
59
|
+
else
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
true
|
|
63
|
+
rescue StandardError
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Ensure the image is transmitted and has a virtual placement for Unicode placeholders.
|
|
68
|
+
# Returns the image_id on success, otherwise nil.
|
|
69
|
+
def prepare_virtual(output:, book_sha:, epub_path:, chapter_entry_path:, src:, cols:, rows:, placement_id: nil,
|
|
70
|
+
**options)
|
|
71
|
+
z = options.fetch(:z, nil)
|
|
72
|
+
return nil unless output
|
|
73
|
+
return nil unless epub_path && File.file?(epub_path)
|
|
74
|
+
|
|
75
|
+
entry_path = EpubResourceLoader.resolve_chapter_relative(chapter_entry_path, src)
|
|
76
|
+
return nil unless entry_path
|
|
77
|
+
|
|
78
|
+
image_id = image_id_for(book_sha, epub_path, entry_path)
|
|
79
|
+
return nil unless ensure_transmitted(output, image_id, book_sha, epub_path, entry_path)
|
|
80
|
+
return nil unless ensure_virtual_placement(output,
|
|
81
|
+
image_id,
|
|
82
|
+
cols.to_i,
|
|
83
|
+
rows.to_i,
|
|
84
|
+
placement_id: placement_id,
|
|
85
|
+
z: z)
|
|
86
|
+
|
|
87
|
+
image_id
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def ensure_transmitted(output, image_id, book_sha, epub_path, entry_path)
|
|
95
|
+
return :cached if @transmitted[image_id]
|
|
96
|
+
|
|
97
|
+
cache_key = png_cache_key(entry_path)
|
|
98
|
+
bytes = @resource_loader.fetch(book_sha: book_sha,
|
|
99
|
+
epub_path: epub_path,
|
|
100
|
+
entry_path: entry_path,
|
|
101
|
+
cache_key: cache_key,
|
|
102
|
+
persist: false)
|
|
103
|
+
png_bytes = @transcoder.to_png(bytes)
|
|
104
|
+
return nil unless png_bytes
|
|
105
|
+
|
|
106
|
+
dims = png_dimensions(png_bytes)
|
|
107
|
+
@dimensions[image_id] = dims if dims
|
|
108
|
+
|
|
109
|
+
@resource_loader.store(book_sha: book_sha, entry_path: cache_key, bytes: png_bytes)
|
|
110
|
+
|
|
111
|
+
KittyGraphics.transmit_png(image_id, png_bytes, quiet: true).each do |seq|
|
|
112
|
+
emit_raw(output, seq)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@transmitted[image_id] = true
|
|
116
|
+
:transmitted
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ensure_virtual_placement(output, image_id, cols, rows, placement_id: nil, **options)
|
|
120
|
+
z = options.fetch(:z, nil)
|
|
121
|
+
cols_i = cols.to_i
|
|
122
|
+
rows_i = rows.to_i
|
|
123
|
+
return false if cols_i <= 0 || rows_i <= 0
|
|
124
|
+
|
|
125
|
+
place_id = placement_id.to_i
|
|
126
|
+
place_id = clamp_id(place_id) if place_id.positive?
|
|
127
|
+
seq = KittyGraphics.virtual_place(image_id,
|
|
128
|
+
cols: cols_i,
|
|
129
|
+
rows: rows_i,
|
|
130
|
+
placement_id: place_id,
|
|
131
|
+
quiet: true,
|
|
132
|
+
z: z)
|
|
133
|
+
emit_raw(output, seq)
|
|
134
|
+
true
|
|
135
|
+
rescue StandardError
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def emit_raw(output, seq)
|
|
140
|
+
if output.respond_to?(:raw)
|
|
141
|
+
output.raw(seq)
|
|
142
|
+
elsif output.respond_to?(:print)
|
|
143
|
+
output.print(seq)
|
|
144
|
+
elsif output.respond_to?(:write)
|
|
145
|
+
output.write(1, 1, seq)
|
|
146
|
+
end
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def fit_geometry(image_id, max_cols, max_rows)
|
|
152
|
+
cols_i = max_cols.to_i
|
|
153
|
+
rows_i = max_rows.to_i
|
|
154
|
+
cols_i = 1 if cols_i <= 0
|
|
155
|
+
rows_i = 1 if rows_i <= 0
|
|
156
|
+
|
|
157
|
+
dims = @dimensions[image_id]
|
|
158
|
+
return { cols: cols_i, rows: rows_i, col_offset: 0, row_offset: 0 } unless dims
|
|
159
|
+
|
|
160
|
+
img_w = dims[:width].to_i
|
|
161
|
+
img_h = dims[:height].to_i
|
|
162
|
+
return { cols: cols_i, rows: rows_i, col_offset: 0, row_offset: 0 } if img_w <= 0 || img_h <= 0
|
|
163
|
+
|
|
164
|
+
aspect = img_w.to_f / img_h
|
|
165
|
+
cell_aspect = DEFAULT_CELL_ASPECT
|
|
166
|
+
|
|
167
|
+
cols_for_rows = (rows_i.to_f * aspect / cell_aspect)
|
|
168
|
+
target_cols = cols_for_rows.floor
|
|
169
|
+
target_cols = 1 if target_cols <= 0
|
|
170
|
+
|
|
171
|
+
if target_cols <= cols_i
|
|
172
|
+
fit_cols = target_cols
|
|
173
|
+
fit_rows = rows_i
|
|
174
|
+
else
|
|
175
|
+
fit_cols = cols_i
|
|
176
|
+
rows_for_cols = (cols_i.to_f * cell_aspect / aspect)
|
|
177
|
+
fit_rows = rows_for_cols.floor
|
|
178
|
+
fit_rows = 1 if fit_rows <= 0
|
|
179
|
+
fit_rows = rows_i if fit_rows > rows_i
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
col_offset = ((cols_i - fit_cols) / 2.0).floor
|
|
183
|
+
col_offset = 0 if col_offset.negative?
|
|
184
|
+
|
|
185
|
+
row_offset = 0
|
|
186
|
+
|
|
187
|
+
{ cols: fit_cols, rows: fit_rows, col_offset: col_offset, row_offset: row_offset }
|
|
188
|
+
rescue StandardError
|
|
189
|
+
{ cols: cols_i, rows: rows_i, col_offset: 0, row_offset: 0 }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def png_dimensions(bytes)
|
|
193
|
+
data = bytes.to_s.b
|
|
194
|
+
return nil unless data.start_with?(PNG_SIGNATURE)
|
|
195
|
+
|
|
196
|
+
# PNG signature (8) + length (4) + type (4) + width (4) + height (4)
|
|
197
|
+
return nil unless data.bytesize >= 24
|
|
198
|
+
return nil unless data.byteslice(12, 4) == 'IHDR'
|
|
199
|
+
|
|
200
|
+
width = data.byteslice(16, 4).unpack1('N')
|
|
201
|
+
height = data.byteslice(20, 4).unpack1('N')
|
|
202
|
+
return nil if width.to_i <= 0 || height.to_i <= 0
|
|
203
|
+
|
|
204
|
+
{ width: width.to_i, height: height.to_i }
|
|
205
|
+
rescue StandardError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def png_cache_key(entry_path)
|
|
210
|
+
"#{entry_path}|kitty_png_v1"
|
|
211
|
+
rescue StandardError
|
|
212
|
+
"#{entry_path}|kitty_png_v1"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def image_id_for(book_sha, epub_path, entry_path)
|
|
216
|
+
seed = "#{book_sha}|#{epub_path}|#{entry_path}"
|
|
217
|
+
hashed_id(seed)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def hashed_id(seed)
|
|
221
|
+
raw = Digest::SHA1.digest(seed.to_s)
|
|
222
|
+
int = raw.unpack1('N')
|
|
223
|
+
int.zero? ? 1 : int
|
|
224
|
+
rescue StandardError
|
|
225
|
+
1
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def clamp_id(value)
|
|
229
|
+
int = value.to_i
|
|
230
|
+
int = 1 if int <= 0
|
|
231
|
+
if int > MAX_ID
|
|
232
|
+
int %= MAX_ID
|
|
233
|
+
int = 1 if int.zero?
|
|
234
|
+
end
|
|
235
|
+
int
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|