Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed crosspoint_reader-v0.1.1.zip
Binary file not shown.
Binary file added crosspoint_reader-v0.2.1.zip
Binary file not shown.
135 changes: 110 additions & 25 deletions crosspoint_reader/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,110 @@
# 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://<host>:<port>/
- Send: START:<filename>:<size>:<path>
- Wait for READY
- Send binary frames with file content
- Wait for DONE (or ERROR:<message>)

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.1

## 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.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)
- 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
17 changes: 17 additions & 0 deletions crosspoint_reader/__init__.py
Original file line number Diff line number Diff line change
@@ -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
178 changes: 165 additions & 13 deletions crosspoint_reader/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,50 @@
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'] = '/'
PREFS.defaults['chunk_size'] = 2048
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)
Expand All @@ -45,33 +64,165 @@ 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)

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)

# 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)
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)
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())
Expand All @@ -84,6 +235,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)
Expand Down
Loading