From 57ef8fa3f8279c3a82bc3f5d9895a3d86333d255 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:35:38 +0100 Subject: [PATCH 01/17] Add files via upload --- crosspoint_reader-v0_2_0.zip | Bin 0 -> 17946 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 crosspoint_reader-v0_2_0.zip diff --git a/crosspoint_reader-v0_2_0.zip b/crosspoint_reader-v0_2_0.zip new file mode 100644 index 0000000000000000000000000000000000000000..7f2fef281c57f0ea1dbce30129afa22767e55562 GIT binary patch literal 17946 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 XiU4m`U>gLuH3JCsfG59Pa|ZDMcm>kZ literal 0 HcmV?d00001 From fcb94b4de22a4ab1afbf2fbeb6b9e1e0262d9903 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:38:44 +0100 Subject: [PATCH 02/17] Add files via upload --- crosspoint_reader/README.md | 130 +++++++-- crosspoint_reader/__init__.py | 17 ++ crosspoint_reader/config.py | 174 ++++++++++- crosspoint_reader/converter.py | 509 +++++++++++++++++++++++++++++++++ crosspoint_reader/driver.py | 142 ++++++--- 5 files changed, 891 insertions(+), 81 deletions(-) create mode 100644 crosspoint_reader/converter.py diff --git a/crosspoint_reader/README.md b/crosspoint_reader/README.md index fa0b7dd..c00bbc6 100644 --- a/crosspoint_reader/README.md +++ b/crosspoint_reader/README.md @@ -1,25 +1,105 @@ -# CrossPoint Reader Calibre Plugin - -This plugin adds CrossPoint Reader as a wireless device in Calibre. It uploads -EPUB files over WebSocket to the CrossPoint web server. - -Protocol: -- Connect to ws://:/ -- Send: START::: -- Wait for READY -- Send binary frames with file content -- Wait for DONE (or ERROR:) - -Default settings: -- Auto-discover device via UDP -- Host fallback: 192.168.4.1 -- Port: 81 -- Upload path: / - -Install: -1. Download the latest release from the [releases page](https://github.com/crosspoint-reader/calibre-plugins/releases) (or zip the contents of this directory). -2. In Calibre: Preferences > Plugins > Load plugin from file. -3. The device should appear in Calibre once it is discoverable on the network. - -No configuration needed. The plugin auto-discovers the device via UDP and -falls back to 192.168.4.1:81. +# CrossPoint Reader - Calibre Plugin + +A Calibre device driver plugin for CrossPoint e-readers with built-in EPUB image conversion for optimal e-reader compatibility. + +## Version 0.2.0 + +## Features + +### Wireless Book Transfer +- Automatic device discovery via UDP broadcast +- WebSocket-based file transfer +- Support for nested folder structures +- Configurable upload paths + +### EPUB Image Conversion +Automatically converts EPUB images before uploading for maximum e-reader compatibility: + +- **Image Format Conversion**: Converts PNG, GIF, WebP, and BMP to baseline JPEG +- **SVG Cover Fix**: Converts SVG-based covers to standard HTML img tags +- **Image Scaling**: Scales oversized images to fit your e-reader screen +- **Light Novel Mode**: Rotates horizontal images 90° and splits them into multiple pages for manga/comics reading on vertical e-reader screens + +### Configuration Options + +#### Connection Settings +- **Host**: Device IP address (default: 192.168.4.1) +- **Port**: WebSocket port (default: 81) +- **Upload Path**: Default upload directory (default: /) +- **Chunk Size**: Transfer chunk size in bytes (default: 2048) +- **Debug Logging**: Enable detailed logging +- **Fetch Metadata**: Read metadata from device (slower) + +#### Image Conversion Settings +- **Enable Conversion**: Turn EPUB image conversion on/off +- **JPEG Quality**: 1-95% (default: 85%) + - Presets: Low (60%), Medium (75%), High (85%), Max (95%) +- **Light Novel Mode**: Rotate and split wide images +- **Screen Size**: Target screen dimensions (default: 480×800 px) +- **Split Overlap**: Overlap percentage for split pages (default: 15%) + +## Installation + +1. Download the plugin ZIP file +2. In Calibre, go to Preferences → Plugins → Load plugin from file +3. Select the downloaded ZIP file +4. Restart Calibre + +## Usage + +1. Connect your CrossPoint Reader to the same WiFi network as your computer +2. The device should appear automatically in Calibre's device list +3. Configure settings via Preferences → Plugins → CrossPoint Reader → Customize plugin +4. Send books to device as usual - images will be automatically converted + +## What the Converter Does + +✓ Converts PNG/GIF/WebP/BMP to baseline JPEG +✓ Fixes SVG covers for e-reader compatibility +✓ Scales large images to fit your screen dimensions +✓ Light Novel Mode: rotates & splits wide images for manga/comics +✓ Maintains EPUB structure and metadata +✓ Preserves original file if conversion fails + +## Requirements + +- Calibre 5.0 or later +- CrossPoint Reader device with WebSocket server enabled +- Same WiFi network for device discovery + +## Troubleshooting + +### Device not detected +1. Ensure device and computer are on the same network +2. Check the Host setting in plugin configuration +3. Enable debug logging to see discovery attempts +4. Try manually entering the device IP address + +### Images not converting +1. Verify "Enable EPUB image conversion" is checked +2. Check the debug log for conversion errors +3. Ensure sufficient disk space for temporary files + +### Poor image quality +- Increase JPEG Quality setting (try 85% or 95%) + +### Split images not aligned +- Adjust Split Overlap percentage (try 15-20%) + +## License + +This plugin is provided as-is for use with CrossPoint Reader devices. + +## Changelog + +### v0.2.0 +- Added EPUB image conversion +- Added Light Novel Mode (rotate & split) +- Added configurable JPEG quality +- Added screen size settings +- Improved configuration UI with grouped settings + +### v0.1.1 +- Initial release +- Wireless book transfer +- Device auto-discovery diff --git a/crosspoint_reader/__init__.py b/crosspoint_reader/__init__.py index 9aaedbd..519bee0 100644 --- a/crosspoint_reader/__init__.py +++ b/crosspoint_reader/__init__.py @@ -1,5 +1,22 @@ +""" +CrossPoint Reader - Calibre Device Driver Plugin + +A wireless device driver for CrossPoint e-readers with built-in +EPUB image conversion for optimal e-reader compatibility. + +Features: +- Wireless book transfer via WebSocket +- Automatic EPUB image conversion to baseline JPEG +- PNG/GIF/WebP/BMP to JPEG conversion +- SVG cover fixing +- Image scaling to fit e-reader screen +- Light Novel Mode: rotate and split wide images for manga/comics +- Configurable JPEG quality and screen dimensions +""" + from .driver import CrossPointDevice class CrossPointReaderDevice(CrossPointDevice): + """CrossPoint Reader device driver for Calibre.""" pass diff --git a/crosspoint_reader/config.py b/crosspoint_reader/config.py index 6e3e241..403e296 100644 --- a/crosspoint_reader/config.py +++ b/crosspoint_reader/config.py @@ -4,19 +4,25 @@ QDialog, QDialogButtonBox, QFormLayout, + QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QPlainTextEdit, QPushButton, + QSlider, QSpinBox, QVBoxLayout, QWidget, + Qt, ) from .log import get_log_text PREFS = JSONConfig('plugins/crosspoint_reader') + +# Connection settings PREFS.defaults['host'] = '192.168.4.1' PREFS.defaults['port'] = 81 PREFS.defaults['path'] = '/' @@ -24,11 +30,24 @@ PREFS.defaults['debug'] = False PREFS.defaults['fetch_metadata'] = False +# Conversion settings +PREFS.defaults['enable_conversion'] = True +PREFS.defaults['jpeg_quality'] = 85 +PREFS.defaults['light_novel_mode'] = False +PREFS.defaults['screen_width'] = 480 +PREFS.defaults['screen_height'] = 800 +PREFS.defaults['split_overlap'] = 15 # percentage + class CrossPointConfigWidget(QWidget): def __init__(self): super().__init__() - layout = QFormLayout(self) + layout = QVBoxLayout(self) + + # Connection Settings Group + conn_group = QGroupBox("Connection Settings") + conn_layout = QFormLayout() + self.host = QLineEdit(self) self.port = QSpinBox(self) self.port.setRange(1, 65535) @@ -45,33 +64,161 @@ def __init__(self): self.debug.setChecked(PREFS['debug']) self.fetch_metadata.setChecked(PREFS['fetch_metadata']) - layout.addRow('Host', self.host) - layout.addRow('Port', self.port) - layout.addRow('Upload path', self.path) - layout.addRow('Chunk size', self.chunk_size) - layout.addRow('', self.debug) - layout.addRow('', self.fetch_metadata) - + conn_layout.addRow('Host', self.host) + conn_layout.addRow('Port', self.port) + conn_layout.addRow('Upload path', self.path) + conn_layout.addRow('Chunk size', self.chunk_size) + conn_layout.addRow('', self.debug) + conn_layout.addRow('', self.fetch_metadata) + conn_group.setLayout(conn_layout) + layout.addWidget(conn_group) + + # Conversion Settings Group + conv_group = QGroupBox("Image Conversion Settings") + conv_layout = QFormLayout() + + self.enable_conversion = QCheckBox('Enable EPUB image conversion', self) + self.enable_conversion.setChecked(PREFS['enable_conversion']) + self.enable_conversion.setToolTip( + "Convert images to baseline JPEG format for e-reader compatibility.\n" + "Converts PNG/GIF/WebP/BMP to JPEG, fixes SVG covers, and scales images." + ) + conv_layout.addRow('', self.enable_conversion) + + # JPEG Quality slider with value display + quality_widget = QWidget() + quality_layout = QHBoxLayout(quality_widget) + quality_layout.setContentsMargins(0, 0, 0, 0) + + self.jpeg_quality = QSlider(Qt.Orientation.Horizontal) + self.jpeg_quality.setRange(1, 95) + self.jpeg_quality.setValue(PREFS['jpeg_quality']) + self.jpeg_quality.setTickPosition(QSlider.TickPosition.TicksBelow) + self.jpeg_quality.setTickInterval(10) + + self.quality_label = QLabel(f"{PREFS['jpeg_quality']}%") + self.quality_label.setMinimumWidth(40) + self.jpeg_quality.valueChanged.connect( + lambda v: self.quality_label.setText(f"{v}%") + ) + + quality_layout.addWidget(self.jpeg_quality) + quality_layout.addWidget(self.quality_label) + conv_layout.addRow('JPEG Quality', quality_widget) + + # Quality presets + presets_widget = QWidget() + presets_layout = QHBoxLayout(presets_widget) + presets_layout.setContentsMargins(0, 0, 0, 0) + + for name, value in [('Low (60%)', 60), ('Medium (75%)', 75), + ('High (85%)', 85), ('Max (95%)', 95)]: + btn = QPushButton(name) + btn.clicked.connect(lambda checked, v=value: self._set_quality(v)) + presets_layout.addWidget(btn) + + conv_layout.addRow('Presets', presets_widget) + + # Light Novel Mode + self.light_novel_mode = QCheckBox('Light Novel Mode (rotate & split wide images)', self) + self.light_novel_mode.setChecked(PREFS['light_novel_mode']) + self.light_novel_mode.setToolTip( + "Rotate horizontal images 90° and split into multiple pages\n" + "for vertical reading on e-readers. Best for manga/comics." + ) + conv_layout.addRow('', self.light_novel_mode) + + # Screen dimensions + screen_widget = QWidget() + screen_layout = QHBoxLayout(screen_widget) + screen_layout.setContentsMargins(0, 0, 0, 0) + + self.screen_width = QSpinBox() + self.screen_width.setRange(100, 2000) + self.screen_width.setValue(PREFS['screen_width']) + self.screen_width.setSuffix(' px') + + screen_layout.addWidget(self.screen_width) + screen_layout.addWidget(QLabel('×')) + + self.screen_height = QSpinBox() + self.screen_height.setRange(100, 2000) + self.screen_height.setValue(PREFS['screen_height']) + self.screen_height.setSuffix(' px') + + screen_layout.addWidget(self.screen_height) + screen_layout.addStretch() + conv_layout.addRow('Screen Size', screen_widget) + + # Split overlap + overlap_widget = QWidget() + overlap_layout = QHBoxLayout(overlap_widget) + overlap_layout.setContentsMargins(0, 0, 0, 0) + + self.split_overlap = QSpinBox() + self.split_overlap.setRange(0, 50) + self.split_overlap.setValue(PREFS['split_overlap']) + self.split_overlap.setSuffix('%') + self.split_overlap.setToolTip("Overlap between split pages (for Light Novel Mode)") + + overlap_layout.addWidget(self.split_overlap) + overlap_layout.addStretch() + conv_layout.addRow('Split Overlap', overlap_widget) + + conv_group.setLayout(conv_layout) + layout.addWidget(conv_group) + + # Enable/disable conversion options based on checkbox + self.enable_conversion.toggled.connect(self._update_conversion_enabled) + self._update_conversion_enabled(self.enable_conversion.isChecked()) + + # Log section + log_group = QGroupBox("Debug Log") + log_layout = QVBoxLayout() + self.log_view = QPlainTextEdit(self) self.log_view.setReadOnly(True) - self.log_view.setPlaceholderText('Discovery log will appear here when debug is enabled.') + self.log_view.setMaximumHeight(150) + self.log_view.setPlaceholderText('Discovery and conversion log will appear here.') self._refresh_logs() refresh_btn = QPushButton('Refresh Log', self) refresh_btn.clicked.connect(self._refresh_logs) - log_layout = QHBoxLayout() + + log_layout.addWidget(self.log_view) log_layout.addWidget(refresh_btn) - - layout.addRow('Log', self.log_view) - layout.addRow('', log_layout) + log_group.setLayout(log_layout) + layout.addWidget(log_group) + + def _set_quality(self, value): + """Set JPEG quality from preset button.""" + self.jpeg_quality.setValue(value) + + def _update_conversion_enabled(self, enabled): + """Enable/disable conversion options based on master checkbox.""" + self.jpeg_quality.setEnabled(enabled) + self.quality_label.setEnabled(enabled) + self.light_novel_mode.setEnabled(enabled) + self.screen_width.setEnabled(enabled) + self.screen_height.setEnabled(enabled) + self.split_overlap.setEnabled(enabled) def save(self): + # Connection settings PREFS['host'] = self.host.text().strip() or PREFS.defaults['host'] PREFS['port'] = int(self.port.value()) PREFS['path'] = self.path.text().strip() or PREFS.defaults['path'] PREFS['chunk_size'] = int(self.chunk_size.value()) PREFS['debug'] = bool(self.debug.isChecked()) PREFS['fetch_metadata'] = bool(self.fetch_metadata.isChecked()) + + # Conversion settings + PREFS['enable_conversion'] = bool(self.enable_conversion.isChecked()) + PREFS['jpeg_quality'] = int(self.jpeg_quality.value()) + PREFS['light_novel_mode'] = bool(self.light_novel_mode.isChecked()) + PREFS['screen_width'] = int(self.screen_width.value()) + PREFS['screen_height'] = int(self.screen_height.value()) + PREFS['split_overlap'] = int(self.split_overlap.value()) def _refresh_logs(self): self.log_view.setPlainText(get_log_text()) @@ -84,6 +231,7 @@ class CrossPointConfigDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('CrossPoint Reader') + self.setMinimumWidth(500) self.widget = CrossPointConfigWidget() layout = QVBoxLayout(self) layout.addWidget(self.widget) diff --git a/crosspoint_reader/converter.py b/crosspoint_reader/converter.py new file mode 100644 index 0000000..280a9fb --- /dev/null +++ b/crosspoint_reader/converter.py @@ -0,0 +1,509 @@ +""" +EPUB Image Converter for CrossPoint Reader + +Converts EPUB images to baseline JPEG format with various optimizations +for e-reader compatibility. + +Features: +- Convert PNG/GIF/WebP/BMP to baseline JPEG +- Fix SVG covers for e-readers +- Scale large images to fit screen +- Light Novel Mode: rotate wide images and split into pages +- Configurable JPEG quality +""" + +import io +import os +import re +import zipfile +from contextlib import contextmanager + +# Pillow is bundled with Calibre +from PIL import Image + + +class EpubConverter: + """Convert EPUB images to baseline JPEG format.""" + + def __init__(self, + jpeg_quality=85, + max_width=480, + max_height=800, + enable_split_rotate=False, + overlap=0.15, + logger=None): + """ + Initialize converter. + + Args: + jpeg_quality: JPEG quality 1-95 (default 85) + max_width: Maximum image width in pixels (default 480) + max_height: Maximum image height in pixels (default 800) + enable_split_rotate: Enable Light Novel Mode (default False) + overlap: Overlap percentage for split images (default 0.15) + logger: Optional logging function + """ + self.jpeg_quality = max(1, min(95, jpeg_quality)) + self.max_width = max_width + self.max_height = max_height + self.enable_split_rotate = enable_split_rotate + self.overlap = overlap + self._log = logger or (lambda x: None) + + # Statistics + self.stats = { + 'images_converted': 0, + 'svg_covers_fixed': 0, + 'images_split': 0, + 'original_size': 0, + 'new_size': 0, + } + + def convert_epub(self, input_path, output_path=None): + """ + Convert an EPUB file. + + Args: + input_path: Path to input EPUB file + output_path: Path to output EPUB file (default: input_baseline.epub) + + Returns: + Path to converted EPUB file + """ + if output_path is None: + base, ext = os.path.splitext(input_path) + output_path = f"{base}_baseline{ext}" + + # Reset stats + self.stats = { + 'images_converted': 0, + 'svg_covers_fixed': 0, + 'images_split': 0, + 'original_size': os.path.getsize(input_path), + 'new_size': 0, + } + + # Track renamed files and split images + renamed = {} # old_path -> new_path + split_images = {} # orig_name -> [{'path', 'imgName', 'id'}, ...] + xhtml_files = {} # path -> content + opf_path = None + opf_content = None + + self._log(f"Converting: {os.path.basename(input_path)}") + self._log(f"Quality: {self.jpeg_quality}%") + self._log(f"Light Novel Mode: {'ON' if self.enable_split_rotate else 'OFF'}") + + with zipfile.ZipFile(input_path, 'r') as zin: + # Build rename map for non-JPEG images + for name in zin.namelist(): + low = name.lower() + if re.match(r'.*\.(png|gif|webp|bmp|jpeg)$', low): + new_name = re.sub(r'\.(png|gif|webp|bmp|jpeg)$', '.jpg', name, flags=re.IGNORECASE) + renamed[name] = new_name + + with zipfile.ZipFile(output_path, 'w') as zout: + # First pass: process images + for name in zin.namelist(): + low = name.lower() + + if re.match(r'.*\.(png|gif|webp|bmp|jpg|jpeg)$', low): + data = zin.read(name) + parts = self._process_image(data, name) + + base_name = re.sub(r'\.[^.]+$', '', name) + + if len(parts) == 1 and parts[0]['suffix'] == '': + # Single image, no split + new_path = renamed.get(name, re.sub(r'\.[^.]+$', '.jpg', name)) + zout.writestr(new_path, parts[0]['data'], + compress_type=zipfile.ZIP_DEFLATED) + self.stats['images_converted'] += 1 + else: + # Split image + orig_name = os.path.basename(name) + split_images[orig_name] = [] + + for part in parts: + part_name = os.path.basename(base_name) + part['suffix'] + '.jpg' + if '/' in name: + part_path = name[:name.rfind('/') + 1] + part_name + else: + part_path = part_name + + zout.writestr(part_path, part['data'], + compress_type=zipfile.ZIP_DEFLATED) + split_images[orig_name].append({ + 'path': part_path, + 'imgName': part_name, + 'id': os.path.basename(base_name) + part['suffix'] + }) + self.stats['images_converted'] += 1 + + self.stats['images_split'] += len(parts) - 1 + + elif re.match(r'.*\.(xhtml|html|htm)$', low): + xhtml_files[name] = zin.read(name).decode('utf-8', errors='ignore') + + elif low.endswith('.opf'): + opf_path = name + opf_content = zin.read(name).decode('utf-8', errors='ignore') + + # Second pass: update XHTML files + for path, content in xhtml_files.items(): + t = content + + # Fix SVG covers + fixed_svg = self._fix_svg_cover(t) + if fixed_svg['fixed']: + t = fixed_svg['content'] + self.stats['svg_covers_fixed'] += 1 + + # Update image references + for old, new in renamed.items(): + old_name = os.path.basename(old) + new_name = os.path.basename(new) + t = t.replace(old_name, new_name) + + # Update split image references + for orig_name, parts in split_images.items(): + new_name = re.sub(r'\.(png|gif|webp|bmp|jpeg)$', '.jpg', orig_name, flags=re.IGNORECASE) + + # Replace block patterns (p/div with span and img) + block_pattern = re.compile( + r'(<(?:p|div)[^>]*>\s*\s*]*src=["\'][^"\']*(?:' + + re.escape(orig_name) + '|' + re.escape(new_name) + + r')[^>]*/?>\s*\s*)', + re.IGNORECASE | re.DOTALL + ) + + def replace_block(match): + result = [] + for i, part in enumerate(parts): + if i > 0: + result.append('\n') + new_block = match.group(0).replace(orig_name, part['imgName']) + new_block = new_block.replace(new_name, part['imgName']) + result.append(new_block) + return ''.join(result) + + t = block_pattern.sub(replace_block, t) + + # Replace simple img patterns + simple_pattern = re.compile( + r'(]*src=["\'])([^"\']*(?:' + + re.escape(orig_name) + '|' + re.escape(new_name) + + r'))([^>]*/>)', + re.IGNORECASE + ) + + def replace_simple(match): + result = [] + for i, part in enumerate(parts): + if i > 0: + result.append('\n') + new_src = match.group(2).replace(orig_name, part['imgName']) + new_src = new_src.replace(new_name, part['imgName']) + result.append(match.group(1) + new_src + match.group(3)) + return ''.join(result) + + t = simple_pattern.sub(replace_simple, t) + + zout.writestr(path, t.encode('utf-8'), + compress_type=zipfile.ZIP_DEFLATED) + + # Third pass: update OPF + if opf_content: + t = opf_content + + # Update image references + for old, new in renamed.items(): + old_name = os.path.basename(old) + new_name = os.path.basename(new) + t = t.replace(old_name, new_name) + + # Fix media-types for converted images + t = re.sub( + r'href="([^"]+\.jpg)"([^>]*)media-type="image/(png|gif|webp|bmp)"', + r'href="\1"\2media-type="image/jpeg"', + t + ) + t = re.sub( + r'media-type="image/(png|gif|webp|bmp)"([^>]*)href="([^"]+\.jpg)"', + r'media-type="image/jpeg"\2href="\3"', + t + ) + + # Update split image references in OPF + for orig_name, parts in split_images.items(): + orig_base = re.sub(r'\.[^.]+$', '', orig_name) + + # Update original reference to part1 + pattern = re.compile( + r'(href=["\'][^"\']*/?)('+re.escape(orig_base)+r')\.(?:jpg|jpeg|png|gif|webp|bmp)(["\'])', + re.IGNORECASE + ) + t = pattern.sub(r'\g<1>' + orig_base + r'_part1.jpg\3', t) + + # Add manifest entries for additional parts + manifest_additions = '' + for j in range(1, len(parts)): + p = parts[j] + manifest_additions += f'\n' + + if manifest_additions: + t = t.replace('', manifest_additions + '') + + # Ensure cover meta + fixed_cover = self._ensure_cover_meta(t) + if fixed_cover['fixed']: + t = fixed_cover['content'] + self._log("Fixed cover meta") + + zout.writestr(opf_path, t.encode('utf-8'), + compress_type=zipfile.ZIP_DEFLATED) + + # Fourth pass: copy remaining files + for name in zin.namelist(): + low = name.lower() + + # Skip already processed files + if re.match(r'.*\.(png|gif|webp|bmp|jpg|jpeg)$', low): + continue + if re.match(r'.*\.(xhtml|html|htm)$', low): + continue + if low.endswith('.opf'): + continue + + data = zin.read(name) + + # Update CSS and NCX references + if low.endswith('.css') or low.endswith('.ncx'): + text = data.decode('utf-8', errors='ignore') + for old, new in renamed.items(): + old_name = os.path.basename(old) + new_name = os.path.basename(new) + text = text.replace(old_name, new_name) + data = text.encode('utf-8') + + # mimetype must be stored uncompressed + compress = zipfile.ZIP_STORED if name == 'mimetype' else zipfile.ZIP_DEFLATED + zout.writestr(name, data, compress_type=compress) + + self.stats['new_size'] = os.path.getsize(output_path) + + saved = self.stats['original_size'] - self.stats['new_size'] + if saved > 0: + pct = (saved / self.stats['original_size']) * 100 + self._log(f"Converted {self.stats['images_converted']} images") + self._log(f"Saved {self._format_bytes(saved)} ({pct:.1f}%)") + else: + self._log(f"Converted {self.stats['images_converted']} images") + self._log(f"Size increased by {self._format_bytes(-saved)}") + + if self.stats['images_split'] > 0: + self._log(f"Created {self.stats['images_split']} additional pages from splits") + if self.stats['svg_covers_fixed'] > 0: + self._log(f"Fixed {self.stats['svg_covers_fixed']} SVG cover(s)") + + return output_path + + def _process_image(self, data, name): + """ + Process a single image. + + Returns list of {'data': bytes, 'suffix': str} + """ + try: + img = Image.open(io.BytesIO(data)) + + # Convert to RGB if necessary + if img.mode in ('RGBA', 'LA', 'P'): + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'P': + img = img.convert('RGBA') + if img.mode in ('RGBA', 'LA'): + background.paste(img, mask=img.split()[-1]) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + orig_w, orig_h = img.size + + # Check if horizontal and exceeds screen + is_horizontal = orig_w > orig_h + exceeds_screen = orig_w > self.max_width or orig_h > self.max_height + needs_rotation = is_horizontal and exceeds_screen + + if needs_rotation and self.enable_split_rotate: + return self._process_split_rotate(img, orig_w, orig_h) + else: + return self._process_normal(img, orig_w, orig_h) + + except Exception as e: + self._log(f"Error processing {name}: {e}") + # Return original data as fallback + return [{'data': data, 'suffix': ''}] + + def _process_normal(self, img, orig_w, orig_h): + """Process image without rotation/split.""" + fits_in_screen = orig_w <= self.max_width and orig_h <= self.max_height + + if not fits_in_screen: + # Scale to fit + scale = min(self.max_width / orig_w, self.max_height / orig_h) + new_w = int(orig_w * scale) + new_h = int(orig_h * scale) + img = img.resize((new_w, new_h), Image.Resampling.LANCZOS) + + # Save as baseline JPEG + buf = io.BytesIO() + img.save(buf, 'JPEG', quality=self.jpeg_quality, progressive=False) + return [{'data': buf.getvalue(), 'suffix': ''}] + + def _process_split_rotate(self, img, orig_w, orig_h): + """Process horizontal image with rotation and optional split.""" + # Step 1: Scale width to max_height (800) + scale = self.max_height / orig_w + scaled_w = self.max_height + scaled_h = int(orig_h * scale) + + img = img.resize((scaled_w, scaled_h), Image.Resampling.LANCZOS) + + # Step 2: Rotate 90° clockwise + img = img.transpose(Image.Transpose.ROTATE_270) + rot_w, rot_h = img.size + + # Step 3: Split if needed + if rot_w <= self.max_width: + # No split needed + buf = io.BytesIO() + img.save(buf, 'JPEG', quality=self.jpeg_quality, progressive=False) + return [{'data': buf.getvalue(), 'suffix': ''}] + else: + # Split by width (vertical cuts), right to left for correct reading order + parts = [] + max_w = self.max_width + overlap_px = int(max_w * self.overlap) + step = max_w - overlap_px + num_parts = (rot_w - overlap_px + step - 1) // step # ceil division + + for i in range(num_parts): + # Start from right side and go left + x = rot_w - max_w - (i * step) + if i == num_parts - 1: + x = 0 # Last part starts at left edge + x = max(0, x) + + part_w = min(max_w, rot_w - x) + + part_img = img.crop((x, 0, x + part_w, rot_h)) + + buf = io.BytesIO() + part_img.save(buf, 'JPEG', quality=self.jpeg_quality, progressive=False) + parts.append({'data': buf.getvalue(), 'suffix': f'_part{i + 1}'}) + + return parts + + def _fix_svg_cover(self, content): + """Fix SVG cover to regular HTML img tag.""" + if 'Cover' not in content): + return {'content': content, 'fixed': False} + + match = re.search(r'xlink:href=["\']([^"\']+)["\']', content) + if not match: + return {'content': content, 'fixed': False} + + img_src = match.group(1) + new_content = f''' + + +Cover +
Cover
+''' + + return {'content': new_content, 'fixed': True} + + def _ensure_cover_meta(self, content): + """Ensure OPF has correct cover meta tag.""" + cover_id = None + + # Try to find cover image ID + patterns = [ + r']+id="([^"]+)"[^>]+properties="[^"]*cover-image[^"]*"', + r']+properties="[^"]*cover-image[^"]*"[^>]+id="([^"]+)"', + r']+id="([^"]+)"[^>]+href="[^"]*cover[^"]*"[^>]*media-type="image/', + r']+href="[^"]*cover[^"]*"[^>]+id="([^"]+)"[^>]*media-type="image/', + r']+id="([^"]*cover[^"]*)"[^>]+media-type="image/', + r']+media-type="image/[^"]*"[^>]+id="([^"]*cover[^"]*)"', + ] + + for pattern in patterns: + match = re.search(pattern, content, re.IGNORECASE) + if match: + cover_id = match.group(1) + break + + if not cover_id: + return {'content': content, 'fixed': False} + + # Check if cover meta exists + meta_match = (re.search(r'', + f'', + content + ) + content = re.sub( + r'', + f'', + content + ) + return {'content': content, 'fixed': True} + return {'content': content, 'fixed': False} + + # Add missing cover meta + content = content.replace( + '', + f' \n ' + ) + return {'content': content, 'fixed': True} + + @staticmethod + def _format_bytes(b): + """Format bytes as human-readable string.""" + if b < 1024: + return f"{b} B" + elif b < 1024 * 1024: + return f"{b / 1024:.1f} KB" + elif b < 1024 * 1024 * 1024: + return f"{b / (1024 * 1024):.1f} MB" + else: + return f"{b / (1024 * 1024 * 1024):.1f} GB" + + +def convert_epub_file(input_path, output_path=None, **kwargs): + """ + Convenience function to convert an EPUB file. + + Args: + input_path: Path to input EPUB + output_path: Path to output EPUB (optional) + **kwargs: Options passed to EpubConverter + + Returns: + Path to converted EPUB + """ + converter = EpubConverter(**kwargs) + return converter.convert_epub(input_path, output_path) diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index 053a391..c0024cc 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -9,19 +9,21 @@ from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.books import Book, BookList from calibre.ebooks.metadata.book.base import Metadata +from calibre.ptempfile import PersistentTemporaryFile, TemporaryDirectory from . import ws_client from .config import CrossPointConfigWidget, PREFS +from .converter import EpubConverter from .log import add_log class CrossPointDevice(DeviceConfig, DevicePlugin): name = 'CrossPoint Reader' gui_name = 'CrossPoint Reader' - description = 'CrossPoint Reader wireless device' + description = 'CrossPoint Reader wireless device with EPUB image conversion' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 1, 1) + version = (0, 2, 0) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] @@ -274,6 +276,41 @@ def _ensure_dir(self, parent_path, subdirs): return '/' + subdir_path return parent_path + '/' + subdir_path + def _convert_epub(self, input_path): + """Convert EPUB images to baseline JPEG format. + + Returns path to converted file (may be a temp file). + """ + if not PREFS['enable_conversion']: + return input_path + + try: + # Create converter with settings from preferences + converter = EpubConverter( + jpeg_quality=PREFS['jpeg_quality'], + max_width=PREFS['screen_width'], + max_height=PREFS['screen_height'], + enable_split_rotate=PREFS['light_novel_mode'], + overlap=PREFS['split_overlap'] / 100.0, + logger=self._log, + ) + + # Create temp file for converted EPUB + temp_file = PersistentTemporaryFile(suffix='_baseline.epub') + temp_path = temp_file.name + temp_file.close() + + # Convert + self._log(f'[CrossPoint] Converting: {os.path.basename(input_path)}') + converter.convert_epub(input_path, temp_path) + + return temp_path + + except Exception as exc: + self._log(f'[CrossPoint] Conversion failed: {exc}') + # Return original file if conversion fails + return input_path + def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): host = self.device_host or PREFS['host'] port = self.device_port or PREFS['port'] @@ -293,47 +330,66 @@ def upload_books(self, files, names, on_card=None, end_session=True, metadata=No paths = [] total = len(files) - for i, (infile, name) in enumerate(zip(files, names)): - if hasattr(infile, 'read'): - filepath = getattr(infile, 'name', None) - if not filepath: - raise ControlError(desc='In-memory uploads are not supported') - else: - filepath = infile - filename = os.path.basename(name) - subdirs = [] - if metadata and i < len(metadata): - subdirs, filename = self._format_upload_path(metadata[i], filename) - - if subdirs: - target_dir = self._ensure_dir(base_path, subdirs) - else: - target_dir = base_path - - if target_dir == '/': - lpath = '/' + filename - else: - lpath = target_dir + '/' + filename - - def _progress(sent, size): - if size > 0: - self.report_progress((i + sent / float(size)) / float(total), - 'Transferring books to device...') - - ws_client.upload_file( - host, - port, - target_dir, - filename, - filepath, - chunk_size=chunk_size, - debug=debug, - progress_cb=_progress, - logger=self._log, - ) - paths.append((lpath, os.path.getsize(filepath))) - - self.report_progress(1.0, 'Transferring books to device...') + temp_files = [] # Track temp files for cleanup + + try: + for i, (infile, name) in enumerate(zip(files, names)): + if hasattr(infile, 'read'): + filepath = getattr(infile, 'name', None) + if not filepath: + raise ControlError(desc='In-memory uploads are not supported') + else: + filepath = infile + + # Convert EPUB if enabled + converted_path = self._convert_epub(filepath) + if converted_path != filepath: + temp_files.append(converted_path) + filepath = converted_path + + filename = os.path.basename(name) + subdirs = [] + if metadata and i < len(metadata): + subdirs, filename = self._format_upload_path(metadata[i], filename) + + if subdirs: + target_dir = self._ensure_dir(base_path, subdirs) + else: + target_dir = base_path + + if target_dir == '/': + lpath = '/' + filename + else: + lpath = target_dir + '/' + filename + + def _progress(sent, size): + if size > 0: + self.report_progress((i + sent / float(size)) / float(total), + 'Transferring books to device...') + + ws_client.upload_file( + host, + port, + target_dir, + filename, + filepath, + chunk_size=chunk_size, + debug=debug, + progress_cb=_progress, + logger=self._log, + ) + paths.append((lpath, os.path.getsize(filepath))) + + self.report_progress(1.0, 'Transferring books to device...') + + finally: + # Clean up temp files + for temp_path in temp_files: + try: + os.remove(temp_path) + except Exception: + pass + return paths def add_books_to_metadata(self, locations, metadata, booklists): From 9834ae37fbf83ed5c23bf7e8b4dc0d5d5a2b1d80 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:40:50 +0100 Subject: [PATCH 03/17] Delete crosspoint_reader-v0.1.1.zip --- crosspoint_reader-v0.1.1.zip | Bin 9409 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crosspoint_reader-v0.1.1.zip diff --git a/crosspoint_reader-v0.1.1.zip b/crosspoint_reader-v0.1.1.zip deleted file mode 100644 index eafef8ef19c71353301e2ce180733ec9785a2938..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9409 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 From 83102a10d8f4d6467bdd075c2e82ceb99734a2ec Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:44:52 +0100 Subject: [PATCH 04/17] Rename crosspoint_reader-v0_2_0.zip to crosspoint_reader-v0.2.0.zip --- ...eader-v0_2_0.zip => crosspoint_reader-v0.2.0.zip | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename crosspoint_reader-v0_2_0.zip => crosspoint_reader-v0.2.0.zip (100%) diff --git a/crosspoint_reader-v0_2_0.zip b/crosspoint_reader-v0.2.0.zip similarity index 100% rename from crosspoint_reader-v0_2_0.zip rename to crosspoint_reader-v0.2.0.zip From dc95ef60f8cb9f7e4db4d5a15ec8bb00752ab73b Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:47:12 +0100 Subject: [PATCH 05/17] Delete crosspoint_reader-v0.2.0.zip --- crosspoint_reader-v0.2.0.zip | Bin 17946 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crosspoint_reader-v0.2.0.zip diff --git a/crosspoint_reader-v0.2.0.zip b/crosspoint_reader-v0.2.0.zip deleted file mode 100644 index 7f2fef281c57f0ea1dbce30129afa22767e55562..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17946 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 XiU4m`U>gLuH3JCsfG59Pa|ZDMcm>kZ From b701cf319c28db10b25f15a012e4fb1edc5c1f89 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:47:42 +0100 Subject: [PATCH 06/17] Add files via upload --- crosspoint_reader-v0_2_1.zip | Bin 0 -> 18301 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 crosspoint_reader-v0_2_1.zip diff --git a/crosspoint_reader-v0_2_1.zip b/crosspoint_reader-v0_2_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..1fc640e6880fe67efba4a9e269637dc4db4fe41e GIT binary patch literal 18301 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%0D)bmczcR}&c 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 literal 0 HcmV?d00001 From 2949f1a256d36dc03eb52b65fd0534503c7d5f41 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:48:00 +0100 Subject: [PATCH 07/17] Rename crosspoint_reader-v0_2_1.zip to crosspoint_reader-v0.2.1.zip --- ...eader-v0_2_1.zip => crosspoint_reader-v0.2.1.zip | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename crosspoint_reader-v0_2_1.zip => crosspoint_reader-v0.2.1.zip (100%) diff --git a/crosspoint_reader-v0_2_1.zip b/crosspoint_reader-v0.2.1.zip similarity index 100% rename from crosspoint_reader-v0_2_1.zip rename to crosspoint_reader-v0.2.1.zip From 4b2a7f0814f849fe7d00d97d79b3ec14d39683bd Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:48:40 +0100 Subject: [PATCH 08/17] Add files via upload --- crosspoint_reader/README.md | 7 ++++++- crosspoint_reader/config.py | 4 ++++ crosspoint_reader/converter.py | 25 ++++++++++++++++++------- crosspoint_reader/driver.py | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/crosspoint_reader/README.md b/crosspoint_reader/README.md index c00bbc6..f15d0b6 100644 --- a/crosspoint_reader/README.md +++ b/crosspoint_reader/README.md @@ -2,7 +2,7 @@ A Calibre device driver plugin for CrossPoint e-readers with built-in EPUB image conversion for optimal e-reader compatibility. -## Version 0.2.0 +## Version 0.2.1 ## Features @@ -92,6 +92,11 @@ This plugin is provided as-is for use with CrossPoint Reader devices. ## Changelog +### v0.2.1 +- Fixed: mimetype now written first in EPUB archive (EPUB OCF spec compliance) +- Fixed: Preset quality buttons now disable when conversion is toggled off +- Fixed: Closure variable binding in replacement functions (B023) + ### v0.2.0 - Added EPUB image conversion - Added Light Novel Mode (rotate & split) diff --git a/crosspoint_reader/config.py b/crosspoint_reader/config.py index 403e296..880128d 100644 --- a/crosspoint_reader/config.py +++ b/crosspoint_reader/config.py @@ -111,11 +111,13 @@ def __init__(self): presets_layout = QHBoxLayout(presets_widget) presets_layout.setContentsMargins(0, 0, 0, 0) + self.preset_buttons = [] # Track for enable/disable for name, value in [('Low (60%)', 60), ('Medium (75%)', 75), ('High (85%)', 85), ('Max (95%)', 95)]: btn = QPushButton(name) btn.clicked.connect(lambda checked, v=value: self._set_quality(v)) presets_layout.addWidget(btn) + self.preset_buttons.append(btn) conv_layout.addRow('Presets', presets_widget) @@ -198,6 +200,8 @@ def _update_conversion_enabled(self, enabled): """Enable/disable conversion options based on master checkbox.""" self.jpeg_quality.setEnabled(enabled) self.quality_label.setEnabled(enabled) + for btn in self.preset_buttons: + btn.setEnabled(enabled) self.light_novel_mode.setEnabled(enabled) self.screen_width.setEnabled(enabled) self.screen_height.setEnabled(enabled) diff --git a/crosspoint_reader/converter.py b/crosspoint_reader/converter.py index 280a9fb..673019e 100644 --- a/crosspoint_reader/converter.py +++ b/crosspoint_reader/converter.py @@ -103,8 +103,15 @@ def convert_epub(self, input_path, output_path=None): renamed[name] = new_name with zipfile.ZipFile(output_path, 'w') as zout: + # CRITICAL: Write mimetype FIRST per EPUB OCF spec + # It must be uncompressed and the first entry in the archive + if 'mimetype' in zin.namelist(): + zout.writestr('mimetype', zin.read('mimetype'), compress_type=zipfile.ZIP_STORED) + # First pass: process images for name in zin.namelist(): + if name == 'mimetype': + continue # Already written low = name.lower() if re.match(r'.*\.(png|gif|webp|bmp|jpg|jpeg)$', low): @@ -177,7 +184,8 @@ def convert_epub(self, input_path, output_path=None): re.IGNORECASE | re.DOTALL ) - def replace_block(match): + # Bind loop variables via default arguments to avoid B023 + def replace_block(match, parts=parts, orig_name=orig_name, new_name=new_name): result = [] for i, part in enumerate(parts): if i > 0: @@ -197,7 +205,8 @@ def replace_block(match): re.IGNORECASE ) - def replace_simple(match): + # Bind loop variables via default arguments to avoid B023 + def replace_simple(match, parts=parts, orig_name=orig_name, new_name=new_name): result = [] for i, part in enumerate(parts): if i > 0: @@ -265,6 +274,8 @@ def replace_simple(match): # Fourth pass: copy remaining files for name in zin.namelist(): + if name == 'mimetype': + continue # Already written first low = name.lower() # Skip already processed files @@ -286,9 +297,7 @@ def replace_simple(match): text = text.replace(old_name, new_name) data = text.encode('utf-8') - # mimetype must be stored uncompressed - compress = zipfile.ZIP_STORED if name == 'mimetype' else zipfile.ZIP_DEFLATED - zout.writestr(name, data, compress_type=compress) + zout.writestr(name, data, compress_type=zipfile.ZIP_DEFLATED) self.stats['new_size'] = os.path.getsize(output_path) @@ -381,7 +390,9 @@ def _process_split_rotate(self, img, orig_w, orig_h): img.save(buf, 'JPEG', quality=self.jpeg_quality, progressive=False) return [{'data': buf.getvalue(), 'suffix': ''}] else: - # Split by width (vertical cuts), right to left for correct reading order + # Split by WIDTH (vertical cuts) - from RIGHT to LEFT + # After 90° CW rotation: right side becomes top, left becomes bottom + # So we cut from right to left to get top-to-bottom order parts = [] max_w = self.max_width overlap_px = int(max_w * self.overlap) @@ -389,7 +400,7 @@ def _process_split_rotate(self, img, orig_w, orig_h): num_parts = (rot_w - overlap_px + step - 1) // step # ceil division for i in range(num_parts): - # Start from right side and go left + # Start from right side (rot_w) and go left x = rot_w - max_w - (i * step) if i == num_parts - 1: x = 0 # Last part starts at left edge diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index c0024cc..b33fa41 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -23,7 +23,7 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): description = 'CrossPoint Reader wireless device with EPUB image conversion' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 2, 0) + version = (0, 2, 1) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] From e0f1d2ba8c06217f61fd008a4b6959be296335c6 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:11:36 +0100 Subject: [PATCH 09/17] Add files via upload --- crosspoint_reader-v0.2.2.zip | Bin 0 -> 18868 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 crosspoint_reader-v0.2.2.zip diff --git a/crosspoint_reader-v0.2.2.zip b/crosspoint_reader-v0.2.2.zip new file mode 100644 index 0000000000000000000000000000000000000000..8cc302ee158d216fd91eff1e4ea8672a085e1efa GIT binary patch literal 18868 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 literal 0 HcmV?d00001 From ba1f741527b3f46505d4fcc3ff5ad7f4c9b6960d Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:12:35 +0100 Subject: [PATCH 10/17] Add files via upload --- crosspoint_reader/README.md | 11 ++++++++++- crosspoint_reader/config.py | 4 ++-- crosspoint_reader/converter.py | 21 +++++++++++++++++---- crosspoint_reader/driver.py | 18 +++++++++++++++--- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/crosspoint_reader/README.md b/crosspoint_reader/README.md index f15d0b6..f460504 100644 --- a/crosspoint_reader/README.md +++ b/crosspoint_reader/README.md @@ -2,7 +2,7 @@ A Calibre device driver plugin for CrossPoint e-readers with built-in EPUB image conversion for optimal e-reader compatibility. -## Version 0.2.1 +## Version 0.2.2 ## Features @@ -92,6 +92,15 @@ This plugin is provided as-is for use with CrossPoint Reader devices. ## Changelog +### v0.2.2 +- Changed: Conversion disabled by default (opt-in for safe upgrades from v0.1.x) +- Fixed: OPF manifest href now uses correct relative paths for images in subdirectories +- Fixed: `` replacement limited to first occurrence +- Fixed: Temp file cleanup on conversion failure +- Fixed: Progress callback closure now correctly captures loop index +- Fixed: Added length validation for files/names to prevent silent truncation +- Removed: Unused TemporaryDirectory import + ### v0.2.1 - Fixed: mimetype now written first in EPUB archive (EPUB OCF spec compliance) - Fixed: Preset quality buttons now disable when conversion is toggled off diff --git a/crosspoint_reader/config.py b/crosspoint_reader/config.py index 880128d..6edd72f 100644 --- a/crosspoint_reader/config.py +++ b/crosspoint_reader/config.py @@ -30,8 +30,8 @@ PREFS.defaults['debug'] = False PREFS.defaults['fetch_metadata'] = False -# Conversion settings -PREFS.defaults['enable_conversion'] = True +# Conversion settings (disabled by default for safe upgrades) +PREFS.defaults['enable_conversion'] = False PREFS.defaults['jpeg_quality'] = 85 PREFS.defaults['light_novel_mode'] = False PREFS.defaults['screen_width'] = 480 diff --git a/crosspoint_reader/converter.py b/crosspoint_reader/converter.py index 673019e..29119a4 100644 --- a/crosspoint_reader/converter.py +++ b/crosspoint_reader/converter.py @@ -244,6 +244,9 @@ def replace_simple(match, parts=parts, orig_name=orig_name, new_name=new_name): ) # Update split image references in OPF + # Calculate OPF directory for relative paths + opf_dir = os.path.dirname(opf_path) if '/' in opf_path else '' + for orig_name, parts in split_images.items(): orig_base = re.sub(r'\.[^.]+$', '', orig_name) @@ -258,10 +261,19 @@ def replace_simple(match, parts=parts, orig_name=orig_name, new_name=new_name): manifest_additions = '' for j in range(1, len(parts)): p = parts[j] - manifest_additions += f'\n' + # Calculate href relative to OPF directory + part_full_path = p['path'] + if opf_dir and part_full_path.startswith(opf_dir + '/'): + href = part_full_path[len(opf_dir) + 1:] + elif opf_dir: + # Image is in different directory, use relative path + href = os.path.relpath(part_full_path, opf_dir).replace('\\', '/') + else: + href = part_full_path + manifest_additions += f'\n' if manifest_additions: - t = t.replace('', manifest_additions + '') + t = t.replace('', manifest_additions + '', 1) # Ensure cover meta fixed_cover = self._ensure_cover_meta(t) @@ -484,10 +496,11 @@ def _ensure_cover_meta(self, content): return {'content': content, 'fixed': True} return {'content': content, 'fixed': False} - # Add missing cover meta + # Add missing cover meta (only replace first occurrence) content = content.replace( '', - f' \n ' + f' \n ', + 1 ) return {'content': content, 'fixed': True} diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index b33fa41..807ea51 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -9,7 +9,7 @@ from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.books import Book, BookList from calibre.ebooks.metadata.book.base import Metadata -from calibre.ptempfile import PersistentTemporaryFile, TemporaryDirectory +from calibre.ptempfile import PersistentTemporaryFile from . import ws_client from .config import CrossPointConfigWidget, PREFS @@ -23,7 +23,7 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): description = 'CrossPoint Reader wireless device with EPUB image conversion' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 2, 1) + version = (0, 2, 2) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] @@ -284,6 +284,7 @@ def _convert_epub(self, input_path): if not PREFS['enable_conversion']: return input_path + temp_path = None try: # Create converter with settings from preferences converter = EpubConverter( @@ -308,6 +309,12 @@ def _convert_epub(self, input_path): except Exception as exc: self._log(f'[CrossPoint] Conversion failed: {exc}') + # Clean up temp file on failure + if temp_path: + try: + os.remove(temp_path) + except Exception: + pass # Return original file if conversion fails return input_path @@ -321,6 +328,10 @@ def upload_books(self, files, names, on_card=None, end_session=True, metadata=No chunk_size = 2048 debug = PREFS['debug'] + # Validate input lengths + if len(files) != len(names): + raise ControlError(desc=f'Mismatch: {len(files)} files but {len(names)} names') + # Normalize base upload path base_path = upload_path if not base_path.startswith('/'): @@ -362,7 +373,8 @@ def upload_books(self, files, names, on_card=None, end_session=True, metadata=No else: lpath = target_dir + '/' + filename - def _progress(sent, size): + # Bind loop variables via default arguments to avoid closure bug + def _progress(sent, size, i=i, total=total): if size > 0: self.report_progress((i + sent / float(size)) / float(total), 'Transferring books to device...') From 32134505eccde7aee74d1e29976b2f79367bf47d Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:15:10 +0100 Subject: [PATCH 11/17] Add files via upload --- crosspoint_reader-v0.2.3.zip | Bin 0 -> 19412 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 crosspoint_reader-v0.2.3.zip diff --git a/crosspoint_reader-v0.2.3.zip b/crosspoint_reader-v0.2.3.zip new file mode 100644 index 0000000000000000000000000000000000000000..05a975e458d76a7d0771b284363f06611116ac49 GIT binary patch literal 19412 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 a+&^f{>$(h}gLA-08y9GqT`}4RyMF@)Cul$b literal 0 HcmV?d00001 From de5db42bf0683a5f582053f086b9b76f8521c02c Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:17:26 +0100 Subject: [PATCH 12/17] Add files via upload --- crosspoint_reader/README.md | 8 ++++- crosspoint_reader/config.py | 13 ++++++- crosspoint_reader/converter.py | 66 ++++++++++++++++++++-------------- crosspoint_reader/driver.py | 10 +++--- 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/crosspoint_reader/README.md b/crosspoint_reader/README.md index f460504..e521621 100644 --- a/crosspoint_reader/README.md +++ b/crosspoint_reader/README.md @@ -2,7 +2,7 @@ A Calibre device driver plugin for CrossPoint e-readers with built-in EPUB image conversion for optimal e-reader compatibility. -## Version 0.2.2 +## Version 0.2.3 ## Features @@ -92,6 +92,12 @@ This plugin is provided as-is for use with CrossPoint Reader devices. ## Changelog +### v0.2.3 +- Fixed: Basename collision when EPUBs have duplicate filenames in different folders (e.g., `Images/cover.png` and `assets/cover.png`) - now uses full path as key +- Fixed: Split Overlap control now disabled when Light Novel Mode is off (reflects actual dependency) +- Fixed: Failed image conversions now preserve original extension instead of writing invalid `.jpg` files +- Fixed: Temp file cleanup errors are now logged instead of silently ignored + ### v0.2.2 - Changed: Conversion disabled by default (opt-in for safe upgrades from v0.1.x) - Fixed: OPF manifest href now uses correct relative paths for images in subdirectories diff --git a/crosspoint_reader/config.py b/crosspoint_reader/config.py index 6edd72f..d904aed 100644 --- a/crosspoint_reader/config.py +++ b/crosspoint_reader/config.py @@ -174,6 +174,10 @@ def __init__(self): self.enable_conversion.toggled.connect(self._update_conversion_enabled) self._update_conversion_enabled(self.enable_conversion.isChecked()) + # Gate split_overlap on light_novel_mode + self.light_novel_mode.toggled.connect(self._update_split_overlap_enabled) + self._update_split_overlap_enabled(self.light_novel_mode.isChecked()) + # Log section log_group = QGroupBox("Debug Log") log_layout = QVBoxLayout() @@ -205,7 +209,14 @@ def _update_conversion_enabled(self, enabled): self.light_novel_mode.setEnabled(enabled) self.screen_width.setEnabled(enabled) self.screen_height.setEnabled(enabled) - self.split_overlap.setEnabled(enabled) + # split_overlap depends on both conversion enabled AND light_novel_mode + self._update_split_overlap_enabled(self.light_novel_mode.isChecked()) + + def _update_split_overlap_enabled(self, light_novel_enabled): + """Enable/disable split overlap based on Light Novel Mode.""" + # split_overlap is only enabled if BOTH conversion AND light_novel_mode are on + conversion_enabled = self.enable_conversion.isChecked() + self.split_overlap.setEnabled(conversion_enabled and light_novel_enabled) def save(self): # Connection settings diff --git a/crosspoint_reader/converter.py b/crosspoint_reader/converter.py index 29119a4..e77b552 100644 --- a/crosspoint_reader/converter.py +++ b/crosspoint_reader/converter.py @@ -116,31 +116,38 @@ def convert_epub(self, input_path, output_path=None): if re.match(r'.*\.(png|gif|webp|bmp|jpg|jpeg)$', low): data = zin.read(name) - parts = self._process_image(data, name) + parts, conversion_success = self._process_image(data, name) base_name = re.sub(r'\.[^.]+$', '', name) if len(parts) == 1 and parts[0]['suffix'] == '': # Single image, no split - new_path = renamed.get(name, re.sub(r'\.[^.]+$', '.jpg', name)) + if conversion_success: + # Conversion succeeded - use .jpg extension + new_path = renamed.get(name, re.sub(r'\.[^.]+$', '.jpg', name)) + else: + # Conversion failed - preserve original extension + new_path = name zout.writestr(new_path, parts[0]['data'], compress_type=zipfile.ZIP_DEFLATED) self.stats['images_converted'] += 1 else: - # Split image - orig_name = os.path.basename(name) - split_images[orig_name] = [] + # Split image - use full path as key to avoid basename collisions + orig_basename = os.path.basename(name) + orig_dir = name[:name.rfind('/') + 1] if '/' in name else '' + split_images[name] = { + 'basename': orig_basename, + 'dir': orig_dir, + 'parts': [] + } for part in parts: part_name = os.path.basename(base_name) + part['suffix'] + '.jpg' - if '/' in name: - part_path = name[:name.rfind('/') + 1] + part_name - else: - part_path = part_name + part_path = orig_dir + part_name zout.writestr(part_path, part['data'], compress_type=zipfile.ZIP_DEFLATED) - split_images[orig_name].append({ + split_images[name]['parts'].append({ 'path': part_path, 'imgName': part_name, 'id': os.path.basename(base_name) + part['suffix'] @@ -173,25 +180,27 @@ def convert_epub(self, input_path, output_path=None): t = t.replace(old_name, new_name) # Update split image references - for orig_name, parts in split_images.items(): - new_name = re.sub(r'\.(png|gif|webp|bmp|jpeg)$', '.jpg', orig_name, flags=re.IGNORECASE) + for orig_path, split_info in split_images.items(): + orig_basename = split_info['basename'] + parts = split_info['parts'] + new_basename = re.sub(r'\.(png|gif|webp|bmp|jpeg)$', '.jpg', orig_basename, flags=re.IGNORECASE) # Replace block patterns (p/div with span and img) block_pattern = re.compile( r'(<(?:p|div)[^>]*>\s*\s*]*src=["\'][^"\']*(?:' + - re.escape(orig_name) + '|' + re.escape(new_name) + + re.escape(orig_basename) + '|' + re.escape(new_basename) + r')[^>]*/?>\s*\s*)', re.IGNORECASE | re.DOTALL ) # Bind loop variables via default arguments to avoid B023 - def replace_block(match, parts=parts, orig_name=orig_name, new_name=new_name): + def replace_block(match, parts=parts, orig_basename=orig_basename, new_basename=new_basename): result = [] for i, part in enumerate(parts): if i > 0: result.append('\n') - new_block = match.group(0).replace(orig_name, part['imgName']) - new_block = new_block.replace(new_name, part['imgName']) + new_block = match.group(0).replace(orig_basename, part['imgName']) + new_block = new_block.replace(new_basename, part['imgName']) result.append(new_block) return ''.join(result) @@ -200,19 +209,19 @@ def replace_block(match, parts=parts, orig_name=orig_name, new_name=new_name): # Replace simple img patterns simple_pattern = re.compile( r'(]*src=["\'])([^"\']*(?:' + - re.escape(orig_name) + '|' + re.escape(new_name) + + re.escape(orig_basename) + '|' + re.escape(new_basename) + r'))([^>]*/>)', re.IGNORECASE ) # Bind loop variables via default arguments to avoid B023 - def replace_simple(match, parts=parts, orig_name=orig_name, new_name=new_name): + def replace_simple(match, parts=parts, orig_basename=orig_basename, new_basename=new_basename): result = [] for i, part in enumerate(parts): if i > 0: result.append('\n') - new_src = match.group(2).replace(orig_name, part['imgName']) - new_src = new_src.replace(new_name, part['imgName']) + new_src = match.group(2).replace(orig_basename, part['imgName']) + new_src = new_src.replace(new_basename, part['imgName']) result.append(match.group(1) + new_src + match.group(3)) return ''.join(result) @@ -247,8 +256,10 @@ def replace_simple(match, parts=parts, orig_name=orig_name, new_name=new_name): # Calculate OPF directory for relative paths opf_dir = os.path.dirname(opf_path) if '/' in opf_path else '' - for orig_name, parts in split_images.items(): - orig_base = re.sub(r'\.[^.]+$', '', orig_name) + for orig_path, split_info in split_images.items(): + orig_basename = split_info['basename'] + parts = split_info['parts'] + orig_base = re.sub(r'\.[^.]+$', '', orig_basename) # Update original reference to part1 pattern = re.compile( @@ -333,7 +344,8 @@ def _process_image(self, data, name): """ Process a single image. - Returns list of {'data': bytes, 'suffix': str} + Returns tuple: (list of {'data': bytes, 'suffix': str}, bool success) + If success is False, the original data is returned unchanged. """ try: img = Image.open(io.BytesIO(data)) @@ -357,14 +369,14 @@ def _process_image(self, data, name): needs_rotation = is_horizontal and exceeds_screen if needs_rotation and self.enable_split_rotate: - return self._process_split_rotate(img, orig_w, orig_h) + return self._process_split_rotate(img, orig_w, orig_h), True else: - return self._process_normal(img, orig_w, orig_h) + return self._process_normal(img, orig_w, orig_h), True except Exception as e: self._log(f"Error processing {name}: {e}") - # Return original data as fallback - return [{'data': data, 'suffix': ''}] + # Return original data as fallback with success=False + return [{'data': data, 'suffix': ''}], False def _process_normal(self, img, orig_w, orig_h): """Process image without rotation/split.""" diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index 807ea51..c26146b 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -23,7 +23,7 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): description = 'CrossPoint Reader wireless device with EPUB image conversion' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 2, 2) + version = (0, 2, 3) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] @@ -313,8 +313,8 @@ def _convert_epub(self, input_path): if temp_path: try: os.remove(temp_path) - except Exception: - pass + except Exception as cleanup_err: + self._log(f'[CrossPoint] Failed to clean up temp file {temp_path}: {cleanup_err}') # Return original file if conversion fails return input_path @@ -399,8 +399,8 @@ def _progress(sent, size, i=i, total=total): for temp_path in temp_files: try: os.remove(temp_path) - except Exception: - pass + except Exception as cleanup_err: + self._log(f'[CrossPoint] Failed to clean up temp file {temp_path}: {cleanup_err}') return paths From 76456efcf53a036363229c20feeb4f6e9d8ef317 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:29:48 +0100 Subject: [PATCH 13/17] Delete crosspoint_reader-v0.2.2.zip --- crosspoint_reader-v0.2.2.zip | Bin 18868 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crosspoint_reader-v0.2.2.zip diff --git a/crosspoint_reader-v0.2.2.zip b/crosspoint_reader-v0.2.2.zip deleted file mode 100644 index 8cc302ee158d216fd91eff1e4ea8672a085e1efa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18868 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 From 4cb0e53ecc0f47d245dff811d3eb293aacdff68c Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:30:03 +0100 Subject: [PATCH 14/17] Delete crosspoint_reader-v0.2.1.zip --- crosspoint_reader-v0.2.1.zip | Bin 18301 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crosspoint_reader-v0.2.1.zip diff --git a/crosspoint_reader-v0.2.1.zip b/crosspoint_reader-v0.2.1.zip deleted file mode 100644 index 1fc640e6880fe67efba4a9e269637dc4db4fe41e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18301 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%0D)bmczcR}&c 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 From 73ea58959ca3060e42e8da5e9a719f6ae63c9512 Mon Sep 17 00:00:00 2001 From: zgredex <112968378+zgredex@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:41:08 +0100 Subject: [PATCH 15/17] Update image conversion instructions in README Clarified the condition for image conversion in the README. --- crosspoint_reader/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crosspoint_reader/README.md b/crosspoint_reader/README.md index e521621..f7551fc 100644 --- a/crosspoint_reader/README.md +++ b/crosspoint_reader/README.md @@ -50,7 +50,7 @@ Automatically converts EPUB images before uploading for maximum e-reader compati 1. Connect your CrossPoint Reader to the same WiFi network as your computer 2. The device should appear automatically in Calibre's device list 3. Configure settings via Preferences → Plugins → CrossPoint Reader → Customize plugin -4. Send books to device as usual - images will be automatically converted ++4. Send books to device as usual — images are converted only when “Enable EPUB image conversion” is turned on ## What the Converter Does From 9c671cd18cbe16c7988720832e61e19116b47958 Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Feb 2026 20:32:40 +0100 Subject: [PATCH 16/17] Fix socket drain on Windows by using platform-specific approach select.select() doesn't work with sockets on Windows. Added platform detection to use short timeout on Windows while keeping select.select() for Unix/Linux/Mac where it works correctly. --- crosspoint_reader/ws_client.py | 43 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/crosspoint_reader/ws_client.py b/crosspoint_reader/ws_client.py index d87fa0b..b9ecd4d 100644 --- a/crosspoint_reader/ws_client.py +++ b/crosspoint_reader/ws_client.py @@ -3,6 +3,7 @@ import select import socket import struct +import sys import time @@ -153,18 +154,42 @@ def _recv_exact(self, n): return data def drain_messages(self): + """Drain all pending messages from the socket. + + Uses different approaches per platform since select.select() + doesn't work with sockets on Windows. + """ if self.sock is None: return [] messages = [] - while True: - r, _, _ = select.select([self.sock], [], [], 0) - if not r: - break - opcode, payload = self._read_frame() - if opcode == 0x1: - messages.append(payload.decode('utf-8', 'ignore')) - elif opcode == 0x8: - raise WebSocketError('Connection closed') + + if sys.platform == 'win32': + # Windows: select.select() doesn't work with sockets + # Use short timeout instead + self.sock.settimeout(0.01) + while True: + try: + opcode, payload = self._read_frame() + if opcode == 0x1: + messages.append(payload.decode('utf-8', 'ignore')) + elif opcode == 0x8: + raise WebSocketError('Connection closed') + except socket.timeout: + break + except WebSocketError: + raise + else: + # Unix/Linux/Mac: select.select() works fine with sockets + while True: + r, _, _ = select.select([self.sock], [], [], 0) + if not r: + break + opcode, payload = self._read_frame() + if opcode == 0x1: + messages.append(payload.decode('utf-8', 'ignore')) + elif opcode == 0x8: + raise WebSocketError('Connection closed') + return messages From 35444ab119a784c6c05309d09a67286db6251931 Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Feb 2026 21:35:45 +0100 Subject: [PATCH 17/17] Fix SVG images and increase upload timeout - Fix ALL SVG-wrapped images (not just covers) - Increase WebSocket timeout: 10s -> 60s for large file transfers - Skip drain_messages on Windows to avoid socket timeout interference --- crosspoint_reader-v0.2.3.zip | Bin 19412 -> 0 bytes crosspoint_reader-v0.2.4.zip | Bin 0 -> 19613 bytes crosspoint_reader/__init__.py | 4 +- crosspoint_reader/converter.py | 70 ++++++++++++++++++++++----------- crosspoint_reader/driver.py | 2 +- crosspoint_reader/ws_client.py | 23 +++-------- 6 files changed, 57 insertions(+), 42 deletions(-) delete mode 100644 crosspoint_reader-v0.2.3.zip create mode 100644 crosspoint_reader-v0.2.4.zip diff --git a/crosspoint_reader-v0.2.3.zip b/crosspoint_reader-v0.2.3.zip deleted file mode 100644 index 05a975e458d76a7d0771b284363f06611116ac49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19412 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 a+&^f{>$(h}gLA-08y9GqT`}4RyMF@)Cul$b diff --git a/crosspoint_reader-v0.2.4.zip b/crosspoint_reader-v0.2.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..79d0a2d3073a3458715e481807678b560d21cc3a GIT binary patch literal 19613 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 literal 0 HcmV?d00001 diff --git a/crosspoint_reader/__init__.py b/crosspoint_reader/__init__.py index 519bee0..f71d93c 100644 --- a/crosspoint_reader/__init__.py +++ b/crosspoint_reader/__init__.py @@ -1,14 +1,14 @@ """ CrossPoint Reader - Calibre Device Driver Plugin -A wireless device driver for CrossPoint e-readers with built-in +A wireless device driver for CrossPoint e-readers with built-in EPUB image conversion for optimal e-reader compatibility. Features: - Wireless book transfer via WebSocket - Automatic EPUB image conversion to baseline JPEG - PNG/GIF/WebP/BMP to JPEG conversion -- SVG cover fixing +- Fix ALL SVG-wrapped images (not just covers) - Image scaling to fit e-reader screen - Light Novel Mode: rotate and split wide images for manga/comics - Configurable JPEG quality and screen dimensions diff --git a/crosspoint_reader/converter.py b/crosspoint_reader/converter.py index e77b552..6a97981 100644 --- a/crosspoint_reader/converter.py +++ b/crosspoint_reader/converter.py @@ -6,7 +6,7 @@ Features: - Convert PNG/GIF/WebP/BMP to baseline JPEG -- Fix SVG covers for e-readers +- Fix ALL SVG-wrapped images for e-readers (not just covers) - Scale large images to fit screen - Light Novel Mode: rotate wide images and split into pages - Configurable JPEG quality @@ -336,7 +336,7 @@ def replace_simple(match, parts=parts, orig_basename=orig_basename, new_basename if self.stats['images_split'] > 0: self._log(f"Created {self.stats['images_split']} additional pages from splits") if self.stats['svg_covers_fixed'] > 0: - self._log(f"Fixed {self.stats['svg_covers_fixed']} SVG cover(s)") + self._log(f"Fixed {self.stats['svg_covers_fixed']} SVG image(s)") return output_path @@ -441,28 +441,54 @@ def _process_split_rotate(self, img, orig_w, orig_h): return parts def _fix_svg_cover(self, content): - """Fix SVG cover to regular HTML img tag.""" + """Fix ALL SVG-wrapped images to regular HTML img tags. + + Replaces with . + Works for all SVG images, not just covers. + """ if 'Cover' not in content): - return {'content': content, 'fixed': False} - - match = re.search(r'xlink:href=["\']([^"\']+)["\']', content) - if not match: - return {'content': content, 'fixed': False} - - img_src = match.group(1) - new_content = f''' - - -Cover -
Cover
-''' - - return {'content': new_content, 'fixed': True} + + fixed_count = 0 + result = content + + # Pattern 1: SVG with xlink:href attribute + svg_pattern = re.compile( + r']*>.*?]*xlink:href\s*=\s*["\']([^"\']+)["\'][^>]*/?>.*?', + re.DOTALL | re.IGNORECASE + ) + + for match in svg_pattern.finditer(result): + svg_tag = match.group(0) + image_path = match.group(1) + + # Extract title if present for alt text + title_match = re.search(r']*>([^<]*)', svg_tag, re.IGNORECASE) + alt_text = title_match.group(1).strip() if title_match else '' + + # Extract class from SVG if present + class_match = re.search(r'class=["\']([^"\']*)["\']', svg_tag, re.IGNORECASE) + svg_class = f' class="{class_match.group(1)}"' if class_match else '' + + # Build replacement img tag + img_tag = f'{alt_text}' + result = result.replace(svg_tag, img_tag) + fixed_count += 1 + + # Pattern 2: SVG with href attribute (without xlink:) + svg_pattern2 = re.compile( + r']*>\s*]*href=["\']([^"\']+)["\'][^>]*/?>\s*', + re.DOTALL | re.IGNORECASE + ) + + for match in svg_pattern2.finditer(result): + svg_tag = match.group(0) + image_path = match.group(1) + img_tag = f'' + result = result.replace(svg_tag, img_tag) + fixed_count += 1 + + return {'content': result, 'fixed': fixed_count > 0} def _ensure_cover_meta(self, content): """Ensure OPF has correct cover meta tag.""" diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index c26146b..e34227d 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -23,7 +23,7 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): description = 'CrossPoint Reader wireless device with EPUB image conversion' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 2, 3) + version = (0, 2, 4) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] diff --git a/crosspoint_reader/ws_client.py b/crosspoint_reader/ws_client.py index b9ecd4d..4e36fc9 100644 --- a/crosspoint_reader/ws_client.py +++ b/crosspoint_reader/ws_client.py @@ -156,28 +156,17 @@ def _recv_exact(self, n): def drain_messages(self): """Drain all pending messages from the socket. - Uses different approaches per platform since select.select() - doesn't work with sockets on Windows. + On Windows, skip draining to avoid socket timeout interference. + On Unix/Linux/Mac, use select.select() which works correctly. """ if self.sock is None: return [] messages = [] if sys.platform == 'win32': - # Windows: select.select() doesn't work with sockets - # Use short timeout instead - self.sock.settimeout(0.01) - while True: - try: - opcode, payload = self._read_frame() - if opcode == 0x1: - messages.append(payload.decode('utf-8', 'ignore')) - elif opcode == 0x8: - raise WebSocketError('Connection closed') - except socket.timeout: - break - except WebSocketError: - raise + # Windows: Skip draining to avoid interfering with socket timeout + # select.select() doesn't work with sockets on Windows + return [] else: # Unix/Linux/Mac: select.select() works fine with sockets while True: @@ -278,7 +267,7 @@ def discover_device(timeout=2.0, debug=False, logger=None, extra_hosts=None): def upload_file(host, port, upload_path, filename, filepath, chunk_size=16384, debug=False, progress_cb=None, logger=None): - client = WebSocketClient(host, port, timeout=10, debug=debug, logger=logger) + client = WebSocketClient(host, port, timeout=60, debug=debug, logger=logger) try: client.connect() size = os.path.getsize(filepath)