OneScript
Foundation

photos

Save images to the system photo library, take photos, and record videos.

photos.save

Saves one image to the system photo library. The returned promise resolves after Photos finishes saving the image and rejects when authorization, download, validation, or saving fails.

type PhotoSaveOptions =
  | {
      url: string | URL;
      headers?: Readonly<Record<string, string>>;
      filename?: string;
    }
  | {
      data: Blob | ArrayBuffer | ArrayBufferView;
      filename?: string;
    };

function photos.save(options: PhotoSaveOptions): Promise<void>;

options accepts exactly one image source:

PropertyTypeDescription
urlstring | URLAn HTTP or HTTPS image URL.
headersRecord<string, string>Optional headers for a URL request.
dataBlob | ArrayBuffer | ArrayBufferViewImage bytes already available to the script. File and Node.js Buffer are supported through these types.
filenamestringOptional original filename stored with the photo.

The first save can display the system add-only photo library permission prompt. The API is available in app, action, and widget scripts; the system determines whether Photos access can complete in the current environment.

The promise resolves with no value after a successful save. Exactly one of url and data is required; empty data, invalid URLs, invalid headers, and native processing errors reject the promise.

Save from a URL

import { photos } from "onescript";

await photos.save({
  url: "https://example.com/photos/original.heic",
  filename: "original.heic"
});

Authenticated downloads accept explicit headers:

await photos.save({
  url: "https://api.example.com/private/photo.jpg",
  headers: {
    Authorization: `Bearer ${token}`
  }
});

URL downloads use GET and an isolated ephemeral session. Requests do not read or write the app's cookies, cache, or saved HTTP credentials. A Cookie passed explicitly through headers applies only to the current request and same-origin redirects; cross-origin redirects remove Authorization, Cookie, and Proxy-Authorization.

Save image data

const response = await fetch("https://example.com/generated.png");

await photos.save({
  data: await response.blob(),
  filename: "generated.png"
});

File supplies its name when filename is omitted:

await photos.save({ data: imageFile });

Node.js Buffer is accepted directly:

import { readFile } from "node:fs/promises";
import { photos } from "onescript";

const data = await readFile("images/result.png");
await photos.save({ data, filename: "result.png" });

The original filename is inferred in this order: explicit filename, File.name, the final URL's last path component, then a name generated by Photos.

Large images

URL responses download directly to a temporary file. In-memory data is transferred in 512 KiB chunks, keeping each Bridge JSON message near or below 700 KiB, and written incrementally to a temporary file before Photos imports it. OneScript sets no fixed total image size limit; available storage, the script environment lifetime, and Photos determine the practical limit. Unfinished downloads and temporary files are cleaned up when a script is cancelled, exits, or fails.

Errors

Use try/catch to handle errors. Native failures use stable prefixes including PHOTO_PERMISSION_DENIED, PHOTO_DOWNLOAD_FAILED, PHOTO_TRANSFER_FAILED, PHOTO_INVALID_IMAGE, and PHOTO_SAVE_FAILED.

try {
  await photos.save({ data: imageData, filename: "photo.png" });
} catch (error) {
  console.error("Could not save photo:", error);
}

photos.takePhoto

Presents the system camera and returns the captured photo as a File. The promise resolves with null when the user cancels.

type CameraDevice = "back" | "front";

interface TakePhotoOptions {
  device?: CameraDevice;
}

function photos.takePhoto(options?: TakePhotoOptions): Promise<File | null>;
PropertyTypeDescription
device"back" | "front"The camera to use. Defaults to "back".

The returned File is a JPEG (image/jpeg) named IMG_yyyyMMdd_HHmmss.jpg, and its lastModified is the time the user confirmed the photo. Pixels are rotated to match how the photo was taken, and metadata such as EXIF and location is not included.

import { photos } from "onescript";

const photo = await photos.takePhoto();
if (photo) {
  await photos.save({ data: photo });
}

The file can be uploaded directly:

const photo = await photos.takePhoto({ device: "front" });
if (photo) {
  const form = new FormData();
  form.append("avatar", photo);
  await fetch("https://api.example.com/avatar", { method: "POST", body: form });
}

photos.recordVideo

Presents the system camera in video mode and returns the absolute path of the recorded file. The promise resolves with null when the user cancels.

interface RecordVideoOptions {
  device?: CameraDevice;
  maxDuration?: number;
  quality?: "high" | "medium" | "low";
}

function photos.recordVideo(options?: RecordVideoOptions): Promise<string | null>;
PropertyTypeDescription
device"back" | "front"The camera to use. Defaults to "back".
maxDurationnumberThe maximum recording length in seconds; recording stops automatically when it is reached. Must be a positive finite number.
quality"high" | "medium" | "low"Recording quality. Defaults to "high".

The video is saved as recorded, in QuickTime format (.mov), named VID_yyyyMMdd_HHmmss.mov. Videos can be large, so they stay on disk and only the path is returned.

The file is stored in a temporary directory that belongs to the current run and is deleted when the script ends. To keep the video, move it with fs.rename, for example into the project's persistent data directory os.homedir():

import { rename } from "node:fs/promises";
import os from "node:os";
import { join } from "node:path";
import { photos } from "onescript";

const video = await photos.recordVideo({ maxDuration: 30 });
if (video) {
  await rename(video, join(os.homedir(), "clip.mov"));
}

Camera availability and permissions

The camera is available only in scripts run by the OneScript app. Widgets have no interface, and iOS does not allow app extensions to use the camera, so calls in widget and action scripts reject with PHOTO_CAMERA_UNAVAILABLE. The same error is reported on devices without the requested camera, including the Simulator.

The first photo shows the system camera permission prompt; the first video also asks for microphone access to record sound. Only one camera session can be open at a time. When a script stops while the camera is open, the camera closes and the promise rejects.

Errors

ErrorCause
PHOTO_CAMERA_UNAVAILABLEThe script is not running in the app, or the requested camera is not available.
PHOTO_CAMERA_PERMISSION_DENIEDCamera or microphone access was denied. The message names which one.
PHOTO_CAPTURE_BUSYAnother camera session is already open.
PHOTO_CAPTURE_FAILEDThe photo or video could not be processed.
try {
  const photo = await photos.takePhoto();
} catch (error) {
  console.error("Could not take a photo:", error);
}

How is this guide?

Last updated on

On this page