Introducing the ImagePicker & Image API
chevron down

Introducing the ImagePicker & Image API

Overview

The latest release of the Fitbit Mobile application added two great new features that allow developers to easily build apps and clock faces which use images directly from a user's mobile phone, or appropriately licensed images downloaded from the internet.

Although developers had previously found some workarounds for dynamically loading images onto a device, these new features now make the whole process painless.

We'll begin by taking a look at the ImagePicker component, then see how the Image API is used to convert images so they're suitable for the device.

The ImagePicker Component

The new ImagePicker component within the Settings API lets users select, crop, and scale a photo from their phone's photo library within their application's settings page. The component is initialized with familiar parameters, plus some additional ones which determine the dimensions of the output image.

Usage

In the following example we will initiate the ImagePicker so it will persist a photo within settingsStorage with a key of background-image. We need this key to retrieve the image data within the companion in order to send the image to the device.

We want to return a full-screen image and we may be dealing with multiple device screen sizes, so we need to dynamically populate the imageWidth and imageHeight values. We can use the Peer API in the companion to persist the screen dimensions into settingsStorage, so we can use those values within the settings page. See the Multiple Devices guide for further information.

Step 1

Persist the device screen dimensions into settingsStorage:

import { device } from "peer";
import { settingsStorage } from "settings";

settingsStorage.setItem("screenWidth", device.screen.width);
settingsStorage.setItem("screenHeight", device.screen.height);

Step 2

Render a settings page which lets the user select, scale, and crop a photo. The ImagePicker will use the imageWidth and imageHeight properties to ensure that the image file will be returned in the correct size, and aspect ratio.

function mySettings(props) {
  let screenWidth = props.settingsStorage.getItem("screenWidth");
  let screenHeight = props.settingsStorage.getItem("screenHeight");

  return (
    <Page>
        <ImagePicker
          title="Background Image"
          description="Pick an image to use as your background."
          label="Pick a Background Image"
          sublabel="Background image picker"
          settingsKey="background-image"
          imageWidth={ screenWidth }
          imageHeight={ screenHeight }
        />
    </Page>
  );
}

registerSettingsPage(mySettings);

Step 3

When the user selects their image, the settingsStorage.onchange event will be emitted to notify the companion that a new image is available.

import { settingsStorage } from "settings";

settingsStorage.onchange = function(evt) {
  if (evt.key === "background-image") {
    let imageData = JSON.parse(evt.newValue);
    // We now have our image data in: imageData.imageUri
  }
}

Step 4

The ImagePicker component returns all images in Base64 encoded image/png Data URI format. This is great, because it means we're not losing any image quality, but unfortunately Fitbit OS devices don't natively support png format images.

We will need to use the Image API to convert our image before sending it to the device.

The Image API

The Image API can be used to convert a source image into a format that is supported by Fitbit OS. The API is Promise based, so it's simple to chain each of the required steps to produce an image.

The source image can either be an image Data URI (e.g. data:image/png;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...), which the ImagePicker provides by default, or it can be an ArrayBuffer, which fetch() can easily provide.

The means the Image API can easily convert images from the ImagePicker, or appropriately licensed images downloaded from the internet using fetch().

Progressive JPEG files cannot be rendered on the device, but you don't need to worry as the Image API will always output JPEG files that are compatible with the device.

Output Formats

Fitbit OS devices currently supports two image formats: jpeg, and a proprietary format called txi.

Each format has different advantages and disadvantages:

txi files have hardware support, so they are fastest to render on the device, but the file size can be relatively large, so they take longer to transfer from the companion to the device. If you need transparency in your image, you should use this format.

jpeg files are slightly slower to render on the device, but support compression. Because the file size is smaller, it's therefore quicker to transfer, but the compression can negatively affect overall image quality.

Both formats are fully supported, and the developer can choose which is best for their intended usage.

One approach which tends to work well is to convert the source image into a jpeg with medium compression (40-50), then use the JPEG API on the device to save the jpeg as a txi file. With this method, you get the benefit of reduced transfer duration, and improved render performance on the device. You can see an example of this approach in the SDK Image Clock example clock face.

Source to JPG Conversion

Let's take a look at how the png image from the ImagePicker can be converted into a jpeg, and queued for File Transfer to the device.

Firstly we pass the imageUri from the ImagePicker into the Image constructor. Then we export the image and we instruct the Image API to replace transparent pixels with white, and use JPEG compression 40. Finally we add the file into the Outbox queue for transfer.

import { outbox } from "file-transfer";
import { Image } from "image";

settingsStorage.onchange = function(evt) {
  if (evt.key === "background-image") {
    compressAndTransferImage(evt.newValue);
  }
};

function compressAndTransferImage(settingsValue) {
  const imageData = JSON.parse(settingsValue);
  Image.from(imageData.imageUri)
    .then(image =>
      image.export("image/jpeg", {
        background: "#FFFFFF",
        quality: 40
      })
    )
    .then(buffer => outbox.enqueue(`${Date.now()}.jpg`, buffer))
    .then(fileTransfer => {
      console.log(`Enqueued ${fileTransfer.name}`);
    });
}

Check out the SDK Photo Picker example clock face for a complete implementation.

Source to TXI Conversion

In this example we will fetch a jpeg from the internet, the convert it into a txi file.

Note that when exporting a txi file, we can specify a background color for transparent pixels, but not compression quality.

import { outbox } from "file-transfer";
import { Image } from "image";

fetch("https://.../some-file.png")
  .then(response => response.arrayBuffer())
  .then(buffer => Image.from(buffer, "image/png"))
  .then(image =>
    image.export("image/vnd.fitbit.txi", {
      background: "#FFFFFF"
    })
  )
  .then(buffer => outbox.enqueue(`${Date.now()}.jpg`, buffer))
  .then(fileTransfer => {
    console.log(`Enqueued ${fileTransfer.name}`);
  });

Check out the SDK Image Clock example for a complete implementation, including how to convert the jpeg into txi format on the device using the JPEG API.

Until Next Time

Follow @fitbitdev on Twitter, join our Fitbit Community Forum, or get news straight to your inbox by signing up below. Curious to see the amazing work Fitbit Developers have done so far? Keep tabs on the #Made4Fitbit Twitter hashtag.