Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion eink/backend/background/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ async def _call_ha_upload(self, device_id, filename):
"media_content_type": "image/png",
},
"dither_mode": "none",
"rotation": 90,
"rotation": 0,
"fit_mode": "crop",
}

Expand Down
26 changes: 22 additions & 4 deletions eink/backend/background/scene_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,27 @@ async def process_slice(
draw_w = (image_record.width * image_meta.get("scaling_factor", 100)) / 100
draw_h = (image_record.height * image_meta.get("scaling_factor", 100)) / 100

# Output pixels
is_portrait = panel.get("orientation") == "portrait"
out_w = panel["dt"].height_px if is_portrait else panel["dt"].width_px
out_h = panel["dt"].width_px if is_portrait else panel["dt"].height_px
# 211: Output pixels
panel_portrait = panel["dt"].panel_orientation == "portrait"

# Determine panel's native pixel dimensions
# Note: The DB currently stores width_px as the larger dimension
# regardless of panel_orientation because the DisplayTypeHandler
# enforces landscape.
if panel_portrait:
out_w = panel["dt"].height_px
out_h = panel["dt"].width_px
else:
out_w = panel["dt"].width_px
out_h = panel["dt"].height_px

# Determine if we need to rotate to match the layout's orientation
layout_orientation = panel.get("orientation", "landscape")
rotation = 0
if layout_orientation != panel["dt"].panel_orientation:
# If layout is portrait but panel is landscape, we need a 90 deg rotation.
# If layout is landscape but panel is portrait, we also need a 90 deg rotation.
rotation = 90

# Paths
src_path = os.path.join(get_storage_path("image"), image_record.file_path)
Expand All @@ -237,6 +254,7 @@ async def process_slice(
"contrast": image_record.contrast,
"saturation": image_record.saturation,
"conversion": image_record.conversion,
"rotation": rotation,
}

# Call node converter
Expand Down
2 changes: 1 addition & 1 deletion eink/backend/tests/unit/test_mqtt_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,5 @@ async def test_handle_layout_update_logic(mock_app):
assert data["device_id"] == device_id
assert filename in data["image"]["media_content_id"]
assert data["dither_mode"] == "none"
assert data["rotation"] == 90
assert data["rotation"] == 0
assert data["fit_mode"] == "crop"
107 changes: 107 additions & 0 deletions eink/backend/tests/unit/test_scene_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,110 @@ def side_effect(*args, **kwargs):
result = await session.execute(stmt)
updated_record = result.scalar_one()
assert updated_record.file_hash is not None


@pytest.mark.asyncio
async def test_scene_processor_rotation(db_setup, tmp_path):
"""Test that scene_processor calculates rotation correctly when orientations mismatch."""

async with database.get_session() as session:
# 1. Setup Display Type (Landscape panel)
dt = models.DisplayType(
id="dt-landscape",
name="Native Landscape",
panel_orientation="landscape",
width_px=800,
height_px=480,
panel_width_mm=160,
panel_height_mm=100,
width_mm=160,
height_mm=100,
colour_type="BW",
frame={},
mat={},
)
session.add(dt)

# 2. Setup Layout using it as PORTRAIT
layout = models.Layout(
id="layout-p",
name="Portrait Layout",
canvas_width_mm=100,
canvas_height_mm=160,
items=[
{
"id": "d1",
"display_type_id": "dt-landscape",
"x_mm": 0,
"y_mm": 0,
"orientation": "portrait", # MISMATCH!
}
],
status="active",
)
session.add(layout)

# 3. Setup Image
img = models.Image(
id="img1",
name="I",
file_path="t.png",
width=100,
height=100,
file_type="png",
status="ACTIVE",
file_hash="h1",
)
session.add(img)
await session.commit()
await session.refresh(img)

# 4. Setup Scene
scene = models.Scene(
id="scene1",
name="S",
layout_id="layout-p",
status="active",
items=[
{
"id": "item1",
"type": "image",
"displays": ["d1"],
"images": [
{
"image_id": "img1",
"scaling_factor": 100,
"offset": {"x": 0, "y": 0},
}
],
}
],
)
session.add(scene)
await session.commit()
await session.refresh(scene)
await scene_handler._update_scene_queue(scene, session)
await session.commit()

# Mock converter
with patch("asyncio.create_subprocess_exec") as mock_exec:
mock_process = AsyncMock()
mock_process.communicate.return_value = (b"Done", b"")
mock_process.returncode = 0
mock_exec.return_value = mock_process

with patch(
"backend.background.scene_processor.get_storage_path",
return_value=str(tmp_path),
):
await check_for_scene_work()

assert mock_exec.call_count == 1
args, _ = mock_exec.call_args
config = json.loads(args[2])

# Output dimensions should match NATIVE panel (landscape)
assert config["width"] == 800
assert config["height"] == 480
# Rotation should be 90 because landscape panel used as portrait
assert config["rotation"] == 90
12 changes: 10 additions & 2 deletions eink/converter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ async function main() {
const {
src, dest, palette, brightness, contrast, saturation, conversion,
width, height, background_color,
draw_x, draw_y, draw_w, draw_h
draw_x, draw_y, draw_w, draw_h,
rotation
} = config;

if (!src || !dest || !palette) {
Expand All @@ -45,14 +46,21 @@ async function main() {
sourceCtx.fillRect(0, 0, finalWidth, finalHeight);
}

// 2. Draw image with scaling and offset
// 2. Draw image with scaling, offset, and optional rotation
sourceCtx.save();
if (rotation === 90) {
sourceCtx.translate(finalWidth, 0);
sourceCtx.rotate(Math.PI / 2);
}

sourceCtx.drawImage(
img,
draw_x ?? 0,
draw_y ?? 0,
draw_w ?? img.width,
draw_h ?? img.height
);
sourceCtx.restore();

const destCanvas = createCanvas(finalWidth, finalHeight);

Expand Down
Loading