cashapp / paparazzi

Render your Android screens without a physical device or emulator
https://cashapp.github.io/paparazzi/
Apache License 2.0
2.3k stars 215 forks source link

paparazzi.gif fails with IllegalArgumentException #726

Closed martirius closed 4 weeks ago

martirius commented 1 year ago

Description Trying to run paparazzi.gif fails with this stacktrace:

java.lang.IllegalArgumentException: Component 1 width should be a multiple of 2 for colorspace: YUV420J
    at org.jcodec.common.model.Picture.<init>(Picture.java:54)
    at org.jcodec.common.model.Picture.createCropped(Picture.java:94)
    at org.jcodec.common.model.Picture.create(Picture.java:75)
    at org.jcodec.api.transcode.PixelStoreImpl.getPicture(PixelStoreImpl.java:25)
    at org.jcodec.api.SequenceEncoder.encodeNativeFrame(SequenceEncoder.java:94)
    at org.jcodec.api.awt.AWTSequenceEncoder.encodeImage(AWTSequenceEncoder.java:49)
    at app.cash.paparazzi.HtmlReportWriter.writeVideo(HtmlReportWriter.kt:170)
    at app.cash.paparazzi.HtmlReportWriter.access$writeVideo(HtmlReportWriter.kt:60)
    at app.cash.paparazzi.HtmlReportWriter$newFrameHandler$1.close(HtmlReportWriter.kt:109)
    at kotlin.io.CloseableKt.closeFinally(Closeable.kt:56)
    at app.cash.paparazzi.Paparazzi.takeSnapshots(Paparazzi.kt:270)
    at app.cash.paparazzi.Paparazzi.gif(Paparazzi.kt:217)
    at app.cash.paparazzi.Paparazzi.gif$default(Paparazzi.kt:204)
    at com.app.testapp.ui.ScreenshotRule.takeGifLocalized(ScreenshotRule.kt:52)
    at com.app.testapp.ui.ScreenshotRule.takeGifLocalized$default(ScreenshotRule.kt:42)
    at com.app.testapp.ui.login.LoginManualScreenshotTest.login_manual(LoginManualScreenshotTest.kt:17)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at app.cash.paparazzi.Paparazzi$apply$statement$1.evaluate(Paparazzi.kt:118)
    at app.cash.paparazzi.agent.AgentTestRule$apply$1.evaluate(AgentTestRule.kt:17)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
    at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Steps to Reproduce Steps to reproduce are simple, just run a test and try to record a gif, even with very simple layout.

The test:

class SimpleGifTest {

    @get:Rule
    val screenshotRule = ScreenshotRule()

    @Test
    fun gifTest() {
        screenshotRule.takeGifLocalized("simple_gif", R.layout.layout_settings_row, locale = "en")
    }
}

The custom test rule:

class ScreenshotRule : TestRule {

    private val paparazzi = Paparazzi(
        deviceConfig = DeviceConfig.PIXEL_4,
        theme = "MaterialTheme.Custom",
        environment = detectEnvironment(),
        supportsRtl = true
    )

    fun takeScreenshotLocalized(
        screenshotName: String,
        @LayoutRes viewResourceId: Int,
        locale: String,
        viewModeller: (view: View) -> View = { v -> v }
    ) {
        paparazzi.unsafeUpdateConfig(deviceConfig = DeviceConfig.PIXEL_4.copy(locale = locale))
        val view = paparazzi.inflate<View>(viewResourceId)

        val modelledView = viewModeller(view)
        paparazzi.snapshot(
            modelledView,
            name = "${screenshotName}_${locale}",
        )
    }

    fun takeGifLocalized(
        screenshotName: String,
        @LayoutRes viewResourceId: Int,
        locale: String,
        viewModeller: (view: View) -> View = { v -> v }
    ) {
        paparazzi.unsafeUpdateConfig(deviceConfig = DeviceConfig.PIXEL_4.copy(locale = locale))
        val view = paparazzi.inflate<View>(viewResourceId)

        val modelledView = viewModeller(view)
        paparazzi.gif(
            modelledView,
            name = "${screenshotName}_${locale}",
        )
    }

    override fun apply(base: Statement, description: Description): Statement {
        return paparazzi.apply(base, description)
    }
}

The layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingVertical="@dimen/default_margin">

    <TextView
        android:id="@+id/title_text_view"
        style="@style/TextAppearance.Custom.Headline4"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/default_margin"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/disclosure_image_view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="This is a long title that may go on two lines and it is supposed to, isn't it?" />

    <ImageView
        android:id="@+id/disclosure_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/default_margin"
        android:scaleType="fitEnd"
        android:src="@drawable/ic_chevron_right_black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Expected behavior Gif recorded

Additional information:

hayuki commented 1 year ago

I'm encountering the same issue with Paparazzi v1.3.1 - any insight on what could be causing this?

yschimke commented 10 months ago

I was getting similar on this PR

https://github.com/cashapp/paparazzi/pull/510

gamepro65 commented 10 months ago

The problem is with the video encoder we use which requires the resolution be an even number. We are moving away from the format to APNG which accommodates arbitrary resolutions and will resolve this issue.

radodado commented 6 months ago

You can go around it by using different device config, for example: DeviceConfig.PIXEL which dimensions are multipliers of 2

geoff-powell commented 4 weeks ago

As Chris mentioned we now use APNG for gif() which no longer has this issue. Closing this