File-based images#

Feature highlight#

The user interface (UI) of a solution app can display both input data (such as strings, integers, floats, and files) and output data (such as 1D/2D/3D graphics, 3D viewers, and images).

Images are a particular case of output data: they are produced as files by the backend, and the frontend needs a URL rather than a path to render them.

In this example, you learn how to:

  • Generate image files in a transaction method and store them in the storage scope.

  • Collect the resulting handles in a typed step field that references a list of blobs.

  • Turn those handles into URLs that the browser can fetch.

  • Wire a callback so a button click regenerates the images and refreshes the page.

When you complete this example, you can expect the following output in the solution UI:

../../_images/output_01.png

Prerequisites#

To mimic the creation of files, you need the pillow Python imaging library. Install the library using the following command:

poetry add pillow

Coding#

To display images from files stored in your solution, work through the following sequence of sections.

Backend#

Create the solution definition.

Key concept — Typed field

A field is a class attribute with a type annotation and a default value. A field annotated with list[EntityHandle] references a list of blobs: the images are not stored in the field itself, only the handles that point to them.

Define the step field

Declare a result_files field to hold the handle of each generated image.

solution/basic_step.py#
result_files: list[EntityHandle] = []

Key concept — Transaction method

A transaction method is the only place where step fields are read and written. Any method that touches a field must be decorated with @transaction, which declares the fields it downloads (reads) and uploads (writes) through its StepSpec.

Add a transaction method to create the images

The create_images transaction method mimics an automated workflow: it generates three PNG files with the pillow library in the storage root of the step, stores each of them through self.storage_scope.store(), and collects the returned handles in the result_files field.

solution/basic_step.py#
@transaction(self=StepSpec(upload=["result_files"]))
def create_images(self) -> None:
    """
    Mimic the creation of 3 png files by using the pillow library.

    The images are written in the storage root of the step and their handles are
    stored in the ``result_files`` field.
    """
    images_path = self.storage_scope.get_storage_root() / "images"
    images_path.mkdir()

    self.result_files = []
    for _ in range(3):
        image_name = f"Image_{str(uuid4())}.png"
        image_path = images_path / image_name
        image_with_background = Image.new("RGB", (400, 300), "grey")
        ImageDraw.Draw(image_with_background).text(  # pyright: ignore[reportUnknownMemberType]
            (20, 150), image_name, font=ImageFont.truetype("arial.ttf", size=20)
        )
        image_with_background.save(image_path)
        image_handle = self.storage_scope.store(image_path)
        self.result_files.append(image_handle)

Frontend#

Expose the solution definition in the UI.

Define the page layout

In the ui/pages folder, the layout function of the page creates the Create and display images button and a div named result-images that holds the images to display. Its children are built by the create_image_div function.

ui/pages/basic/display_images_page.py#
def layout(project: ExamplesSolution) -> html.Div:
    """Layout of the display_images step UI."""
    return html.Div(
        [
            html.H1(
                "File-based image display", className="display-3", style={"font-size": "40px", "font-weight": "bold"}
            ),
            dmc.Blockquote(
                "Use SAF GLOW to parse and store files and display them as images in a solution UI.",
                icon=DashIconify(icon="material-symbols:info", width=30),
                style={"font-size": "18px", "fontStyle": "italic"},
            ),
            html.Br(),
            html.Br(),
            dmc.Button(
                "Create and display images",
                id="create-and-display-images",
                variant="filled",
                radius="sm",
                style={"font-size": "16px", "background-color": "#2790F1"},
                leftSection=DashIconify(icon="streamline:startup-solid"),
            ),
            html.Br(),
            html.Br(),
            html.Div(id="result-images", children=create_image_div(project)),
        ],
        style={"paddingLeft": "20px"},
    )

Key concept — Callback

A callback is a Dash-decorated function that fires in response to a UI event. In a SAF solution, callbacks reach the backend through project.steps.<step_name>, read or write fields, and invoke transaction methods. No manual HTTP calls are needed.

Trigger the image creation from the frontend

Write a callback that invokes the create_images transaction method when the button is clicked, then rebuilds the content of the result-images div with the newly created images.

ui/pages/basic/display_images_page.py#
@callback(
    Output("result-images", "children"),
    Input("create-and-display-images", "n_clicks"),
    State("url", "pathname"),
    prevent_initial_call=True,
)
def create_images(n_clicks: int, project: ExamplesSolution) -> list[Any]:
    """Display images through callback."""
    step = project.steps.basic_step
    step.create_images()
    image_div_children = create_image_div(project)
    return image_div_children

Key concept — File URL

The browser cannot read a blob from its path. Call get_data with substitute_file_handles_with_urls=True to retrieve a field whose entity handles are replaced by URLs that the browser can fetch.

Render the images

The create_image_div function reads the result_files field as a list of URLs and wraps each of them in an html.Img component. A timestamp is appended to every URL so that the browser does not serve a cached version of a previously generated image.

ui/pages/basic/display_images_page.py#
def create_image_div(project: ExamplesSolution) -> list[Any]:
    """Create image div."""
    all_images: list[Any] = []
    step = project.steps.basic_step
    for image_url in step.get_data("result_files", substitute_file_handles_with_urls=True):
        # Adding a timestamp to the image URL to prevent browser caching issues
        all_images += [
            html.Div(html.Img(src=f"{image_url}?t={int(time.time())}", height="400px"), style={"flex": "1"}),
            dmc.Space(h=20),
        ]

    return all_images

Now that your implementation is complete, continue to the Testing section.

Testing#

Finally, test your implementation to confirm it works as expected.

Run the solution and compare your results with the results shown in the Feature highlight section.