rotateImageByHash<DataT extends Data> method

Future<String?> rotateImageByHash<DataT extends Data>(
  1. DataT? data,
  2. String hash, {
  3. int quarterTurns = 1,
  4. Data? caller,
  5. bool forceUpdate = false,
})

Implementation

Future<String?> rotateImageByHash<DataT extends Data>(
  DataT? data,
  String hash, {
  int quarterTurns = 1,
  Data? caller,
  bool forceUpdate = false,
}) async {
  try {
    if (data == null) return 'no data to rotate';

    var turns = quarterTurns % 4;
    if (turns < 0) turns += 4;
    if (turns == 0) return 'rotation unchanged';

    final requested = hash.trim();
    if (requested.isEmpty) return 'no image selected';
    final scope = local.scopeFor(data, caller: caller).trim();

    File? sourceFile = await resolveImageFileByHash(requested, scope: scope);
    if (sourceFile == null && !_isLocalImageReference(requested)) {
      final mapped = await lookupImageNameForHash(requested, scope: scope);
      if (mapped != null && mapped.trim().isNotEmpty) {
        sourceFile =
            await resolveImageFileByHash(mapped.trim(), scope: scope);
      }
    }
    if (sourceFile == null) {
      return 'image not found locally';
    }

    final originalBytes = await sourceFile.readAsBytes();
    final decoded = imglib.decodeImage(originalBytes);
    if (decoded == null) return 'could not decode image';
    final rotated = imglib.copyRotate(decoded, angle: 90 * turns);
    final rotatedBytes =
        _encodeRotatedImageBytes(rotated, sourceFile.path.toLowerCase());

    // Local-only image refs (timestamp/scoped/local prefixes) are uploaded by
    // pending multipart requests. Overwrite the file in place so sync uploads
    // the rotated bytes.
    if (_isLocalImageReference(requested)) {
      await sourceFile.writeAsBytes(rotatedBytes, flush: true);
      await FileImage(sourceFile).evict();
      return 'image rotated locally';
    }

    // Backend hash: create a rotated replacement image, upload it, then remove
    // the old hash so backend and local state stay in sync.
    final oldRefs = _collectImageRefs(data);
    final wasMainImage = data.mainhash == requested;
    final newScopedName = await _nextScopedImageName(scope);
    final rotatedFile = await storeImage(rotatedBytes, newScopedName);
    if (rotatedFile == null) return 'could not persist rotated image';
    await FileImage(rotatedFile).evict();

    final uploadResult = await uploadNewImagesOrFiles<DataT>(
      data,
      [XFile(rotatedFile.path)],
      caller: caller,
      forceUpdate: forceUpdate,
    );

    final newRefs = _collectImageRefs(data);
    final addedRefs = newRefs.difference(oldRefs);
    final replacementRef = addedRefs.isEmpty ? null : addedRefs.first;

    if (wasMainImage && replacementRef != null) {
      await setMainImageByHash(
        data,
        replacementRef,
        caller: caller,
        forceUpdate: forceUpdate,
      );
    }

    await deleteImageByHash(
      data,
      requested,
      caller: caller,
      forceUpdate: forceUpdate,
    );
    return uploadResult ?? 'rotated image queued for sync';
  } catch (e) {
    return 'rotate failed: $e';
  }
}