Files
simplex-chat/apps/multiplatform/spec/services/files.md
T
fbca2cae81 core, ui: resize images in the core for sharper previews and images (#7941)
* WIP: Haskell impl of resizeImageToStrSize

* WIP: add FFI export wrapper

* WIP: switch ios resizeImageToStrSizeSync to backend

* WIP: fix resizeImageToStrSizeSync

* WIP: add JNI wrappers

* Add non-base64 resize and cWriteImage

* WIP: add chat_write_image

* fix resizeToStr

* fixes

* fix syntax, raise jpeg quality back

* add chatWriteImage

* Fix cfArgs in tests

* Use jpeg-turbo encoder

* add nix pins

* bump jpeg-turbo

* add CI deps

* add encoding funs to android lib

* bump jpeg-turbo for 20.04 fix

* jpeg-turbo fix for 8.10

* android fixes

* Bump jpeg-turbo

* set static flag for jpeg-turbo

* Fix nix for aarch64/android

* Fix android C api

* Fix resize in android

* Bump hs-libjpeg-turbo

* target correct override

* override android lib from android pkgs

* add macos build tweaks

* bump hs-jpeg-turbo for 8.10 fixes

* more build fixes

* make ios compile

* Always use pkgconfig for libjpeg-turbo

* WIP: debug libjpeg in ubuntu pkg-config CI

* Add pkg-config to dockerfiles

* Add libturbojpeg to dockerfiles

* Use -dev version of libturbojpeg in dockerfiles

* Install pkg-config in windows CI

* Disable flag for custom libturbojpeg linking options

Workarounds relocation error on x86_64-linux with gcc:
  /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libturbojpeg.a(turbojpeg.c.o): relocation R_X86_64_TPOFF32 against `errStr' can not be used when making a shared object; recompile with -fPIC
  /usr/bin/ld: failed to set dynamic section sizes: bad value

* WIP: debug libjpeg in windows pkg-config CI

* WIP: debug libjpeg package in windows CI

* core: image resize module and buffer FFI

* ci: link libjpeg-turbo statically on all platforms

* ui: resize images in the core

* core: rename image resize functions to chat_resize_image and chat_resize_image_uri

* ci: restore whitespace in build.yml

* ci: build libjpeg-turbo 3.1.4.1 from a submodule

* core: hs-jpeg-turbo from github.com/simplex-chat

* nodejs: resize images

* python: resize images

* core: hs-jpeg-turbo from gitlab.com, the GitHub import lacks LFS objects

* core: generate /img previews

* ci: link libjpeg-turbo on Windows without pkg-config

* ci: build libjpeg-turbo on Windows with the GHC toolchain

* docs: plan for image resizing in the core

* docs: update image resizing plan

* ios: prevent crash when wallpaper image cannot be resized

* android, desktop: raise an error when an image cannot be resized

* docs: update spec references for image resizing

* docker: build the CLI image from a clone without submodules

* docs: apply review fixes to image resizing plan

* docker: build the CLI from a git worktree with the submodule present

* docs: apply second review to image resizing plan

* docs: point image resizing plan at the rewritten commit

* core: choose image format by transparency in chat_resize_image

* nodejs: resize images to the app sizes

* python: resize images to the app sizes

* nodejs, python: describe the core modules as used for image resizing

* docs: add license notices of the image libraries

* ci: describe the avatar method without attributing it to the apps

* ci, ios, nix: remove the libjpeg-turbo build outside cabal

* python: name resize_image in the core module text

* nodejs, python: describe resizing to the app image sizes

* core: refuse images larger than the supported size

* core: resize to JPEG images with a side above 65500 pixels

* docs: record image resizing decisions for the bundled libjpeg-turbo

* core, docs: build libjpeg-turbo and stb from the fork submodules

* docs: record the fork pins in the image resizing plan

* core: resize images to at most 4320 pixels per side, refuse above 16384

* docs: record the image side limits in the image resizing plan

* core: pin the rebased hs-jpeg-turbo fork

* core: pin the hs-jpeg-turbo fork without package.yaml

* docs: plan scaled JPEG decoding and EXIF orientation of images

* docs: copy the JuicyPixels-stbir licence of the pinned fork

* core: pin the hs-jpeg-turbo fork with scaled JPEG decoding

* core: decode JPEG at the display size, up to 65500 pixels per side

* core: rotate JPEG images by their EXIF orientation

* docs: refer the image plan to the JPEG decoding code

* docs: record the approved image processing decisions

* core: limit JPEG decoding memory to 1024 MB outside iOS and Android

* docs: name JPEG decoding in the libjpeg-turbo notice

* docs: plan image resizing from files and the decisions of 26 Oct

* core: scale images in one Catmull-Rom pass with sharpening

* docs: plan image sizes at a fixed quality

* core: resize images at a fixed quality to fill the size limit

* core: resize images from a file path or a file descriptor

* ios, android, desktop: resize picked images from their files

* core: encode resized images with mozjpeg

* tests: check descriptor closing by the file it refers to

* core: raise JPEG quality only for a full-size image below 97% of the limit

* ci: name the mozjpeg submodule in the Windows cleanup comments

* docs: refer the image plan to the current code

* docs: refer the image plan to the code merged with master

* core: export every image resizing function from each library

* core: close the file descriptor when its handle cannot be created

* docs: count the C sources of mozjpeg with the arithmetic decoder

* core: use the reviewed hs-jpeg-turbo and JuicyPixels-stbir forks

* ci: build the armv7a Android core library on request

* core: round image heights half up in the size search

* ios: describe the file image resizing functions in SimpleX.h

* ci: pick, preview and send images in the Claude app tests

* core: decode PNG images with libspng and refuse other formats

* core: encode PNG with libspng, keeping the smaller filter choice

* ci: pick, preview and send images in the scripted app smoke tests

* flake: build the PNG decoder package in nix builds

* ios: remove the copy of a picked image after sending it

* core: refuse image files larger than the decode memory limit

* core: scale a JPEG image to its largest size once per resize

* core: bisect JPEG quality without a step function

* core: reduce the memory used to sharpen and to decode PNG images

* android, desktop: pass the image format to the core as an integer

* android, desktop: fuse the preview conditions of picked media

* android, desktop, ci: check that the core resized the smoke test images

* python: remove the docstrings of the explicit image resizing functions

* docs: add the second review fixes to the image plan

* ci: tap the iOS attachment button by its accessibility label

* core: keep the colour profiles of resized images

* docs: add the colour profiles to the image plan

* ci: wait up to 30 s for the new group screens in the iOS smoke test

* ci: pick the Android smoke test images from their own photo folders

* ci: tap the iOS photo picker by position, as its tree is not shown

* ci: pick the test card and the logo by their iOS picker positions

* core: update JuicyPixels-stbir with the stb_image_resize licence terms

* core: write a fixed Display P3 profile for Display P3 images only

* core: do not keep the input image in memory for its colour profile

* core: read the JPEG segments once for the orientation and the profile

* core: read an image file into one buffer of its size

* docs: add the third review fixes to the image plan

* core: resize images with Display P3 or no profile, not profile bytes

* ci: check the pixel sizes of the images sent in the iOS smoke test

* core: update hs-jpeg-turbo with PNG chunk limits and encoder fixes

* docs: list Compact ICC Profiles, libspng and miniz in the dependencies

* android, desktop: reword the picked media preview comment

* docs: refer the image plan to the code merged with master

* ci: render the iOS snapshots before the images case

* ci: dismiss the snapshot tests' camera alert before the iOS images case

* core: update hs-jpeg-turbo with tests and the JuicyPixels-spng synopsis

* core: pin hs-jpeg-turbo and JuicyPixels-stbir in simplex-chat repositories

* update ios core library

* core, ui: accept only JPEG and PNG image data

* core: update JuicyPixels-stbir

* docs: plan for the iOS copy of picked images

---------

Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Alexandre Esteves <alexfmpe@proton.me>
2026-10-30 12:14:51 +00:00

14 KiB

File Transfer Service

Table of Contents

  1. Overview
  2. File Size Constants
  3. CryptoFile
  4. File Storage Paths
  5. API Commands
  6. Auto-Receive Logic
  7. Source Files

Executive Summary

SimpleX Chat uses two file transfer mechanisms: inline SMP transfers for small files (embedded in message bodies) and XFTP (eXtended File Transfer Protocol) for larger files up to 1 GB. Files are optionally encrypted at rest using CryptoFile functions backed by the chat core's native crypto library. File storage paths are platform-specific: Android uses Context.dataDir-based directories while Desktop uses platform-appropriate data directories (XDG on Linux, AppData on Windows, Application Support on macOS). Auto-receive logic automatically accepts images, voice messages, and videos below configurable size thresholds.


1. Overview

File transfer decision logic:

  • Inline (SMP): Files small enough to be base64-encoded and embedded directly in an SMP message body. The practical limit is defined by MAX_IMAGE_SIZE (255 KB) for compressed images. The maximum SMP inline size is MAX_FILE_SIZE_SMP (~7.6 MB).
  • XFTP: For files exceeding the inline threshold, up to MAX_FILE_SIZE_XFTP (1 GB). XFTP uses dedicated file relay servers with chunked, encrypted transfers.

The receiveFile / receiveFiles API commands handle both protocols transparently -- the chat core selects the appropriate transfer mechanism based on file metadata received from the sender.


2. File Size Constants

Defined in Utils.kt:

Constant Value Human-Readable Line Purpose
MAX_IMAGE_SIZE 261,120 255 KB L118 Inline image compression target
MAX_IMAGE_SIZE_AUTO_RCV 522,240 510 KB L119 Auto-receive threshold for images (2 * MAX_IMAGE_SIZE)
MAX_VOICE_SIZE_AUTO_RCV 522,240 510 KB L120 Auto-receive threshold for voice messages (2 * MAX_IMAGE_SIZE)
MAX_VIDEO_SIZE_AUTO_RCV 1,047,552 1023 KB L121 Auto-receive threshold for video
MAX_VOICE_MILLIS_FOR_SENDING 300,000 5 min L123 Maximum voice message duration
MAX_FILE_SIZE_SMP 8,000,000 ~7.6 MB L125 Maximum SMP inline file size
MAX_FILE_SIZE_XFTP 1,073,741,824 1 GB L127 Maximum XFTP transfer size
MAX_FILE_SIZE_LOCAL Long.MAX_VALUE Unlimited L129 Local file protocol (no size limit)

The getMaxFileSize() function (Utils.kt) selects the limit based on FileProtocol:

FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP
FileProtocol.SMP  -> MAX_FILE_SIZE_SMP
FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL

3. CryptoFile

CryptoFile.kt (58 lines)

Provides encrypted file I/O backed by the chat core's native cryptography (via JNI/JNA calls to chatWriteFile, chatReadFile, chatEncryptFile, chatDecryptFile).

Data types

@Serializable
sealed class WriteFileResult {
  @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult()
  @SerialName("error") data class Error(val writeError: String): WriteFileResult()
}

CryptoFileArgs contains fileKey and fileNonce -- the symmetric encryption key and nonce for AES-GCM encryption.

Functions

Function Line Signature Description
writeCryptoFile L23 (path: String, data: ByteArray): CryptoFileArgs Writes data to an encrypted file via a direct ByteBuffer. Returns the generated key and nonce. Requires initialized ChatController.
readCryptoFile L32 (path: String, cryptoArgs: CryptoFileArgs): ByteArray Reads and decrypts a file given its key and nonce. Returns the plaintext bytes. Throws on error (status != 0).
encryptCryptoFile L43 (fromPath: String, toPath: String): CryptoFileArgs Encrypts an existing plaintext file to a new encrypted file. Returns the generated key and nonce.
decryptCryptoFile L53 (fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) Decrypts an encrypted file to a plaintext output file. Throws on non-empty error string.

All functions delegate to native C library functions through the chat core JNI bridge.


4. File Storage Paths

Common expect declarations

Files.kt (191 lines, commonMain)

Property Line Description
dataDir L18 Root application data directory
tmpDir L19 Temporary files directory
filesDir L20 Base files directory
appFilesDir L21 Application files (chat attachments)
wallpapersDir L22 Theme wallpaper images
coreTmpDir L23 Temporary files for the chat core
dbAbsolutePrefixPath L24 Database file path prefix
preferencesDir L25 Preferences/config directory
databaseExportDir L35 Temporary DB archive storage for export
remoteHostsDir L37 Remote host connection data

Android implementation

Files.android.kt (79 lines)

Property Value
dataDir androidAppContext.dataDir
tmpDir androidAppContext.getDir("temp", MODE_PRIVATE)
filesDir dataDir/files
appFilesDir dataDir/files/app_files
wallpapersDir dataDir/files/assets/wallpapers
coreTmpDir dataDir/files/temp_files
dbAbsolutePrefixPath dataDir/files
preferencesDir dataDir/shared_prefs
databaseExportDir androidAppContext.cacheDir
remoteHostsDir tmpDir/remote_hosts

Desktop implementation

Files.desktop.kt (116 lines)

Property Value
dataDir desktopPlatform.dataPath (XDG_DATA_HOME on Linux, AppData on Windows, Application Support on macOS)
tmpDir java.io.tmpdir/simplex (deleted on exit)
filesDir dataDir/simplex_v1_files
appFilesDir Same as filesDir
wallpapersDir dataDir/simplex_v1_assets/wallpapers
coreTmpDir dataDir/tmp
dbAbsolutePrefixPath dataDir/simplex_v1
preferencesDir desktopPlatform.configPath
databaseExportDir Same as tmpDir
remoteHostsDir dataDir/remote_hosts

Helper functions (common)

Function Line Description
getAppFilePath L81 Resolves file path considering remote hosts
getWallpaperFilePath L91 Resolves wallpaper image path, creates parent directories
getLoadedFilePath L105 Returns path if file exists and is fully loaded
getLoadedFileSource L115 Returns CryptoFile source if file is loaded
readThemeOverrides L125 Reads theme overrides from themes.yaml
writeThemeOverrides L151 Atomically writes theme overrides to themes.yaml
copyFileToFile L47 Copies a File to a URI destination with toast feedback
copyBytesToFile L63 Copies a ByteArrayInputStream to a URI destination

5. API Commands

Defined in SimpleXAPI.kt:

Function Line Signature Description
receiveFiles L1946 (rhId, user, fileIds, userApprovedRelays, auto) Receive multiple files. Sends CC.ReceiveFile for each ID. Handles relay approval workflow: collects unapproved files, shows alert, re-calls with userApprovedRelays=true. Respects privacyEncryptLocalFiles preference.
receiveFile L2062 (rhId, user, fileId, userApprovedRelays, auto) Delegates to receiveFiles with a single-element list.
cancelFile L2072 (rh, user, fileId) Cancels an in-progress file transfer (send or receive). Cleans up the local file.
apiCancelFile L2080 (rh, fileId, ctrl?) Low-level cancel. Returns AChatItem? on success (SndFileCancelled or RcvFileCancelled).
uploadStandaloneFile L1916 (user, file, ctrl?) Upload a standalone file (for database migration). Returns FileTransferMeta? with XFTP link.
downloadStandaloneFile L1926 (user, url, file, ctrl?) Download a standalone file from an XFTP URL. Returns RcvFileTransfer?.

6. Auto-Receive Logic

Located in SimpleXAPI.kt within the CR.NewChatItems handler:

if (file != null &&
    appPrefs.privacyAcceptImages.get() &&
    ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
        || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
        || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV
            && file.fileStatus !is CIFileStatus.RcvAccepted))
) {
    receiveFile(rhId, r.user, file.fileId, auto = true)
}

Conditions for auto-receive:

  1. The privacyAcceptImages preference is enabled (user opt-in).
  2. The content type and size match one of:
    • Images (MCImage): file size <= 510 KB (MAX_IMAGE_SIZE_AUTO_RCV)
    • Video (MCVideo): file size <= 1023 KB (MAX_VIDEO_SIZE_AUTO_RCV)
    • Voice (MCVoice): file size <= 510 KB (MAX_VOICE_SIZE_AUTO_RCV) AND file is not already accepted
  3. The file has a non-null file attachment.

When auto = true, relay approval alerts are suppressed (the file is silently received).


7. Source Files

File Path Lines Description
CryptoFile.kt common/src/commonMain/.../model/CryptoFile.kt 58 Encrypted file read/write via native crypto
Files.kt common/src/commonMain/.../platform/Files.kt 191 Common file path declarations, theme I/O, file helpers
Files.android.kt common/src/androidMain/.../platform/Files.android.kt 79 Android file path implementations
Files.desktop.kt common/src/desktopMain/.../platform/Files.desktop.kt 116 Desktop file path implementations
Utils.kt common/src/commonMain/.../views/helpers/Utils.kt -- File size constants (L117--L128), getMaxFileSize() (L442)
SimpleXAPI.kt common/src/commonMain/.../model/SimpleXAPI.kt -- File transfer API commands (L1911--L2085), auto-receive (L2690)