Two related bugs that surfaced on the first 13.3" device's first photo: 1) Web-UI portrait preview was 90° sideways. DeviceApiController:: renderBinToPng rotated whenever the device was Portrait — correct for V1 (landscape-native, Portrait => renderer rotated, so preview un-rotates) but wrong for V2 (portrait-native — the renderer doesn't rotate, so the preview shouldn't either). Now mirrors the render-pipeline check: rotate only when `orientation !== model->nativeOrientation()`. Two new functional tests pin the V2 portrait and V2 landscape PNG dimensions to guard against regressions. 2) Cropped photo letterboxed on the 13.3" panel. CropEditor / StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960 = 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200 landscape), so a "full crop" came out the wrong shape and the server's white-canvas composite added bars. New `panelDims(model, orientation)` helper in @/types is the single source of truth on the frontend; matches DeviceModel::width/height on the server. Threaded `model` through Device serializer → Device type → UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard. FrameCard tests updated to cover all four model × orientation placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -576,6 +576,101 @@ class DeviceApiControllerTest extends AppWebTestCase
|
||||
@unlink(preg_replace('/\.bin$/', '.png', $absPath));
|
||||
}
|
||||
|
||||
// V2 panel (13.3", 1200×1600 portrait-native) + Portrait orientation MUST
|
||||
// NOT rotate the preview — Portrait is the natural scan orientation, the
|
||||
// render pipeline didn't rotate the source, so the preview must mirror
|
||||
// that. The bug before this guard: every V2 portrait preview came out
|
||||
// 90° sideways in the web UI.
|
||||
public function test_preview_v2_portrait_not_rotated(): void
|
||||
{
|
||||
$user = $this->createUser('ppng-v2p@example.com');
|
||||
$device = $this->makeDevice('AA:BB:CC:DD:EE:E7', $user);
|
||||
$device->setModel(DeviceModel::V2);
|
||||
$device->setOrientation(Orientation::Portrait);
|
||||
$image = $this->makeImage($user);
|
||||
$device->setCurrentImage($image);
|
||||
|
||||
// 1200×1600 = 1,920,000 nibbles = 960,000 bytes.
|
||||
$projectDir = static::getContainer()->getParameter('kernel.project_dir');
|
||||
$relPath = 'var/storage/images/test-preview-v2-portrait/v2_portrait.bin';
|
||||
$absPath = $projectDir . '/' . $relPath;
|
||||
if (!is_dir(dirname($absPath))) {
|
||||
mkdir(dirname($absPath), 0755, true);
|
||||
}
|
||||
file_put_contents($absPath, str_repeat(chr(0x11), 960000));
|
||||
|
||||
$asset = (new \App\Entity\RenderedAsset())
|
||||
->setImage($image)
|
||||
->setDeviceModel(DeviceModel::V2)
|
||||
->setOrientation(Orientation::Portrait)
|
||||
->setStatus(\App\Enum\RenderStatus::Ready)
|
||||
->setFilePath($relPath);
|
||||
$this->em()->persist($asset);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($user);
|
||||
$client->request('GET', '/api/devices/' . $device->getId() . '/preview');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame('image/png', $client->getResponse()->headers->get('Content-Type'));
|
||||
|
||||
// Decode the PNG and confirm orientation — taller than wide means
|
||||
// no spurious 90° rotation happened (would be 1600×1200 if it had).
|
||||
$pngPath = preg_replace('/\.bin$/', '.png', $absPath);
|
||||
$this->assertFileExists($pngPath);
|
||||
$im = new \Imagick($pngPath);
|
||||
$this->assertSame(1200, $im->getImageWidth(), 'V2 portrait PNG must be 1200 wide (not rotated)');
|
||||
$this->assertSame(1600, $im->getImageHeight(), 'V2 portrait PNG must be 1600 tall (not rotated)');
|
||||
$im->destroy();
|
||||
|
||||
@unlink($absPath);
|
||||
@unlink($pngPath);
|
||||
}
|
||||
|
||||
// V2 panel + Landscape orientation MUST rotate — Landscape is non-native
|
||||
// for V2 (portrait-native), so the renderer pre-rotated and the preview
|
||||
// needs to rotate back to upright landscape.
|
||||
public function test_preview_v2_landscape_rotated(): void
|
||||
{
|
||||
$user = $this->createUser('ppng-v2l@example.com');
|
||||
$device = $this->makeDevice('AA:BB:CC:DD:EE:E8', $user);
|
||||
$device->setModel(DeviceModel::V2);
|
||||
$device->setOrientation(Orientation::Landscape);
|
||||
$image = $this->makeImage($user);
|
||||
$device->setCurrentImage($image);
|
||||
|
||||
$projectDir = static::getContainer()->getParameter('kernel.project_dir');
|
||||
$relPath = 'var/storage/images/test-preview-v2-landscape/v2_landscape.bin';
|
||||
$absPath = $projectDir . '/' . $relPath;
|
||||
if (!is_dir(dirname($absPath))) {
|
||||
mkdir(dirname($absPath), 0755, true);
|
||||
}
|
||||
file_put_contents($absPath, str_repeat(chr(0x11), 960000));
|
||||
|
||||
$asset = (new \App\Entity\RenderedAsset())
|
||||
->setImage($image)
|
||||
->setDeviceModel(DeviceModel::V2)
|
||||
->setOrientation(Orientation::Landscape)
|
||||
->setStatus(\App\Enum\RenderStatus::Ready)
|
||||
->setFilePath($relPath);
|
||||
$this->em()->persist($asset);
|
||||
$this->em()->flush();
|
||||
|
||||
$client = $this->loginAs($user);
|
||||
$client->request('GET', '/api/devices/' . $device->getId() . '/preview');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// The .bin is panel-native 1200×1600 (tall); after the 90° rotation
|
||||
// for non-native orientation the PNG must be 1600×1200 (wide).
|
||||
$pngPath = preg_replace('/\.bin$/', '.png', $absPath);
|
||||
$im = new \Imagick($pngPath);
|
||||
$this->assertSame(1600, $im->getImageWidth(), 'V2 landscape PNG must be 1600 wide (rotated)');
|
||||
$this->assertSame(1200, $im->getImageHeight(), 'V2 landscape PNG must be 1200 tall (rotated)');
|
||||
$im->destroy();
|
||||
|
||||
@unlink($absPath);
|
||||
@unlink($pngPath);
|
||||
}
|
||||
|
||||
// ── DELETE /api/devices/{id} (sell/give-away) ────────────────────────
|
||||
|
||||
public function test_delete_removes_device_and_purges_history_and_approvals(): void
|
||||
|
||||
@@ -34,7 +34,7 @@ class DeviceSerializerTest extends TestCase
|
||||
$payload = $this->serializer->serialize($device);
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['id', 'mac', 'name', 'orientation', 'rotationIntervalMinutes',
|
||||
['id', 'mac', 'name', 'model', 'orientation', 'rotationIntervalMinutes',
|
||||
'wakeTimes', 'timezone', 'uniquenessWindow', 'rotationMode',
|
||||
'prioritizeNeverShown', 'linkedAt', 'lastSeenAt',
|
||||
'nextPollExpectedAt', 'lockedImageId', 'currentImageId'],
|
||||
@@ -42,6 +42,14 @@ class DeviceSerializerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_serializes_model_field(): void
|
||||
{
|
||||
$device = $this->makeDevice();
|
||||
$device->setModel(\App\Enum\DeviceModel::V2);
|
||||
$payload = $this->serializer->serialize($device);
|
||||
$this->assertSame('v2', $payload['model']);
|
||||
}
|
||||
|
||||
public function test_serializes_scalars_in_expected_shapes(): void
|
||||
{
|
||||
$device = $this->makeDevice();
|
||||
|
||||
Reference in New Issue
Block a user